diff --git a/bin/yamllintrules b/bin/yamllintrules index 2e0f39742d8f..48422ba780ec 100755 --- a/bin/yamllintrules +++ b/bin/yamllintrules @@ -12,6 +12,10 @@ rules: colons: ignore: | /galley/tools/gen-meta/metadata.yaml + /galley/pkg/config/processor/metadata/metadata.yaml + /galley/pkg/config/testing/basicmeta/basicmeta.yaml + /galley/pkg/config/testing/basicmeta/basicmeta2.yaml + /galley/pkg/config/testing/k8smeta/k8smeta.yaml commas: disable comments: disable comments-indentation: disable diff --git a/galley/cmd/galley/cmd/server.go b/galley/cmd/galley/cmd/server.go index 20ef2fcebed5..1493ef9a13f7 100644 --- a/galley/cmd/galley/cmd/server.go +++ b/galley/cmd/galley/cmd/server.go @@ -150,6 +150,8 @@ func serverCmd() *cobra.Command { serverArgs.SinkMeta, "Comma-separated list of key=values to attach as metadata to outgoing sink connections. Ex: 'key=value,key2=value2'") serverCmd.PersistentFlags().BoolVar(&serverArgs.EnableServiceDiscovery, "enableServiceDiscovery", false, "Enable service discovery processing in Galley") + serverCmd.PersistentFlags().BoolVar(&serverArgs.UseOldProcessor, "useOldProcessor", serverArgs.UseOldProcessor, + "Use the old processing pipeline for config processing") // validation config serverCmd.PersistentFlags().StringVar(&serverArgs.ValidationArgs.WebhookConfigFile, diff --git a/galley/pkg/config/collection/instance.go b/galley/pkg/config/collection/instance.go new file mode 100644 index 000000000000..87461678e2c5 --- /dev/null +++ b/galley/pkg/config/collection/instance.go @@ -0,0 +1,117 @@ +// 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 collection + +import ( + "sync" + + "istio.io/istio/galley/pkg/config/resource" +) + +// ChangeNotifierFn is called when a collection instance changes. +type ChangeNotifierFn func() + +// Instance is collection of resources, indexed by name. +type Instance struct { + mu sync.RWMutex // TODO: This lock will most likely cause contention. We should investigate whether removing it would help. + collection Name + generation int64 + entries map[resource.Name]*resource.Entry + copyOnWrite bool +} + +// New returns a new collection.Instance +func New(collection Name) *Instance { + return &Instance{ + collection: collection, + entries: make(map[resource.Name]*resource.Entry), + } +} + +// Generation of the current state of the collection.Instance +func (c *Instance) Generation() int64 { + c.mu.RLock() + defer c.mu.RUnlock() + return c.generation +} + +// Size returns the number of items in the set +func (c *Instance) Size() int { + c.mu.RLock() + defer c.mu.RUnlock() + return len(c.entries) +} + +// ForEach executes the given function for each entry +func (c *Instance) ForEach(fn func(e *resource.Entry)) { + c.mu.RLock() + defer c.mu.RUnlock() + for _, e := range c.entries { + fn(e) + } +} + +// Set an entry in the collection +func (c *Instance) Set(r *resource.Entry) { + c.mu.Lock() + defer c.mu.Unlock() + c.doCopyOnWrite() + c.generation++ + c.entries[r.Metadata.Name] = r +} + +// Remove an entry from the collection. +func (c *Instance) Remove(n resource.Name) { + c.mu.Lock() + defer c.mu.Unlock() + c.doCopyOnWrite() + c.generation++ + delete(c.entries, n) +} + +// Clear the contents of this instance. +func (c *Instance) Clear() { + c.mu.Lock() + defer c.mu.Unlock() + c.doCopyOnWrite() + c.generation++ + c.entries = make(map[resource.Name]*resource.Entry) +} + +func (c *Instance) doCopyOnWrite() { // TODO: we should optimize copy-on write. + if !c.copyOnWrite { + return + } + + m := make(map[resource.Name]*resource.Entry) + for k, v := range c.entries { + m[k] = v + } + c.entries = m + c.copyOnWrite = false +} + +// Clone the instance +func (c *Instance) Clone() *Instance { + c.mu.Lock() + defer c.mu.Unlock() + c.copyOnWrite = true + return &Instance{ + collection: c.collection, + generation: c.generation, + entries: c.entries, + copyOnWrite: true, + } +} diff --git a/galley/pkg/config/collection/instance_test.go b/galley/pkg/config/collection/instance_test.go new file mode 100644 index 000000000000..0b515477af09 --- /dev/null +++ b/galley/pkg/config/collection/instance_test.go @@ -0,0 +1,109 @@ +// 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 collection_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/testing/data" +) + +func TestInstance_Basics(t *testing.T) { + g := NewGomegaWithT(t) + + inst := collection.New(data.Collection1) + + g.Expect(inst.Size()).To(Equal(0)) + + var fe []*resource.Entry + inst.ForEach(func(e *resource.Entry) { + fe = append(fe, e) + }) + g.Expect(fe).To(HaveLen(0)) + + g.Expect(inst.Generation()).To(Equal(int64(0))) + + inst.Set(data.EntryN1I1V2) + inst.Set(data.EntryN2I2V2) + + g.Expect(inst.Size()).To(Equal(2)) + + fe = nil + inst.ForEach(func(e *resource.Entry) { + fe = append(fe, e) + }) + g.Expect(fe).To(HaveLen(2)) + + g.Expect(inst.Generation()).To(Equal(int64(2))) + + inst.Remove(data.EntryN1I1V1.Metadata.Name) + + g.Expect(inst.Size()).To(Equal(1)) + + fe = nil + inst.ForEach(func(e *resource.Entry) { + fe = append(fe, e) + }) + g.Expect(fe).To(HaveLen(1)) + + g.Expect(inst.Generation()).To(Equal(int64(3))) + + inst.Clear() + + fe = nil + inst.ForEach(func(e *resource.Entry) { + fe = append(fe, e) + }) + g.Expect(fe).To(HaveLen(0)) + + g.Expect(inst.Generation()).To(Equal(int64(4))) + g.Expect(inst.Size()).To(Equal(0)) + +} + +func TestInstance_Clone(t *testing.T) { + g := NewGomegaWithT(t) + + inst := collection.New(data.Collection1) + inst.Set(data.EntryN1I1V1) + inst.Set(data.EntryN2I2V2) + + inst2 := inst.Clone() + + g.Expect(inst2.Size()).To(Equal(2)) + g.Expect(inst2.Generation()).To(Equal(int64(2))) + + var fe []*resource.Entry + inst2.ForEach(func(e *resource.Entry) { + fe = append(fe, e) + }) + g.Expect(fe).To(HaveLen(2)) + + inst.Remove(data.EntryN1I1V1.Metadata.Name) + + g.Expect(inst2.Size()).To(Equal(2)) + g.Expect(inst2.Generation()).To(Equal(int64(2))) + + fe = nil + inst2.ForEach(func(e *resource.Entry) { + fe = append(fe, e) + }) + + g.Expect(fe).To(HaveLen(2)) +} diff --git a/galley/pkg/config/collection/name.go b/galley/pkg/config/collection/name.go new file mode 100644 index 000000000000..7440898e8360 --- /dev/null +++ b/galley/pkg/config/collection/name.go @@ -0,0 +1,43 @@ +// 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 collection + +import "regexp" + +// Name of a collection. +type Name struct{ string } + +var validNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_][a-zA-Z0-9_\.]*(/[a-zA-Z0-9_][a-zA-Z0-9_\.]*)*$`) + +// EmptyName is a sentinel value +var EmptyName = Name{} + +// NewName returns a strongly typed collection. Panics if the name is not valid. +func NewName(n string) Name { + if !IsValidName(n) { + panic("collection.NewName: invalid collection name: " + n) + } + return Name{n} +} + +// String interface method implementation. +func (t Name) String() string { + return t.string +} + +// IsValidName returns true if the given collection is a valid name. +func IsValidName(name string) bool { + return validNameRegex.Match([]byte(name)) +} diff --git a/galley/pkg/config/collection/name_test.go b/galley/pkg/config/collection/name_test.go new file mode 100644 index 000000000000..6ef28154cb13 --- /dev/null +++ b/galley/pkg/config/collection/name_test.go @@ -0,0 +1,85 @@ +// 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 collection + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestNewName(t *testing.T) { + g := NewGomegaWithT(t) + + c := NewName("c1") + g.Expect(c.String()).To(Equal("c1")) +} + +func TestNewName_Invalid(t *testing.T) { + g := NewGomegaWithT(t) + defer func() { + r := recover() + g.Expect(r).NotTo(BeNil()) + }() + + _ = NewName("/") +} + +func TestName_String(t *testing.T) { + g := NewGomegaWithT(t) + c := NewName("c1") + + g.Expect(c.String()).To(Equal("c1")) +} + +func TestIsValidName_Valid(t *testing.T) { + data := []string{ + "foo", + "9", + "b", + "a", + "_", + "a0_9", + "a0_9/fruj_", + "abc/def", + } + + for _, d := range data { + t.Run(d, func(t *testing.T) { + g := NewGomegaWithT(t) + b := IsValidName(d) + g.Expect(b).To(BeTrue()) + }) + } +} + +func TestIsValidName_Invalid(t *testing.T) { + data := []string{ + "", + "/", + "/a", + "a/", + "$a/bc", + "z//a", + } + + for _, d := range data { + t.Run(d, func(t *testing.T) { + g := NewGomegaWithT(t) + b := IsValidName(d) + g.Expect(b).To(BeFalse()) + }) + } +} diff --git a/galley/pkg/config/collection/names.go b/galley/pkg/config/collection/names.go new file mode 100644 index 000000000000..7346bf647d9e --- /dev/null +++ b/galley/pkg/config/collection/names.go @@ -0,0 +1,25 @@ +// 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 collection + +// Names is a collection of names +type Names []Name + +// Clone names +func (n Names) Clone() Names { + r := make([]Name, len(n)) + copy(r, n) + return r +} diff --git a/galley/pkg/config/collection/names_test.go b/galley/pkg/config/collection/names_test.go new file mode 100644 index 000000000000..73f4fb9d647e --- /dev/null +++ b/galley/pkg/config/collection/names_test.go @@ -0,0 +1,33 @@ +// 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 collection_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/testing/data" +) + +func TestNames_Clone(t *testing.T) { + g := NewGomegaWithT(t) + + n := collection.Names{data.Collection1, data.Collection2} + + n2 := n.Clone() + g.Expect(n2).To(Equal(n)) +} diff --git a/galley/pkg/config/collection/set.go b/galley/pkg/config/collection/set.go new file mode 100644 index 000000000000..7f7f921bdea3 --- /dev/null +++ b/galley/pkg/config/collection/set.go @@ -0,0 +1,80 @@ +// 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 collection + +import ( + "sort" + "strings" +) + +// Set of collections +type Set struct { + collections map[Name]*Instance +} + +// NewSet returns a new set of collections +func NewSet(names []Name) *Set { + c := make(map[Name]*Instance) + for _, n := range names { + c[n] = New(n) + } + + return &Set{ + collections: c, + } +} + +// NewSetFromCollections creates a new set based on the given collections +func NewSetFromCollections(collections []*Instance) *Set { + c := make(map[Name]*Instance, len(collections)) + for _, col := range collections { + c[col.collection] = col + } + + return &Set{ + collections: c, + } +} + +// Collection returns the named collection +func (s *Set) Collection(n Name) *Instance { + return s.collections[n] +} + +// Clone the set. +func (s *Set) Clone() *Set { + c := make(map[Name]*Instance, len(s.collections)) + for k, v := range s.collections { + c[k] = v.Clone() + } + + return &Set{ + collections: c, + } +} + +// Names of the collections in the set. +func (s *Set) Names() Names { + result := make([]Name, 0, len(s.collections)) + for name := range s.collections { + result = append(result, name) + } + + sort.Slice(result, func(i, j int) bool { + return strings.Compare(result[i].String(), result[j].String()) < 0 + }) + + return result +} diff --git a/galley/pkg/config/collection/set_test.go b/galley/pkg/config/collection/set_test.go new file mode 100644 index 000000000000..a8361b117d36 --- /dev/null +++ b/galley/pkg/config/collection/set_test.go @@ -0,0 +1,91 @@ +// 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 collection_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/testing/data" +) + +func TestNewSet(t *testing.T) { + g := NewGomegaWithT(t) + + s := collection.NewSet([]collection.Name{data.Collection1, data.Collection2}) + + s1 := s.Collection(data.Collection1) + g.Expect(s1).NotTo(BeNil()) + s2 := s.Collection(data.Collection2) + g.Expect(s2).NotTo(BeNil()) + + s3 := s.Collection(collection.NewName("foobar")) + g.Expect(s3).To(BeNil()) +} + +func TestNewSetFromCollections(t *testing.T) { + g := NewGomegaWithT(t) + + s1 := collection.New(data.Collection1) + g.Expect(s1).NotTo(BeNil()) + s2 := collection.New(data.Collection2) + g.Expect(s2).NotTo(BeNil()) + + s := collection.NewSetFromCollections([]*collection.Instance{s1, s2}) + + c := s.Collection(data.Collection1) + g.Expect(c).NotTo(BeNil()) + c = s.Collection(data.Collection2) + g.Expect(c).NotTo(BeNil()) + + c = s.Collection(collection.NewName("foobar")) + g.Expect(c).To(BeNil()) +} + +func TestSet_Clone(t *testing.T) { + g := NewGomegaWithT(t) + + s1 := collection.New(data.Collection1) + g.Expect(s1).NotTo(BeNil()) + s2 := collection.New(data.Collection2) + g.Expect(s2).NotTo(BeNil()) + + s := collection.NewSetFromCollections([]*collection.Instance{s1, s2}) + + s = s.Clone() + + c := s.Collection(data.Collection1) + g.Expect(c).NotTo(BeNil()) + c = s.Collection(data.Collection2) + g.Expect(c).NotTo(BeNil()) + + c = s.Collection(collection.NewName("foobar")) + g.Expect(c).To(BeNil()) +} + +func TestSet_Names(t *testing.T) { + g := NewGomegaWithT(t) + + s1 := collection.New(data.Collection1) + s2 := collection.New(data.Collection2) + + s := collection.NewSetFromCollections([]*collection.Instance{s1, s2}) + names := s.Names() + g.Expect(names).To(ConsistOf( + data.Collection1, + data.Collection2)) +} diff --git a/galley/pkg/config/collection/spec.go b/galley/pkg/config/collection/spec.go new file mode 100644 index 000000000000..f20888d0e0d9 --- /dev/null +++ b/galley/pkg/config/collection/spec.go @@ -0,0 +1,99 @@ +// 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 collection + +import ( + "fmt" + "reflect" + + "github.com/gogo/protobuf/proto" +) + +// Spec is the metadata for a resource collection. +type Spec struct { + // Source of the resource that this info is about + Name Name + + // Proto package name that contains the MessageName type. This is mainly used by code-generation tools. + ProtoPackage string + + // The MessageName for the proto + MessageName string +} + +// NewSpec returns a new instance of Spec, or error if the input is not valid. +func NewSpec(name, protoPackage, messageName string) (Spec, error) { + if !IsValidName(name) { + return Spec{}, fmt.Errorf("invalid collection name: %s", name) + } + + return Spec{ + Name: NewName(name), + ProtoPackage: protoPackage, + MessageName: messageName, + }, nil +} + +// MustNewSpec calls NewSpec and panics if it fails. +func MustNewSpec(name, protoPackage, messageName string) Spec { + s, err := NewSpec(name, protoPackage, messageName) + if err != nil { + panic(fmt.Sprintf("MustNewSpec: %v", err)) + } + + return s +} + +// Validate the specs. Returns error if there is a problem. +func (s *Spec) Validate() error { + if getProtoMessageType(s.MessageName) == nil { + return fmt.Errorf("proto message not found: %v", s.MessageName) + } + return nil +} + +// String interface method implementation. +func (s *Spec) String() string { + return fmt.Sprintf("[Spec](%s, %q, %s)", s.Name, s.ProtoPackage, s.MessageName) +} + +// NewProtoInstance returns a new instance of the underlying proto for this resource. +func (s *Spec) NewProtoInstance() proto.Message { + goType := getProtoMessageType(s.MessageName) + if goType == nil { + panic(fmt.Errorf("message not found: %q", s.MessageName)) + } + + instance := reflect.New(goType).Interface() + + if p, ok := instance.(proto.Message); !ok { + panic(fmt.Sprintf( + "NewProtoInstance: message is not an instance of proto.Message. kind:%s, type:%v, value:%v", + s.Name, goType, instance)) + } else { + return p + } +} + +// getProtoMessageType returns the Go lang type of the proto with the specified name. +func getProtoMessageType(protoMessageName string) reflect.Type { + t := protoMessageType(protoMessageName) + if t == nil { + return nil + } + return t.Elem() +} + +var protoMessageType = proto.MessageType diff --git a/galley/pkg/config/collection/spec_test.go b/galley/pkg/config/collection/spec_test.go new file mode 100644 index 000000000000..711bb4744c7e --- /dev/null +++ b/galley/pkg/config/collection/spec_test.go @@ -0,0 +1,143 @@ +// 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 collection + +import ( + "reflect" + "testing" + + "github.com/gogo/protobuf/types" + . "github.com/onsi/gomega" +) + +func TestSpec_NewSpec(t *testing.T) { + g := NewGomegaWithT(t) + + s, err := NewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + g.Expect(err).To(BeNil()) + g.Expect(s.Name).To(Equal(NewName("foo"))) + g.Expect(s.ProtoPackage).To(Equal("github.com/gogo/protobuf/types")) + g.Expect(s.MessageName).To(Equal("google.protobuf.Empty")) +} + +func TestSpec_NewSpec_Error(t *testing.T) { + g := NewGomegaWithT(t) + + _, err := NewSpec("$", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + g.Expect(err).NotTo(BeNil()) +} + +func TestSpec_MustNewSpec(t *testing.T) { + g := NewGomegaWithT(t) + defer func() { + r := recover() + g.Expect(r).To(BeNil()) + }() + + s := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + g.Expect(s.Name).To(Equal(NewName("foo"))) + g.Expect(s.ProtoPackage).To(Equal("github.com/gogo/protobuf/types")) + g.Expect(s.MessageName).To(Equal("google.protobuf.Empty")) +} + +func TestSpec_MustNewSpec_Error(t *testing.T) { + g := NewGomegaWithT(t) + defer func() { + r := recover() + g.Expect(r).NotTo(BeNil()) + }() + + MustNewSpec("$", "github.com/gogo/protobuf/types", "google.protobuf.Empty") +} + +func TestSpec_NewProtoInstance(t *testing.T) { + g := NewGomegaWithT(t) + + s, err := NewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + g.Expect(err).To(BeNil()) + + p := s.NewProtoInstance() + g.Expect(p).To(Equal(&types.Empty{})) +} + +func TestSpec_NewProtoInstance_Panic_Nil(t *testing.T) { + g := NewGomegaWithT(t) + defer func() { + r := recover() + g.Expect(r).NotTo(BeNil()) + }() + old := protoMessageType + defer func() { + protoMessageType = old + }() + protoMessageType = func(name string) reflect.Type { + return nil + } + + s, err := NewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + g.Expect(err).To(BeNil()) + + _ = s.NewProtoInstance() +} + +func TestSpec_NewProtoInstance_Panic_NonProto(t *testing.T) { + g := NewGomegaWithT(t) + defer func() { + r := recover() + g.Expect(r).NotTo(BeNil()) + }() + old := protoMessageType + defer func() { + protoMessageType = old + }() + protoMessageType = func(name string) reflect.Type { + return reflect.TypeOf(&struct{}{}) + } + + s, err := NewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + g.Expect(err).To(BeNil()) + + _ = s.NewProtoInstance() +} + +func TestSpec_Validate(t *testing.T) { + g := NewGomegaWithT(t) + + s, err := NewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + g.Expect(err).To(BeNil()) + + err = s.Validate() + g.Expect(err).To(BeNil()) +} + +func TestSpec_Validate_Failure(t *testing.T) { + g := NewGomegaWithT(t) + + s, err := NewSpec("foo", "github.com/gogo/protobuf/types", "boo") + g.Expect(err).To(BeNil()) + + err = s.Validate() + g.Expect(err).NotTo(BeNil()) +} + +func TestSpec_String(t *testing.T) { + g := NewGomegaWithT(t) + b := NewSpecsBuilder() + + spec := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + b.MustAdd(spec) + + g.Expect(spec.String()).To(Equal(`[Spec](foo, "github.com/gogo/protobuf/types", google.protobuf.Empty)`)) +} diff --git a/galley/pkg/config/collection/specs.go b/galley/pkg/config/collection/specs.go new file mode 100644 index 000000000000..6680e8107593 --- /dev/null +++ b/galley/pkg/config/collection/specs.go @@ -0,0 +1,141 @@ +// 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 collection + +import ( + "fmt" + "reflect" + "sort" +) + +// Specs contains metadata about configuration resources. +type Specs struct { + byCollection map[string]Spec +} + +// SpecsBuilder is a builder for the specs type. +type SpecsBuilder struct { + specs Specs + messageTypeFn messageTypeFn +} + +// injectable function for overriding proto.MessageType, for testing purposes. +type messageTypeFn func(name string) reflect.Type + +// NewSpecsBuilder returns a new instance of SpecsBuilder. +func NewSpecsBuilder() *SpecsBuilder { + return newSpecsBuilder(getProtoMessageType) +} + +// newSpecsBuilder returns a new instance of SpecsBuilder. +func newSpecsBuilder(messageTypeFn messageTypeFn) *SpecsBuilder { + s := Specs{ + byCollection: make(map[string]Spec), + } + + return &SpecsBuilder{ + specs: s, + messageTypeFn: messageTypeFn, + } +} + +// Add a new collection to the specs. +func (b *SpecsBuilder) Add(s Spec) error { + if _, found := b.specs.byCollection[s.Name.String()]; found { + return fmt.Errorf("collection already exists: %v", s.Name) + } + + b.specs.byCollection[s.Name.String()] = s + return nil +} + +// MustAdd calls Add and panics if it fails. +func (b *SpecsBuilder) MustAdd(s Spec) { + if err := b.Add(s); err != nil { + panic(fmt.Sprintf("SpecsBuilder.MustAdd: %v", err)) + } +} + +// UnregisterSpecs unregisters all specs in s from this builder. +func (b *SpecsBuilder) UnregisterSpecs(s Specs) *SpecsBuilder { + for _, info := range s.All() { + b.Remove(info.Name) + } + return b +} + +// Remove a Spec from the builder. +func (b *SpecsBuilder) Remove(c Name) { // nolint:interfacer + delete(b.specs.byCollection, c.String()) +} + +// Build a new specs from this SpecsBuilder. +func (b *SpecsBuilder) Build() Specs { + s := b.specs + + // Avoid modify after Build. + b.specs = Specs{} + + return s +} + +// Lookup looks up a Spec by its collection name. +func (s Specs) Lookup(collection string) (Spec, bool) { + i, ok := s.byCollection[collection] + return i, ok +} + +// Get looks up a resource.Spec by its collection. Panics if it is not found. +func (s Specs) Get(collection string) Spec { + i, ok := s.Lookup(collection) + if !ok { + panic(fmt.Sprintf("specs.Get: matching entry not found for collection: %q", collection)) + } + return i +} + +// All returns all known Specs +func (s Specs) All() []Spec { + result := make([]Spec, 0, len(s.byCollection)) + + for _, info := range s.byCollection { + result = append(result, info) + } + + return result +} + +// CollectionNames returns all known collections. +func (s Specs) CollectionNames() []string { + result := make([]string, 0, len(s.byCollection)) + + for _, info := range s.byCollection { + result = append(result, info.Name.String()) + } + + sort.Strings(result) + + return result +} + +// Validate the specs. Returns error if there is a problem. +func (s Specs) Validate() error { + for _, c := range s.All() { + if err := c.Validate(); err != nil { + return err + } + } + return nil +} diff --git a/galley/pkg/config/collection/specs_test.go b/galley/pkg/config/collection/specs_test.go new file mode 100644 index 000000000000..64723e07bc60 --- /dev/null +++ b/galley/pkg/config/collection/specs_test.go @@ -0,0 +1,180 @@ +// 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 collection + +import ( + "testing" + + _ "github.com/gogo/protobuf/types" + . "github.com/onsi/gomega" +) + +func TestSpecs_Basic(t *testing.T) { + g := NewGomegaWithT(t) + b := NewSpecsBuilder() + + s := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + err := b.Add(s) + g.Expect(err).To(BeNil()) + + specs := b.Build() + g.Expect(specs.All()).To(HaveLen(1)) + g.Expect(specs.All()[0]).To(Equal(s)) +} + +func TestSpecs_MustAdd(t *testing.T) { + g := NewGomegaWithT(t) + defer func() { + r := recover() + g.Expect(r).To(BeNil()) + }() + b := NewSpecsBuilder() + + spec := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + b.MustAdd(spec) +} + +func TestSpecs_MustRegister_Panic(t *testing.T) { + g := NewGomegaWithT(t) + defer func() { + r := recover() + g.Expect(r).NotTo(BeNil()) + }() + b := NewSpecsBuilder() + + spec := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + b.MustAdd(spec) + b.MustAdd(spec) +} + +func TestSpecsBuilder_Remove(t *testing.T) { + g := NewGomegaWithT(t) + b := NewSpecsBuilder() + + spec := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + b.MustAdd(spec) + + b.Remove(spec.Name) + + specs := b.Build() + g.Expect(specs.All()).To(HaveLen(0)) +} + +func TestSpecsBuilder_RemoveSpecs(t *testing.T) { + g := NewGomegaWithT(t) + + spec := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + + b1 := NewSpecsBuilder() + b1.MustAdd(spec) + + b2 := NewSpecsBuilder() + b2.MustAdd(spec) + s := b2.Build() + + b1.UnregisterSpecs(s) + s = b1.Build() + g.Expect(s.All()).To(HaveLen(0)) +} + +func TestSpecs_Lookup(t *testing.T) { + g := NewGomegaWithT(t) + b := NewSpecsBuilder() + + spec := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + + b.MustAdd(spec) + specs := b.Build() + + s2, found := specs.Lookup("foo") + g.Expect(found).To(BeTrue()) + g.Expect(s2).To(Equal(spec)) + + _, found = specs.Lookup("zoo") + g.Expect(found).To(BeFalse()) +} + +func TestSpecs_Get(t *testing.T) { + g := NewGomegaWithT(t) + defer func() { + r := recover() + g.Expect(r).To(BeNil()) + }() + + b := NewSpecsBuilder() + + spec := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + + b.MustAdd(spec) + specs := b.Build() + + s2 := specs.Get("foo") + g.Expect(s2).To(Equal(spec)) +} + +func TestSpecs_Get_Panic(t *testing.T) { + g := NewGomegaWithT(t) + defer func() { + r := recover() + g.Expect(r).NotTo(BeNil()) + }() + + b := NewSpecsBuilder() + + spec := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + + b.MustAdd(spec) + specs := b.Build() + + _ = specs.Get("zoo") +} + +func TestSpecs_CollectionNames(t *testing.T) { + g := NewGomegaWithT(t) + b := NewSpecsBuilder() + + s1 := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + s2 := MustNewSpec("bar", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + b.MustAdd(s1) + b.MustAdd(s2) + + names := b.Build().CollectionNames() + expected := []string{"bar", "foo"} + g.Expect(names).To(Equal(expected)) +} + +func TestSpecs_Validate(t *testing.T) { + g := NewGomegaWithT(t) + b := NewSpecsBuilder() + + s1 := MustNewSpec("foo", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + s2 := MustNewSpec("bar", "github.com/gogo/protobuf/types", "google.protobuf.Empty") + b.MustAdd(s1) + b.MustAdd(s2) + + err := b.Build().Validate() + g.Expect(err).To(BeNil()) +} + +func TestSpecs_Validate_Error(t *testing.T) { + g := NewGomegaWithT(t) + b := NewSpecsBuilder() + + s1 := MustNewSpec("foo", "github.com/gogo/protobuf/types", "zoo") + b.MustAdd(s1) + + err := b.Build().Validate() + g.Expect(err).NotTo(BeNil()) +} diff --git a/galley/pkg/config/event/buffer.go b/galley/pkg/config/event/buffer.go new file mode 100644 index 000000000000..e659e4d41d6e --- /dev/null +++ b/galley/pkg/config/event/buffer.go @@ -0,0 +1,109 @@ +// 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 event + +import ( + "sync" + + "istio.io/istio/galley/pkg/config/scope" +) + +// Buffer is a growing event buffer. +type Buffer struct { + mu sync.Mutex + queue queue + handler Handler + cond *sync.Cond + processing bool +} + +var _ Handler = &Buffer{} +var _ Dispatcher = &Buffer{} + +// NewBuffer returns new Buffer instance +func NewBuffer() *Buffer { + b := &Buffer{} + b.cond = sync.NewCond(&b.mu) + return b +} + +// WithBuffer returns a new Buffer instance that listens to the given Source. +func WithBuffer(s Dispatcher) *Buffer { + b := NewBuffer() + s.Dispatch(b) + + return b +} + +// Handle implements Handler +func (b *Buffer) Handle(e Event) { + b.mu.Lock() + b.queue.add(e) + b.cond.Broadcast() + b.mu.Unlock() +} + +// Dispatch implements Source +func (b *Buffer) Dispatch(handler Handler) { + b.handler = CombineHandlers(b.handler, handler) +} + +// Clear the buffer contents. +func (b *Buffer) Clear() { + b.mu.Lock() + b.queue.clear() + b.mu.Unlock() +} + +// Stop processing +func (b *Buffer) Stop() { + b.mu.Lock() + b.processing = false + b.cond.Broadcast() + b.mu.Unlock() +} + +// Process events in the buffer. This method will not return until the Buffer is stopped. +func (b *Buffer) Process() { + b.mu.Lock() + if b.processing { + b.mu.Unlock() + return + } + b.processing = true + + for { + // lock must be held when entering the for loop (whether from beginning, or through loop continuation). + // this makes locking/unlocking slightly more efficient. + if !b.processing { + scope.Processing.Debug(">>> Buffer.Process: exiting") + b.mu.Unlock() + return + } + + e, ok := b.queue.pop() + if !ok { + scope.Processing.Debug("Buffer.Process: no more items to process, waiting") + b.cond.Wait() + continue + } + + if b.handler != nil { + b.mu.Unlock() + b.handler.Handle(e) + b.mu.Lock() + } + } +} diff --git a/galley/pkg/config/event/buffer_test.go b/galley/pkg/config/event/buffer_test.go new file mode 100644 index 000000000000..2865945c7320 --- /dev/null +++ b/galley/pkg/config/event/buffer_test.go @@ -0,0 +1,140 @@ +// 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 event_test + +import ( + "sync/atomic" + "testing" + "time" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestBuffer_Basics(t *testing.T) { + g := NewGomegaWithT(t) + + s := &fixtures.Source{} + acc := &fixtures.Accumulator{} + + b := event.WithBuffer(s) + b.Dispatch(acc) + + s.Handlers.Handle(data.Event1Col1AddItem1) + + go b.Process() + + g.Eventually(acc.Events).Should(HaveLen(1)) + g.Eventually(acc.Events).Should(ContainElement(data.Event1Col1AddItem1)) + + b.Stop() +} + +func TestBuffer_Clear(t *testing.T) { + g := NewGomegaWithT(t) + + s := &fixtures.Source{} + acc := &fixtures.Accumulator{} + + b := event.WithBuffer(s) + b.Dispatch(acc) + + s.Handlers.Handle(data.Event1Col1AddItem1) + + b.Clear() + g.Consistently(acc.Events).Should(HaveLen(0)) + + b.Stop() +} + +func TestBuffer_DoubleProcess(t *testing.T) { + g := NewGomegaWithT(t) + + s := &fixtures.Source{} + acc := &fixtures.Accumulator{} + + b := event.WithBuffer(s) + b.Dispatch(acc) + + s.Handlers.Handle(data.Event1Col1AddItem1) + + var ready int32 + var cnt int32 + go func() { + atomic.AddInt32(&cnt, 1) + atomic.AddInt32(&ready, 1) + b.Process() + atomic.AddInt32(&cnt, -1) + }() + go func() { + atomic.AddInt32(&cnt, 1) + atomic.AddInt32(&ready, 1) + b.Process() + atomic.AddInt32(&cnt, -1) + }() + + // Wait for cnt to update. + g.Eventually(func() int32 { return atomic.LoadInt32(&ready) }).Should(Equal(int32(2))) + + // Only one of the process calls should keep executing. + g.Eventually(func() int32 { return atomic.LoadInt32(&cnt) }).Should(Equal(int32(1))) + + // Both process calls should exit. + b.Stop() + g.Eventually(func() int32 { return atomic.LoadInt32(&cnt) }).Should(Equal(int32(0))) +} + +func TestBuffer_Stress(t *testing.T) { + g := NewGomegaWithT(t) + + s := &fixtures.Source{} + acc := &fixtures.Accumulator{} + + b := event.WithBuffer(s) + b.Dispatch(acc) + + go b.Process() + time.Sleep(time.Millisecond) // Yield + + var pumps int32 + var cnt int32 + var done int32 + pump := func() { + atomic.AddInt32(&pumps, 1) + for { + b.Handle(data.Event1Col1AddItem1) + atomic.AddInt32(&cnt, 1) + if atomic.LoadInt32(&done) != 0 { + break + } + } + atomic.AddInt32(&pumps, -1) + } + + for i := 0; i < 100; i++ { + go pump() + } + + g.Eventually(func() int32 { return atomic.LoadInt32(&pumps) }).Should(Equal(int32(100))) + time.Sleep(time.Second) // Let the pumps run for a second. + atomic.StoreInt32(&done, 1) + g.Eventually(func() int32 { return atomic.LoadInt32(&pumps) }).Should(Equal(int32(0))) + t.Logf("Pumped '%d' events.", atomic.LoadInt32(&cnt)) + g.Eventually(acc.Events, time.Second*5, time.Millisecond*100).Should(HaveLen(int(atomic.LoadInt32(&cnt)))) + b.Stop() +} diff --git a/galley/pkg/config/event/dispatcher.go b/galley/pkg/config/event/dispatcher.go new file mode 100644 index 000000000000..a7f054dba3a1 --- /dev/null +++ b/galley/pkg/config/event/dispatcher.go @@ -0,0 +1,21 @@ +// 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 event + +// Dispatcher is an event source that can dispatch events to Handlers. +type Dispatcher interface { + // Dispatch events to the given handler. + Dispatch(handler Handler) +} diff --git a/galley/pkg/config/event/event.go b/galley/pkg/config/event/event.go new file mode 100644 index 000000000000..7719cb24927b --- /dev/null +++ b/galley/pkg/config/event/event.go @@ -0,0 +1,115 @@ +// 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 event + +import ( + "fmt" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/resource" +) + +// Event represents a change that occurred against a resource in the source config system. +type Event struct { + Kind Kind + + // The collection that this event is emanating from. + Source collection.Name + + // A single entry, in case the event is Added, Updated or Deleted. + Entry *resource.Entry +} + +// IsSource checks whether the event has the appropriate source and returns false if it does not. +func (e *Event) IsSource(s collection.Name) bool { + return e.Source == s +} + +// IsSourceAny checks whether the event has the appropriate source and returns false if it does not. +func (e *Event) IsSourceAny(names ...collection.Name) bool { + for _, n := range names { + if n == e.Source { + return true + } + } + + return false +} + +// WithSource returns a new event with the source changed to the given collection.Name, if the event.Kind != Reset. +func (e *Event) WithSource(s collection.Name) Event { + if e.Kind == Reset { + return *e + } + + r := *e + r.Source = s + return r +} + +// Clone creates a deep clone of the event. +func (e *Event) Clone() Event { + entry := e.Entry + if entry != nil { + entry = entry.Clone() + } + return Event{ + Entry: entry, + Source: e.Source, + Kind: e.Kind, + } +} + +// String implements Stringer.String +func (e Event) String() string { + switch e.Kind { + case Added, Updated, Deleted: + return fmt.Sprintf("[Event](%s: %v/%v)", e.Kind.String(), e.Source, e.Entry.Metadata.Name) + case FullSync: + return fmt.Sprintf("[Event](%s: %v)", e.Kind.String(), e.Source) + default: + return fmt.Sprintf("[Event](%s)", e.Kind.String()) + } +} + +// FullSyncFor creates a FullSync event for the given source. +func FullSyncFor(source collection.Name) Event { + return Event{Kind: FullSync, Source: source} +} + +// AddFor creates an Add event for the given source and entry. +func AddFor(source collection.Name, r *resource.Entry) Event { + return Event{Kind: Added, Source: source, Entry: r} +} + +// UpdateFor creates an Update event for the given source and entry. +func UpdateFor(source collection.Name, r *resource.Entry) Event { + return Event{Kind: Updated, Source: source, Entry: r} +} + +// DeleteForResource creates a Deleted event for the given source and entry. +func DeleteForResource(source collection.Name, r *resource.Entry) Event { + return Event{Kind: Deleted, Source: source, Entry: r} +} + +// DeleteFor creates a Delete event for the given source and name. +func DeleteFor(source collection.Name, name resource.Name, v resource.Version) Event { + return DeleteForResource(source, &resource.Entry{ + Metadata: resource.Metadata{ + Name: name, + Version: v, + }, + }) +} diff --git a/galley/pkg/config/event/event_test.go b/galley/pkg/config/event/event_test.go new file mode 100644 index 000000000000..85d242defa2a --- /dev/null +++ b/galley/pkg/config/event/event_test.go @@ -0,0 +1,293 @@ +// 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 event + +import ( + "strings" + "testing" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/resource" + + . "github.com/onsi/gomega" + + "github.com/gogo/protobuf/types" +) + +func TestEvent_String(t *testing.T) { + e := resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns1", "rs1"), + Version: "v1", + }, + Item: &types.Empty{}, + } + + tests := []struct { + i Event + exp string + }{ + { + i: Event{}, + exp: "[Event](None)", + }, + { + i: Event{Kind: Added, Entry: &e}, + exp: "[Event](Added: /ns1/rs1)", + }, + { + i: Event{Kind: Updated, Entry: &e}, + exp: "[Event](Updated: /ns1/rs1)", + }, + { + i: Event{Kind: Deleted, Entry: &e}, + exp: "[Event](Deleted: /ns1/rs1)", + }, + { + i: Event{Kind: FullSync, Source: collection.NewName("foo")}, + exp: "[Event](FullSync: foo)", + }, + { + i: Event{Kind: Kind(99), Source: collection.NewName("foo")}, + exp: "[Event](<>)", + }, + } + + for _, tc := range tests { + t.Run("", func(t *testing.T) { + g := NewGomegaWithT(t) + actual := tc.i.String() + g.Expect(strings.TrimSpace(actual)).To(Equal(strings.TrimSpace(tc.exp))) + }) + } +} + +func TestEvent_DetailedString(t *testing.T) { + e := resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns1", "rs1"), + Version: "v1", + }, + Item: &types.Empty{}, + } + + tests := []struct { + i Event + prefix string + }{ + { + i: Event{}, + prefix: "[Event](None", + }, + { + i: Event{Kind: Added, Entry: &e}, + prefix: "[Event](Added: /ns1/rs1", + }, + { + i: Event{Kind: Updated, Entry: &e}, + prefix: "[Event](Updated: /ns1/rs1", + }, + { + i: Event{Kind: Deleted, Entry: &e}, + prefix: "[Event](Deleted: /ns1/rs1", + }, + { + i: Event{Kind: FullSync, Source: collection.NewName("foo")}, + prefix: "[Event](FullSync: foo", + }, + { + i: Event{Kind: Kind(99), Source: collection.NewName("foo")}, + prefix: "[Event](<>", + }, + } + + for _, tc := range tests { + t.Run("", func(t *testing.T) { + g := NewGomegaWithT(t) + actual := tc.i.String() + actual = strings.TrimSpace(actual) + expected := strings.TrimSpace(tc.prefix) + g.Expect(actual).To(HavePrefix(expected)) + }) + } +} + +func TestEvent_Clone(t *testing.T) { + g := NewGomegaWithT(t) + + r := resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns1", "rs1"), + Labels: map[string]string{ + "foo": "bar", + }, + Version: "v1", + }, + Item: &types.Empty{}, + } + + e := Event{Kind: Added, Source: collection.NewName("boo"), Entry: &r} + + g.Expect(e.Clone()).To(Equal(e)) +} + +func TestEvent_FullSyncFor(t *testing.T) { + g := NewGomegaWithT(t) + + e := FullSyncFor(collection.NewName("boo")) + + expected := Event{ + Kind: FullSync, + Source: collection.NewName("boo"), + } + g.Expect(e).To(Equal(expected)) +} + +func TestEvent_AddFor(t *testing.T) { + g := NewGomegaWithT(t) + + r := resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns1", "rs1"), + Labels: map[string]string{ + "foo": "bar", + }, + Version: "v1", + }, + Item: &types.Empty{}, + } + + e := AddFor(collection.NewName("boo"), &r) + + expected := Event{ + Kind: Added, + Source: collection.NewName("boo"), + Entry: &r, + } + g.Expect(e).To(Equal(expected)) +} + +func TestEvent_UpdateFor(t *testing.T) { + g := NewGomegaWithT(t) + + r := resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns1", "rs1"), + Labels: map[string]string{ + "foo": "bar", + }, + Version: "v1", + }, + Item: &types.Empty{}, + } + + e := UpdateFor(collection.NewName("boo"), &r) + + expected := Event{ + Kind: Updated, + Source: collection.NewName("boo"), + Entry: &r, + } + g.Expect(e).To(Equal(expected)) +} + +func TestEvent_DeleteFor(t *testing.T) { + g := NewGomegaWithT(t) + + n := resource.NewName("ns1", "rs1") + v := resource.Version("v1") + e := DeleteFor(collection.NewName("boo"), n, v) + + expected := Event{ + Kind: Deleted, + Source: collection.NewName("boo"), + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: n, + Version: v, + }, + }, + } + g.Expect(e).To(Equal(expected)) +} + +func TestEvent_UpdateForResource(t *testing.T) { + g := NewGomegaWithT(t) + + r := resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns1", "rs1"), + Labels: map[string]string{ + "foo": "bar", + }, + Version: "v1", + }, + Item: &types.Empty{}, + } + + e := DeleteForResource(collection.NewName("boo"), &r) + + expected := Event{ + Kind: Deleted, + Source: collection.NewName("boo"), + Entry: &r, + } + g.Expect(e).To(Equal(expected)) +} + +func TestEvent_IsSource(t *testing.T) { + g := NewGomegaWithT(t) + e := Event{ + Kind: Deleted, + Source: collection.NewName("boo"), + } + g.Expect(e.IsSource(collection.NewName("boo"))).To(BeTrue()) + g.Expect(e.IsSource(collection.NewName("noo"))).To(BeFalse()) +} + +func TestEvent_IsSourceAny(t *testing.T) { + g := NewGomegaWithT(t) + e := Event{ + Kind: Deleted, + Source: collection.NewName("boo"), + } + g.Expect(e.IsSourceAny(collection.NewName("foo"))).To(BeFalse()) + g.Expect(e.IsSourceAny(collection.NewName("boo"))).To(BeTrue()) + g.Expect(e.IsSourceAny(collection.NewName("boo"), collection.NewName("foo"))).To(BeTrue()) +} + +func TestEvent_WithSource(t *testing.T) { + g := NewGomegaWithT(t) + oldCol := collection.NewName("boo") + e := Event{ + Kind: Deleted, + Source: oldCol, + } + newCol := collection.NewName("far") + a := e.WithSource(newCol) + g.Expect(a.Source).To(Equal(newCol)) + g.Expect(e.Source).To(Equal(oldCol)) +} + +func TestEvent_WithSource_Reset(t *testing.T) { + g := NewGomegaWithT(t) + e := Event{ + Kind: Reset, + } + newCol := collection.NewName("far") + a := e.WithSource(newCol) + g.Expect(a.Source).To(Equal(collection.EmptyName)) + g.Expect(e.Source).To(Equal(collection.EmptyName)) +} diff --git a/galley/pkg/config/event/handler.go b/galley/pkg/config/event/handler.go new file mode 100644 index 000000000000..5757053bb823 --- /dev/null +++ b/galley/pkg/config/event/handler.go @@ -0,0 +1,54 @@ +// 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 event + +// Handler handles an incoming resource event. +type Handler interface { + Handle(e Event) +} + +// SentinelHandler is a special handler that does nothing with the event. It is useful to avoid +// nil checks on Handler fields. Specialized operations, such as CombineHandlers recognize SentinelHandlers +// and elide them when merging. +func SentinelHandler() Handler { + return &sentinelInstance +} + +var sentinelInstance sentinelHandler + +type sentinelHandler struct{} + +var _ Handler = &sentinelHandler{} + +// Handle implements Handler +func (s *sentinelHandler) Handle(_ Event) {} + +// HandlerFromFn returns a new Handler, based on the Handler function. +func HandlerFromFn(fn func(e Event)) Handler { + return &fnHandler{ + fn: fn, + } +} + +var _ Handler = &fnHandler{} + +type fnHandler struct { + fn func(e Event) +} + +// Handle implements Handler. +func (h *fnHandler) Handle(e Event) { + h.fn(e) +} diff --git a/galley/pkg/config/event/handler_test.go b/galley/pkg/config/event/handler_test.go new file mode 100644 index 000000000000..ec9ee75e856a --- /dev/null +++ b/galley/pkg/config/event/handler_test.go @@ -0,0 +1,205 @@ +// 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 event + +import ( + "testing" + + "github.com/gogo/protobuf/types" + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/resource" +) + +func TestHandlerFromFn(t *testing.T) { + g := NewGomegaWithT(t) + var received Event + h := HandlerFromFn(func(e Event) { + received = e + }) + + sent := Event{ + Kind: Added, + Entry: &resource.Entry{ + Item: &types.Empty{}, + }, + } + + h.Handle(sent) + + g.Expect(received).To(Equal(sent)) +} + +func TestHandlers(t *testing.T) { + g := NewGomegaWithT(t) + + var received1 Event + h1 := HandlerFromFn(func(e Event) { + received1 = e + }) + + var received2 Event + h2 := HandlerFromFn(func(e Event) { + received2 = e + }) + + var hs Handlers + hs.Add(h1) + hs.Add(h2) + + sent := Event{ + Kind: Added, + Entry: &resource.Entry{ + Item: &types.Empty{}, + }, + } + + hs.Handle(sent) + + g.Expect(received1).To(Equal(sent)) + g.Expect(received2).To(Equal(sent)) +} + +func TestCombineHandlers(t *testing.T) { + g := NewGomegaWithT(t) + + var received1 Event + h1 := HandlerFromFn(func(e Event) { + received1 = e + }) + + var received2 Event + h2 := HandlerFromFn(func(e Event) { + received2 = e + }) + + h3 := CombineHandlers(h1, h2) + + sent := Event{ + Kind: Added, + Entry: &resource.Entry{ + Item: &types.Empty{}, + }, + } + + h3.Handle(sent) + + g.Expect(received1).To(Equal(sent)) + g.Expect(received2).To(Equal(sent)) +} + +func TestCombineHandlers_Nil1(t *testing.T) { + g := NewGomegaWithT(t) + + var received1 Event + h1 := HandlerFromFn(func(e Event) { + received1 = e + }) + + h3 := CombineHandlers(h1, nil) + + sent := Event{ + Kind: Added, + Entry: &resource.Entry{ + Item: &types.Empty{}, + }, + } + + h3.Handle(sent) + + g.Expect(received1).To(Equal(sent)) +} + +func TestCombineHandlers_Nil2(t *testing.T) { + g := NewGomegaWithT(t) + + var received1 Event + h1 := HandlerFromFn(func(e Event) { + received1 = e + }) + + h3 := CombineHandlers(nil, h1) + + sent := Event{ + Kind: Added, + Entry: &resource.Entry{ + Item: &types.Empty{}, + }, + } + + h3.Handle(sent) + + g.Expect(received1).To(Equal(sent)) +} + +func TestCombineHandlers_MultipleHandlers(t *testing.T) { + g := NewGomegaWithT(t) + + var received1 Event + h1 := HandlerFromFn(func(e Event) { + received1 = e + }) + + var received2 Event + h2 := HandlerFromFn(func(e Event) { + received2 = e + }) + + hs1 := &Handlers{} + hs1.Add(h1) + hs1.Add(h2) + + var received3 Event + h3 := HandlerFromFn(func(e Event) { + received3 = e + }) + + var received4 Event + h4 := HandlerFromFn(func(e Event) { + received4 = e + }) + + hs2 := &Handlers{} + hs2.Add(h3) + hs2.Add(h4) + + sent := Event{ + Kind: Added, + Entry: &resource.Entry{ + Item: &types.Empty{}, + }, + } + + hc := CombineHandlers(hs1, hs2) + hc.Handle(sent) + + g.Expect(received1).To(Equal(sent)) + g.Expect(received2).To(Equal(sent)) + g.Expect(received3).To(Equal(sent)) + g.Expect(received4).To(Equal(sent)) +} + +func TestSentinelHandler(t *testing.T) { + h := SentinelHandler() + e := Event{ + Kind: Added, + Entry: &resource.Entry{ + Item: &types.Empty{}, + }, + } + + // Does not crash + h.Handle(e) +} diff --git a/galley/pkg/config/event/handlers.go b/galley/pkg/config/event/handlers.go new file mode 100644 index 000000000000..398f025d9043 --- /dev/null +++ b/galley/pkg/config/event/handlers.go @@ -0,0 +1,75 @@ +// 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 event + +// Handlers is a group of zero or more Handlers. Handlers is an instance of Handler, dispatching incoming events +// to the Handlers it contains. +type Handlers struct { + handlers []Handler +} + +var _ Handler = &Handlers{} + +// Handle implements Handler +func (hs *Handlers) Handle(e Event) { + for _, h := range hs.handlers { + h.Handle(e) + } +} + +// Add a new handler to handlers +func (hs *Handlers) Add(h Handler) { + hs.handlers = append(hs.handlers, h) +} + +// Size returns number of handlers in this handler set. +func (hs *Handlers) Size() int { + return len(hs.handlers) +} + +// CombineHandlers combines two handlers into a single set of handlers and returns. If any of the Handlers is an +// instance of Handlers, then their contains will be flattened into a single list. +func CombineHandlers(h1 Handler, h2 Handler) Handler { + if h1 == nil { + return h2 + } + + if h2 == nil { + return h1 + } + + if _, ok := h1.(*sentinelHandler); ok { + return h2 + } + + if _, ok := h2.(*sentinelHandler); ok { + return h1 + } + + var r Handlers + if hs, ok := h1.(*Handlers); ok { + r.handlers = append(r.handlers, hs.handlers...) + } else { + r.handlers = append(r.handlers, h1) + } + + if hs, ok := h2.(*Handlers); ok { + r.handlers = append(r.handlers, hs.handlers...) + } else { + r.handlers = append(r.handlers, h2) + } + + return &r +} diff --git a/galley/pkg/config/event/handlers_test.go b/galley/pkg/config/event/handlers_test.go new file mode 100644 index 000000000000..832266d1e223 --- /dev/null +++ b/galley/pkg/config/event/handlers_test.go @@ -0,0 +1,123 @@ +// 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 event_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestHandlers_Handle_Zero(t *testing.T) { + g := NewGomegaWithT(t) + hs := &event.Handlers{} + g.Expect(hs.Size()).To(Equal(0)) + + hs.Handle(data.Event1Col1AddItem1) +} + +func TestHandlers_Handle_One(t *testing.T) { + g := NewGomegaWithT(t) + + hs := &event.Handlers{} + + h1 := &fixtures.Accumulator{} + hs.Add(h1) + g.Expect(hs.Size()).To(Equal(1)) + + hs.Handle(data.Event1Col1AddItem1) + + g.Expect(h1.Events()).To(HaveLen(1)) + g.Expect(h1.Events()[0]).To(Equal(data.Event1Col1AddItem1)) +} + +func TestHandlers_Handle_Multiple(t *testing.T) { + g := NewGomegaWithT(t) + + hs := &event.Handlers{} + + h1 := &fixtures.Accumulator{} + hs.Add(h1) + + h2 := &fixtures.Accumulator{} + hs.Add(h2) + g.Expect(hs.Size()).To(Equal(2)) + + hs.Handle(data.Event1Col1AddItem1) + + g.Expect(h1.Events()).To(HaveLen(1)) + g.Expect(h1.Events()[0]).To(Equal(data.Event1Col1AddItem1)) + + g.Expect(h2.Events()).To(HaveLen(1)) + g.Expect(h2.Events()[0]).To(Equal(data.Event1Col1AddItem1)) +} + +func TestHandlers_Handle_Multiple_MultipleEvents(t *testing.T) { + g := NewGomegaWithT(t) + + hs := &event.Handlers{} + + h1 := &fixtures.Accumulator{} + hs.Add(h1) + + h2 := &fixtures.Accumulator{} + hs.Add(h2) + + hs.Handle(data.Event1Col1AddItem1) + hs.Handle(data.Event2Col1AddItem2) + + expected := []event.Event{data.Event1Col1AddItem1, data.Event2Col1AddItem2} + + g.Expect(h1.Events()).To(Equal(expected)) + g.Expect(h2.Events()).To(Equal(expected)) +} + +func TestHandlers_CombineHandlers_SentinelFirst(t *testing.T) { + g := NewGomegaWithT(t) + + h1 := event.SentinelHandler() + h2 := &fixtures.Accumulator{} + hs := event.CombineHandlers(h1, h2) + + g.Expect(hs).To(BeAssignableToTypeOf(&fixtures.Accumulator{})) + + hs.Handle(data.Event1Col1AddItem1) + hs.Handle(data.Event2Col1AddItem2) + + expected := []event.Event{data.Event1Col1AddItem1, data.Event2Col1AddItem2} + + g.Expect(h2.Events()).To(Equal(expected)) +} + +func TestHandlers_CombineHandlers_SentinelSecond(t *testing.T) { + g := NewGomegaWithT(t) + + h1 := &fixtures.Accumulator{} + h2 := event.SentinelHandler() + hs := event.CombineHandlers(h1, h2) + + g.Expect(hs).To(BeAssignableToTypeOf(&fixtures.Accumulator{})) + + hs.Handle(data.Event1Col1AddItem1) + hs.Handle(data.Event2Col1AddItem2) + + expected := []event.Event{data.Event1Col1AddItem1, data.Event2Col1AddItem2} + + g.Expect(h1.Events()).To(Equal(expected)) +} diff --git a/galley/pkg/config/event/kind.go b/galley/pkg/config/event/kind.go new file mode 100644 index 000000000000..c8486ca31d9f --- /dev/null +++ b/galley/pkg/config/event/kind.go @@ -0,0 +1,65 @@ +// 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 event + +import ( + "fmt" +) + +// Kind is the type of an event for a resource collection. +type Kind int + +const ( + // None is a sentinel value. Should not be used. + None Kind = iota + + // Added indicates that a new resource has been added to a collection. + Added + + // Updated indicates that an existing resource has been updated in a collection. + Updated + + // Deleted indicates an existing resource has been deleted from a collection. + Deleted + + // FullSync indicates that the source has completed the publishing of initial state of a collection as a series + // of add events. Events after FullSync are incremental change events that were applied to the origin collection. + FullSync + + // Reset indicates that the originating event.Source had a change that cannot be recovered from (e.g. CRDs have + // changed). It indicates that the listener should abandon its internal state and restart. This is a source-level + // event and applies to all collections. + Reset +) + +// String implements Stringer.String +func (k Kind) String() string { + switch k { + case None: + return "None" + case Added: + return "Added" + case Updated: + return "Updated" + case Deleted: + return "Deleted" + case FullSync: + return "FullSync" + case Reset: + return "Reset" + default: + return fmt.Sprintf("<>", k) + } +} diff --git a/galley/pkg/config/event/kind_test.go b/galley/pkg/config/event/kind_test.go new file mode 100644 index 000000000000..61cc816a7ac1 --- /dev/null +++ b/galley/pkg/config/event/kind_test.go @@ -0,0 +1,40 @@ +// 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 event + +import ( + "testing" +) + +func TestEventKind_String(t *testing.T) { + tests := map[Kind]string{ + None: "None", + Added: "Added", + Updated: "Updated", + Deleted: "Deleted", + FullSync: "FullSync", + Reset: "Reset", + 55: "<>", + } + + for i, e := range tests { + t.Run(e, func(t *testing.T) { + a := i.String() + if a != e { + t.Fatalf("Mismatch: Actual=%v, Expected=%v", a, e) + } + }) + } +} diff --git a/galley/pkg/config/event/processor.go b/galley/pkg/config/event/processor.go new file mode 100644 index 000000000000..90509c9a901b --- /dev/null +++ b/galley/pkg/config/event/processor.go @@ -0,0 +1,31 @@ +// 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 event + +// Processor is a config event processor. +// +// - Start and Stop can be called multiple times, idempotently. +// - A processor can keep internal state after it is started, but it *must* not carry state over +// between multiple start/stop calls. +// - It must complete all its internal initialization, by the time Start call returns. That is, +// the callers will assume that events can be sent (be be processed) after Start returns. +// - Processor may still receive events after Stop is called. These events must be discarded. +// +type Processor interface { + Handler + + Start() + Stop() +} diff --git a/galley/pkg/config/event/queue.go b/galley/pkg/config/event/queue.go new file mode 100644 index 000000000000..4bf8910f034d --- /dev/null +++ b/galley/pkg/config/event/queue.go @@ -0,0 +1,107 @@ +// 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 event + +const ( + defaultSizeIncrement = 32 +) + +// A circular queue for events that can expand as needed. Queue is not thread-safe. It needs to be protected externally +// in a concurrent context. +type queue struct { + items []Event + head int + end int +} + +// add a new item to the queue. +func (q *queue) add(e Event) { + if q.isFull() { + q.expand() + } + + q.items[q.end] = e + q.end = incAndWrap(q.end, len(q.items)) +} + +func (q *queue) pop() (Event, bool) { + if q.isEmpty() { + return Event{}, false + } + + idx := wrap(q.head, len(q.items)) + q.head = incAndWrap(q.head, len(q.items)) + + return q.items[idx], true +} + +func (q *queue) clear() { + q.items = nil + q.head = 0 + q.end = 0 +} + +func (q *queue) expand() { + oldSize := len(q.items) + newSize := len(q.items) + defaultSizeIncrement + old := q.items + oldh := q.head + + q.items = make([]Event, newSize) + q.head = 0 + q.end = 0 + + for i := 0; i < oldSize-1; i++ { + idx := wrap(oldh+i, oldSize) + q.add(old[idx]) + } +} + +func (q *queue) size() int { + h := q.head + e := q.end + if e < h { + e += len(q.items) + } + + return e - h +} + +func (q *queue) isFull() bool { + l := len(q.items) + if l == 0 { + return true + } + + e := q.end + 1 + e = wrap(e, l) + return e == q.head +} + +func (q *queue) isEmpty() bool { + return q.head == q.end +} + +func wrap(i, length int) int { + if length == 0 { + return i + } + return i % length +} + +func incAndWrap(i, length int) int { + i++ + return wrap(i, length) +} diff --git a/galley/pkg/config/event/queue_test.go b/galley/pkg/config/event/queue_test.go new file mode 100644 index 000000000000..68c3f0721ffd --- /dev/null +++ b/galley/pkg/config/event/queue_test.go @@ -0,0 +1,113 @@ +// 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 event + +import ( + "fmt" + "strconv" + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/resource" +) + +func TestQueue_Empty(t *testing.T) { + g := NewGomegaWithT(t) + + q := &queue{} + + g.Expect(q.isEmpty()).To(BeTrue()) + g.Expect(q.isFull()).To(BeTrue()) + + _, ok := q.pop() + g.Expect(ok).To(BeFalse()) +} + +func TestQueueWrapEmpty(t *testing.T) { + g := NewGomegaWithT(t) + + for i := 0; i < 100; i++ { + a := wrap(i, 0) + g.Expect(a).To((Equal(i))) + } +} + +func TestQueue_Expand_And_Use(t *testing.T) { + + q := &queue{} + + addCtr := 0 + popCtr := 0 + for max := 1; max < 513; max++ { + t.Run(fmt.Sprintf("M%d", max), func(t *testing.T) { + g := NewGomegaWithT(t) + + g.Expect(q.isEmpty()).To(BeTrue()) + g.Expect(q.size()).To(Equal(0)) + + for i := 0; i < max; i++ { + e := genEvent(&addCtr) + q.add(e) + + g.Expect(q.size()).To(Equal(i + 1)) + } + + if max == len(q.items)-1 { + g.Expect(q.isFull()).To(BeTrue()) + } else { + g.Expect(q.isFull()).To(BeFalse()) + } + + for i := 0; i < max; i++ { + a, ok := q.pop() + g.Expect(ok).To(BeTrue()) + + g.Expect(matchesEventSequence(&popCtr, a)).To(BeTrue()) + } + + g.Expect(q.isEmpty()).To(BeTrue()) + g.Expect(q.isFull()).To(BeFalse()) + g.Expect(q.size()).To(Equal(0)) + + _, ok := q.pop() + g.Expect(ok).To(BeFalse()) + }) + } +} + +func genEvent(ctr *int) Event { + vStr := fmt.Sprintf("%d", *ctr) + *ctr++ + + return Event{ + Kind: Added, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Version: resource.Version(vStr), + }, + }, + } +} + +func matchesEventSequence(ctr *int, e Event) bool { + *ctr++ + i, err := strconv.ParseInt(string(e.Entry.Metadata.Version), 10, 64) + if err != nil { + return false + } + + return int(i) == *ctr-1 +} diff --git a/galley/pkg/config/event/router.go b/galley/pkg/config/event/router.go new file mode 100644 index 000000000000..c2464577787a --- /dev/null +++ b/galley/pkg/config/event/router.go @@ -0,0 +1,136 @@ +// 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 event + +import ( + "fmt" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/scope" +) + +// Router distributes events to different handlers, based on collection name. +type Router interface { + Handler + Broadcast(e Event) +} + +// emptyRouter +type emptyRouter struct { +} + +var _ Router = &emptyRouter{} + +// Handle implements Router +func (r *emptyRouter) Handle(_ Event) {} + +// Broadcast implements Router +func (r *emptyRouter) Broadcast(_ Event) {} + +type singleRouter struct { + source collection.Name + handler Handler +} + +var _ Router = &singleRouter{} + +// Handle implements Handler +func (r *singleRouter) Handle(e Event) { + if e.Kind == Reset || e.IsSource(r.source) { + r.handler.Handle(e) + } +} + +// Broadcast implements Router +func (r *singleRouter) Broadcast(e Event) { + e = e.WithSource(r.source) + r.handler.Handle(e) +} + +// Router distributes events to multiple different handlers, based on collection name. +type router struct { + handlers map[collection.Name]Handler +} + +var _ Router = &router{} + +// Handle implements Handler +func (r *router) Handle(e Event) { + h, found := r.handlers[e.Source] + if found { + h.Handle(e) + } else { + scope.Processing.Warna("Router.Handle: No handler for event, dropping: ", e) + } +} + +// Broadcast implements Router +func (r *router) Broadcast(e Event) { + for d, h := range r.handlers { + e = e.WithSource(d) + h.Handle(e) + } +} + +// NewRouter returns a new instance of Router +func NewRouter() Router { + return &emptyRouter{} +} + +// AddToRouter adds the given handler for the given source collection. +func AddToRouter(r Router, source collection.Name, handler Handler) Router { + if r == nil { + return &singleRouter{ + source: source, + handler: handler, + } + } + + switch v := r.(type) { + case *emptyRouter: + return &singleRouter{ + source: source, + handler: handler, + } + + case *singleRouter: + if v.source == source { + return &singleRouter{ + source: source, + handler: CombineHandlers(v.handler, handler), + } + } + s := &router{ + handlers: make(map[collection.Name]Handler), + } + s.handlers[v.source] = v.handler + s.handlers[source] = handler + return s + + case *router: + s := &router{ + handlers: make(map[collection.Name]Handler), + } + for k, v := range v.handlers { + s.handlers[k] = v + } + old := s.handlers[source] + s.handlers[source] = CombineHandlers(old, handler) + return s + + default: + panic(fmt.Sprintf("unkown Router: %T", v)) + } +} diff --git a/galley/pkg/config/event/router_test.go b/galley/pkg/config/event/router_test.go new file mode 100644 index 000000000000..a56005dc0c74 --- /dev/null +++ b/galley/pkg/config/event/router_test.go @@ -0,0 +1,156 @@ +// 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 event_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestRouter_Empty(t *testing.T) { + s := event.NewRouter() + // No crash + s.Handle(data.Event1Col1AddItem1) + s.Broadcast(data.Event1Col1DeleteItem1) +} + +func TestRouter_Single_Handle(t *testing.T) { + g := NewGomegaWithT(t) + + s := event.NewRouter() + acc := &fixtures.Accumulator{} + s = event.AddToRouter(s, data.Collection1, acc) + s.Handle(data.Event1Col1AddItem1) + + g.Expect(acc.Events()).To(HaveLen(1)) +} + +func TestRouter_Single_Handle_AddToNil(t *testing.T) { + g := NewGomegaWithT(t) + + var s event.Router + acc := &fixtures.Accumulator{} + s = event.AddToRouter(s, data.Collection1, acc) + s.Handle(data.Event1Col1AddItem1) + + g.Expect(acc.Events()).To(HaveLen(1)) +} + +func TestRouter_Single_Handle_NoMatch(t *testing.T) { + g := NewGomegaWithT(t) + + s := event.NewRouter() + acc := &fixtures.Accumulator{} + s = event.AddToRouter(s, data.Collection2, acc) + s.Handle(data.Event1Col1AddItem1) + + g.Expect(acc.Events()).To(HaveLen(0)) +} + +func TestRouter_Single_MultiListener(t *testing.T) { + g := NewGomegaWithT(t) + + s := event.NewRouter() + acc1 := &fixtures.Accumulator{} + acc2 := &fixtures.Accumulator{} + s = event.AddToRouter(s, data.Collection1, acc1) + s = event.AddToRouter(s, data.Collection1, acc2) + s.Handle(data.Event1Col1AddItem1) + + g.Expect(acc1.Events()).To(HaveLen(1)) + g.Expect(acc2.Events()).To(HaveLen(1)) +} + +func TestRouter_Single_Broadcast(t *testing.T) { + g := NewGomegaWithT(t) + + s := event.NewRouter() + acc := &fixtures.Accumulator{} + s = event.AddToRouter(s, data.Collection1, acc) + s.Broadcast(event.Event{Kind: event.Reset}) + + g.Expect(acc.Events()).To(HaveLen(1)) +} + +func TestRouter_Multi_Handle(t *testing.T) { + g := NewGomegaWithT(t) + + s := event.NewRouter() + acc1 := &fixtures.Accumulator{} + acc2 := &fixtures.Accumulator{} + acc3 := &fixtures.Accumulator{} + s = event.AddToRouter(s, data.Collection1, acc1) + s = event.AddToRouter(s, data.Collection2, acc2) + s = event.AddToRouter(s, data.Collection3, acc3) + s.Handle(data.Event1Col1AddItem1) + s.Handle(data.Event3Col2AddItem1) + + g.Expect(acc1.Events()).To(HaveLen(1)) + g.Expect(acc2.Events()).To(HaveLen(1)) + g.Expect(acc3.Events()).To(HaveLen(0)) +} + +func TestRouter_Multi_NoTarget(t *testing.T) { + g := NewGomegaWithT(t) + + s := event.NewRouter() + acc1 := &fixtures.Accumulator{} + acc2 := &fixtures.Accumulator{} + s = event.AddToRouter(s, data.Collection1, acc1) + s = event.AddToRouter(s, data.Collection3, acc2) + s.Handle(data.Event3Col2AddItem1) + + g.Expect(acc1.Events()).To(HaveLen(0)) + g.Expect(acc2.Events()).To(HaveLen(0)) +} + +func TestRouter_Multi_Broadcast(t *testing.T) { + g := NewGomegaWithT(t) + + s := event.NewRouter() + acc1 := &fixtures.Accumulator{} + acc2 := &fixtures.Accumulator{} + acc3 := &fixtures.Accumulator{} + s = event.AddToRouter(s, data.Collection1, acc1) + s = event.AddToRouter(s, data.Collection2, acc2) + s = event.AddToRouter(s, data.Collection3, acc3) + s.Broadcast(event.Event{Kind: event.Reset}) + + g.Expect(acc1.Events()).To(HaveLen(1)) + g.Expect(acc2.Events()).To(HaveLen(1)) + g.Expect(acc3.Events()).To(HaveLen(1)) +} + +func TestRouter_Multi_Unknown_Panic(t *testing.T) { + g := NewGomegaWithT(t) + + defer func() { + r := recover() + g.Expect(r).ToNot(BeNil()) + }() + _ = event.AddToRouter(&unknownSelector{}, data.Collection3, &fixtures.Accumulator{}) +} + +type unknownSelector struct{} + +var _ event.Router = &unknownSelector{} + +func (u *unknownSelector) Handle(e event.Event) {} +func (u *unknownSelector) Broadcast(e event.Event) {} diff --git a/galley/pkg/config/event/source.go b/galley/pkg/config/event/source.go new file mode 100644 index 000000000000..4c4b5376a98c --- /dev/null +++ b/galley/pkg/config/event/source.go @@ -0,0 +1,75 @@ +// 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 event + +import "sync" + +// Source is an event source for a single collection. +// +// - A Source can be started/stopped multiple times, idempotently. +// - Every time a Source is started, it is expected to send the full list of events, including a FullSync event for +// each collection. +// - It must halt its dispatch of events before the Stop() call returns. The callers will assume that +// once Stop() returns, none of the registered handlers will receive any new events from this source. +type Source interface { + Dispatcher + + // Start sending events. + Start() + + // Stop sending events. + Stop() +} + +type compositeSource struct { + mu sync.Mutex + sources []Source +} + +var _ Source = &compositeSource{} + +// Start implements Source +func (s *compositeSource) Start() { + s.mu.Lock() + defer s.mu.Unlock() + for _, src := range s.sources { + src.Start() + } +} + +// Stop implements Source +func (s *compositeSource) Stop() { + s.mu.Lock() + defer s.mu.Unlock() + for _, src := range s.sources { + src.Stop() + } +} + +// Dispatch implements Source +func (s *compositeSource) Dispatch(h Handler) { + s.mu.Lock() + defer s.mu.Unlock() + for _, src := range s.sources { + src.Dispatch(h) + } +} + +// CombineSources combines multiple Sources and returns it as a single Source +func CombineSources(s ...Source) Source { + return &compositeSource{ + sources: s, + } +} diff --git a/galley/pkg/config/event/source_test.go b/galley/pkg/config/event/source_test.go new file mode 100644 index 000000000000..4ba3c5c93f5e --- /dev/null +++ b/galley/pkg/config/event/source_test.go @@ -0,0 +1,75 @@ +// 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 event_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestMergeSources_Basic(t *testing.T) { + g := NewGomegaWithT(t) + + s1 := &fixtures.Source{} + s2 := &fixtures.Source{} + + s := event.CombineSources(s1, s2) + + h := &fixtures.Accumulator{} + s.Dispatch(h) + + g.Expect(s1.Handlers).To(Equal(h)) + g.Expect(s2.Handlers).To(Equal(h)) + + s.Start() + g.Expect(s1.Running()).To(BeTrue()) + g.Expect(s2.Running()).To(BeTrue()) + + s.Stop() + g.Expect(s1.Running()).To(BeFalse()) + g.Expect(s2.Running()).To(BeFalse()) +} + +func TestMergeSources_Composite(t *testing.T) { + g := NewGomegaWithT(t) + + s1 := &fixtures.Source{} + s2a := &fixtures.Source{} + s2b := &fixtures.Source{} + s2 := event.CombineSources(s2a, s2b) + + s := event.CombineSources(s1, s2) + + h := &fixtures.Accumulator{} + s.Dispatch(h) + + g.Expect(s1.Handlers).To(Equal(h)) + g.Expect(s2a.Handlers).To(Equal(h)) + g.Expect(s2b.Handlers).To(Equal(h)) + + s.Start() + g.Expect(s1.Running()).To(BeTrue()) + g.Expect(s2a.Running()).To(BeTrue()) + g.Expect(s2b.Running()).To(BeTrue()) + + s.Stop() + g.Expect(s1.Running()).To(BeFalse()) + g.Expect(s2a.Running()).To(BeFalse()) + g.Expect(s2b.Running()).To(BeFalse()) +} diff --git a/galley/pkg/config/event/transformer.go b/galley/pkg/config/event/transformer.go new file mode 100644 index 000000000000..6acb460a067a --- /dev/null +++ b/galley/pkg/config/event/transformer.go @@ -0,0 +1,133 @@ +// 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 event + +import ( + "sync/atomic" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/scope" +) + +// Transformer is a Processor that transforms input events from one or more collections to a set of output events to +// one or more collections. +// +// - A transformer must declare its inputs and outputs collections. via Inputs and Outputs methods. These must return +// idempotent results. +// - For every output collection that Transformer exposes, it must send a FullSync event, once the Transformer is +// started. +// +type Transformer interface { + Processor + + // DispatchFor registers the given handler for a particular output collection. + DispatchFor(c collection.Name, h Handler) + + // Inputs for this transformer + Inputs() collection.Names + + // Outputs for this transformer + Outputs() collection.Names +} + +// FnTransform is a base type for handling common Transformer operations. +type FnTransform struct { + in collection.Names + out collection.Names + selector Router + startFn func() + stopFn func() + handleFn func(e Event, h Handler) + syncCtr int32 +} + +// Inputs partially implements Transformer +func (t *FnTransform) Inputs() collection.Names { + return t.in +} + +// Outputs partially implements Transformer +func (t *FnTransform) Outputs() collection.Names { + return t.out +} + +// Start implements Transformer +func (t *FnTransform) Start() { + scope.Processing.Debug("FnTransform.Start") + if t.selector == nil { + t.selector = NewRouter() + } + + atomic.StoreInt32(&t.syncCtr, int32(len(t.in))) + + if t.startFn != nil { + t.startFn() + } +} + +// Stop implements Transformer +func (t *FnTransform) Stop() { + scope.Processing.Debug("FnTransform.Stop") + if t.stopFn != nil { + t.stopFn() + } +} + +// DispatchFor implements Transformer +func (t *FnTransform) DispatchFor(c collection.Name, h Handler) { + scope.Processing.Debugf("FnTransform.DispatchFor: %v => %T", c, h) + t.selector = AddToRouter(t.selector, c, h) +} + +// Handle implements Transformer +func (t *FnTransform) Handle(e Event) { + if e.Kind == Reset { + t.selector.Broadcast(e) + return + } + + if !e.IsSourceAny(t.in...) { + scope.Processing.Warnf("Event with unexpected source received: %v", e) + return + } + + if e.Kind == FullSync { + for { + old := atomic.LoadInt32(&t.syncCtr) + swapped := atomic.CompareAndSwapInt32(&t.syncCtr, old, old-1) + if swapped { + if old == 1 { + // Limit reached to 0. + t.selector.Broadcast(e) + } + break + } + } + return + } + + t.handleFn(e, t.selector) +} + +// NewFnTransform returns a Transformer based on the given start, stop and input event handler functions. +func NewFnTransform(inputs, outputs collection.Names, startFn, stopFn func(), fn func(e Event, handler Handler)) *FnTransform { + return &FnTransform{ + in: inputs, + out: outputs, + startFn: startFn, + stopFn: stopFn, + handleFn: fn, + } +} diff --git a/galley/pkg/config/event/transformer_test.go b/galley/pkg/config/event/transformer_test.go new file mode 100644 index 000000000000..e37b2ee0ceb0 --- /dev/null +++ b/galley/pkg/config/event/transformer_test.go @@ -0,0 +1,225 @@ +// 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 event_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestTransformer_Basics(t *testing.T) { + g := NewGomegaWithT(t) + + inputs := collection.Names{collection.NewName("foo"), collection.NewName("bar")} + outputs := collection.Names{collection.NewName("boo"), collection.NewName("baz")} + + var started, stopped bool + xform := event.NewFnTransform( + inputs, + outputs, + func() { started = true }, + func() { stopped = true }, + func(e event.Event, h event.Handler) {}, + ) + + g.Expect(xform.Inputs()).To(Equal(inputs)) + g.Expect(xform.Outputs()).To(Equal(outputs)) + + g.Expect(started).To(BeFalse()) + g.Expect(stopped).To(BeFalse()) + + xform.Start() + g.Expect(started).To(BeTrue()) + g.Expect(stopped).To(BeFalse()) + + xform.Stop() + g.Expect(stopped).To(BeTrue()) +} + +func TestTransformer_Selection(t *testing.T) { + g := NewGomegaWithT(t) + + foo := collection.NewName("foo") + bar := collection.NewName("bar") + boo := collection.NewName("boo") + baz := collection.NewName("baz") + inputs := collection.Names{foo, bar} + outputs := collection.Names{boo, baz} + + xform := event.NewFnTransform( + inputs, + outputs, + nil, + nil, + func(e event.Event, h event.Handler) { + // Simply translate events + if e.IsSource(foo) { + h.Handle(e.WithSource(boo)) + } + if e.IsSource(bar) { + h.Handle(e.WithSource(baz)) + } + }, + ) + + accBoo := &fixtures.Accumulator{} + accBaz := &fixtures.Accumulator{} + xform.DispatchFor(boo, accBoo) + xform.DispatchFor(baz, accBaz) + + xform.Start() + + xform.Handle(data.Event1Col1AddItem1.WithSource(foo)) + xform.Handle(data.Event1Col1AddItem1.WithSource(bar)) + + g.Expect(accBoo.Events()).To(ConsistOf( + data.Event1Col1AddItem1.WithSource(boo), + )) + g.Expect(accBaz.Events()).To(ConsistOf( + data.Event1Col1AddItem1.WithSource(baz), + )) +} + +func TestTransformer_InvalidEvent(t *testing.T) { + g := NewGomegaWithT(t) + + foo := collection.NewName("foo") + bar := collection.NewName("bar") + inputs := collection.Names{foo} + outputs := collection.Names{bar} + + xform := event.NewFnTransform( + inputs, + outputs, + nil, + nil, + func(e event.Event, h event.Handler) { + // Simply translate events + if e.IsSource(foo) { + h.Handle(e.WithSource(bar)) + } + }, + ) + + acc := &fixtures.Accumulator{} + xform.DispatchFor(bar, acc) + + xform.Start() + + xform.Handle(data.Event1Col1AddItem1.WithSource(bar)) + + g.Expect(acc.Events()).To(BeEmpty()) +} + +func TestTransformer_Reset(t *testing.T) { + g := NewGomegaWithT(t) + + foo := collection.NewName("foo") + bar := collection.NewName("bar") + baz := collection.NewName("baz") + inputs := collection.Names{foo} + outputs := collection.Names{bar, baz} + + xform := event.NewFnTransform( + inputs, + outputs, + nil, + nil, + func(e event.Event, h event.Handler) { + // Simply translate events + if e.IsSource(foo) { + h.Handle(e.WithSource(bar)) + } + }, + ) + + accBar := &fixtures.Accumulator{} // it is a trap! + xform.DispatchFor(bar, accBar) + accBaz := &fixtures.Accumulator{} + xform.DispatchFor(baz, accBaz) + + xform.Start() + + xform.Handle(event.Event{Kind: event.Reset}) + + g.Expect(accBar.Events()).To(ConsistOf( + event.Event{Kind: event.Reset}, + )) + g.Expect(accBar.Events()).To(ConsistOf( + event.Event{Kind: event.Reset}, + )) +} + +func TestTransformer_FullSync(t *testing.T) { + g := NewGomegaWithT(t) + + foo := collection.NewName("foo") + bar := collection.NewName("bar") + boo := collection.NewName("boo") + baz := collection.NewName("baz") + inputs := collection.Names{foo, bar} + outputs := collection.Names{boo, baz} + + xform := event.NewFnTransform( + inputs, + outputs, + nil, + nil, + func(e event.Event, h event.Handler) { + // Simply translate events + if e.IsSource(foo) { + h.Handle(e.WithSource(boo)) + } + if e.IsSource(bar) { + h.Handle(e.WithSource(baz)) + } + }, + ) + + accBoo := &fixtures.Accumulator{} + accBaz := &fixtures.Accumulator{} + xform.DispatchFor(boo, accBoo) + xform.DispatchFor(baz, accBaz) + + xform.Start() + + xform.Handle(event.FullSyncFor(foo)) + g.Expect(accBoo.Events()).To(BeEmpty()) + g.Expect(accBaz.Events()).To(BeEmpty()) + + xform.Handle(event.FullSyncFor(bar)) + g.Expect(accBoo.Events()).To(ConsistOf(event.FullSyncFor(boo))) + g.Expect(accBaz.Events()).To(ConsistOf(event.FullSyncFor(baz))) + + // redo + accBoo.Clear() + accBaz.Clear() + xform.Stop() + xform.Start() + + xform.Handle(event.FullSyncFor(bar)) + g.Expect(accBoo.Events()).To(BeEmpty()) + g.Expect(accBaz.Events()).To(BeEmpty()) + + xform.Handle(event.FullSyncFor(foo)) + g.Expect(accBoo.Events()).To(ConsistOf(event.FullSyncFor(boo))) + g.Expect(accBaz.Events()).To(ConsistOf(event.FullSyncFor(baz))) +} diff --git a/galley/pkg/config/meshcfg/const.go b/galley/pkg/config/meshcfg/const.go new file mode 100644 index 000000000000..1ecc46be624b --- /dev/null +++ b/galley/pkg/config/meshcfg/const.go @@ -0,0 +1,27 @@ +// 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 meshcfg + +import ( + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/resource" +) + +// IstioMeshconfig is the name of collection istio/meshconfig +// It is captured here explicitly, as some of the core pieces of code need to reference this. +var IstioMeshconfig = collection.NewName("istio/mesh/v1alpha1/MeshConfig") + +// ResourceName for the Istio Mesh Config resource +var ResourceName = resource.NewName("istio-system", "meshconfig") diff --git a/galley/pkg/config/meshcfg/defaults.go b/galley/pkg/config/meshcfg/defaults.go new file mode 100644 index 000000000000..b9f51538a20b --- /dev/null +++ b/galley/pkg/config/meshcfg/defaults.go @@ -0,0 +1,41 @@ +// 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 meshcfg + +import ( + "time" + + "github.com/gogo/protobuf/types" + + "istio.io/api/mesh/v1alpha1" +) + +// Default mesh configuration +func Default() *v1alpha1.MeshConfig { + return &v1alpha1.MeshConfig{ + MixerCheckServer: "", + MixerReportServer: "", + DisablePolicyChecks: false, + PolicyCheckFailOpen: false, + ProxyListenPort: 15001, + ConnectTimeout: types.DurationProto(1 * time.Second), + IngressClass: "istio", + IngressControllerMode: v1alpha1.MeshConfig_STRICT, + EnableTracing: true, + AccessLogFile: "/dev/stdout", + SdsUdsPath: "", + OutboundTrafficPolicy: &v1alpha1.MeshConfig_OutboundTrafficPolicy{Mode: v1alpha1.MeshConfig_OutboundTrafficPolicy_ALLOW_ANY}, + } +} diff --git a/galley/pkg/config/meshcfg/defaults_test.go b/galley/pkg/config/meshcfg/defaults_test.go new file mode 100644 index 000000000000..f8da4295d88b --- /dev/null +++ b/galley/pkg/config/meshcfg/defaults_test.go @@ -0,0 +1,33 @@ +// 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 meshcfg + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/api/mesh/v1alpha1" +) + +func TestDefaults(t *testing.T) { + g := NewGomegaWithT(t) + + m := Default() + + // A couple of point-wise checks. + g.Expect(m.IngressClass).To(Equal("istio")) + g.Expect(m.IngressControllerMode).To(Equal(v1alpha1.MeshConfig_STRICT)) +} diff --git a/galley/pkg/config/meshcfg/fs.go b/galley/pkg/config/meshcfg/fs.go new file mode 100644 index 000000000000..dc290c0da18b --- /dev/null +++ b/galley/pkg/config/meshcfg/fs.go @@ -0,0 +1,116 @@ +// 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 meshcfg + +import ( + "io/ioutil" + + "github.com/ghodss/yaml" + "github.com/gogo/protobuf/jsonpb" + + "istio.io/pkg/filewatcher" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/scope" +) + +// For overriding in tests +var yamlToJSON = yaml.YAMLToJSON + +// FsSource is a event.InMemorySource implementation that reads mesh from file. +type FsSource struct { + path string + fw filewatcher.FileWatcher + + inmemory *InMemorySource +} + +var _ event.Source = &FsSource{} + +// NewFS returns a new mesh cache, based on watching a file. +func NewFS(path string) (*FsSource, error) { + fw := filewatcher.NewWatcher() + + err := fw.Add(path) + if err != nil { + return nil, err + } + + c := &FsSource{ + path: path, + fw: fw, + inmemory: NewInmemory(), + } + + c.reload() + // If we were not able to load mesh config, start with the default. + if !c.inmemory.IsSynced() { + scope.Processing.Infof("Unable to load up mesh config, using default values (path: %s)", path) + c.inmemory.Set(Default()) + } + + go func() { + for range fw.Events(path) { + c.reload() + } + }() + + return c, nil +} + +// Start implements event.Source +func (c *FsSource) Start() { + c.inmemory.Start() +} + +// Stop implements event.Source +func (c *FsSource) Stop() { + scope.Processing.Debugf("meshcfg.FsSource.Stop >>>") + c.inmemory.Stop() + scope.Processing.Debugf("meshcfg.FsSource.Stop <<<") +} + +// Dispatch implements event.Source +func (c *FsSource) Dispatch(h event.Handler) { + c.inmemory.Dispatch(h) +} + +func (c *FsSource) reload() { + by, err := ioutil.ReadFile(c.path) + if err != nil { + scope.Processing.Errorf("Error loading mesh config (path: %s): %v", c.path, err) + return + } + + js, err := yamlToJSON(by) + if err != nil { + scope.Processing.Errorf("Error converting mesh config Yaml to JSON (path: %s): %v", c.path, err) + return + } + + cfg := Default() + if err = jsonpb.UnmarshalString(string(js), cfg); err != nil { + scope.Processing.Errorf("Error reading mesh config as JSON (path: %s): %v", c.path, err) + return + } + + c.inmemory.Set(cfg) + scope.Processing.Infof("Reloaded mesh config (path: %s): \n%s\n", c.path, string(by)) +} + +// Close closes this cache. +func (c *FsSource) Close() error { + return c.fw.Close() +} diff --git a/galley/pkg/config/meshcfg/fs_test.go b/galley/pkg/config/meshcfg/fs_test.go new file mode 100644 index 000000000000..38ad669dace6 --- /dev/null +++ b/galley/pkg/config/meshcfg/fs_test.go @@ -0,0 +1,407 @@ +// 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 meshcfg + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "testing" + "time" + + "github.com/gogo/protobuf/jsonpb" + . "github.com/onsi/gomega" + + "istio.io/api/mesh/v1alpha1" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestFsSource_NoInitialFile(t *testing.T) { + g := NewGomegaWithT(t) + + file := setupDir(t, nil) + + fs, err := NewFS(file) + g.Expect(err).To(BeNil()) + defer func() { + err = fs.Close() + g.Expect(err).To(BeNil()) + }() + acc := &fixtures.Accumulator{} + fs.Dispatch(acc) + + fs.Start() + + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("istio-system", "meshconfig"), + }, + Item: Default(), + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) +} + +func TestFsSource_NoInitialFile_UpdateAfterStart(t *testing.T) { + g := NewGomegaWithT(t) + + file := setupDir(t, nil) + + fs, err := NewFS(file) + g.Expect(err).To(BeNil()) + defer func() { + err = fs.Close() + g.Expect(err).To(BeNil()) + }() + acc := &fixtures.Accumulator{} + fs.Dispatch(acc) + + fs.Start() + + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("istio-system", "meshconfig"), + }, + Item: Default(), + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) + + acc.Clear() + mcfg := Default() + mcfg.IngressClass = "foo" + writeMeshCfg(t, file, mcfg) + + expected = []event.Event{ + { + Kind: event.Reset, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) +} + +func TestFsSource_InitialFile_UpdateAfterStart(t *testing.T) { + g := NewGomegaWithT(t) + + mcfg := Default() + mcfg.IngressClass = "foo" + file := setupDir(t, mcfg) + + fs, err := NewFS(file) + g.Expect(err).To(BeNil()) + defer func() { + err = fs.Close() + g.Expect(err).To(BeNil()) + }() + acc := &fixtures.Accumulator{} + fs.Dispatch(acc) + + fs.Start() + + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("istio-system", "meshconfig"), + }, + Item: mcfg, + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) + + acc.Clear() + mcfg2 := Default() + mcfg2.IngressClass = "bar" + writeMeshCfg(t, file, mcfg2) + + expected = []event.Event{ + { + Kind: event.Reset, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) +} + +func TestFsSource_InitialFile(t *testing.T) { + g := NewGomegaWithT(t) + + mcfg := Default() + mcfg.IngressClass = "foo" + file := setupDir(t, mcfg) + + fs, err := NewFS(file) + g.Expect(err).To(BeNil()) + defer func() { + err = fs.Close() + g.Expect(err).To(BeNil()) + }() + acc := &fixtures.Accumulator{} + fs.Dispatch(acc) + + fs.Start() + + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("istio-system", "meshconfig"), + }, + Item: mcfg, + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) +} + +func TestFsSource_StartStopStart(t *testing.T) { + g := NewGomegaWithT(t) + + mcfg := Default() + mcfg.IngressClass = "foo" + file := setupDir(t, mcfg) + + fs, err := NewFS(file) + g.Expect(err).To(BeNil()) + defer func() { + err = fs.Close() + g.Expect(err).To(BeNil()) + }() + acc := &fixtures.Accumulator{} + fs.Dispatch(acc) + + fs.Start() + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("istio-system", "meshconfig"), + }, + Item: mcfg, + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) + + acc.Clear() + fs.Stop() + g.Consistently(acc.Events()).Should(HaveLen(0)) + + fs.Start() + g.Eventually(acc.Events).Should(Equal(expected)) +} + +func TestFsSource_FileRemoved_NoChange(t *testing.T) { + g := NewGomegaWithT(t) + + mcfg := Default() + mcfg.IngressClass = "foo" + file := setupDir(t, mcfg) + + fs, err := NewFS(file) + g.Expect(err).To(BeNil()) + defer func() { + err = fs.Close() + g.Expect(err).To(BeNil()) + }() + acc := &fixtures.Accumulator{} + fs.Dispatch(acc) + + fs.Start() + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("istio-system", "meshconfig"), + }, + Item: mcfg, + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) + acc.Clear() + + err = os.Remove(file) + g.Expect(err).To(BeNil()) + time.Sleep(time.Millisecond * 100) + g.Consistently(acc.Events()).Should(HaveLen(0)) +} + +func TestFsSource_BogusFile_NoChange(t *testing.T) { + g := NewGomegaWithT(t) + + mcfg := Default() + mcfg.IngressClass = "foo" + file := setupDir(t, mcfg) + + fs, err := NewFS(file) + g.Expect(err).To(BeNil()) + defer func() { + err = fs.Close() + g.Expect(err).To(BeNil()) + }() + acc := &fixtures.Accumulator{} + fs.Dispatch(acc) + + fs.Start() + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("istio-system", "meshconfig"), + }, + Item: mcfg, + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) + acc.Clear() + + err = ioutil.WriteFile(file, []byte(":@#Hallo!"), os.ModePerm) + g.Expect(err).To(BeNil()) + + time.Sleep(time.Millisecond * 100) + g.Consistently(acc.Events()).Should(HaveLen(0)) +} + +func setupDir(t *testing.T, m *v1alpha1.MeshConfig) string { + g := NewGomegaWithT(t) + + p, err := ioutil.TempDir(os.TempDir(), t.Name()) + g.Expect(err).To(BeNil()) + file := path.Join(p, "meshconfig.yaml") + + if m != nil { + writeMeshCfg(t, file, m) + } + + return file +} + +func writeMeshCfg(t *testing.T, file string, m *v1alpha1.MeshConfig) { // nolint:interfacer + g := NewGomegaWithT(t) + s, err := (&jsonpb.Marshaler{Indent: " "}).MarshalToString(m) + g.Expect(err).To(BeNil()) + err = ioutil.WriteFile(file, []byte(s), os.ModePerm) + g.Expect(err).To(BeNil()) +} + +func TestFsSource_InvalidPath(t *testing.T) { + g := NewGomegaWithT(t) + + file := setupDir(t, nil) + file = path.Join(file, "bogus") + + _, err := NewFS(file) + g.Expect(err).NotTo(BeNil()) +} + +func TestFsSource_YamlToJSONError(t *testing.T) { + g := NewGomegaWithT(t) + old := yamlToJSON + yamlToJSON = func([]byte) ([]byte, error) { + return nil, fmt.Errorf("horror") + } + defer func() { + yamlToJSON = old + }() + + mcfg := Default() + mcfg.IngressClass = "foo" + file := setupDir(t, mcfg) + + fs, err := NewFS(file) + g.Expect(err).To(BeNil()) + defer func() { + err = fs.Close() + g.Expect(err).To(BeNil()) + }() + acc := &fixtures.Accumulator{} + fs.Dispatch(acc) + + fs.Start() + + // Expect default config + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("istio-system", "meshconfig"), + }, + Item: Default(), + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) +} diff --git a/galley/pkg/config/meshcfg/inmemory.go b/galley/pkg/config/meshcfg/inmemory.go new file mode 100644 index 000000000000..60c978500183 --- /dev/null +++ b/galley/pkg/config/meshcfg/inmemory.go @@ -0,0 +1,124 @@ +// 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 meshcfg + +import ( + "sync" + + "github.com/gogo/protobuf/proto" + + "istio.io/api/mesh/v1alpha1" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" +) + +// InMemorySource is an event.InMemorySource implementation for meshconfig. When the mesh config is first set, add & fullsync events +// will be published. Otherwise a reset event will be sent. +type InMemorySource struct { + mu sync.Mutex + current *v1alpha1.MeshConfig + + handlers event.Handler + + synced bool + started bool +} + +var _ event.Source = &InMemorySource{} + +// NewInmemory returns a new meshconfig.InMemorySource. +func NewInmemory() *InMemorySource { + return &InMemorySource{ + current: Default(), + } +} + +// Dispatch implements event.Dispatcher +func (s *InMemorySource) Dispatch(handler event.Handler) { + s.mu.Lock() + defer s.mu.Unlock() + s.handlers = event.CombineHandlers(s.handlers, handler) +} + +// Start implements event.InMemorySource +func (s *InMemorySource) Start() { + s.mu.Lock() + defer s.mu.Unlock() + + if s.started { + // Already started + return + } + s.started = true + + if s.synced { + s.send(event.Added) + s.send(event.FullSync) + } +} + +// Stop implements event.InMemorySource +func (s *InMemorySource) Stop() { + s.mu.Lock() + defer s.mu.Unlock() + s.started = false +} + +// Set new meshconfig +func (s *InMemorySource) Set(cfg *v1alpha1.MeshConfig) { + s.mu.Lock() + defer s.mu.Unlock() + + cfg = proto.Clone(cfg).(*v1alpha1.MeshConfig) + s.current = cfg + + if s.started { + if !s.synced { + s.send(event.Added) + s.send(event.FullSync) + } else { + s.send(event.Reset) + } + } + + s.synced = true +} + +// IsSynced indicates that the InMemorySource has been given a Mesh config at least once. +func (s *InMemorySource) IsSynced() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.synced +} + +func (s *InMemorySource) send(k event.Kind) { + // must be called under lock + e := event.Event{ + Kind: k, + Source: IstioMeshconfig, + } + + switch k { + case event.Added, event.Updated: + e.Entry = &resource.Entry{ + Metadata: resource.Metadata{ + Name: ResourceName, + }, + Item: proto.Clone(s.current), + } + } + + s.handlers.Handle(e) +} diff --git a/galley/pkg/config/meshcfg/inmemory_test.go b/galley/pkg/config/meshcfg/inmemory_test.go new file mode 100644 index 000000000000..120a124ffe5b --- /dev/null +++ b/galley/pkg/config/meshcfg/inmemory_test.go @@ -0,0 +1,207 @@ +// 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 meshcfg + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestInMemorySource_Empty(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewInmemory() + + acc := &fixtures.Accumulator{} + s.Dispatch(acc) + + g.Expect(s.IsSynced()).To(BeFalse()) + + s.Start() + + g.Consistently(s.IsSynced).Should(BeFalse()) + g.Consistently(acc.Events).Should(HaveLen(0)) +} + +func TestInMemorySource_SetBeforeStart(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewInmemory() + + acc := &fixtures.Accumulator{} + s.Dispatch(acc) + + s.Set(Default()) + g.Expect(s.IsSynced()).To(BeTrue()) + + s.Start() + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: ResourceName, + }, + Item: Default(), + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) +} + +func TestInMemorySource_SetAfterStart(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewInmemory() + + acc := &fixtures.Accumulator{} + s.Dispatch(acc) + + s.Start() + s.Set(Default()) + g.Expect(s.IsSynced()).To(BeTrue()) + + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: ResourceName, + }, + Item: Default(), + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) +} + +func TestInMemorySource_DoubleStart(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewInmemory() + + acc := &fixtures.Accumulator{} + s.Dispatch(acc) + + s.Set(Default()) + s.Start() + s.Start() + g.Expect(s.IsSynced()).To(BeTrue()) + + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: ResourceName, + }, + Item: Default(), + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) +} + +func TestInMemorySource_StartStop(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewInmemory() + + acc := &fixtures.Accumulator{} + s.Dispatch(acc) + + s.Start() + s.Set(Default()) + s.Stop() + acc.Clear() + + s.Start() + g.Expect(s.IsSynced()).To(BeTrue()) + + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: ResourceName, + }, + Item: Default(), + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) +} + +func TestInMemorySource_ResetOnUpdate(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewInmemory() + + acc := &fixtures.Accumulator{} + s.Dispatch(acc) + + s.Start() + s.Set(Default()) + m := Default() + m.IngressClass = "foo" + s.Set(m) + + expected := []event.Event{ + { + Kind: event.Added, + Source: IstioMeshconfig, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: ResourceName, + }, + Item: Default(), + }, + }, + { + Kind: event.FullSync, + Source: IstioMeshconfig, + }, + { + Kind: event.Reset, + Source: IstioMeshconfig, + }, + } + g.Eventually(acc.Events).Should(Equal(expected)) +} diff --git a/galley/pkg/config/processing/processor.go b/galley/pkg/config/processing/processor.go new file mode 100644 index 000000000000..d956bcbfab50 --- /dev/null +++ b/galley/pkg/config/processing/processor.go @@ -0,0 +1,30 @@ +// 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 processing + +import ( + "istio.io/api/mesh/v1alpha1" + + "istio.io/istio/galley/pkg/config/event" +) + +// ProcessorOptions are options that are passed to event.Processors during startup. +type ProcessorOptions struct { + MeshConfig *v1alpha1.MeshConfig + DomainSuffix string +} + +// ProcessorProvider returns a new Processor instance for the given ProcessorOptions. +type ProcessorProvider func(o ProcessorOptions) event.Processor diff --git a/galley/pkg/config/processing/runtime.go b/galley/pkg/config/processing/runtime.go new file mode 100644 index 000000000000..50771bc7b501 --- /dev/null +++ b/galley/pkg/config/processing/runtime.go @@ -0,0 +1,165 @@ +// 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 processing + +import ( + "sync" + "sync/atomic" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/scope" +) + +// RuntimeOptions is options for Runtime +type RuntimeOptions struct { + Source event.Source + ProcessorProvider ProcessorProvider + DomainSuffix string +} + +// Clone returns a cloned copy of the RuntimeOptions. +func (o RuntimeOptions) Clone() RuntimeOptions { + return o +} + +// Runtime is the top-level config processing machinery. Through runtime options, it takes in a set of Sources and +// a Processor. Once started, Runtime will go through a startup phase, where it waits for MeshConfig to arrive before +// starting the Processor. If, the Runtime receives any event.RESET events, or if there is a change to the MeshConfig, +// then the Runtime will stop the processor and sources and will restart them again. +// +// Internally, Runtime uses the session type to implement this stateful behavior. The session handles state transitions +// and is responsible for starting/stopping the Sources, Processors, in the correct order. +type Runtime struct { // nolint:maligned + mu sync.RWMutex + + // counter for session id. The current value reflects the processing session's id. + sessionIDCtr int32 + + // runtime options that was passed as parameters to the command-line. + options RuntimeOptions + + // stopCh is used to send stop signal completion to the background go-routine. + stopCh chan struct{} + + // wg is used to synchronize the completion of Stop call with the completion of the background + // go routine. + wg sync.WaitGroup + session atomic.Value +} + +// NewRuntime returns a new instance of a processing.Runtime. +func NewRuntime(o RuntimeOptions) *Runtime { + + r := &Runtime{ + options: o.Clone(), + } + + h := event.HandlerFromFn(r.handle) + o.Source.Dispatch(h) + + return r +} + +// Start the Runtime +func (r *Runtime) Start() { + r.mu.Lock() + defer r.mu.Unlock() + + if r.stopCh != nil { + scope.Processing.Warnf("Runtime.Start: already started") + return + } + r.stopCh = make(chan struct{}) + + r.wg.Add(1) + startedCh := make(chan struct{}) + go r.run(startedCh, r.stopCh) + <-startedCh +} + +// Stop the Runtime +func (r *Runtime) Stop() { + r.mu.Lock() + defer r.mu.Unlock() + + if r.stopCh == nil { + return + } + close(r.stopCh) + r.wg.Wait() + + r.stopCh = nil +} + +// currentSessionID is a numeric identifier of internal Runtime state. It is used for debugging purposes. +func (r *Runtime) currentSessionID() int32 { + var id int32 + se := r.session.Load() + if se != nil { + s := se.(*session) + id = s.id + } + return id +} + +// currentSessionState is the state of the internal Runtime state. It is used for debugging purposes. +func (r *Runtime) currentSessionState() sessionState { + var state sessionState + se := r.session.Load() + if se != nil { + s := se.(*session) + state = s.state + } + return state +} + +func (r *Runtime) run(startedCh, stopCh chan struct{}) { +loop: + for { + sid := atomic.AddInt32(&r.sessionIDCtr, 1) + scope.Processing.Infof("Runtime.run: Starting new session id:%d", sid) + se, done := newSession(sid, r.options) + r.session.Store(se) + se.start() + + if startedCh != nil { + close(startedCh) + startedCh = nil + } + + select { + case <-done: + scope.Processing.Infof("Runtime.run: Completing session: id:%d", sid) + + case <-stopCh: + scope.Processing.Infof("Runtime.run: Stopping session: id%d", sid) + se.stop() + break loop + } + } + + r.wg.Done() + scope.Processing.Info("Runtime.run: Exiting...") +} + +func (r *Runtime) handle(e event.Event) { + se := r.session.Load() + if se == nil { + return + } + + s := se.(*session) + s.handle(e) +} diff --git a/galley/pkg/config/processing/runtime_test.go b/galley/pkg/config/processing/runtime_test.go new file mode 100644 index 000000000000..138be211cca5 --- /dev/null +++ b/galley/pkg/config/processing/runtime_test.go @@ -0,0 +1,454 @@ +// 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 processing + +import ( + "sync" + "testing" + + "github.com/gogo/protobuf/types" + . "github.com/onsi/gomega" + + "istio.io/api/mesh/v1alpha1" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/meshcfg" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/source/kube/inmemory" + "istio.io/istio/galley/pkg/config/testing/basicmeta" + "istio.io/istio/galley/pkg/config/testing/fixtures" + "istio.io/pkg/log" +) + +func init() { + scope.Processing.SetOutputLevel(log.DebugLevel) +} + +func TestRuntime_Startup_NoMeshConfig(t *testing.T) { + g := NewGomegaWithT(t) + + f := initFixture() + f.rt.Start() + defer f.rt.Stop() + + coll := basicmeta.Collection1 + r := &resource.Entry{ + Metadata: resource.Metadata{}, + Item: &types.Empty{}, + } + f.src.Get(coll).Set(r) + + g.Consistently(f.p.acc.Events).Should(HaveLen(0)) + g.Consistently(f.p.HasStarted).Should(BeFalse()) +} + +func TestRuntime_Startup_MeshConfig_Arrives_No_Resources(t *testing.T) { + g := NewGomegaWithT(t) + + f := initFixture() + f.rt.Start() + defer f.rt.Stop() + + f.meshsrc.Set(meshcfg.Default()) + + g.Eventually(f.p.acc.Events).Should(HaveLen(3)) + g.Eventually(f.p.acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection1), + event.FullSyncFor(meshcfg.IstioMeshconfig), + event.AddFor(meshcfg.IstioMeshconfig, meshConfigEntry(meshcfg.Default())), + )) + g.Eventually(f.p.HasStarted).Should(BeTrue()) +} + +func TestRuntime_Startup_MeshConfig_Arrives(t *testing.T) { + g := NewGomegaWithT(t) + + f := initFixture() + f.rt.Start() + defer f.rt.Stop() + + coll := basicmeta.Collection1 + r := &resource.Entry{ + Metadata: resource.Metadata{}, + Item: &types.Empty{}, + } + f.src.Get(coll).Set(r) + + f.meshsrc.Set(meshcfg.Default()) + g.Eventually(f.p.acc.Events).Should(HaveLen(4)) + g.Eventually(f.p.acc.Events).Should(ConsistOf( + event.AddFor(basicmeta.Collection1, r), + event.FullSyncFor(basicmeta.Collection1), + event.FullSyncFor(meshcfg.IstioMeshconfig), + event.AddFor(meshcfg.IstioMeshconfig, meshConfigEntry(meshcfg.Default())), + )) + + g.Eventually(f.p.HasStarted).Should(BeTrue()) +} + +func TestRuntime_Startup_Stop(t *testing.T) { + g := NewGomegaWithT(t) + + f := initFixture() + f.rt.Start() + + coll := basicmeta.Collection1 + r := &resource.Entry{ + Metadata: resource.Metadata{}, + Item: &types.Empty{}, + } + f.src.Get(coll).Set(r) + + f.meshsrc.Set(meshcfg.Default()) + + g.Eventually(f.p.acc.Events).Should(HaveLen(4)) + g.Eventually(f.p.HasStarted).Should(BeTrue()) + f.rt.Stop() + g.Eventually(f.p.HasStarted).Should(BeFalse()) +} + +func TestRuntime_Start_Start_Stop(t *testing.T) { + g := NewGomegaWithT(t) + + f := initFixture() + f.rt.Start() + f.rt.Start() // Double start + + coll := basicmeta.Collection1 + r := &resource.Entry{ + Metadata: resource.Metadata{}, + Item: &types.Empty{}, + } + f.src.Get(coll).Set(r) + + f.meshsrc.Set(meshcfg.Default()) + g.Eventually(f.p.acc.Events).Should(HaveLen(4)) + g.Eventually(f.p.HasStarted).Should(BeTrue()) + f.rt.Stop() + g.Eventually(f.p.HasStarted).Should(BeFalse()) +} + +func TestRuntime_Start_Stop_Stop(t *testing.T) { + g := NewGomegaWithT(t) + + f := initFixture() + f.rt.Start() + + coll := basicmeta.Collection1 + r := &resource.Entry{ + Metadata: resource.Metadata{}, + Item: &types.Empty{}, + } + f.src.Get(coll).Set(r) + + f.meshsrc.Set(meshcfg.Default()) + + g.Eventually(f.p.acc.Events).Should(HaveLen(4)) + g.Eventually(f.p.HasStarted).Should(BeTrue()) + f.rt.Stop() + f.rt.Stop() + g.Eventually(f.p.HasStarted).Should(BeFalse()) +} + +func TestRuntime_MeshConfig_Causing_Restart(t *testing.T) { + g := NewGomegaWithT(t) + + f := initFixture() + f.rt.Start() + defer f.rt.Stop() + + coll := basicmeta.Collection1 + r := &resource.Entry{ + Metadata: resource.Metadata{}, + Item: &types.Empty{}, + } + f.src.Get(coll).Set(r) + + f.meshsrc.Set(meshcfg.Default()) + g.Eventually(f.p.acc.Events).Should(ConsistOf( + event.AddFor(meshcfg.IstioMeshconfig, &resource.Entry{ + Metadata: resource.Metadata{ + Name: meshcfg.ResourceName, + }, + Item: meshcfg.Default(), + }), + event.FullSyncFor(meshcfg.IstioMeshconfig), + event.AddFor(coll, r), + event.FullSyncFor(coll), + )) + + oldSessionID := f.rt.currentSessionID() + + f.p.acc.Clear() + + mcfg := meshcfg.Default() + mcfg.IngressClass = "ing" + + f.meshsrc.Set(mcfg) + g.Eventually(f.rt.currentSessionID).Should(Equal(oldSessionID + 1)) + g.Eventually(f.p.acc.Events).Should(HaveLen(4)) +} + +func TestRuntime_Event_Before_Start(t *testing.T) { + g := NewGomegaWithT(t) + + f := initFixture() + + coll := basicmeta.Collection1 + r := &resource.Entry{ + Metadata: resource.Metadata{}, + Item: &types.Empty{}, + } + f.src.Start() + f.src.Get(coll).Set(r) + + g.Consistently(f.p.acc.Events).Should(HaveLen(0)) +} + +func TestRuntime_Stop_WhileStarting(t *testing.T) { + g := NewGomegaWithT(t) + + f := newFixture() + f.meshsrc = nil + f.src = nil + f.init() + + // Wait until mockSrc.Start is called, but block it from completing. + f.mockSrc.blockStart() + + f.rt.Start() + + g.Eventually(f.rt.currentSessionState).Should(Equal(starting)) + g.Eventually(f.mockSrc.hasStarted).Should(BeTrue()) + + go f.rt.Stop() + + g.Eventually(f.rt.currentSessionState).Should(Equal(terminating)) + + // release Start call. Things should cleanup and release. + f.mockSrc.releaseStart() + + // Once Stop returns, both started and stopped should be + g.Eventually(f.mockSrc.hasStopped).Should(BeTrue()) +} + +func TestRuntime_Reset_WhileStarting(t *testing.T) { + g := NewGomegaWithT(t) + + f := newFixture() + f.meshsrc = nil + f.src = nil + f.init() + + // Wait until mockSrc.Start is called, but block it from completing. + f.mockSrc.blockStart() + + f.rt.Start() + + g.Eventually(f.rt.currentSessionState).Should(Equal(starting)) + g.Eventually(f.mockSrc.hasStarted).Should(BeTrue()) + + oldSessionID := f.rt.currentSessionID() + + f.mockSrc.h.Handle(event.Event{Kind: event.Reset}) + + f.mockSrc.releaseStart() + + g.Eventually(f.rt.currentSessionID).Should(Equal(oldSessionID + 1)) + + g.Eventually(f.rt.currentSessionState).Should(Equal(buffering)) + g.Consistently(f.p.acc.Events).Should(BeEmpty()) + + f.rt.Stop() +} + +func TestRuntime_MeshEvent_WhileBuffering(t *testing.T) { + g := NewGomegaWithT(t) + + f := newFixture() + f.meshsrc = nil + f.src = nil + f.init() + + f.rt.Start() + g.Eventually(f.rt.currentSessionState).Should(Equal(buffering)) + + f.mockSrc.h.Handle(event.DeleteFor(meshcfg.IstioMeshconfig, meshcfg.ResourceName, resource.Version("vxx"))) + + g.Consistently(f.rt.currentSessionState).Should(Equal(buffering)) + + f.mockSrc.h.Handle(event.FullSyncFor(meshcfg.IstioMeshconfig)) + + g.Eventually(f.rt.currentSessionState).Should(Equal(processing)) + + f.rt.Stop() +} + +func TestRuntime_MeshEvent_WhileRunning(t *testing.T) { + g := NewGomegaWithT(t) + + f := initFixture() + f.rt.Start() + defer f.rt.Stop() + + f.meshsrc.Set(meshcfg.Default()) + g.Eventually(f.p.acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection1), + event.FullSyncFor(meshcfg.IstioMeshconfig), + event.AddFor(meshcfg.IstioMeshconfig, meshConfigEntry(meshcfg.Default())), + )) + + oldSessionID := f.rt.currentSessionID() + f.p.acc.Clear() + + // Send a mesh event out-of-band + f.mockSrc.h.Handle(event.DeleteFor(meshcfg.IstioMeshconfig, meshcfg.ResourceName, resource.Version("vxx"))) + + g.Eventually(f.rt.currentSessionID).Should(Equal(oldSessionID + 1)) + g.Eventually(f.p.acc.Events).Should(And( + ContainElement(event.FullSyncFor(basicmeta.Collection1)), + ContainElement(event.FullSyncFor(meshcfg.IstioMeshconfig)), + ContainElement(event.AddFor(meshcfg.IstioMeshconfig, meshConfigEntry(meshcfg.Default()))))) + + g.Eventually(f.p.HasStarted).Should(BeTrue()) +} + +type fixture struct { + meshsrc *meshcfg.InMemorySource + src *inmemory.KubeSource + mockSrc *testSource + p *testProcessor + rt *Runtime +} + +func newFixture() *fixture { + p := &testProcessor{} + f := &fixture{ + meshsrc: meshcfg.NewInmemory(), + src: inmemory.NewKubeSource(basicmeta.MustGet().KubeSource().Resources()), + mockSrc: &testSource{}, + p: p, + } + + f.mockSrc.startCalled = sync.NewCond(&f.mockSrc.mu) + return f +} + +func initFixture() *fixture { + f := newFixture() + f.init() + return f +} + +func (f *fixture) init() { + var srcs []event.Source + + if f.meshsrc != nil { + srcs = append(srcs, f.meshsrc) + } + if f.src != nil { + srcs = append(srcs, f.src) + } + if f.mockSrc != nil { + srcs = append(srcs, f.mockSrc) + } + + o := RuntimeOptions{ + DomainSuffix: "local.svc", + Source: event.CombineSources(srcs...), + ProcessorProvider: func(_ ProcessorOptions) event.Processor { return f.p }, + } + + f.rt = NewRuntime(o) +} + +type testSource struct { + mu sync.Mutex + h event.Handler + startCalled *sync.Cond + startWG sync.WaitGroup + started bool + stopped bool +} + +var _ event.Source = &testSource{} + +func (s *testSource) Dispatch(handler event.Handler) { + s.h = event.CombineHandlers(s.h, handler) +} + +func (s *testSource) Start() { + s.mu.Lock() + s.startCalled.Broadcast() + s.started = true + s.mu.Unlock() + s.startWG.Wait() +} + +func (s *testSource) Stop() { + s.mu.Lock() + defer s.mu.Unlock() + s.stopped = true +} + +func (s *testSource) blockStart() { + s.startWG.Add(1) +} + +func (s *testSource) releaseStart() { + s.startWG.Done() +} + +func (s *testSource) hasStarted() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.started +} + +func (s *testSource) hasStopped() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.stopped +} + +type testProcessor struct { + acc fixtures.Accumulator + started bool +} + +func (t *testProcessor) Handle(e event.Event) { + t.acc.Handle(e) +} + +func (t *testProcessor) Start() { + t.started = true +} + +func (t *testProcessor) Stop() { + t.started = false +} + +func (t *testProcessor) HasStarted() bool { + return t.started +} + +func meshConfigEntry(m *v1alpha1.MeshConfig) *resource.Entry { // nolint:interfacer + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("istio-system", "meshconfig"), + }, + Item: m, + } +} diff --git a/galley/pkg/config/processing/session.go b/galley/pkg/config/processing/session.go new file mode 100644 index 000000000000..264cbab2f90f --- /dev/null +++ b/galley/pkg/config/processing/session.go @@ -0,0 +1,300 @@ +// 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 processing + +import ( + "fmt" + "sync" + + "github.com/gogo/protobuf/proto" + + "istio.io/api/mesh/v1alpha1" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/meshcfg" + "istio.io/istio/galley/pkg/config/scope" +) + +type sessionState string + +const ( + // The session is inactive. This is both the initial, and the terminal state of the session. + // Allowed transitions are: starting + inactive = sessionState("inactive") + + // The session is starting up. In this phase, the sources are being initialized. The starting session is an explicit + // state, since it is possible to get lifecycle events during the startup of sources. Having an explicit state for + // startup enables handling such lifecycle events appropriately. + // Allowed transitions are: buffering, terminating + starting = sessionState("starting") + + // The session is buffering events until a mesh configuration arrives. + // Allowed transitions are: processing, terminating + buffering = sessionState("buffering") + + // The session is in full execution mode, processing events. + // Allowed transitions are: terminating + processing = sessionState("processing") + + // The session is terminating. It will ignore all incoming events, while processors & sources are being stopped. + // Allowed transitions are: inactive + terminating = sessionState("terminating") +) + +// session represents a config processing session. It is a stateful controller type whose main responsibility is to manage +// state transitions and react to the events that impact lifecycle. +// +// A session starts with an external request (through the start() method, called by Runtime) which puts the session into +// the "starting" state. During this phase, the Sources are started, and the events from Sources start to come in. Once +// all sources are started, the session transitions to the "buffering" state. +// +// In "buffering" (and also in "starting") state, the incoming events are selectively buffered, until a usable mesh +// configuration is received. Once received, the buffered events start getting processed. +// +// The main difference between buffering & starting states is how life-cycle events (such as a stop() call from +// Runtime, or a received reset event) are handled. In starting state, the cleanup is performed right within the context +// of the startup call. +// +// Once a mesh config is received, the state transitions to the "processing" state, where the processor is initialized +// and buffered events starts getting processed. This is a steady-state, and persists until a life-cycle event occurs +// (i.e. an explicit stop call from Runtime, a Reset event, or a change in mesh config). Once such a life-cycle event +// occurs, the state transitions to "terminating", and teardown operations take place (i.e. stop Processor, stop +// Sources etc.). +type session struct { // nolint:maligned + mu sync.Mutex + + id int32 + options RuntimeOptions + buffer *event.Buffer + + state sessionState + + // mesh config state + meshCfg *v1alpha1.MeshConfig + meshSynced bool + + processor event.Processor + doneCh chan struct{} +} + +// newSession creates a new config processing session state. It returns the session, as well as a channel +// that will be closed upon termination of the session. +func newSession(id int32, o RuntimeOptions) (*session, chan struct{}) { + s := &session{ + id: id, + options: o, + buffer: event.NewBuffer(), + state: inactive, + doneCh: make(chan struct{}), + } + + return s, s.doneCh +} + +// start the session. This must be called when state == inactive. +func (s *session) start() { + s.mu.Lock() + defer s.mu.Unlock() + if s.state != inactive { + panic(fmt.Sprintf("invalid state: %s (expecting inactive)", s.state)) + } + s.transitionTo(starting) + + go s.startSources() +} + +func (s *session) startSources() { + scope.Processing.Debug("session starting sources...") + // start source after relinquishing lock. This avoids deadlocks. + s.options.Source.Start() + + scope.Processing.Debugf("session source start complete") + + // check the state again. During startup we might have received mesh config, or got signaled for stop. + var terminate bool + s.mu.Lock() + switch s.state { + case starting: + // This is the expected state. Depending on whether we received mesh config or not we can transition to the + // buffering, or processing states. + s.transitionTo(buffering) + if s.meshSynced { + s.startProcessing() + } + + case terminating: + // stop was called during startup. There is nothing we can do, simply exit. + terminate = true + + default: + panic(fmt.Sprintf("session.start: unexpected state during session startup: %v", s.state)) + } + s.mu.Unlock() + + if terminate { + s.terminate() + } +} + +func (s *session) stop() { + scope.Processing.Debug("session.stop()") + + var terminate bool + s.mu.Lock() + switch s.state { + case starting: + // set the state to terminating and let the startup code complete startup steps and deal with termination. + s.transitionTo(terminating) + + case buffering, processing: + s.transitionTo(terminating) + terminate = true + + default: + panic(fmt.Errorf("session.stop: Invalid state: %v", s.state)) + } + s.mu.Unlock() + if terminate { + s.terminate() + } +} + +func (s *session) terminate() { + // must be called outside lock. + s.mu.Lock() + if s.state != terminating { + panic(fmt.Sprintf("invalid state: %s (expecting terminating)", s.state)) + } + s.mu.Unlock() + + scope.Processing.Debug("session.terminate: stopping buffer...") + s.buffer.Stop() + scope.Processing.Debug("session.terminate: stopping processor...") + if s.processor != nil { + s.processor.Stop() + } + scope.Processing.Debug("session.terminate: stopping sources...") + s.options.Source.Stop() + + scope.Processing.Debug("session.terminate: signalling session termination...") + s.mu.Lock() + if s.doneCh != nil { + close(s.doneCh) + s.doneCh = nil + } + s.transitionTo(inactive) + s.mu.Unlock() +} + +func (s *session) startProcessing() { + // must be called under lock. + if s.state != buffering { + panic(fmt.Sprintf("invalid state: %s (expecting buffering)", s.state)) + } + + // immediately transition to the processing state + o := ProcessorOptions{ + DomainSuffix: s.options.DomainSuffix, + MeshConfig: proto.Clone(s.meshCfg).(*v1alpha1.MeshConfig), + } + s.processor = s.options.ProcessorProvider(o) + s.buffer.Dispatch(s.processor) + s.processor.Start() + s.transitionTo(processing) + go s.buffer.Process() +} + +func (s *session) handle(e event.Event) { + // Check the event kind first to avoid excessive locking. + if e.Kind != event.Reset { + s.buffer.Handle(e) + + if e.Source == meshcfg.IstioMeshconfig { + s.handleMeshEvent(e) + } + return + } + + // Handle the reset event + s.mu.Lock() + switch s.state { + case inactive, terminating: + // nothing to do + + case starting: + // set the state to terminating and let the startup code complete startup steps and deal with termination. + s.transitionTo(terminating) + + case buffering, processing: + s.transitionTo(terminating) + go s.terminate() + + default: + panic(fmt.Errorf("session.handle: invalid session state: %v", s.state)) + } + s.mu.Unlock() +} + +func (s *session) handleMeshEvent(e event.Event) { + s.mu.Lock() + + switch s.state { + case inactive, terminating: + // nothing to do + + case processing: + scope.Processing.Infof("session.handleMeshEvent: Mesh event received during running state, restarting: %+v", e) + s.transitionTo(terminating) + go s.terminate() + + case starting: + s.applyMeshEvent(e) + + case buffering: + s.applyMeshEvent(e) + if s.meshSynced { + s.startProcessing() + } + + default: + panic(fmt.Errorf("session.handleMeshEvent: mesh event in unsupported state '%v': %+v", s.state, e)) + } + + s.mu.Unlock() +} + +func (s *session) applyMeshEvent(e event.Event) { + // Apply the meshconfig changes directly to the internal state. + switch e.Kind { + case event.Added, event.Updated: + scope.Processing.Infof("session.handleMeshEvent: received an add/update mesh config event: %v", e) + s.meshCfg = proto.Clone(e.Entry.Item).(*v1alpha1.MeshConfig) + case event.Deleted: + scope.Processing.Infof("session.handleMeshEvent: received a delete mesh config event: %v", e) + s.meshCfg = meshcfg.Default() + case event.FullSync: + scope.Processing.Infof("session.applyMeshEvent meshSynced: %v => %v", s.meshSynced, true) + s.meshSynced = true + + // reset case is already handled by the time call arrives here. + + default: + panic(fmt.Errorf("session.handleMeshEvent: unrecognized event kind: %v", e.Kind)) + } +} + +func (s *session) transitionTo(st sessionState) { + scope.Processing.Infof("session[%d] %q => %q", s.id, s.state, st) + s.state = st +} diff --git a/galley/pkg/config/processing/snapshotter/distributor.go b/galley/pkg/config/processing/snapshotter/distributor.go new file mode 100644 index 000000000000..236796109978 --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/distributor.go @@ -0,0 +1,66 @@ +// 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 snapshotter + +import ( + "sync" + + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/pkg/mcp/snapshot" +) + +// Distributor interface abstracts the snapshotImpl distribution mechanism. Typically, this is implemented by the MCP layer. +type Distributor interface { + SetSnapshot(name string, s snapshot.Snapshot) +} + +// InMemoryDistributor is an in-memory Distributor implementation. +type InMemoryDistributor struct { + snapshotsLock sync.Mutex + snapshots map[string]snapshot.Snapshot +} + +var _ Distributor = &InMemoryDistributor{} + +// NewInMemoryDistributor returns a new instance of InMemoryDistributor +func NewInMemoryDistributor() *InMemoryDistributor { + return &InMemoryDistributor{ + snapshots: make(map[string]snapshot.Snapshot), + } +} + +// SetSnapshot is an implementation of Distributor.SetSnapshot +func (d *InMemoryDistributor) SetSnapshot(name string, s snapshot.Snapshot) { + d.snapshotsLock.Lock() + defer d.snapshotsLock.Unlock() + + scope.Processing.Debugf("InmemoryDistributor.SetSnapshot: %s: %v", name, s) + d.snapshots[name] = s +} + +// GetSnapshot get the snapshotImpl of the specified name +func (d *InMemoryDistributor) GetSnapshot(name string) snapshot.Snapshot { + d.snapshotsLock.Lock() + defer d.snapshotsLock.Unlock() + if s, ok := d.snapshots[name]; ok { + return s + } + return nil +} + +// NumSnapshots returns the current number of snapshots. +func (d *InMemoryDistributor) NumSnapshots() int { + return len(d.snapshots) +} diff --git a/galley/pkg/config/processing/snapshotter/distributor_test.go b/galley/pkg/config/processing/snapshotter/distributor_test.go new file mode 100644 index 000000000000..907895684809 --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/distributor_test.go @@ -0,0 +1,70 @@ +// 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 snapshotter + +import ( + "testing" + + sn "istio.io/istio/pkg/mcp/snapshot" +) + +func TestDistributor_SetSnapshot(t *testing.T) { + d := NewInMemoryDistributor() + + b := sn.NewInMemoryBuilder() + s := b.Build() + d.SetSnapshot("foo", s) + if _, ok := d.snapshots["foo"]; !ok { + t.Fatal("The snapshotImpl should have been set") + } +} + +func TestDistributor_GetSnapshot(t *testing.T) { + d := NewInMemoryDistributor() + + b := sn.NewInMemoryBuilder() + s := b.Build() + d.SetSnapshot("foo", s) + + sn := d.GetSnapshot("foo") + if sn != s { + t.Fatal("The snapshots should have been the same") + } +} + +func TestDistributor_GetSnapshot_Unknown(t *testing.T) { + d := NewInMemoryDistributor() + + b := sn.NewInMemoryBuilder() + s := b.Build() + d.SetSnapshot("foo", s) + + sn := d.GetSnapshot("bar") + if sn != nil { + t.Fatal("The snapshots should have been nil") + } +} + +func TestDistributor_NumSnapshots(t *testing.T) { + d := NewInMemoryDistributor() + + b := sn.NewInMemoryBuilder() + s := b.Build() + d.SetSnapshot("foo", s) + + if d.NumSnapshots() != 1 { + t.Fatal("The snapshots should have been 1") + } +} diff --git a/galley/pkg/config/processing/snapshotter/snapshot.go b/galley/pkg/config/processing/snapshotter/snapshot.go new file mode 100644 index 000000000000..0724abdcc7be --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/snapshot.go @@ -0,0 +1,80 @@ +// 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 snapshotter + +import ( + "fmt" + "strconv" + "strings" + + mcp "istio.io/api/mcp/v1alpha1" + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/pkg/mcp/snapshot" +) + +type snapshotImpl struct { + set *collection.Set +} + +var _ snapshot.Snapshot = &snapshotImpl{} + +// Resources implements snapshotImpl.Snapshot +func (s *snapshotImpl) Resources(col string) []*mcp.Resource { + c := s.set.Collection(collection.NewName(col)) + + if c == nil { + return nil + } + + result := make([]*mcp.Resource, 0, c.Size()) + + s.set.Collection(collection.NewName(col)).ForEach(func(e *resource.Entry) { + // TODO: We should add (LRU based) caching of serialized content here. + r, err := resource.Serialize(e) + if err != nil { + scope.Processing.Errorf("Unable to serialize resource.Entry: %v", err) + } else { + result = append(result, r) + } + }) + + return result +} + +// Version implements snapshotImpl.Snapshot +func (s *snapshotImpl) Version(col string) string { + coll := s.set.Collection(collection.NewName(col)) + if coll == nil { + return "" + } + g := coll.Generation() + return col + "/" + strconv.FormatInt(g, 10) +} + +// String implements io.Stringer +func (s *snapshotImpl) String() string { + var b strings.Builder + + for i, n := range s.set.Names() { + b.WriteString(fmt.Sprintf("[%d] %s (@%s)\n", i, n.String(), s.Version(n.String()))) + for j, e := range s.Resources(n.String()) { + b.WriteString(fmt.Sprintf(" [%d] %s\n", j, e.Metadata.Name)) + } + } + + return b.String() +} diff --git a/galley/pkg/config/processing/snapshotter/snapshot_test.go b/galley/pkg/config/processing/snapshotter/snapshot_test.go new file mode 100644 index 000000000000..7f7cc2111fed --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/snapshot_test.go @@ -0,0 +1,72 @@ +// 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 snapshotter + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/testing/data" +) + +func TestSnapshot_Basics(t *testing.T) { + g := NewGomegaWithT(t) + + set := collection.NewSet([]collection.Name{data.Collection1}) + set.Collection(data.Collection1).Set(data.EntryN1I1V1) + sn := &snapshotImpl{set: set} + + resources := sn.Resources(data.Collection1.String()) + g.Expect(resources).To(HaveLen(1)) + + r, err := resource.Deserialize(resources[0]) + g.Expect(err).To(BeNil()) + g.Expect(r).To(Equal(data.EntryN1I1V1)) + + v := sn.Version(data.Collection1.String()) + g.Expect(v).To(Equal("collection1/1")) + + expected := `[0] collection1 (@collection1/1) + [0] n1/i1 +` + g.Expect(sn.String()).To(Equal(expected)) +} + +func TestSnapshot_SerializeError(t *testing.T) { + g := NewGomegaWithT(t) + + set := collection.NewSet([]collection.Name{data.Collection1}) + e := data.Event1Col1AddItem1.Entry.Clone() + e.Item = nil + set.Collection(data.Collection1).Set(e) + sn := &snapshotImpl{set: set} + + resources := sn.Resources(data.Collection1.String()) + g.Expect(resources).To(HaveLen(0)) +} + +func TestSnapshot_WrongCollection(t *testing.T) { + g := NewGomegaWithT(t) + + set := collection.NewSet([]collection.Name{data.Collection1}) + set.Collection(data.Collection1).Set(data.Event1Col1AddItem1.Entry) + sn := &snapshotImpl{set: set} + + g.Expect(sn.Version("foo")).To(Equal("")) + g.Expect(sn.Resources("foo")).To(BeEmpty()) +} diff --git a/galley/pkg/config/processing/snapshotter/snapshotoptions.go b/galley/pkg/config/processing/snapshotter/snapshotoptions.go new file mode 100644 index 000000000000..19f97e6faaa7 --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/snapshotoptions.go @@ -0,0 +1,34 @@ +// 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 snapshotter + +import ( + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/processing/snapshotter/strategy" +) + +// SnapshotOptions is settings for a single snapshotImpl target. +type SnapshotOptions struct { + Distributor Distributor + + // The group name for the snapshotImpl. + Group string + + // The publishing strategy for the snapshotImpl. + Strategy strategy.Instance + + // The set of collections to Snapshot. + Collections []collection.Name +} diff --git a/galley/pkg/config/processing/snapshotter/snapshotter.go b/galley/pkg/config/processing/snapshotter/snapshotter.go new file mode 100644 index 000000000000..febfe25219ac --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/snapshotter.go @@ -0,0 +1,183 @@ +// 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 snapshotter + +import ( + "fmt" + "time" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processing/snapshotter/strategy" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/runtime/monitoring" +) + +// Snapshotter is a processor that handles input events and creates snapshotImpl collections. +type Snapshotter struct { + accumulators map[collection.Name]*accumulator + selector event.Router + xforms []event.Transformer + settings []SnapshotOptions + + // lastEventTime records the last time an event was received. + lastEventTime time.Time + + // pendingEvents counts the number of events awaiting publishing. + pendingEvents int64 + + // lastSnapshotTime records the last time a snapshotImpl was published. + lastSnapshotTime time.Time +} + +var _ event.Processor = &Snapshotter{} + +// HandlerFn handles generated snapshots +type HandlerFn func(*collection.Set) + +type accumulator struct { + reqSyncCount int + syncCount int + collection *collection.Instance + strategies []strategy.Instance +} + +// Handle implements event.Handler +func (a *accumulator) Handle(e event.Event) { + switch e.Kind { + case event.Added, event.Updated: + a.collection.Set(e.Entry) + monitoring.RecordStateTypeCount(e.Source.String(), a.collection.Size()) + case event.Deleted: + a.collection.Remove(e.Entry.Metadata.Name) + monitoring.RecordStateTypeCount(e.Source.String(), a.collection.Size()) + case event.FullSync: + a.syncCount++ + default: + panic(fmt.Errorf("accumulator.Handle: unhandled event type: %v", e.Kind)) + } + + if a.syncCount >= a.reqSyncCount { + for _, s := range a.strategies { + s.OnChange() + } + } +} + +func (a *accumulator) reset() { + a.syncCount = 0 + a.collection.Clear() +} + +// NewSnapshotter returns a new Snapshotter. +func NewSnapshotter(xforms []event.Transformer, settings []SnapshotOptions) (*Snapshotter, error) { + s := &Snapshotter{ + accumulators: make(map[collection.Name]*accumulator), + selector: event.NewRouter(), + xforms: xforms, + settings: settings, + lastEventTime: time.Now(), + } + + for _, xform := range xforms { + for _, i := range xform.Inputs() { + s.selector = event.AddToRouter(s.selector, i, xform) + } + + for _, o := range xform.Outputs() { + a, found := s.accumulators[o] + if !found { + a = &accumulator{ + collection: collection.New(o), + } + s.accumulators[o] = a + } + a.reqSyncCount++ + xform.DispatchFor(o, a) + } + } + + for _, o := range settings { + for _, c := range o.Collections { + a := s.accumulators[c] + if a == nil { + return nil, fmt.Errorf("unrecognized collection in SnapshotOptions: %v (Group: %s)", c, o.Group) + } + + a.strategies = append(a.strategies, o.Strategy) + } + } + + return s, nil +} + +// Start implements Processor +func (s *Snapshotter) Start() { + for _, x := range s.xforms { + x.Start() + } + + for _, o := range s.settings { + // Capture the iteration variable in a local + opt := o + o.Strategy.Start(func() { + s.publish(opt) + }) + } +} + +func (s *Snapshotter) publish(o SnapshotOptions) { + var collections []*collection.Instance + + for _, n := range o.Collections { + col := s.accumulators[n].collection.Clone() + collections = append(collections, col) + } + + set := collection.NewSetFromCollections(collections) + sn := &snapshotImpl{set: set} + + now := time.Now() + monitoring.RecordProcessorSnapshotPublished(s.pendingEvents, now.Sub(s.lastSnapshotTime)) + s.lastSnapshotTime = now + s.pendingEvents = 0 + scope.Processing.Infoa("Publishing snapshot for group: ", o.Group) + scope.Processing.Debuga(sn) + o.Distributor.SetSnapshot(o.Group, sn) +} + +// Stop implements Processor +func (s *Snapshotter) Stop() { + for _, o := range s.settings { + o.Strategy.Stop() + } + + for _, x := range s.xforms { + x.Stop() + } + + for _, a := range s.accumulators { + a.reset() + } +} + +// Handle implements Processor +func (s *Snapshotter) Handle(e event.Event) { + now := time.Now() + monitoring.RecordProcessorEventProcessed(now.Sub(s.lastEventTime)) + s.lastEventTime = now + s.pendingEvents++ + s.selector.Handle(e) +} diff --git a/galley/pkg/config/processing/snapshotter/snapshotter_test.go b/galley/pkg/config/processing/snapshotter/snapshotter_test.go new file mode 100644 index 000000000000..35c34c1e45eb --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/snapshotter_test.go @@ -0,0 +1,117 @@ +// 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 snapshotter + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processing/snapshotter/strategy" + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestSnapshotter_Basic(t *testing.T) { + g := NewGomegaWithT(t) + + tr := fixtures.NewTransformer( + []collection.Name{data.Collection1}, + []collection.Name{data.Collection2}, + func(tr *fixtures.Transformer, e event.Event) { + switch e.Kind { + case event.Reset: + tr.Publish(data.Collection2, e) + default: + e.Source = data.Collection2 + tr.Publish(data.Collection2, e) + } + }) + + d := NewInMemoryDistributor() + + options := []SnapshotOptions{ + { + Collections: []collection.Name{data.Collection2}, + Strategy: strategy.NewImmediate(), + Group: "default", + Distributor: d, + }, + } + + s, err := NewSnapshotter([]event.Transformer{tr}, options) + g.Expect(err).To(BeNil()) + s.Start() + + g.Expect(tr.Started).To(BeTrue()) + + s.Stop() + g.Expect(tr.Started).To(BeFalse()) + + s.Start() + + sn := d.GetSnapshot("default") + g.Expect(sn).To(BeNil()) + + s.Handle(data.Event1Col1AddItem1) + s.Handle(data.Event1Col1Synced) + + sn = d.GetSnapshot("default") + g.Expect(sn).NotTo(BeNil()) + g.Expect(sn.Version(data.Collection2.String())).To(Equal("collection2/2")) + g.Expect(sn.Resources(data.Collection2.String())).To(HaveLen(1)) + + s.Handle(data.Event1Col1UpdateItem1) + s.Handle(data.Event1Col1DeleteItem1) + s.Handle(data.Event1Col1Synced) + + sn = d.GetSnapshot("default") + g.Expect(sn).NotTo(BeNil()) + g.Expect(sn.Version(data.Collection2.String())).To(Equal("collection2/4")) + g.Expect(sn.Resources(data.Collection2.String())).To(HaveLen(0)) +} + +func TestSnapshotter_SnapshotMismatch(t *testing.T) { + g := NewGomegaWithT(t) + + tr := fixtures.NewTransformer( + []collection.Name{data.Collection1}, + []collection.Name{data.Collection2}, + func(tr *fixtures.Transformer, e event.Event) { + switch e.Kind { + case event.Reset: + tr.Publish(data.Collection2, e) + default: + e.Source = data.Collection2 + tr.Publish(data.Collection2, e) + } + }) + + d := NewInMemoryDistributor() + + options := []SnapshotOptions{ + { + Collections: []collection.Name{data.Collection3}, + Strategy: strategy.NewImmediate(), + Group: "default", + Distributor: d, + }, + } + + _, err := NewSnapshotter([]event.Transformer{tr}, options) + g.Expect(err).NotTo(BeNil()) +} diff --git a/galley/pkg/config/processing/snapshotter/strategy/create.go b/galley/pkg/config/processing/snapshotter/strategy/create.go new file mode 100644 index 000000000000..c1b2ddb6f490 --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/strategy/create.go @@ -0,0 +1,34 @@ +// 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 strategy + +import "fmt" + +const ( + debounce = "debounce" + immediate = "immediate" +) + +// Create a strategy with the given name. +func Create(name string) (Instance, error) { + switch name { + case debounce: + return NewDebounceWithDefaults(), nil + case immediate: + return NewImmediate(), nil + default: + return nil, fmt.Errorf("unknown strategy: %q", name) + } +} diff --git a/galley/pkg/config/processing/snapshotter/strategy/create_test.go b/galley/pkg/config/processing/snapshotter/strategy/create_test.go new file mode 100644 index 000000000000..0736b1848736 --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/strategy/create_test.go @@ -0,0 +1,47 @@ +// 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 strategy + +import ( + "reflect" + "testing" + + . "github.com/onsi/gomega" +) + +func TestCreate_Immediate(t *testing.T) { + g := NewGomegaWithT(t) + + s, err := Create(immediate) + g.Expect(err).To(BeNil()) + + g.Expect(reflect.TypeOf(s)).To(Equal(reflect.TypeOf(&Immediate{}))) +} + +func TestCreate_Debounce(t *testing.T) { + g := NewGomegaWithT(t) + + s, err := Create(debounce) + g.Expect(err).To(BeNil()) + + g.Expect(reflect.TypeOf(s)).To(Equal(reflect.TypeOf(&Debounce{}))) +} + +func TestCreate_Unknown(t *testing.T) { + g := NewGomegaWithT(t) + + _, err := Create("foo") + g.Expect(err).NotTo(BeNil()) +} diff --git a/galley/pkg/config/processing/snapshotter/strategy/debounce.go b/galley/pkg/config/processing/snapshotter/strategy/debounce.go new file mode 100644 index 000000000000..cdd3724264e5 --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/strategy/debounce.go @@ -0,0 +1,192 @@ +// 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 strategy + +import ( + "sync" + "time" + + // TODO: Referencing this package directly, as duplicating it would cause conflicts. + "istio.io/istio/galley/pkg/runtime/monitoring" +) + +const ( + // Maximum wait time before deciding to publish the events. + defaultMaxWaitDuration = time.Second * 2 + + // Minimum time distance between two events for deciding on the quiesce point. If the time delay + // between two events is larger than this, then we can deduce that we hit a quiesce point. + defaultQuiesceDuration = time.Second / 2 +) + +// Debounce is a heuristic model for deciding when to publish snapshots. It tries to detect +// quiesce points for events with a total bounded wait time. +type Debounce struct { + mu sync.Mutex + + maxWaitDuration time.Duration + quiesceDuration time.Duration + + changeCh chan struct{} + stopCh chan struct{} + doneCh chan struct{} +} + +var _ Instance = &Debounce{} + +// NewDebounceWithDefaults creates a new debounce strategy with default values. +func NewDebounceWithDefaults() *Debounce { + return NewDebounce(defaultMaxWaitDuration, defaultQuiesceDuration) +} + +// NewDebounce creates a new debounce strategy with the given values. +func NewDebounce(maxWaitDuration, quiesceDuration time.Duration) *Debounce { + return &Debounce{ + maxWaitDuration: maxWaitDuration, + quiesceDuration: quiesceDuration, + changeCh: make(chan struct{}, 1), + } +} + +// Start implements Instance +func (d *Debounce) Start(fn OnSnapshotFn) { + d.mu.Lock() + defer d.mu.Unlock() + + if d.stopCh != nil { + scope.Debug("Debounce.Start: already started") + return + } + d.stopCh = make(chan struct{}) + d.doneCh = make(chan struct{}) + + // Drain the changeCh, to avoid events from a previous incarnation. + drainCh(d.changeCh) + + go d.run(d.stopCh, d.doneCh, fn) +} + +// Stop implements Instance +func (d *Debounce) Stop() { + d.mu.Lock() + + if d.stopCh != nil { + scope.Debug("Debounce.Stop: stopping") + close(d.stopCh) + d.stopCh = nil + } else { + scope.Debug("Debounce.Stop: already stopped") + } + d.mu.Unlock() + + <-d.doneCh +} + +func (d *Debounce) run(stopCh, doneCh chan struct{}, fn OnSnapshotFn) { + var maxDurationTimer *time.Timer + var quiesceTimer *time.Timer + +mainloop: + for { + select { + case <-stopCh: + scope.Debug("Debounce.run: stopping") + break mainloop + + case <-d.changeCh: + scope.Debug("Debounce.run: change") + monitoring.RecordStrategyOnChange() + // fallthrough to start the timer. + } + + maxDurationTimer = time.NewTimer(d.maxWaitDuration) + quiesceTimer = time.NewTimer(d.quiesceDuration) + + loop: + for { + select { + case <-stopCh: + scope.Debug("Debounce.run: stopping") + break mainloop + + case <-d.changeCh: + scope.Debug("Debounce.run: change") + monitoring.RecordStrategyOnChange() + + quiesceTimer.Stop() + drainTimeCh(quiesceTimer.C) + quiesceTimer.Reset(d.quiesceDuration) + monitoring.RecordOnTimer(false, false, true) + + case <-quiesceTimer.C: + scope.Debug("Debounce.run: quiesce timer") + monitoring.RecordOnTimer(false, true, false) + break loop + + case <-maxDurationTimer.C: + scope.Debug("Debounce.run: maxDuration timer") + monitoring.RecordOnTimer(true, false, false) + break loop + } + } + + quiesceTimer.Stop() + drainTimeCh(quiesceTimer.C) + maxDurationTimer.Stop() + drainTimeCh(maxDurationTimer.C) + scope.Debug("Debounce.run: calling callback...") + fn() + } + + close(doneCh) +} + +// OnChange implements Instance +func (d *Debounce) OnChange() { + select { + case d.changeCh <- struct{}{}: + default: + } +} + +func drainCh(ch chan struct{}) { +loop: + for { + select { + case _, ok := <-ch: + if !ok { + break loop + } + default: + break loop + + } + } +} + +func drainTimeCh(ch <-chan time.Time) { +loop: + for { + select { + case _, ok := <-ch: + if !ok { + break loop + } + default: + break loop + + } + } +} diff --git a/galley/pkg/config/processing/snapshotter/strategy/debounce_test.go b/galley/pkg/config/processing/snapshotter/strategy/debounce_test.go new file mode 100644 index 000000000000..65edbc3d0d12 --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/strategy/debounce_test.go @@ -0,0 +1,221 @@ +// 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 strategy + +import ( + "testing" + "time" + + . "github.com/onsi/gomega" +) + +func TestStrategy_StartStop(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewDebounce(time.Minute, time.Millisecond*500) + + called := false + s.Start(func() { + called = true + }) + s.Stop() + + s.Start(func() { + called = true + }) + s.Stop() + + g.Expect(called).To(BeFalse()) +} + +func TestStrategy_DoubleStart(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewDebounce(time.Minute, time.Millisecond*500) + + called := false + s.Start(func() { + called = true + }) + s.Start(func() { + called = true + }) + s.Stop() + + g.Expect(called).To(BeFalse()) +} + +func TestStrategy_DoubleStop(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewDebounce(time.Hour, time.Millisecond*500) + + called := false + s.Start(func() { + called = true + }) + s.Stop() + s.Stop() + + g.Expect(called).To(BeFalse()) +} + +func TestStrategy_ChangeBeforeStart(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewDebounce(time.Millisecond*100, time.Millisecond) + s.OnChange() + s.OnChange() + + called := false + s.Start(func() { + called = true + }) + defer s.Stop() + + time.Sleep(time.Millisecond * 500) + g.Expect(called).To(BeFalse()) +} + +func TestStrategy_FireEvent(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewDebounce(time.Millisecond*200, time.Millisecond*100) + + called := false + s.Start(func() { + called = true + }) + defer s.Stop() + + s.OnChange() + + time.Sleep(time.Millisecond * 210) + g.Expect(called).To(BeTrue()) +} + +func TestStrategy_StopBeforeMaxTimeout(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewDebounce(time.Second*5, time.Second) + + called := false + s.Start(func() { + called = true + }) + + s.OnChange() + time.Sleep(time.Millisecond * 200) + s.Stop() + + g.Expect(called).To(BeFalse()) +} + +func TestStrategy_ChangeBeforeQuiesce(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewDebounce(time.Second*5, time.Second) + + called := false + s.Start(func() { + called = true + }) + + s.OnChange() + time.Sleep(time.Millisecond * 10) + s.OnChange() + + s.Stop() + + g.Expect(called).To(BeFalse()) +} + +func TestStrategy_MaxTimeout(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewDebounce(time.Second, time.Millisecond*500) + + called := false + s.Start(func() { + called = true + }) + + for i := 0; i < 120; i++ { + s.OnChange() + time.Sleep(time.Millisecond * 10) + } + s.Stop() + + g.Expect(called).To(BeTrue()) +} + +func TestStrategy_NewWithDefaults(t *testing.T) { + g := NewGomegaWithT(t) + s := NewDebounceWithDefaults() + g.Expect(s.quiesceDuration).To(Equal(defaultQuiesceDuration)) + g.Expect(s.maxWaitDuration).To(Equal(defaultMaxWaitDuration)) +} + +func TestDrainCh(t *testing.T) { + ch := make(chan struct{}, 1) + ch <- struct{}{} + + drainCh(ch) + select { + case <-ch: + t.Fail() + default: + } +} + +func TestDrainCh_Empty(t *testing.T) { + ch := make(chan struct{}, 1) + drainCh(ch) + // Does not block or crash +} + +func TestDrainCh_Closed(t *testing.T) { + ch := make(chan struct{}, 1) + ch <- struct{}{} + close(ch) + drainCh(ch) + // Does not block or crash +} + +func TestDrainTimeCh(t *testing.T) { + ch := make(chan time.Time, 1) + ch <- time.Now() + + drainTimeCh(ch) + select { + case <-ch: + t.Fail() + default: + } +} + +func TestDrainTimeCh_Empty(t *testing.T) { + ch := make(chan time.Time, 1) + drainTimeCh(ch) + // Does not block or crash +} + +func TestDrainTimeCh_Closed(t *testing.T) { + ch := make(chan time.Time, 1) + ch <- time.Now() + close(ch) + drainTimeCh(ch) + // Does not block or crash +} diff --git a/galley/pkg/config/processing/snapshotter/strategy/immediate.go b/galley/pkg/config/processing/snapshotter/strategy/immediate.go new file mode 100644 index 000000000000..db1200db6854 --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/strategy/immediate.go @@ -0,0 +1,57 @@ +// 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 strategy + +import ( + "sync/atomic" + + "istio.io/istio/galley/pkg/runtime/monitoring" +) + +// Immediate is a snapshotting strategy for creating snapshots immediately, as events arrive. +type Immediate struct { + handler atomic.Value +} + +var sentinelOnSnapshot OnSnapshotFn = func() {} + +var _ Instance = &Immediate{} + +// NewImmediate returns a new Immediate. +func NewImmediate() *Immediate { + i := &Immediate{} + i.handler.Store(sentinelOnSnapshot) + + return i +} + +// Start implements processing.Debounce +func (i *Immediate) Start(handler OnSnapshotFn) { + i.handler.Store(handler) +} + +// Stop implements processing.Debounce +func (i *Immediate) Stop() { + i.handler.Store(sentinelOnSnapshot) +} + +// OnChange implements processing.Debounce +func (i *Immediate) OnChange() { + scope.Debug("Immediate.OnChange") + fn := i.handler.Load().(OnSnapshotFn) + + monitoring.RecordStrategyOnChange() + fn() +} diff --git a/galley/pkg/config/processing/snapshotter/strategy/immediate_test.go b/galley/pkg/config/processing/snapshotter/strategy/immediate_test.go new file mode 100644 index 000000000000..309cb954b07f --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/strategy/immediate_test.go @@ -0,0 +1,40 @@ +// 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 strategy + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestNewImmediate(t *testing.T) { + g := NewGomegaWithT(t) + s := NewImmediate() + + var changed bool + onChange := func() { + changed = true + } + g.Expect(changed).To(BeFalse()) + s.Start(onChange) + s.OnChange() + g.Expect(changed).To(BeTrue()) + + changed = false + s.Stop() + s.OnChange() + g.Expect(changed).To(BeFalse()) +} diff --git a/galley/pkg/config/processing/snapshotter/strategy/instance.go b/galley/pkg/config/processing/snapshotter/strategy/instance.go new file mode 100644 index 000000000000..7f5f6bfa593b --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/strategy/instance.go @@ -0,0 +1,26 @@ +// 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 strategy + +// Instance is a strategy for publishing snapshots. It listens to to the given snapshotters +// and creates snapshots that get published to the given Distributor. +type Instance interface { + Start(fn OnSnapshotFn) + OnChange() + Stop() +} + +// OnSnapshotFn is called to indicate that the snapshot +type OnSnapshotFn func() diff --git a/galley/pkg/config/processing/snapshotter/strategy/scope.go b/galley/pkg/config/processing/snapshotter/strategy/scope.go new file mode 100644 index 000000000000..a3f935222bd0 --- /dev/null +++ b/galley/pkg/config/processing/snapshotter/strategy/scope.go @@ -0,0 +1,21 @@ +// 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 strategy + +import ( + "istio.io/pkg/log" +) + +var scope = log.RegisterScope("processing", "", 0) diff --git a/galley/pkg/config/processor/build.go b/galley/pkg/config/processor/build.go new file mode 100644 index 000000000000..0632875a7887 --- /dev/null +++ b/galley/pkg/config/processor/build.go @@ -0,0 +1,88 @@ +// 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 processor + +import ( + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processing" + "istio.io/istio/galley/pkg/config/processing/snapshotter" + "istio.io/istio/galley/pkg/config/processing/snapshotter/strategy" + "istio.io/istio/galley/pkg/config/processor/transforms/authpolicy" + "istio.io/istio/galley/pkg/config/processor/transforms/direct" + "istio.io/istio/galley/pkg/config/processor/transforms/ingress" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry" + "istio.io/istio/galley/pkg/config/schema" +) + +// Initialize a processing runtime for Galley. +func Initialize( + m *schema.Metadata, + domainSuffix string, + source event.Source, + distributor snapshotter.Distributor) (*processing.Runtime, error) { + + var options []snapshotter.SnapshotOptions + for _, s := range m.Snapshots() { + str, err := strategy.Create(s.Strategy) + if err != nil { + return nil, err + } + + opt := snapshotter.SnapshotOptions{ + Group: s.Name, + Distributor: distributor, + Collections: s.Collections, + Strategy: str, + } + options = append(options, opt) + } + + // TODO: Add a precondition test here to ensure the panic below will not fire during runtime. + + provider := func(o processing.ProcessorOptions) event.Processor { + xforms := createTransforms(o, m) + s, err := snapshotter.NewSnapshotter(xforms, options) + if err != nil { + panic(err) + } + return s + } + + rtOpt := processing.RuntimeOptions{ + ProcessorProvider: provider, + DomainSuffix: domainSuffix, + Source: source, + } + + return processing.NewRuntime(rtOpt), nil +} + +func createTransforms(o processing.ProcessorOptions, m *schema.Metadata) []event.Transformer { + var xforms []event.Transformer + + xf := direct.Create(m.DirectTransform().Mapping()) + xforms = append(xforms, xf...) + + xf = ingress.Create(o) + xforms = append(xforms, xf...) + + xf = authpolicy.Create() + xforms = append(xforms, xf...) + + xf = serviceentry.Create(o) + xforms = append(xforms, xf...) + + return xforms +} diff --git a/galley/pkg/config/processor/build_test.go b/galley/pkg/config/processor/build_test.go new file mode 100644 index 000000000000..1181a5b26624 --- /dev/null +++ b/galley/pkg/config/processor/build_test.go @@ -0,0 +1,71 @@ +// 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 processor + +import ( + "testing" + "time" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/meshcfg" + "istio.io/istio/galley/pkg/config/processing/snapshotter" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/source/kube/inmemory" +) + +const yml = ` +apiVersion: networking.istio.io/v1alpha3 +kind: Gateway +metadata: + name: helloworld-gateway +spec: + selector: + istio: ingressgateway # use istio default controller + servers: + - port: + number: 80 + name: http + protocol: HTTP + hosts: + - "*" + +` + +func TestProcessor(t *testing.T) { + g := NewGomegaWithT(t) + + meshSrc := meshcfg.NewInmemory() + src := inmemory.NewKubeSource(metadata.MustGet().KubeSource().Resources()) + srcs := []event.Source{ + meshSrc, + src, + } + + meshSrc.Set(meshcfg.Default()) + distributor := snapshotter.NewInMemoryDistributor() + + rt, err := Initialize(metadata.MustGet(), "svc.local", event.CombineSources(srcs...), distributor) + g.Expect(err).To(BeNil()) + + rt.Start() + + err = src.ApplyContent("foo", yml) + g.Expect(err).To(BeNil()) + + time.Sleep(time.Second) + _ = distributor.GetSnapshot("default") +} diff --git a/galley/pkg/config/processor/metadata/collections.gen.go b/galley/pkg/config/processor/metadata/collections.gen.go new file mode 100755 index 000000000000..453cf97b885e --- /dev/null +++ b/galley/pkg/config/processor/metadata/collections.gen.go @@ -0,0 +1,481 @@ +// GENERATED FILE -- DO NOT EDIT +// + +package metadata + +import ( + "istio.io/istio/galley/pkg/config/collection" +) + +var ( + + // IstioAuthenticationV1Alpha1Meshpolicies is the name of collection istio/authentication/v1alpha1/meshpolicies + IstioAuthenticationV1Alpha1Meshpolicies = collection.NewName("istio/authentication/v1alpha1/meshpolicies") + + // IstioAuthenticationV1Alpha1Policies is the name of collection istio/authentication/v1alpha1/policies + IstioAuthenticationV1Alpha1Policies = collection.NewName("istio/authentication/v1alpha1/policies") + + // IstioConfigV1Alpha2Adapters is the name of collection istio/config/v1alpha2/adapters + IstioConfigV1Alpha2Adapters = collection.NewName("istio/config/v1alpha2/adapters") + + // IstioConfigV1Alpha2Httpapispecbindings is the name of collection istio/config/v1alpha2/httpapispecbindings + IstioConfigV1Alpha2Httpapispecbindings = collection.NewName("istio/config/v1alpha2/httpapispecbindings") + + // IstioConfigV1Alpha2Httpapispecs is the name of collection istio/config/v1alpha2/httpapispecs + IstioConfigV1Alpha2Httpapispecs = collection.NewName("istio/config/v1alpha2/httpapispecs") + + // IstioConfigV1Alpha2LegacyApikeys is the name of collection istio/config/v1alpha2/legacy/apikeys + IstioConfigV1Alpha2LegacyApikeys = collection.NewName("istio/config/v1alpha2/legacy/apikeys") + + // IstioConfigV1Alpha2LegacyAuthorizations is the name of collection istio/config/v1alpha2/legacy/authorizations + IstioConfigV1Alpha2LegacyAuthorizations = collection.NewName("istio/config/v1alpha2/legacy/authorizations") + + // IstioConfigV1Alpha2LegacyBypasses is the name of collection istio/config/v1alpha2/legacy/bypasses + IstioConfigV1Alpha2LegacyBypasses = collection.NewName("istio/config/v1alpha2/legacy/bypasses") + + // IstioConfigV1Alpha2LegacyChecknothings is the name of collection istio/config/v1alpha2/legacy/checknothings + IstioConfigV1Alpha2LegacyChecknothings = collection.NewName("istio/config/v1alpha2/legacy/checknothings") + + // IstioConfigV1Alpha2LegacyCirconuses is the name of collection istio/config/v1alpha2/legacy/circonuses + IstioConfigV1Alpha2LegacyCirconuses = collection.NewName("istio/config/v1alpha2/legacy/circonuses") + + // IstioConfigV1Alpha2LegacyCloudwatches is the name of collection istio/config/v1alpha2/legacy/cloudwatches + IstioConfigV1Alpha2LegacyCloudwatches = collection.NewName("istio/config/v1alpha2/legacy/cloudwatches") + + // IstioConfigV1Alpha2LegacyDeniers is the name of collection istio/config/v1alpha2/legacy/deniers + IstioConfigV1Alpha2LegacyDeniers = collection.NewName("istio/config/v1alpha2/legacy/deniers") + + // IstioConfigV1Alpha2LegacyDogstatsds is the name of collection istio/config/v1alpha2/legacy/dogstatsds + IstioConfigV1Alpha2LegacyDogstatsds = collection.NewName("istio/config/v1alpha2/legacy/dogstatsds") + + // IstioConfigV1Alpha2LegacyEdges is the name of collection istio/config/v1alpha2/legacy/edges + IstioConfigV1Alpha2LegacyEdges = collection.NewName("istio/config/v1alpha2/legacy/edges") + + // IstioConfigV1Alpha2LegacyFluentds is the name of collection istio/config/v1alpha2/legacy/fluentds + IstioConfigV1Alpha2LegacyFluentds = collection.NewName("istio/config/v1alpha2/legacy/fluentds") + + // IstioConfigV1Alpha2LegacyKubernetesenvs is the name of collection istio/config/v1alpha2/legacy/kubernetesenvs + IstioConfigV1Alpha2LegacyKubernetesenvs = collection.NewName("istio/config/v1alpha2/legacy/kubernetesenvs") + + // IstioConfigV1Alpha2LegacyKuberneteses is the name of collection istio/config/v1alpha2/legacy/kuberneteses + IstioConfigV1Alpha2LegacyKuberneteses = collection.NewName("istio/config/v1alpha2/legacy/kuberneteses") + + // IstioConfigV1Alpha2LegacyListcheckers is the name of collection istio/config/v1alpha2/legacy/listcheckers + IstioConfigV1Alpha2LegacyListcheckers = collection.NewName("istio/config/v1alpha2/legacy/listcheckers") + + // IstioConfigV1Alpha2LegacyListentries is the name of collection istio/config/v1alpha2/legacy/listentries + IstioConfigV1Alpha2LegacyListentries = collection.NewName("istio/config/v1alpha2/legacy/listentries") + + // IstioConfigV1Alpha2LegacyLogentries is the name of collection istio/config/v1alpha2/legacy/logentries + IstioConfigV1Alpha2LegacyLogentries = collection.NewName("istio/config/v1alpha2/legacy/logentries") + + // IstioConfigV1Alpha2LegacyMemquotas is the name of collection istio/config/v1alpha2/legacy/memquotas + IstioConfigV1Alpha2LegacyMemquotas = collection.NewName("istio/config/v1alpha2/legacy/memquotas") + + // IstioConfigV1Alpha2LegacyMetrics is the name of collection istio/config/v1alpha2/legacy/metrics + IstioConfigV1Alpha2LegacyMetrics = collection.NewName("istio/config/v1alpha2/legacy/metrics") + + // IstioConfigV1Alpha2LegacyNoops is the name of collection istio/config/v1alpha2/legacy/noops + IstioConfigV1Alpha2LegacyNoops = collection.NewName("istio/config/v1alpha2/legacy/noops") + + // IstioConfigV1Alpha2LegacyOpas is the name of collection istio/config/v1alpha2/legacy/opas + IstioConfigV1Alpha2LegacyOpas = collection.NewName("istio/config/v1alpha2/legacy/opas") + + // IstioConfigV1Alpha2LegacyPrometheuses is the name of collection istio/config/v1alpha2/legacy/prometheuses + IstioConfigV1Alpha2LegacyPrometheuses = collection.NewName("istio/config/v1alpha2/legacy/prometheuses") + + // IstioConfigV1Alpha2LegacyQuotas is the name of collection istio/config/v1alpha2/legacy/quotas + IstioConfigV1Alpha2LegacyQuotas = collection.NewName("istio/config/v1alpha2/legacy/quotas") + + // IstioConfigV1Alpha2LegacyRbacs is the name of collection istio/config/v1alpha2/legacy/rbacs + IstioConfigV1Alpha2LegacyRbacs = collection.NewName("istio/config/v1alpha2/legacy/rbacs") + + // IstioConfigV1Alpha2LegacyRedisquotas is the name of collection istio/config/v1alpha2/legacy/redisquotas + IstioConfigV1Alpha2LegacyRedisquotas = collection.NewName("istio/config/v1alpha2/legacy/redisquotas") + + // IstioConfigV1Alpha2LegacyReportnothings is the name of collection istio/config/v1alpha2/legacy/reportnothings + IstioConfigV1Alpha2LegacyReportnothings = collection.NewName("istio/config/v1alpha2/legacy/reportnothings") + + // IstioConfigV1Alpha2LegacySignalfxs is the name of collection istio/config/v1alpha2/legacy/signalfxs + IstioConfigV1Alpha2LegacySignalfxs = collection.NewName("istio/config/v1alpha2/legacy/signalfxs") + + // IstioConfigV1Alpha2LegacySolarwindses is the name of collection istio/config/v1alpha2/legacy/solarwindses + IstioConfigV1Alpha2LegacySolarwindses = collection.NewName("istio/config/v1alpha2/legacy/solarwindses") + + // IstioConfigV1Alpha2LegacyStackdrivers is the name of collection istio/config/v1alpha2/legacy/stackdrivers + IstioConfigV1Alpha2LegacyStackdrivers = collection.NewName("istio/config/v1alpha2/legacy/stackdrivers") + + // IstioConfigV1Alpha2LegacyStatsds is the name of collection istio/config/v1alpha2/legacy/statsds + IstioConfigV1Alpha2LegacyStatsds = collection.NewName("istio/config/v1alpha2/legacy/statsds") + + // IstioConfigV1Alpha2LegacyStdios is the name of collection istio/config/v1alpha2/legacy/stdios + IstioConfigV1Alpha2LegacyStdios = collection.NewName("istio/config/v1alpha2/legacy/stdios") + + // IstioConfigV1Alpha2LegacyTracespans is the name of collection istio/config/v1alpha2/legacy/tracespans + IstioConfigV1Alpha2LegacyTracespans = collection.NewName("istio/config/v1alpha2/legacy/tracespans") + + // IstioConfigV1Alpha2LegacyZipkins is the name of collection istio/config/v1alpha2/legacy/zipkins + IstioConfigV1Alpha2LegacyZipkins = collection.NewName("istio/config/v1alpha2/legacy/zipkins") + + // IstioConfigV1Alpha2Templates is the name of collection istio/config/v1alpha2/templates + IstioConfigV1Alpha2Templates = collection.NewName("istio/config/v1alpha2/templates") + + // IstioMeshV1Alpha1MeshConfig is the name of collection istio/mesh/v1alpha1/MeshConfig + IstioMeshV1Alpha1MeshConfig = collection.NewName("istio/mesh/v1alpha1/MeshConfig") + + // IstioMixerV1ConfigClientQuotaspecbindings is the name of collection istio/mixer/v1/config/client/quotaspecbindings + IstioMixerV1ConfigClientQuotaspecbindings = collection.NewName("istio/mixer/v1/config/client/quotaspecbindings") + + // IstioMixerV1ConfigClientQuotaspecs is the name of collection istio/mixer/v1/config/client/quotaspecs + IstioMixerV1ConfigClientQuotaspecs = collection.NewName("istio/mixer/v1/config/client/quotaspecs") + + // IstioNetworkingV1Alpha3Destinationrules is the name of collection istio/networking/v1alpha3/destinationrules + IstioNetworkingV1Alpha3Destinationrules = collection.NewName("istio/networking/v1alpha3/destinationrules") + + // IstioNetworkingV1Alpha3Envoyfilters is the name of collection istio/networking/v1alpha3/envoyfilters + IstioNetworkingV1Alpha3Envoyfilters = collection.NewName("istio/networking/v1alpha3/envoyfilters") + + // IstioNetworkingV1Alpha3Gateways is the name of collection istio/networking/v1alpha3/gateways + IstioNetworkingV1Alpha3Gateways = collection.NewName("istio/networking/v1alpha3/gateways") + + // IstioNetworkingV1Alpha3Serviceentries is the name of collection istio/networking/v1alpha3/serviceentries + IstioNetworkingV1Alpha3Serviceentries = collection.NewName("istio/networking/v1alpha3/serviceentries") + + // IstioNetworkingV1Alpha3Sidecars is the name of collection istio/networking/v1alpha3/sidecars + IstioNetworkingV1Alpha3Sidecars = collection.NewName("istio/networking/v1alpha3/sidecars") + + // IstioNetworkingV1Alpha3SyntheticServiceentries is the name of collection istio/networking/v1alpha3/synthetic/serviceentries + IstioNetworkingV1Alpha3SyntheticServiceentries = collection.NewName("istio/networking/v1alpha3/synthetic/serviceentries") + + // IstioNetworkingV1Alpha3Virtualservices is the name of collection istio/networking/v1alpha3/virtualservices + IstioNetworkingV1Alpha3Virtualservices = collection.NewName("istio/networking/v1alpha3/virtualservices") + + // IstioPolicyV1Beta1Attributemanifests is the name of collection istio/policy/v1beta1/attributemanifests + IstioPolicyV1Beta1Attributemanifests = collection.NewName("istio/policy/v1beta1/attributemanifests") + + // IstioPolicyV1Beta1Handlers is the name of collection istio/policy/v1beta1/handlers + IstioPolicyV1Beta1Handlers = collection.NewName("istio/policy/v1beta1/handlers") + + // IstioPolicyV1Beta1Instances is the name of collection istio/policy/v1beta1/instances + IstioPolicyV1Beta1Instances = collection.NewName("istio/policy/v1beta1/instances") + + // IstioPolicyV1Beta1Rules is the name of collection istio/policy/v1beta1/rules + IstioPolicyV1Beta1Rules = collection.NewName("istio/policy/v1beta1/rules") + + // IstioRbacV1Alpha1Authorizationpolicies is the name of collection istio/rbac/v1alpha1/authorizationpolicies + IstioRbacV1Alpha1Authorizationpolicies = collection.NewName("istio/rbac/v1alpha1/authorizationpolicies") + + // IstioRbacV1Alpha1Clusterrbacconfigs is the name of collection istio/rbac/v1alpha1/clusterrbacconfigs + IstioRbacV1Alpha1Clusterrbacconfigs = collection.NewName("istio/rbac/v1alpha1/clusterrbacconfigs") + + // IstioRbacV1Alpha1Rbacconfigs is the name of collection istio/rbac/v1alpha1/rbacconfigs + IstioRbacV1Alpha1Rbacconfigs = collection.NewName("istio/rbac/v1alpha1/rbacconfigs") + + // IstioRbacV1Alpha1Servicerolebindings is the name of collection istio/rbac/v1alpha1/servicerolebindings + IstioRbacV1Alpha1Servicerolebindings = collection.NewName("istio/rbac/v1alpha1/servicerolebindings") + + // IstioRbacV1Alpha1Serviceroles is the name of collection istio/rbac/v1alpha1/serviceroles + IstioRbacV1Alpha1Serviceroles = collection.NewName("istio/rbac/v1alpha1/serviceroles") + + // K8SAuthenticationIstioIoV1Alpha1Meshpolicies is the name of collection k8s/authentication.istio.io/v1alpha1/meshpolicies + K8SAuthenticationIstioIoV1Alpha1Meshpolicies = collection.NewName("k8s/authentication.istio.io/v1alpha1/meshpolicies") + + // K8SAuthenticationIstioIoV1Alpha1Policies is the name of collection k8s/authentication.istio.io/v1alpha1/policies + K8SAuthenticationIstioIoV1Alpha1Policies = collection.NewName("k8s/authentication.istio.io/v1alpha1/policies") + + // K8SConfigIstioIoV1Alpha2Adapters is the name of collection k8s/config.istio.io/v1alpha2/adapters + K8SConfigIstioIoV1Alpha2Adapters = collection.NewName("k8s/config.istio.io/v1alpha2/adapters") + + // K8SConfigIstioIoV1Alpha2Apikeys is the name of collection k8s/config.istio.io/v1alpha2/apikeys + K8SConfigIstioIoV1Alpha2Apikeys = collection.NewName("k8s/config.istio.io/v1alpha2/apikeys") + + // K8SConfigIstioIoV1Alpha2Attributemanifests is the name of collection k8s/config.istio.io/v1alpha2/attributemanifests + K8SConfigIstioIoV1Alpha2Attributemanifests = collection.NewName("k8s/config.istio.io/v1alpha2/attributemanifests") + + // K8SConfigIstioIoV1Alpha2Authorizations is the name of collection k8s/config.istio.io/v1alpha2/authorizations + K8SConfigIstioIoV1Alpha2Authorizations = collection.NewName("k8s/config.istio.io/v1alpha2/authorizations") + + // K8SConfigIstioIoV1Alpha2Bypasses is the name of collection k8s/config.istio.io/v1alpha2/bypasses + K8SConfigIstioIoV1Alpha2Bypasses = collection.NewName("k8s/config.istio.io/v1alpha2/bypasses") + + // K8SConfigIstioIoV1Alpha2Checknothings is the name of collection k8s/config.istio.io/v1alpha2/checknothings + K8SConfigIstioIoV1Alpha2Checknothings = collection.NewName("k8s/config.istio.io/v1alpha2/checknothings") + + // K8SConfigIstioIoV1Alpha2Circonuses is the name of collection k8s/config.istio.io/v1alpha2/circonuses + K8SConfigIstioIoV1Alpha2Circonuses = collection.NewName("k8s/config.istio.io/v1alpha2/circonuses") + + // K8SConfigIstioIoV1Alpha2Cloudwatches is the name of collection k8s/config.istio.io/v1alpha2/cloudwatches + K8SConfigIstioIoV1Alpha2Cloudwatches = collection.NewName("k8s/config.istio.io/v1alpha2/cloudwatches") + + // K8SConfigIstioIoV1Alpha2Deniers is the name of collection k8s/config.istio.io/v1alpha2/deniers + K8SConfigIstioIoV1Alpha2Deniers = collection.NewName("k8s/config.istio.io/v1alpha2/deniers") + + // K8SConfigIstioIoV1Alpha2Dogstatsds is the name of collection k8s/config.istio.io/v1alpha2/dogstatsds + K8SConfigIstioIoV1Alpha2Dogstatsds = collection.NewName("k8s/config.istio.io/v1alpha2/dogstatsds") + + // K8SConfigIstioIoV1Alpha2Edges is the name of collection k8s/config.istio.io/v1alpha2/edges + K8SConfigIstioIoV1Alpha2Edges = collection.NewName("k8s/config.istio.io/v1alpha2/edges") + + // K8SConfigIstioIoV1Alpha2Fluentds is the name of collection k8s/config.istio.io/v1alpha2/fluentds + K8SConfigIstioIoV1Alpha2Fluentds = collection.NewName("k8s/config.istio.io/v1alpha2/fluentds") + + // K8SConfigIstioIoV1Alpha2Handlers is the name of collection k8s/config.istio.io/v1alpha2/handlers + K8SConfigIstioIoV1Alpha2Handlers = collection.NewName("k8s/config.istio.io/v1alpha2/handlers") + + // K8SConfigIstioIoV1Alpha2Httpapispecbindings is the name of collection k8s/config.istio.io/v1alpha2/httpapispecbindings + K8SConfigIstioIoV1Alpha2Httpapispecbindings = collection.NewName("k8s/config.istio.io/v1alpha2/httpapispecbindings") + + // K8SConfigIstioIoV1Alpha2Httpapispecs is the name of collection k8s/config.istio.io/v1alpha2/httpapispecs + K8SConfigIstioIoV1Alpha2Httpapispecs = collection.NewName("k8s/config.istio.io/v1alpha2/httpapispecs") + + // K8SConfigIstioIoV1Alpha2Instances is the name of collection k8s/config.istio.io/v1alpha2/instances + K8SConfigIstioIoV1Alpha2Instances = collection.NewName("k8s/config.istio.io/v1alpha2/instances") + + // K8SConfigIstioIoV1Alpha2Kubernetesenvs is the name of collection k8s/config.istio.io/v1alpha2/kubernetesenvs + K8SConfigIstioIoV1Alpha2Kubernetesenvs = collection.NewName("k8s/config.istio.io/v1alpha2/kubernetesenvs") + + // K8SConfigIstioIoV1Alpha2Kuberneteses is the name of collection k8s/config.istio.io/v1alpha2/kuberneteses + K8SConfigIstioIoV1Alpha2Kuberneteses = collection.NewName("k8s/config.istio.io/v1alpha2/kuberneteses") + + // K8SConfigIstioIoV1Alpha2Listcheckers is the name of collection k8s/config.istio.io/v1alpha2/listcheckers + K8SConfigIstioIoV1Alpha2Listcheckers = collection.NewName("k8s/config.istio.io/v1alpha2/listcheckers") + + // K8SConfigIstioIoV1Alpha2Listentries is the name of collection k8s/config.istio.io/v1alpha2/listentries + K8SConfigIstioIoV1Alpha2Listentries = collection.NewName("k8s/config.istio.io/v1alpha2/listentries") + + // K8SConfigIstioIoV1Alpha2Logentries is the name of collection k8s/config.istio.io/v1alpha2/logentries + K8SConfigIstioIoV1Alpha2Logentries = collection.NewName("k8s/config.istio.io/v1alpha2/logentries") + + // K8SConfigIstioIoV1Alpha2Memquotas is the name of collection k8s/config.istio.io/v1alpha2/memquotas + K8SConfigIstioIoV1Alpha2Memquotas = collection.NewName("k8s/config.istio.io/v1alpha2/memquotas") + + // K8SConfigIstioIoV1Alpha2Metrics is the name of collection k8s/config.istio.io/v1alpha2/metrics + K8SConfigIstioIoV1Alpha2Metrics = collection.NewName("k8s/config.istio.io/v1alpha2/metrics") + + // K8SConfigIstioIoV1Alpha2Noops is the name of collection k8s/config.istio.io/v1alpha2/noops + K8SConfigIstioIoV1Alpha2Noops = collection.NewName("k8s/config.istio.io/v1alpha2/noops") + + // K8SConfigIstioIoV1Alpha2Opas is the name of collection k8s/config.istio.io/v1alpha2/opas + K8SConfigIstioIoV1Alpha2Opas = collection.NewName("k8s/config.istio.io/v1alpha2/opas") + + // K8SConfigIstioIoV1Alpha2Prometheuses is the name of collection k8s/config.istio.io/v1alpha2/prometheuses + K8SConfigIstioIoV1Alpha2Prometheuses = collection.NewName("k8s/config.istio.io/v1alpha2/prometheuses") + + // K8SConfigIstioIoV1Alpha2Quotas is the name of collection k8s/config.istio.io/v1alpha2/quotas + K8SConfigIstioIoV1Alpha2Quotas = collection.NewName("k8s/config.istio.io/v1alpha2/quotas") + + // K8SConfigIstioIoV1Alpha2Quotaspecbindings is the name of collection k8s/config.istio.io/v1alpha2/quotaspecbindings + K8SConfigIstioIoV1Alpha2Quotaspecbindings = collection.NewName("k8s/config.istio.io/v1alpha2/quotaspecbindings") + + // K8SConfigIstioIoV1Alpha2Quotaspecs is the name of collection k8s/config.istio.io/v1alpha2/quotaspecs + K8SConfigIstioIoV1Alpha2Quotaspecs = collection.NewName("k8s/config.istio.io/v1alpha2/quotaspecs") + + // K8SConfigIstioIoV1Alpha2Rbacs is the name of collection k8s/config.istio.io/v1alpha2/rbacs + K8SConfigIstioIoV1Alpha2Rbacs = collection.NewName("k8s/config.istio.io/v1alpha2/rbacs") + + // K8SConfigIstioIoV1Alpha2Redisquotas is the name of collection k8s/config.istio.io/v1alpha2/redisquotas + K8SConfigIstioIoV1Alpha2Redisquotas = collection.NewName("k8s/config.istio.io/v1alpha2/redisquotas") + + // K8SConfigIstioIoV1Alpha2Reportnothings is the name of collection k8s/config.istio.io/v1alpha2/reportnothings + K8SConfigIstioIoV1Alpha2Reportnothings = collection.NewName("k8s/config.istio.io/v1alpha2/reportnothings") + + // K8SConfigIstioIoV1Alpha2Rules is the name of collection k8s/config.istio.io/v1alpha2/rules + K8SConfigIstioIoV1Alpha2Rules = collection.NewName("k8s/config.istio.io/v1alpha2/rules") + + // K8SConfigIstioIoV1Alpha2Signalfxs is the name of collection k8s/config.istio.io/v1alpha2/signalfxs + K8SConfigIstioIoV1Alpha2Signalfxs = collection.NewName("k8s/config.istio.io/v1alpha2/signalfxs") + + // K8SConfigIstioIoV1Alpha2Solarwindses is the name of collection k8s/config.istio.io/v1alpha2/solarwindses + K8SConfigIstioIoV1Alpha2Solarwindses = collection.NewName("k8s/config.istio.io/v1alpha2/solarwindses") + + // K8SConfigIstioIoV1Alpha2Stackdrivers is the name of collection k8s/config.istio.io/v1alpha2/stackdrivers + K8SConfigIstioIoV1Alpha2Stackdrivers = collection.NewName("k8s/config.istio.io/v1alpha2/stackdrivers") + + // K8SConfigIstioIoV1Alpha2Statsds is the name of collection k8s/config.istio.io/v1alpha2/statsds + K8SConfigIstioIoV1Alpha2Statsds = collection.NewName("k8s/config.istio.io/v1alpha2/statsds") + + // K8SConfigIstioIoV1Alpha2Stdios is the name of collection k8s/config.istio.io/v1alpha2/stdios + K8SConfigIstioIoV1Alpha2Stdios = collection.NewName("k8s/config.istio.io/v1alpha2/stdios") + + // K8SConfigIstioIoV1Alpha2Templates is the name of collection k8s/config.istio.io/v1alpha2/templates + K8SConfigIstioIoV1Alpha2Templates = collection.NewName("k8s/config.istio.io/v1alpha2/templates") + + // K8SConfigIstioIoV1Alpha2Tracespans is the name of collection k8s/config.istio.io/v1alpha2/tracespans + K8SConfigIstioIoV1Alpha2Tracespans = collection.NewName("k8s/config.istio.io/v1alpha2/tracespans") + + // K8SConfigIstioIoV1Alpha2Zipkins is the name of collection k8s/config.istio.io/v1alpha2/zipkins + K8SConfigIstioIoV1Alpha2Zipkins = collection.NewName("k8s/config.istio.io/v1alpha2/zipkins") + + // K8SCoreV1Endpoints is the name of collection k8s/core/v1/endpoints + K8SCoreV1Endpoints = collection.NewName("k8s/core/v1/endpoints") + + // K8SCoreV1Namespaces is the name of collection k8s/core/v1/namespaces + K8SCoreV1Namespaces = collection.NewName("k8s/core/v1/namespaces") + + // K8SCoreV1Nodes is the name of collection k8s/core/v1/nodes + K8SCoreV1Nodes = collection.NewName("k8s/core/v1/nodes") + + // K8SCoreV1Pods is the name of collection k8s/core/v1/pods + K8SCoreV1Pods = collection.NewName("k8s/core/v1/pods") + + // K8SCoreV1Services is the name of collection k8s/core/v1/services + K8SCoreV1Services = collection.NewName("k8s/core/v1/services") + + // K8SExtensionsV1Beta1Ingresses is the name of collection k8s/extensions/v1beta1/ingresses + K8SExtensionsV1Beta1Ingresses = collection.NewName("k8s/extensions/v1beta1/ingresses") + + // K8SNetworkingIstioIoV1Alpha3Destinationrules is the name of collection k8s/networking.istio.io/v1alpha3/destinationrules + K8SNetworkingIstioIoV1Alpha3Destinationrules = collection.NewName("k8s/networking.istio.io/v1alpha3/destinationrules") + + // K8SNetworkingIstioIoV1Alpha3Envoyfilters is the name of collection k8s/networking.istio.io/v1alpha3/envoyfilters + K8SNetworkingIstioIoV1Alpha3Envoyfilters = collection.NewName("k8s/networking.istio.io/v1alpha3/envoyfilters") + + // K8SNetworkingIstioIoV1Alpha3Gateways is the name of collection k8s/networking.istio.io/v1alpha3/gateways + K8SNetworkingIstioIoV1Alpha3Gateways = collection.NewName("k8s/networking.istio.io/v1alpha3/gateways") + + // K8SNetworkingIstioIoV1Alpha3Serviceentries is the name of collection k8s/networking.istio.io/v1alpha3/serviceentries + K8SNetworkingIstioIoV1Alpha3Serviceentries = collection.NewName("k8s/networking.istio.io/v1alpha3/serviceentries") + + // K8SNetworkingIstioIoV1Alpha3Sidecars is the name of collection k8s/networking.istio.io/v1alpha3/sidecars + K8SNetworkingIstioIoV1Alpha3Sidecars = collection.NewName("k8s/networking.istio.io/v1alpha3/sidecars") + + // K8SNetworkingIstioIoV1Alpha3Virtualservices is the name of collection k8s/networking.istio.io/v1alpha3/virtualservices + K8SNetworkingIstioIoV1Alpha3Virtualservices = collection.NewName("k8s/networking.istio.io/v1alpha3/virtualservices") + + // K8SRbacIstioIoV1Alpha1Authorizationpolicies is the name of collection k8s/rbac.istio.io/v1alpha1/authorizationpolicies + K8SRbacIstioIoV1Alpha1Authorizationpolicies = collection.NewName("k8s/rbac.istio.io/v1alpha1/authorizationpolicies") + + // K8SRbacIstioIoV1Alpha1Clusterrbacconfigs is the name of collection k8s/rbac.istio.io/v1alpha1/clusterrbacconfigs + K8SRbacIstioIoV1Alpha1Clusterrbacconfigs = collection.NewName("k8s/rbac.istio.io/v1alpha1/clusterrbacconfigs") + + // K8SRbacIstioIoV1Alpha1Policy is the name of collection k8s/rbac.istio.io/v1alpha1/policy + K8SRbacIstioIoV1Alpha1Policy = collection.NewName("k8s/rbac.istio.io/v1alpha1/policy") + + // K8SRbacIstioIoV1Alpha1Rbacconfigs is the name of collection k8s/rbac.istio.io/v1alpha1/rbacconfigs + K8SRbacIstioIoV1Alpha1Rbacconfigs = collection.NewName("k8s/rbac.istio.io/v1alpha1/rbacconfigs") + + // K8SRbacIstioIoV1Alpha1Serviceroles is the name of collection k8s/rbac.istio.io/v1alpha1/serviceroles + K8SRbacIstioIoV1Alpha1Serviceroles = collection.NewName("k8s/rbac.istio.io/v1alpha1/serviceroles") +) + +// CollectionNames returns the collection names declared in this package. +func CollectionNames() []collection.Name { + return []collection.Name{ + IstioAuthenticationV1Alpha1Meshpolicies, + IstioAuthenticationV1Alpha1Policies, + IstioConfigV1Alpha2Adapters, + IstioConfigV1Alpha2Httpapispecbindings, + IstioConfigV1Alpha2Httpapispecs, + IstioConfigV1Alpha2LegacyApikeys, + IstioConfigV1Alpha2LegacyAuthorizations, + IstioConfigV1Alpha2LegacyBypasses, + IstioConfigV1Alpha2LegacyChecknothings, + IstioConfigV1Alpha2LegacyCirconuses, + IstioConfigV1Alpha2LegacyCloudwatches, + IstioConfigV1Alpha2LegacyDeniers, + IstioConfigV1Alpha2LegacyDogstatsds, + IstioConfigV1Alpha2LegacyEdges, + IstioConfigV1Alpha2LegacyFluentds, + IstioConfigV1Alpha2LegacyKubernetesenvs, + IstioConfigV1Alpha2LegacyKuberneteses, + IstioConfigV1Alpha2LegacyListcheckers, + IstioConfigV1Alpha2LegacyListentries, + IstioConfigV1Alpha2LegacyLogentries, + IstioConfigV1Alpha2LegacyMemquotas, + IstioConfigV1Alpha2LegacyMetrics, + IstioConfigV1Alpha2LegacyNoops, + IstioConfigV1Alpha2LegacyOpas, + IstioConfigV1Alpha2LegacyPrometheuses, + IstioConfigV1Alpha2LegacyQuotas, + IstioConfigV1Alpha2LegacyRbacs, + IstioConfigV1Alpha2LegacyRedisquotas, + IstioConfigV1Alpha2LegacyReportnothings, + IstioConfigV1Alpha2LegacySignalfxs, + IstioConfigV1Alpha2LegacySolarwindses, + IstioConfigV1Alpha2LegacyStackdrivers, + IstioConfigV1Alpha2LegacyStatsds, + IstioConfigV1Alpha2LegacyStdios, + IstioConfigV1Alpha2LegacyTracespans, + IstioConfigV1Alpha2LegacyZipkins, + IstioConfigV1Alpha2Templates, + IstioMeshV1Alpha1MeshConfig, + IstioMixerV1ConfigClientQuotaspecbindings, + IstioMixerV1ConfigClientQuotaspecs, + IstioNetworkingV1Alpha3Destinationrules, + IstioNetworkingV1Alpha3Envoyfilters, + IstioNetworkingV1Alpha3Gateways, + IstioNetworkingV1Alpha3Serviceentries, + IstioNetworkingV1Alpha3Sidecars, + IstioNetworkingV1Alpha3SyntheticServiceentries, + IstioNetworkingV1Alpha3Virtualservices, + IstioPolicyV1Beta1Attributemanifests, + IstioPolicyV1Beta1Handlers, + IstioPolicyV1Beta1Instances, + IstioPolicyV1Beta1Rules, + IstioRbacV1Alpha1Authorizationpolicies, + IstioRbacV1Alpha1Clusterrbacconfigs, + IstioRbacV1Alpha1Rbacconfigs, + IstioRbacV1Alpha1Servicerolebindings, + IstioRbacV1Alpha1Serviceroles, + K8SAuthenticationIstioIoV1Alpha1Meshpolicies, + K8SAuthenticationIstioIoV1Alpha1Policies, + K8SConfigIstioIoV1Alpha2Adapters, + K8SConfigIstioIoV1Alpha2Apikeys, + K8SConfigIstioIoV1Alpha2Attributemanifests, + K8SConfigIstioIoV1Alpha2Authorizations, + K8SConfigIstioIoV1Alpha2Bypasses, + K8SConfigIstioIoV1Alpha2Checknothings, + K8SConfigIstioIoV1Alpha2Circonuses, + K8SConfigIstioIoV1Alpha2Cloudwatches, + K8SConfigIstioIoV1Alpha2Deniers, + K8SConfigIstioIoV1Alpha2Dogstatsds, + K8SConfigIstioIoV1Alpha2Edges, + K8SConfigIstioIoV1Alpha2Fluentds, + K8SConfigIstioIoV1Alpha2Handlers, + K8SConfigIstioIoV1Alpha2Httpapispecbindings, + K8SConfigIstioIoV1Alpha2Httpapispecs, + K8SConfigIstioIoV1Alpha2Instances, + K8SConfigIstioIoV1Alpha2Kubernetesenvs, + K8SConfigIstioIoV1Alpha2Kuberneteses, + K8SConfigIstioIoV1Alpha2Listcheckers, + K8SConfigIstioIoV1Alpha2Listentries, + K8SConfigIstioIoV1Alpha2Logentries, + K8SConfigIstioIoV1Alpha2Memquotas, + K8SConfigIstioIoV1Alpha2Metrics, + K8SConfigIstioIoV1Alpha2Noops, + K8SConfigIstioIoV1Alpha2Opas, + K8SConfigIstioIoV1Alpha2Prometheuses, + K8SConfigIstioIoV1Alpha2Quotas, + K8SConfigIstioIoV1Alpha2Quotaspecbindings, + K8SConfigIstioIoV1Alpha2Quotaspecs, + K8SConfigIstioIoV1Alpha2Rbacs, + K8SConfigIstioIoV1Alpha2Redisquotas, + K8SConfigIstioIoV1Alpha2Reportnothings, + K8SConfigIstioIoV1Alpha2Rules, + K8SConfigIstioIoV1Alpha2Signalfxs, + K8SConfigIstioIoV1Alpha2Solarwindses, + K8SConfigIstioIoV1Alpha2Stackdrivers, + K8SConfigIstioIoV1Alpha2Statsds, + K8SConfigIstioIoV1Alpha2Stdios, + K8SConfigIstioIoV1Alpha2Templates, + K8SConfigIstioIoV1Alpha2Tracespans, + K8SConfigIstioIoV1Alpha2Zipkins, + K8SCoreV1Endpoints, + K8SCoreV1Namespaces, + K8SCoreV1Nodes, + K8SCoreV1Pods, + K8SCoreV1Services, + K8SExtensionsV1Beta1Ingresses, + K8SNetworkingIstioIoV1Alpha3Destinationrules, + K8SNetworkingIstioIoV1Alpha3Envoyfilters, + K8SNetworkingIstioIoV1Alpha3Gateways, + K8SNetworkingIstioIoV1Alpha3Serviceentries, + K8SNetworkingIstioIoV1Alpha3Sidecars, + K8SNetworkingIstioIoV1Alpha3Virtualservices, + K8SRbacIstioIoV1Alpha1Authorizationpolicies, + K8SRbacIstioIoV1Alpha1Clusterrbacconfigs, + K8SRbacIstioIoV1Alpha1Policy, + K8SRbacIstioIoV1Alpha1Rbacconfigs, + K8SRbacIstioIoV1Alpha1Serviceroles, + } +} diff --git a/galley/pkg/config/processor/metadata/collections_test.go b/galley/pkg/config/processor/metadata/collections_test.go new file mode 100644 index 000000000000..ed88df2fc8cb --- /dev/null +++ b/galley/pkg/config/processor/metadata/collections_test.go @@ -0,0 +1,25 @@ +// 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 metadata + +import "testing" + +func TestCollectionNames(t *testing.T) { + + c := CollectionNames() + if len(c) != len(MustGet().Collections().CollectionNames()) { + t.Fatalf("Unexpected number of collections: %v", len(c)) + } +} diff --git a/galley/pkg/config/processor/metadata/gen.go b/galley/pkg/config/processor/metadata/gen.go new file mode 100644 index 000000000000..df1997b578ff --- /dev/null +++ b/galley/pkg/config/processor/metadata/gen.go @@ -0,0 +1,28 @@ +// 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 metadata + +// TODO: Switch go-bindata to be scripts/run_gobindata.sh +// Embed the core metadata file containing the collections as a resource +//go:generate go-bindata --nocompress --nometadata --pkg metadata -o metadata.gen.go metadata.yaml + +// Create static initializers file +//go:generate go run $GOPATH/src/istio.io/istio/galley/pkg/config/schema/codegen/tools/staticinit.main.go metadata metadata.yaml staticinit.gen.go + +// Create collection constants +//go:generate go run $GOPATH/src/istio.io/istio/galley/pkg/config/schema/codegen/tools/collections.main.go metadata metadata.yaml collections.gen.go + +//go:generate goimports -w -local istio.io "$GOPATH/src/istio.io/istio/galley/pkg/config/processor/metadata/collections.gen.go" +//go:generate goimports -w -local istio.io "$GOPATH/src/istio.io/istio/galley/pkg/config/processor/metadata/staticinit.gen.go" diff --git a/galley/pkg/config/processor/metadata/get.go b/galley/pkg/config/processor/metadata/get.go new file mode 100644 index 000000000000..73cf5201906f --- /dev/null +++ b/galley/pkg/config/processor/metadata/get.go @@ -0,0 +1,45 @@ +// 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 metadata + +import ( + "fmt" + + "istio.io/istio/galley/pkg/config/schema" +) + +// Get returns the contained resources.yaml file, in parsed form. +func Get() (*schema.Metadata, error) { + b, err := Asset("metadata.yaml") + if err != nil { + return nil, err + } + + m, err := schema.ParseAndBuild(string(b)) + if err != nil { + return nil, err + } + + return m, nil +} + +// MustGet calls Get and panics if it returns and error. +func MustGet() *schema.Metadata { + s, err := Get() + if err != nil { + panic(fmt.Sprintf("metadata.MustGet: %v", err)) + } + return s +} diff --git a/galley/pkg/config/processor/metadata/metadata.gen.go b/galley/pkg/config/processor/metadata/metadata.gen.go new file mode 100644 index 000000000000..8d67830b5574 --- /dev/null +++ b/galley/pkg/config/processor/metadata/metadata.gen.go @@ -0,0 +1,1226 @@ +// Code generated by go-bindata. +// sources: +// metadata.yaml +// DO NOT EDIT! + +package metadata + +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 _metadataYaml = []byte(`# Copyright 2019 Istio Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in conformance 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. + +# +# This is the main metadata file for Galley processing. +# #### KEEP ENTRIES ALPHASORTED! #### +# + +# The total set of collections, both Istio (i.e. MCP) and K8s (API Server/K8s). +collections: + ## Istio collections + - name: "istio/authentication/v1alpha1/meshpolicies" + proto: "istio.authentication.v1alpha1.Policy" + protoPackage: "istio.io/api/authentication/v1alpha1" + + - name: "istio/authentication/v1alpha1/policies" + proto: "istio.authentication.v1alpha1.Policy" + protoPackage: "istio.io/api/authentication/v1alpha1" + + - name: "istio/config/v1alpha2/adapters" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/httpapispecs" + proto: "istio.mixer.v1.config.client.HTTPAPISpec" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "istio/config/v1alpha2/httpapispecbindings" + proto: "istio.mixer.v1.config.client.HTTPAPISpecBinding" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "istio/config/v1alpha2/templates" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/mesh/v1alpha1/MeshConfig" + proto: "istio.mesh.v1alpha1.MeshConfig" + protoPackage: "istio.io/api/mesh/v1alpha1" + + - name: "istio/mixer/v1/config/client/quotaspecs" + proto: "istio.mixer.v1.config.client.QuotaSpec" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "istio/mixer/v1/config/client/quotaspecbindings" + proto: "istio.mixer.v1.config.client.QuotaSpecBinding" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "istio/networking/v1alpha3/destinationrules" + proto: "istio.networking.v1alpha3.DestinationRule" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/envoyfilters" + proto: "istio.networking.v1alpha3.EnvoyFilter" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/gateways" + proto: "istio.networking.v1alpha3.Gateway" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/serviceentries" + proto: "istio.networking.v1alpha3.ServiceEntry" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/synthetic/serviceentries" + proto: "istio.networking.v1alpha3.ServiceEntry" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/sidecars" + proto: "istio.networking.v1alpha3.Sidecar" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/policy/v1beta1/attributemanifests" + proto: "istio.policy.v1beta1.AttributeManifest" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "istio/policy/v1beta1/instances" + proto: "istio.policy.v1beta1.Instance" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "istio/policy/v1beta1/handlers" + proto: "istio.policy.v1beta1.Handler" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "istio/policy/v1beta1/rules" + proto: "istio.policy.v1beta1.Rule" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "istio/rbac/v1alpha1/authorizationpolicies" + proto: "istio.rbac.v1alpha1.AuthorizationPolicy" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "istio/rbac/v1alpha1/clusterrbacconfigs" + proto: "istio.rbac.v1alpha1.RbacConfig" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "istio/rbac/v1alpha1/rbacconfigs" + proto: "istio.rbac.v1alpha1.RbacConfig" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "istio/rbac/v1alpha1/servicerolebindings" + proto: "istio.rbac.v1alpha1.ServiceRoleBindings" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "istio/rbac/v1alpha1/serviceroles" + proto: "istio.rbac.v1alpha1.ServiceRole" + protoPackage: "istio.io/api/rbac/v1alpha1" + + ### K8s collections ### + + # Built-in K8s collections + - name: "k8s/core/v1/endpoints" + proto: "k8s.io.api.core.v1.Endpoints" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/namespaces" + proto: "k8s.io.api.core.v1.NamespaceSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/nodes" + proto: "k8s.io.api.core.v1.NodeSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/pods" + proto: "k8s.io.api.core.v1.Pod" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/services" + proto: "k8s.io.api.core.v1.ServiceSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/extensions/v1beta1/ingresses" + proto: "k8s.io.api.extensions.v1beta1.IngressSpec" + protoPackage: "k8s.io/api/extensions/v1beta1" + + # Istio CRD collections + - name: "k8s/authentication.istio.io/v1alpha1/meshpolicies" + proto: "istio.authentication.v1alpha1.Policy" + protoPackage: "istio.io/api/authentication/v1alpha1" + + - name: "k8s/authentication.istio.io/v1alpha1/policies" + proto: "istio.authentication.v1alpha1.Policy" + protoPackage: "istio.io/api/authentication/v1alpha1" + + - name: "k8s/config.istio.io/v1alpha2/adapters" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/attributemanifests" + proto: "istio.policy.v1beta1.AttributeManifest" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "k8s/config.istio.io/v1alpha2/httpapispecs" + proto: "istio.mixer.v1.config.client.HTTPAPISpec" + + - name: "k8s/config.istio.io/v1alpha2/httpapispecbindings" + proto: "istio.mixer.v1.config.client.HTTPAPISpecBinding" + + - name: "k8s/config.istio.io/v1alpha2/instances" + proto: "istio.policy.v1beta1.Instance" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "k8s/config.istio.io/v1alpha2/templates" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/quotaspecs" + proto: "istio.mixer.v1.config.client.QuotaSpec" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "k8s/config.istio.io/v1alpha2/quotaspecbindings" + proto: "istio.mixer.v1.config.client.QuotaSpecBinding" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "k8s/config.istio.io/v1alpha2/rules" + proto: "istio.policy.v1beta1.Rule" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "k8s/networking.istio.io/v1alpha3/destinationrules" + proto: "istio.networking.v1alpha3.DestinationRule" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/networking.istio.io/v1alpha3/envoyfilters" + proto: "istio.networking.v1alpha3.EnvoyFilter" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/networking.istio.io/v1alpha3/gateways" + proto: "istio.networking.v1alpha3.Gateway" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/networking.istio.io/v1alpha3/serviceentries" + proto: "istio.networking.v1alpha3.ServiceEntry" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/networking.istio.io/v1alpha3/sidecars" + proto: "istio.networking.v1alpha3.Sidecar" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/networking.istio.io/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/config.istio.io/v1alpha2/handlers" + proto: "istio.policy.v1beta1.Handler" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "k8s/rbac.istio.io/v1alpha1/authorizationpolicies" + proto: "istio.rbac.v1alpha1.AuthorizationPolicy" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "k8s/rbac.istio.io/v1alpha1/clusterrbacconfigs" + proto: "istio.rbac.v1alpha1.RbacConfig" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "k8s/rbac.istio.io/v1alpha1/policy" + proto: "istio.rbac.v1alpha1.ServiceRoleBinding" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "k8s/rbac.istio.io/v1alpha1/rbacconfigs" + proto: "istio.rbac.v1alpha1.RbacConfig" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "k8s/rbac.istio.io/v1alpha1/serviceroles" + proto: "istio.rbac.v1alpha1.ServiceRole" + protoPackage: "istio.io/api/rbac/v1alpha1" + + # Keep Legacy Mixer CRD related collections separate, as these will be gone soon. + - name: "k8s/config.istio.io/v1alpha2/apikeys" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/apikeys" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/authorizations" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/authorizations" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/bypasses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/bypasses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/checknothings" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/checknothings" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/circonuses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/circonuses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/cloudwatches" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/cloudwatches" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/deniers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/deniers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/dogstatsds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/dogstatsds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/edges" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/edges" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/fluentds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/fluentds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/kuberneteses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/kuberneteses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/kubernetesenvs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/kubernetesenvs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/listcheckers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/listcheckers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/listentries" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/listentries" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/logentries" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/logentries" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/memquotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/memquotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/metrics" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/metrics" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/noops" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/noops" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/opas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/opas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/prometheuses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/prometheuses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/quotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/quotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/rbacs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/rbacs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/redisquotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/redisquotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/reportnothings" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/reportnothings" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/signalfxs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/signalfxs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/solarwindses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/solarwindses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/stackdrivers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/stackdrivers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/statsds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/statsds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/stdios" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/stdios" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/tracespans" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/tracespans" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/zipkins" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/zipkins" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + +# The snapshots to generate +snapshots: + - name: "default" + strategy: debounce + collections: + - "istio/authentication/v1alpha1/meshpolicies" + - "istio/authentication/v1alpha1/policies" + - "istio/config/v1alpha2/adapters" + - "istio/config/v1alpha2/httpapispecs" + - "istio/config/v1alpha2/httpapispecbindings" + - "istio/config/v1alpha2/templates" + - "istio/mesh/v1alpha1/MeshConfig" + - "istio/mixer/v1/config/client/quotaspecbindings" + - "istio/mixer/v1/config/client/quotaspecs" + - "istio/networking/v1alpha3/destinationrules" + - "istio/networking/v1alpha3/envoyfilters" + - "istio/networking/v1alpha3/gateways" + - "istio/networking/v1alpha3/serviceentries" + - "istio/networking/v1alpha3/sidecars" + - "istio/networking/v1alpha3/virtualservices" + - "istio/policy/v1beta1/attributemanifests" + - "istio/policy/v1beta1/handlers" + - "istio/policy/v1beta1/instances" + - "istio/policy/v1beta1/rules" + - "istio/rbac/v1alpha1/authorizationpolicies" + - "istio/rbac/v1alpha1/clusterrbacconfigs" + - "istio/rbac/v1alpha1/rbacconfigs" + - "istio/rbac/v1alpha1/servicerolebindings" + - "istio/rbac/v1alpha1/serviceroles" + - "k8s/core/v1/namespaces" + - "k8s/core/v1/services" + # Legacy Mixer CRDs + - "istio/config/v1alpha2/legacy/apikeys" + - "istio/config/v1alpha2/legacy/authorizations" + - "istio/config/v1alpha2/legacy/bypasses" + - "istio/config/v1alpha2/legacy/checknothings" + - "istio/config/v1alpha2/legacy/circonuses" + - "istio/config/v1alpha2/legacy/cloudwatches" + - "istio/config/v1alpha2/legacy/deniers" + - "istio/config/v1alpha2/legacy/dogstatsds" + - "istio/config/v1alpha2/legacy/edges" + - "istio/config/v1alpha2/legacy/fluentds" + - "istio/config/v1alpha2/legacy/kuberneteses" + - "istio/config/v1alpha2/legacy/kubernetesenvs" + - "istio/config/v1alpha2/legacy/listcheckers" + - "istio/config/v1alpha2/legacy/listentries" + - "istio/config/v1alpha2/legacy/logentries" + - "istio/config/v1alpha2/legacy/memquotas" + - "istio/config/v1alpha2/legacy/metrics" + - "istio/config/v1alpha2/legacy/noops" + - "istio/config/v1alpha2/legacy/opas" + - "istio/config/v1alpha2/legacy/prometheuses" + - "istio/config/v1alpha2/legacy/quotas" + - "istio/config/v1alpha2/legacy/rbacs" + - "istio/config/v1alpha2/legacy/redisquotas" + - "istio/config/v1alpha2/legacy/reportnothings" + - "istio/config/v1alpha2/legacy/signalfxs" + - "istio/config/v1alpha2/legacy/solarwindses" + - "istio/config/v1alpha2/legacy/stackdrivers" + - "istio/config/v1alpha2/legacy/statsds" + - "istio/config/v1alpha2/legacy/stdios" + - "istio/config/v1alpha2/legacy/tracespans" + - "istio/config/v1alpha2/legacy/zipkins" + + - name: "syntheticServiceEntry" + strategy: immediate + collections: + - "istio/networking/v1alpha3/synthetic/serviceentries" + +# Configuration for input sources +sources: + # Kubernetes specific configuration. + - type: kubernetes + resources: + - collection: "k8s/extensions/v1beta1/ingresses" + kind: "Ingress" + plural: "ingresses" + group: "extensions" + version: "v1beta1" + + - collection: "k8s/core/v1/services" + kind: "Service" + plural: "services" + version: "v1" + + - collection: "k8s/core/v1/namespaces" + kind: "Namespace" + plural: "namespaces" + version: "v1" + + - collection: "k8s/core/v1/nodes" + kind: "Node" + plural: "nodes" + version: "v1" + + - collection: "k8s/core/v1/pods" + kind: "Pod" + plural: "pods" + version: "v1" + + - collection: "k8s/core/v1/endpoints" + kind: "Endpoints" + plural: "endpoints" + version: "v1" + + - collection: "k8s/networking.istio.io/v1alpha3/virtualservices" + kind: "VirtualService" + plural: "virtualservices" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/networking.istio.io/v1alpha3/gateways" + kind: "Gateway" + plural: "gateways" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/networking.istio.io/v1alpha3/serviceentries" + kind: "ServiceEntry" + plural: "serviceentries" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/networking.istio.io/v1alpha3/destinationrules" + kind: "DestinationRule" + plural: "destinationrules" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/networking.istio.io/v1alpha3/envoyfilters" + kind: "EnvoyFilter" + plural: "envoyfilters" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/networking.istio.io/v1alpha3/sidecars" + kind: "Sidecar" + plural: "sidecars" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/config.istio.io/v1alpha2/httpapispecs" + kind: "HTTPAPISpec" + plural: "httpapispecs" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/httpapispecbindings" + kind: "HTTPAPISpecBinding" + plural: "httpapispecbindings" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/quotaspecs" + kind: "QuotaSpec" + plural: "quotaspecs" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/quotaspecbindings" + kind: "QuotaSpecBinding" + plural: "quotaspecbindings" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/authentication.istio.io/v1alpha1/policies" + kind: "Policy" + plural: "policies" + group: "authentication.istio.io" + version: "v1alpha1" + + - collection: "k8s/authentication.istio.io/v1alpha1/meshpolicies" + kind: "MeshPolicy" + plural: "meshpolicies" + group: "authentication.istio.io" + version: "v1alpha1" + + - collection: "k8s/rbac.istio.io/v1alpha1/serviceroles" + kind: "ServiceRole" + plural: "serviceroles" + group: "rbac.istio.io" + version: "v1alpha1" + + - collection: "k8s/rbac.istio.io/v1alpha1/policy" + kind: "ServiceRoleBinding" + plural: "servicerolebindings" + group: "rbac.istio.io" + version: "v1alpha1" + + - collection: "k8s/rbac.istio.io/v1alpha1/authorizationpolicies" + kind: "AuthorizationPolicy" + plural: "authorizationpolicies" + group: "rbac.istio.io" + version: "v1alpha1" + + - collection: "k8s/rbac.istio.io/v1alpha1/rbacconfigs" + kind: "RbacConfig" + plural: "rbacconfigs" + group: "rbac.istio.io" + version: "v1alpha1" + + - collection: "k8s/rbac.istio.io/v1alpha1/clusterrbacconfigs" + kind: "ClusterRbacConfig" + plural: "clusterrbacconfigs" + group: "rbac.istio.io" + version: "v1alpha1" + + - collection: "k8s/config.istio.io/v1alpha2/rules" + kind: "rule" + plural: "rules" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/attributemanifests" + kind: "attributemanifest" + plural: "attributemanifests" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/instances" + kind: "instance" + plural: "instances" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/handlers" + kind: "handler" + plural: "handlers" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/templates" + kind: "template" + plural: "templates" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/adapters" + kind: "adapter" + plural: "adapters" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/authentication.istio.io/v1alpha1/meshpolicies" + kind: "MeshPolicy" + plural: "meshpolicies" + group: "authentication.istio.io" + version: "v1alpha1" + + # Legacy Mixer CRD Types + - collection: "k8s/config.istio.io/v1alpha2/apikeys" + kind: "apikey" + plural: "apikeys" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/authorizations" + kind: "authorization" + plural: "authorizations" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/bypasses" + kind: "bypass" + plural: "bypasses" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/checknothings" + kind: "checknothing" + plural: "checknothings" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/circonuses" + kind: "circonus" + plural: "circonuses" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/cloudwatches" + kind: "cloudwatch" + plural: "cloudwatches" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/deniers" + kind: "denier" + plural: "deniers" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/dogstatsds" + kind: "dogstatsd" + plural: "dogstatsds" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/edges" + kind: "edge" + plural: "edges" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/fluentds" + kind: "fluentd" + plural: "fluentds" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/kuberneteses" + kind: "kubernetes" + plural: "kuberneteses" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/kubernetesenvs" + kind: "kubernetesenv" + plural: "kubernetesenvs" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/listcheckers" + kind: "listchecker" + plural: "listcheckers" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/listentries" + kind: "listentry" + plural: "listentries" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/logentries" + kind: "logentry" + plural: "logentries" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/memquotas" + kind: "memquota" + plural: "memquotas" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/metrics" + kind: "metric" + plural: "metrics" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/noops" + kind: "noop" + plural: "noops" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/opas" + kind: "opa" + plural: "opas" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/prometheuses" + kind: "prometheus" + plural: "prometheuses" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/quotas" + kind: "quota" + plural: "quotas" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/rbacs" + kind: "rbac" + plural: "rbacs" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/redisquotas" + kind: "redisquota" + plural: "redisquotas" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/reportnothings" + kind: "reportnothing" + plural: "reportnothings" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/signalfxs" + kind: "signalfx" + plural: "signalfxs" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/solarwindses" + kind: "solarwinds" + plural: "solarwindses" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/stackdrivers" + kind: "stackdriver" + plural: "stackdrivers" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/statsds" + kind: "statsd" + plural: "statsds" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/stdios" + kind: "stdio" + plural: "stdios" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/tracespans" + kind: "tracespan" + plural: "tracespans" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/zipkins" + kind: "zipkin" + plural: "zipkins" + group: "config.istio.io" + version: "v1alpha2" + optional: true + +# Transform specific configurations +transforms: + - type: direct + mapping: + "k8s/config.istio.io/v1alpha2/adapters": "istio/config/v1alpha2/adapters" + "k8s/config.istio.io/v1alpha2/attributemanifests": "istio/policy/v1beta1/attributemanifests" + "k8s/config.istio.io/v1alpha2/handlers": "istio/policy/v1beta1/handlers" + "k8s/config.istio.io/v1alpha2/httpapispecs": "istio/config/v1alpha2/httpapispecs" + "k8s/config.istio.io/v1alpha2/httpapispecbindings": "istio/config/v1alpha2/httpapispecbindings" + "k8s/config.istio.io/v1alpha2/instances": "istio/policy/v1beta1/instances" + "k8s/config.istio.io/v1alpha2/quotaspecs": "istio/mixer/v1/config/client/quotaspecs" + "k8s/config.istio.io/v1alpha2/quotaspecbindings": "istio/mixer/v1/config/client/quotaspecbindings" + "k8s/config.istio.io/v1alpha2/rules": "istio/policy/v1beta1/rules" + "k8s/config.istio.io/v1alpha2/templates": "istio/config/v1alpha2/templates" + "k8s/networking.istio.io/v1alpha3/destinationrules": "istio/networking/v1alpha3/destinationrules" + "k8s/networking.istio.io/v1alpha3/envoyfilters": "istio/networking/v1alpha3/envoyfilters" + "k8s/networking.istio.io/v1alpha3/gateways": "istio/networking/v1alpha3/gateways" + "k8s/networking.istio.io/v1alpha3/serviceentries": "istio/networking/v1alpha3/serviceentries" + "k8s/networking.istio.io/v1alpha3/sidecars": "istio/networking/v1alpha3/sidecars" + "k8s/networking.istio.io/v1alpha3/virtualservices": "istio/networking/v1alpha3/virtualservices" + "k8s/rbac.istio.io/v1alpha1/authorizationpolicies": "istio/rbac/v1alpha1/authorizationpolicies" + "k8s/rbac.istio.io/v1alpha1/policy": "istio/rbac/v1alpha1/servicerolebindings" + "k8s/rbac.istio.io/v1alpha1/rbacconfigs": "istio/rbac/v1alpha1/rbacconfigs" + "k8s/rbac.istio.io/v1alpha1/clusterrbacconfigs": "istio/rbac/v1alpha1/clusterrbacconfigs" + "k8s/rbac.istio.io/v1alpha1/serviceroles": "istio/rbac/v1alpha1/serviceroles" + "k8s/core/v1/namespaces": "k8s/core/v1/namespaces" + "k8s/core/v1/services": "k8s/core/v1/services" + "istio/mesh/v1alpha1/MeshConfig": "istio/mesh/v1alpha1/MeshConfig" + + # Legacy Mixer CRD mappings + "k8s/config.istio.io/v1alpha2/apikeys": "istio/config/v1alpha2/legacy/apikeys" + "k8s/config.istio.io/v1alpha2/authorizations": "istio/config/v1alpha2/legacy/authorizations" + "k8s/config.istio.io/v1alpha2/bypasses": "istio/config/v1alpha2/legacy/bypasses" + "k8s/config.istio.io/v1alpha2/checknothings": "istio/config/v1alpha2/legacy/checknothings" + "k8s/config.istio.io/v1alpha2/circonuses": "istio/config/v1alpha2/legacy/circonuses" + "k8s/config.istio.io/v1alpha2/cloudwatches": "istio/config/v1alpha2/legacy/cloudwatches" + "k8s/config.istio.io/v1alpha2/deniers": "istio/config/v1alpha2/legacy/deniers" + "k8s/config.istio.io/v1alpha2/dogstatsds": "istio/config/v1alpha2/legacy/dogstatsds" + "k8s/config.istio.io/v1alpha2/edges": "istio/config/v1alpha2/legacy/edges" + "k8s/config.istio.io/v1alpha2/fluentds": "istio/config/v1alpha2/legacy/fluentds" + "k8s/config.istio.io/v1alpha2/kuberneteses": "istio/config/v1alpha2/legacy/kuberneteses" + "k8s/config.istio.io/v1alpha2/kubernetesenvs": "istio/config/v1alpha2/legacy/kubernetesenvs" + "k8s/config.istio.io/v1alpha2/listcheckers": "istio/config/v1alpha2/legacy/listcheckers" + "k8s/config.istio.io/v1alpha2/listentries": "istio/config/v1alpha2/legacy/listentries" + "k8s/config.istio.io/v1alpha2/logentries": "istio/config/v1alpha2/legacy/logentries" + "k8s/config.istio.io/v1alpha2/memquotas": "istio/config/v1alpha2/legacy/memquotas" + "k8s/config.istio.io/v1alpha2/metrics": "istio/config/v1alpha2/legacy/metrics" + "k8s/config.istio.io/v1alpha2/noops": "istio/config/v1alpha2/legacy/noops" + "k8s/config.istio.io/v1alpha2/opas": "istio/config/v1alpha2/legacy/opas" + "k8s/config.istio.io/v1alpha2/prometheuses": "istio/config/v1alpha2/legacy/prometheuses" + "k8s/config.istio.io/v1alpha2/quotas": "istio/config/v1alpha2/legacy/quotas" + "k8s/config.istio.io/v1alpha2/rbacs": "istio/config/v1alpha2/legacy/rbacs" + "k8s/config.istio.io/v1alpha2/redisquotas": "istio/config/v1alpha2/legacy/redisquotas" + "k8s/config.istio.io/v1alpha2/reportnothings": "istio/config/v1alpha2/legacy/reportnothings" + "k8s/config.istio.io/v1alpha2/signalfxs": "istio/config/v1alpha2/legacy/signalfxs" + "k8s/config.istio.io/v1alpha2/solarwindses": "istio/config/v1alpha2/legacy/solarwindses" + "k8s/config.istio.io/v1alpha2/stackdrivers": "istio/config/v1alpha2/legacy/stackdrivers" + "k8s/config.istio.io/v1alpha2/statsds": "istio/config/v1alpha2/legacy/statsds" + "k8s/config.istio.io/v1alpha2/stdios": "istio/config/v1alpha2/legacy/stdios" + "k8s/config.istio.io/v1alpha2/tracespans": "istio/config/v1alpha2/legacy/tracespans" + "k8s/config.istio.io/v1alpha2/zipkins": "istio/config/v1alpha2/legacy/zipkins" +`) + +func metadataYamlBytes() ([]byte, error) { + return _metadataYaml, nil +} + +func metadataYaml() (*asset, error) { + bytes, err := metadataYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "metadata.yaml", 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){ + "metadata.yaml": metadataYaml, +} + +// 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{ + "metadata.yaml": &bintree{metadataYaml, 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/galley/pkg/config/processor/metadata/metadata.yaml b/galley/pkg/config/processor/metadata/metadata.yaml new file mode 100644 index 000000000000..c8f7f0ee717e --- /dev/null +++ b/galley/pkg/config/processor/metadata/metadata.yaml @@ -0,0 +1,1018 @@ +# Copyright 2019 Istio Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in conformance 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. + +# +# This is the main metadata file for Galley processing. +# #### KEEP ENTRIES ALPHASORTED! #### +# + +# The total set of collections, both Istio (i.e. MCP) and K8s (API Server/K8s). +collections: + ## Istio collections + - name: "istio/authentication/v1alpha1/meshpolicies" + proto: "istio.authentication.v1alpha1.Policy" + protoPackage: "istio.io/api/authentication/v1alpha1" + + - name: "istio/authentication/v1alpha1/policies" + proto: "istio.authentication.v1alpha1.Policy" + protoPackage: "istio.io/api/authentication/v1alpha1" + + - name: "istio/config/v1alpha2/adapters" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/httpapispecs" + proto: "istio.mixer.v1.config.client.HTTPAPISpec" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "istio/config/v1alpha2/httpapispecbindings" + proto: "istio.mixer.v1.config.client.HTTPAPISpecBinding" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "istio/config/v1alpha2/templates" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/mesh/v1alpha1/MeshConfig" + proto: "istio.mesh.v1alpha1.MeshConfig" + protoPackage: "istio.io/api/mesh/v1alpha1" + + - name: "istio/mixer/v1/config/client/quotaspecs" + proto: "istio.mixer.v1.config.client.QuotaSpec" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "istio/mixer/v1/config/client/quotaspecbindings" + proto: "istio.mixer.v1.config.client.QuotaSpecBinding" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "istio/networking/v1alpha3/destinationrules" + proto: "istio.networking.v1alpha3.DestinationRule" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/envoyfilters" + proto: "istio.networking.v1alpha3.EnvoyFilter" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/gateways" + proto: "istio.networking.v1alpha3.Gateway" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/serviceentries" + proto: "istio.networking.v1alpha3.ServiceEntry" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/synthetic/serviceentries" + proto: "istio.networking.v1alpha3.ServiceEntry" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/sidecars" + proto: "istio.networking.v1alpha3.Sidecar" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/policy/v1beta1/attributemanifests" + proto: "istio.policy.v1beta1.AttributeManifest" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "istio/policy/v1beta1/instances" + proto: "istio.policy.v1beta1.Instance" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "istio/policy/v1beta1/handlers" + proto: "istio.policy.v1beta1.Handler" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "istio/policy/v1beta1/rules" + proto: "istio.policy.v1beta1.Rule" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "istio/rbac/v1alpha1/authorizationpolicies" + proto: "istio.rbac.v1alpha1.AuthorizationPolicy" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "istio/rbac/v1alpha1/clusterrbacconfigs" + proto: "istio.rbac.v1alpha1.RbacConfig" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "istio/rbac/v1alpha1/rbacconfigs" + proto: "istio.rbac.v1alpha1.RbacConfig" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "istio/rbac/v1alpha1/servicerolebindings" + proto: "istio.rbac.v1alpha1.ServiceRoleBindings" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "istio/rbac/v1alpha1/serviceroles" + proto: "istio.rbac.v1alpha1.ServiceRole" + protoPackage: "istio.io/api/rbac/v1alpha1" + + ### K8s collections ### + + # Built-in K8s collections + - name: "k8s/core/v1/endpoints" + proto: "k8s.io.api.core.v1.Endpoints" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/namespaces" + proto: "k8s.io.api.core.v1.NamespaceSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/nodes" + proto: "k8s.io.api.core.v1.NodeSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/pods" + proto: "k8s.io.api.core.v1.Pod" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/services" + proto: "k8s.io.api.core.v1.ServiceSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/extensions/v1beta1/ingresses" + proto: "k8s.io.api.extensions.v1beta1.IngressSpec" + protoPackage: "k8s.io/api/extensions/v1beta1" + + # Istio CRD collections + - name: "k8s/authentication.istio.io/v1alpha1/meshpolicies" + proto: "istio.authentication.v1alpha1.Policy" + protoPackage: "istio.io/api/authentication/v1alpha1" + + - name: "k8s/authentication.istio.io/v1alpha1/policies" + proto: "istio.authentication.v1alpha1.Policy" + protoPackage: "istio.io/api/authentication/v1alpha1" + + - name: "k8s/config.istio.io/v1alpha2/adapters" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/attributemanifests" + proto: "istio.policy.v1beta1.AttributeManifest" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "k8s/config.istio.io/v1alpha2/httpapispecs" + proto: "istio.mixer.v1.config.client.HTTPAPISpec" + + - name: "k8s/config.istio.io/v1alpha2/httpapispecbindings" + proto: "istio.mixer.v1.config.client.HTTPAPISpecBinding" + + - name: "k8s/config.istio.io/v1alpha2/instances" + proto: "istio.policy.v1beta1.Instance" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "k8s/config.istio.io/v1alpha2/templates" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/quotaspecs" + proto: "istio.mixer.v1.config.client.QuotaSpec" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "k8s/config.istio.io/v1alpha2/quotaspecbindings" + proto: "istio.mixer.v1.config.client.QuotaSpecBinding" + protoPackage: "istio.io/api/mixer/v1/config/client" + + - name: "k8s/config.istio.io/v1alpha2/rules" + proto: "istio.policy.v1beta1.Rule" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "k8s/networking.istio.io/v1alpha3/destinationrules" + proto: "istio.networking.v1alpha3.DestinationRule" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/networking.istio.io/v1alpha3/envoyfilters" + proto: "istio.networking.v1alpha3.EnvoyFilter" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/networking.istio.io/v1alpha3/gateways" + proto: "istio.networking.v1alpha3.Gateway" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/networking.istio.io/v1alpha3/serviceentries" + proto: "istio.networking.v1alpha3.ServiceEntry" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/networking.istio.io/v1alpha3/sidecars" + proto: "istio.networking.v1alpha3.Sidecar" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/networking.istio.io/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "k8s/config.istio.io/v1alpha2/handlers" + proto: "istio.policy.v1beta1.Handler" + protoPackage: "istio.io/api/policy/v1beta1" + + - name: "k8s/rbac.istio.io/v1alpha1/authorizationpolicies" + proto: "istio.rbac.v1alpha1.AuthorizationPolicy" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "k8s/rbac.istio.io/v1alpha1/clusterrbacconfigs" + proto: "istio.rbac.v1alpha1.RbacConfig" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "k8s/rbac.istio.io/v1alpha1/policy" + proto: "istio.rbac.v1alpha1.ServiceRoleBinding" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "k8s/rbac.istio.io/v1alpha1/rbacconfigs" + proto: "istio.rbac.v1alpha1.RbacConfig" + protoPackage: "istio.io/api/rbac/v1alpha1" + + - name: "k8s/rbac.istio.io/v1alpha1/serviceroles" + proto: "istio.rbac.v1alpha1.ServiceRole" + protoPackage: "istio.io/api/rbac/v1alpha1" + + # Keep Legacy Mixer CRD related collections separate, as these will be gone soon. + - name: "k8s/config.istio.io/v1alpha2/apikeys" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/apikeys" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/authorizations" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/authorizations" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/bypasses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/bypasses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/checknothings" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/checknothings" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/circonuses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/circonuses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/cloudwatches" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/cloudwatches" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/deniers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/deniers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/dogstatsds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/dogstatsds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/edges" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/edges" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/fluentds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/fluentds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/kuberneteses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/kuberneteses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/kubernetesenvs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/kubernetesenvs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/listcheckers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/listcheckers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/listentries" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/listentries" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/logentries" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/logentries" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/memquotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/memquotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/metrics" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/metrics" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/noops" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/noops" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/opas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/opas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/prometheuses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/prometheuses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/quotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/quotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/rbacs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/rbacs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/redisquotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/redisquotas" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/reportnothings" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/reportnothings" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/signalfxs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/signalfxs" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/solarwindses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/solarwindses" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/stackdrivers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/stackdrivers" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/statsds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/statsds" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/stdios" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/stdios" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/tracespans" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/tracespans" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "k8s/config.istio.io/v1alpha2/zipkins" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "istio/config/v1alpha2/legacy/zipkins" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + +# The snapshots to generate +snapshots: + - name: "default" + strategy: debounce + collections: + - "istio/authentication/v1alpha1/meshpolicies" + - "istio/authentication/v1alpha1/policies" + - "istio/config/v1alpha2/adapters" + - "istio/config/v1alpha2/httpapispecs" + - "istio/config/v1alpha2/httpapispecbindings" + - "istio/config/v1alpha2/templates" + - "istio/mesh/v1alpha1/MeshConfig" + - "istio/mixer/v1/config/client/quotaspecbindings" + - "istio/mixer/v1/config/client/quotaspecs" + - "istio/networking/v1alpha3/destinationrules" + - "istio/networking/v1alpha3/envoyfilters" + - "istio/networking/v1alpha3/gateways" + - "istio/networking/v1alpha3/serviceentries" + - "istio/networking/v1alpha3/sidecars" + - "istio/networking/v1alpha3/virtualservices" + - "istio/policy/v1beta1/attributemanifests" + - "istio/policy/v1beta1/handlers" + - "istio/policy/v1beta1/instances" + - "istio/policy/v1beta1/rules" + - "istio/rbac/v1alpha1/authorizationpolicies" + - "istio/rbac/v1alpha1/clusterrbacconfigs" + - "istio/rbac/v1alpha1/rbacconfigs" + - "istio/rbac/v1alpha1/servicerolebindings" + - "istio/rbac/v1alpha1/serviceroles" + - "k8s/core/v1/namespaces" + - "k8s/core/v1/services" + # Legacy Mixer CRDs + - "istio/config/v1alpha2/legacy/apikeys" + - "istio/config/v1alpha2/legacy/authorizations" + - "istio/config/v1alpha2/legacy/bypasses" + - "istio/config/v1alpha2/legacy/checknothings" + - "istio/config/v1alpha2/legacy/circonuses" + - "istio/config/v1alpha2/legacy/cloudwatches" + - "istio/config/v1alpha2/legacy/deniers" + - "istio/config/v1alpha2/legacy/dogstatsds" + - "istio/config/v1alpha2/legacy/edges" + - "istio/config/v1alpha2/legacy/fluentds" + - "istio/config/v1alpha2/legacy/kuberneteses" + - "istio/config/v1alpha2/legacy/kubernetesenvs" + - "istio/config/v1alpha2/legacy/listcheckers" + - "istio/config/v1alpha2/legacy/listentries" + - "istio/config/v1alpha2/legacy/logentries" + - "istio/config/v1alpha2/legacy/memquotas" + - "istio/config/v1alpha2/legacy/metrics" + - "istio/config/v1alpha2/legacy/noops" + - "istio/config/v1alpha2/legacy/opas" + - "istio/config/v1alpha2/legacy/prometheuses" + - "istio/config/v1alpha2/legacy/quotas" + - "istio/config/v1alpha2/legacy/rbacs" + - "istio/config/v1alpha2/legacy/redisquotas" + - "istio/config/v1alpha2/legacy/reportnothings" + - "istio/config/v1alpha2/legacy/signalfxs" + - "istio/config/v1alpha2/legacy/solarwindses" + - "istio/config/v1alpha2/legacy/stackdrivers" + - "istio/config/v1alpha2/legacy/statsds" + - "istio/config/v1alpha2/legacy/stdios" + - "istio/config/v1alpha2/legacy/tracespans" + - "istio/config/v1alpha2/legacy/zipkins" + + - name: "syntheticServiceEntry" + strategy: immediate + collections: + - "istio/networking/v1alpha3/synthetic/serviceentries" + +# Configuration for input sources +sources: + # Kubernetes specific configuration. + - type: kubernetes + resources: + - collection: "k8s/extensions/v1beta1/ingresses" + kind: "Ingress" + plural: "ingresses" + group: "extensions" + version: "v1beta1" + + - collection: "k8s/core/v1/services" + kind: "Service" + plural: "services" + version: "v1" + + - collection: "k8s/core/v1/namespaces" + kind: "Namespace" + plural: "namespaces" + version: "v1" + + - collection: "k8s/core/v1/nodes" + kind: "Node" + plural: "nodes" + version: "v1" + + - collection: "k8s/core/v1/pods" + kind: "Pod" + plural: "pods" + version: "v1" + + - collection: "k8s/core/v1/endpoints" + kind: "Endpoints" + plural: "endpoints" + version: "v1" + + - collection: "k8s/networking.istio.io/v1alpha3/virtualservices" + kind: "VirtualService" + plural: "virtualservices" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/networking.istio.io/v1alpha3/gateways" + kind: "Gateway" + plural: "gateways" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/networking.istio.io/v1alpha3/serviceentries" + kind: "ServiceEntry" + plural: "serviceentries" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/networking.istio.io/v1alpha3/destinationrules" + kind: "DestinationRule" + plural: "destinationrules" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/networking.istio.io/v1alpha3/envoyfilters" + kind: "EnvoyFilter" + plural: "envoyfilters" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/networking.istio.io/v1alpha3/sidecars" + kind: "Sidecar" + plural: "sidecars" + group: "networking.istio.io" + version: "v1alpha3" + + - collection: "k8s/config.istio.io/v1alpha2/httpapispecs" + kind: "HTTPAPISpec" + plural: "httpapispecs" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/httpapispecbindings" + kind: "HTTPAPISpecBinding" + plural: "httpapispecbindings" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/quotaspecs" + kind: "QuotaSpec" + plural: "quotaspecs" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/quotaspecbindings" + kind: "QuotaSpecBinding" + plural: "quotaspecbindings" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/authentication.istio.io/v1alpha1/policies" + kind: "Policy" + plural: "policies" + group: "authentication.istio.io" + version: "v1alpha1" + + - collection: "k8s/authentication.istio.io/v1alpha1/meshpolicies" + kind: "MeshPolicy" + plural: "meshpolicies" + group: "authentication.istio.io" + version: "v1alpha1" + + - collection: "k8s/rbac.istio.io/v1alpha1/serviceroles" + kind: "ServiceRole" + plural: "serviceroles" + group: "rbac.istio.io" + version: "v1alpha1" + + - collection: "k8s/rbac.istio.io/v1alpha1/policy" + kind: "ServiceRoleBinding" + plural: "servicerolebindings" + group: "rbac.istio.io" + version: "v1alpha1" + + - collection: "k8s/rbac.istio.io/v1alpha1/authorizationpolicies" + kind: "AuthorizationPolicy" + plural: "authorizationpolicies" + group: "rbac.istio.io" + version: "v1alpha1" + + - collection: "k8s/rbac.istio.io/v1alpha1/rbacconfigs" + kind: "RbacConfig" + plural: "rbacconfigs" + group: "rbac.istio.io" + version: "v1alpha1" + + - collection: "k8s/rbac.istio.io/v1alpha1/clusterrbacconfigs" + kind: "ClusterRbacConfig" + plural: "clusterrbacconfigs" + group: "rbac.istio.io" + version: "v1alpha1" + + - collection: "k8s/config.istio.io/v1alpha2/rules" + kind: "rule" + plural: "rules" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/attributemanifests" + kind: "attributemanifest" + plural: "attributemanifests" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/instances" + kind: "instance" + plural: "instances" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/handlers" + kind: "handler" + plural: "handlers" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/templates" + kind: "template" + plural: "templates" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/config.istio.io/v1alpha2/adapters" + kind: "adapter" + plural: "adapters" + group: "config.istio.io" + version: "v1alpha2" + + - collection: "k8s/authentication.istio.io/v1alpha1/meshpolicies" + kind: "MeshPolicy" + plural: "meshpolicies" + group: "authentication.istio.io" + version: "v1alpha1" + + # Legacy Mixer CRD Types + - collection: "k8s/config.istio.io/v1alpha2/apikeys" + kind: "apikey" + plural: "apikeys" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/authorizations" + kind: "authorization" + plural: "authorizations" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/bypasses" + kind: "bypass" + plural: "bypasses" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/checknothings" + kind: "checknothing" + plural: "checknothings" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/circonuses" + kind: "circonus" + plural: "circonuses" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/cloudwatches" + kind: "cloudwatch" + plural: "cloudwatches" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/deniers" + kind: "denier" + plural: "deniers" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/dogstatsds" + kind: "dogstatsd" + plural: "dogstatsds" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/edges" + kind: "edge" + plural: "edges" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/fluentds" + kind: "fluentd" + plural: "fluentds" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/kuberneteses" + kind: "kubernetes" + plural: "kuberneteses" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/kubernetesenvs" + kind: "kubernetesenv" + plural: "kubernetesenvs" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/listcheckers" + kind: "listchecker" + plural: "listcheckers" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/listentries" + kind: "listentry" + plural: "listentries" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/logentries" + kind: "logentry" + plural: "logentries" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/memquotas" + kind: "memquota" + plural: "memquotas" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/metrics" + kind: "metric" + plural: "metrics" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/noops" + kind: "noop" + plural: "noops" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/opas" + kind: "opa" + plural: "opas" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/prometheuses" + kind: "prometheus" + plural: "prometheuses" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/quotas" + kind: "quota" + plural: "quotas" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/rbacs" + kind: "rbac" + plural: "rbacs" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/redisquotas" + kind: "redisquota" + plural: "redisquotas" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/reportnothings" + kind: "reportnothing" + plural: "reportnothings" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/signalfxs" + kind: "signalfx" + plural: "signalfxs" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/solarwindses" + kind: "solarwinds" + plural: "solarwindses" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/stackdrivers" + kind: "stackdriver" + plural: "stackdrivers" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/statsds" + kind: "statsd" + plural: "statsds" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/stdios" + kind: "stdio" + plural: "stdios" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/tracespans" + kind: "tracespan" + plural: "tracespans" + group: "config.istio.io" + version: "v1alpha2" + optional: true + + - collection: "k8s/config.istio.io/v1alpha2/zipkins" + kind: "zipkin" + plural: "zipkins" + group: "config.istio.io" + version: "v1alpha2" + optional: true + +# Transform specific configurations +transforms: + - type: direct + mapping: + "k8s/config.istio.io/v1alpha2/adapters": "istio/config/v1alpha2/adapters" + "k8s/config.istio.io/v1alpha2/attributemanifests": "istio/policy/v1beta1/attributemanifests" + "k8s/config.istio.io/v1alpha2/handlers": "istio/policy/v1beta1/handlers" + "k8s/config.istio.io/v1alpha2/httpapispecs": "istio/config/v1alpha2/httpapispecs" + "k8s/config.istio.io/v1alpha2/httpapispecbindings": "istio/config/v1alpha2/httpapispecbindings" + "k8s/config.istio.io/v1alpha2/instances": "istio/policy/v1beta1/instances" + "k8s/config.istio.io/v1alpha2/quotaspecs": "istio/mixer/v1/config/client/quotaspecs" + "k8s/config.istio.io/v1alpha2/quotaspecbindings": "istio/mixer/v1/config/client/quotaspecbindings" + "k8s/config.istio.io/v1alpha2/rules": "istio/policy/v1beta1/rules" + "k8s/config.istio.io/v1alpha2/templates": "istio/config/v1alpha2/templates" + "k8s/networking.istio.io/v1alpha3/destinationrules": "istio/networking/v1alpha3/destinationrules" + "k8s/networking.istio.io/v1alpha3/envoyfilters": "istio/networking/v1alpha3/envoyfilters" + "k8s/networking.istio.io/v1alpha3/gateways": "istio/networking/v1alpha3/gateways" + "k8s/networking.istio.io/v1alpha3/serviceentries": "istio/networking/v1alpha3/serviceentries" + "k8s/networking.istio.io/v1alpha3/sidecars": "istio/networking/v1alpha3/sidecars" + "k8s/networking.istio.io/v1alpha3/virtualservices": "istio/networking/v1alpha3/virtualservices" + "k8s/rbac.istio.io/v1alpha1/authorizationpolicies": "istio/rbac/v1alpha1/authorizationpolicies" + "k8s/rbac.istio.io/v1alpha1/policy": "istio/rbac/v1alpha1/servicerolebindings" + "k8s/rbac.istio.io/v1alpha1/rbacconfigs": "istio/rbac/v1alpha1/rbacconfigs" + "k8s/rbac.istio.io/v1alpha1/clusterrbacconfigs": "istio/rbac/v1alpha1/clusterrbacconfigs" + "k8s/rbac.istio.io/v1alpha1/serviceroles": "istio/rbac/v1alpha1/serviceroles" + "k8s/core/v1/namespaces": "k8s/core/v1/namespaces" + "k8s/core/v1/services": "k8s/core/v1/services" + "istio/mesh/v1alpha1/MeshConfig": "istio/mesh/v1alpha1/MeshConfig" + + # Legacy Mixer CRD mappings + "k8s/config.istio.io/v1alpha2/apikeys": "istio/config/v1alpha2/legacy/apikeys" + "k8s/config.istio.io/v1alpha2/authorizations": "istio/config/v1alpha2/legacy/authorizations" + "k8s/config.istio.io/v1alpha2/bypasses": "istio/config/v1alpha2/legacy/bypasses" + "k8s/config.istio.io/v1alpha2/checknothings": "istio/config/v1alpha2/legacy/checknothings" + "k8s/config.istio.io/v1alpha2/circonuses": "istio/config/v1alpha2/legacy/circonuses" + "k8s/config.istio.io/v1alpha2/cloudwatches": "istio/config/v1alpha2/legacy/cloudwatches" + "k8s/config.istio.io/v1alpha2/deniers": "istio/config/v1alpha2/legacy/deniers" + "k8s/config.istio.io/v1alpha2/dogstatsds": "istio/config/v1alpha2/legacy/dogstatsds" + "k8s/config.istio.io/v1alpha2/edges": "istio/config/v1alpha2/legacy/edges" + "k8s/config.istio.io/v1alpha2/fluentds": "istio/config/v1alpha2/legacy/fluentds" + "k8s/config.istio.io/v1alpha2/kuberneteses": "istio/config/v1alpha2/legacy/kuberneteses" + "k8s/config.istio.io/v1alpha2/kubernetesenvs": "istio/config/v1alpha2/legacy/kubernetesenvs" + "k8s/config.istio.io/v1alpha2/listcheckers": "istio/config/v1alpha2/legacy/listcheckers" + "k8s/config.istio.io/v1alpha2/listentries": "istio/config/v1alpha2/legacy/listentries" + "k8s/config.istio.io/v1alpha2/logentries": "istio/config/v1alpha2/legacy/logentries" + "k8s/config.istio.io/v1alpha2/memquotas": "istio/config/v1alpha2/legacy/memquotas" + "k8s/config.istio.io/v1alpha2/metrics": "istio/config/v1alpha2/legacy/metrics" + "k8s/config.istio.io/v1alpha2/noops": "istio/config/v1alpha2/legacy/noops" + "k8s/config.istio.io/v1alpha2/opas": "istio/config/v1alpha2/legacy/opas" + "k8s/config.istio.io/v1alpha2/prometheuses": "istio/config/v1alpha2/legacy/prometheuses" + "k8s/config.istio.io/v1alpha2/quotas": "istio/config/v1alpha2/legacy/quotas" + "k8s/config.istio.io/v1alpha2/rbacs": "istio/config/v1alpha2/legacy/rbacs" + "k8s/config.istio.io/v1alpha2/redisquotas": "istio/config/v1alpha2/legacy/redisquotas" + "k8s/config.istio.io/v1alpha2/reportnothings": "istio/config/v1alpha2/legacy/reportnothings" + "k8s/config.istio.io/v1alpha2/signalfxs": "istio/config/v1alpha2/legacy/signalfxs" + "k8s/config.istio.io/v1alpha2/solarwindses": "istio/config/v1alpha2/legacy/solarwindses" + "k8s/config.istio.io/v1alpha2/stackdrivers": "istio/config/v1alpha2/legacy/stackdrivers" + "k8s/config.istio.io/v1alpha2/statsds": "istio/config/v1alpha2/legacy/statsds" + "k8s/config.istio.io/v1alpha2/stdios": "istio/config/v1alpha2/legacy/stdios" + "k8s/config.istio.io/v1alpha2/tracespans": "istio/config/v1alpha2/legacy/tracespans" + "k8s/config.istio.io/v1alpha2/zipkins": "istio/config/v1alpha2/legacy/zipkins" diff --git a/galley/pkg/config/processor/metadata/metadata_test.go b/galley/pkg/config/processor/metadata/metadata_test.go new file mode 100644 index 000000000000..a63ac2368737 --- /dev/null +++ b/galley/pkg/config/processor/metadata/metadata_test.go @@ -0,0 +1,29 @@ +// 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 metadata + +import ( + "testing" + + "istio.io/istio/galley/pkg/config/meshcfg" +) + +func TestMeshConfigNameValidity(t *testing.T) { + m := MustGet() + _, found := m.Collections().Lookup(meshcfg.IstioMeshconfig.String()) + if !found { + t.Fatalf("Mesh config collection not found in metadata.") + } +} diff --git a/galley/pkg/config/processor/metadata/staticinit.gen.go b/galley/pkg/config/processor/metadata/staticinit.gen.go new file mode 100755 index 000000000000..1d268a89f056 --- /dev/null +++ b/galley/pkg/config/processor/metadata/staticinit.gen.go @@ -0,0 +1,35 @@ +// GENERATED FILE -- DO NOT EDIT +// + +package metadata + +import ( + // Pull in all the known proto types to ensure we get their types registered. + + // Register protos in "github.com/gogo/protobuf/types" + _ "github.com/gogo/protobuf/types" + + // Register protos in "istio.io/api/authentication/v1alpha1" + _ "istio.io/api/authentication/v1alpha1" + + // Register protos in "istio.io/api/mesh/v1alpha1" + _ "istio.io/api/mesh/v1alpha1" + + // Register protos in "istio.io/api/mixer/v1/config/client" + _ "istio.io/api/mixer/v1/config/client" + + // Register protos in "istio.io/api/networking/v1alpha3" + _ "istio.io/api/networking/v1alpha3" + + // Register protos in "istio.io/api/policy/v1beta1" + _ "istio.io/api/policy/v1beta1" + + // Register protos in "istio.io/api/rbac/v1alpha1" + _ "istio.io/api/rbac/v1alpha1" + + // Register protos in "k8s.io/api/core/v1" + _ "k8s.io/api/core/v1" + + // Register protos in "k8s.io/api/extensions/v1beta1" + _ "k8s.io/api/extensions/v1beta1" +) diff --git a/galley/pkg/config/processor/transforms/authpolicy/create.go b/galley/pkg/config/processor/transforms/authpolicy/create.go new file mode 100644 index 000000000000..e9657dc61e2f --- /dev/null +++ b/galley/pkg/config/processor/transforms/authpolicy/create.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 authpolicy + +import ( + "reflect" + + authn "istio.io/api/authentication/v1alpha1" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/scope" +) + +// Create a new Direct transformer. +func Create() []event.Transformer { + return []event.Transformer{ + event.NewFnTransform( + collection.Names{metadata.K8SAuthenticationIstioIoV1Alpha1Policies}, + collection.Names{metadata.IstioAuthenticationV1Alpha1Policies}, + nil, + nil, + handler(metadata.IstioAuthenticationV1Alpha1Policies), + ), + event.NewFnTransform( + collection.Names{metadata.K8SAuthenticationIstioIoV1Alpha1Meshpolicies}, + collection.Names{metadata.IstioAuthenticationV1Alpha1Meshpolicies}, + nil, + nil, + handler(metadata.IstioAuthenticationV1Alpha1Meshpolicies), + ), + } +} + +func handler(destination collection.Name) func(e event.Event, h event.Handler) { + return func(e event.Event, h event.Handler) { + e = e.WithSource(destination) + + if e.Entry != nil && e.Entry.Item != nil { + policy, ok := e.Entry.Item.(*authn.Policy) + if !ok { + scope.Processing.Errorf("unexpected proto found when converting authn.Policy: %v", reflect.TypeOf(e.Entry.Item)) + return + } + + // The pilot authentication plugin's config handling allows the mtls + // peer method object value to be nil. See pilot/pkg/networking/plugin/authn/authentication.go#L68 + // + // For example, + // + // metadata: + // name: d-ports-mtls-enabled + // spec: + // targets: + // - name: d + // ports: + // - number: 80 + // peers: + // - mtls: + // + // This translates to the following in-memory representation: + // + // policy := &authn.Policy{ + // Peers: []*authn.PeerAuthenticationMethod{{ + // &authn.PeerAuthenticationMethod_Mtls{}, + // }}, + // } + // + // The PeerAuthenticationMethod_Mtls object with nil field is lost when + // the proto is re-encoded for transport via MCP. As a workaround, fill + // in the missing field value which is functionality equivalent. + for _, peer := range policy.Peers { + if mtls, ok := peer.Params.(*authn.PeerAuthenticationMethod_Mtls); ok && mtls.Mtls == nil { + mtls.Mtls = &authn.MutualTls{} + } + } + } + + h.Handle(e) + } +} diff --git a/galley/pkg/config/processor/transforms/authpolicy/create_test.go b/galley/pkg/config/processor/transforms/authpolicy/create_test.go new file mode 100644 index 000000000000..37c866ed9fdb --- /dev/null +++ b/galley/pkg/config/processor/transforms/authpolicy/create_test.go @@ -0,0 +1,344 @@ +// 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 authpolicy + +import ( + "testing" + + "github.com/gogo/protobuf/types" + . "github.com/onsi/gomega" + + authn "istio.io/api/authentication/v1alpha1" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestAuthPolicy_Input_Output(t *testing.T) { + g := NewGomegaWithT(t) + + xform, _, _ := setup(g, 0) + + g.Expect(xform.Inputs()).To(Equal(collection.Names{ + metadata.K8SAuthenticationIstioIoV1Alpha1Policies, + })) + g.Expect(xform.Outputs()).To(Equal(collection.Names{ + metadata.IstioAuthenticationV1Alpha1Policies, + })) + + xform, _, _ = setup(g, 1) + + g.Expect(xform.Inputs()).To(Equal(collection.Names{ + metadata.K8SAuthenticationIstioIoV1Alpha1Meshpolicies, + })) + g.Expect(xform.Outputs()).To(Equal(collection.Names{ + metadata.IstioAuthenticationV1Alpha1Meshpolicies, + })) +} + +func TestAuthPolicy_AddSync(t *testing.T) { + g := NewGomegaWithT(t) + + for i := 0; i < 2; i++ { + xform, src, acc := setup(g, i) + + acc.Clear() + xform.Start() + + src.Handlers.Handle(event.AddFor(xform.Inputs()[0], input())) + src.Handlers.Handle(event.FullSyncFor(xform.Inputs()[0])) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(xform.Outputs()[0], output()), + event.FullSyncFor(xform.Outputs()[0]), + )) + xform.Stop() + } +} + +func TestAuthPolicy_SyncAdd(t *testing.T) { + g := NewGomegaWithT(t) + + for i := 0; i < 2; i++ { + xform, src, acc := setup(g, i) + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(xform.Inputs()[0])) + src.Handlers.Handle(event.AddFor(xform.Inputs()[0], input())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(xform.Outputs()[0]), + event.AddFor(xform.Outputs()[0], output()), + )) + + xform.Stop() + } +} + +func TestAuthPolicy_AddUpdateDelete(t *testing.T) { + g := NewGomegaWithT(t) + + r2 := input() + r2.Item.(*authn.Policy).OriginIsOptional = true + + for i := 0; i < 2; i++ { + xform, src, acc := setup(g, i) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(xform.Inputs()[0])) + src.Handlers.Handle(event.AddFor(xform.Inputs()[0], input())) + src.Handlers.Handle(event.UpdateFor(xform.Inputs()[0], r2)) + src.Handlers.Handle(event.DeleteForResource(xform.Inputs()[0], r2)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(xform.Outputs()[0]), + event.AddFor(xform.Outputs()[0], output()), + event.UpdateFor(xform.Outputs()[0], r2), + event.DeleteForResource(xform.Outputs()[0], r2), + )) + xform.Stop() + } +} + +func TestAuthPolicy_SyncReset(t *testing.T) { + g := NewGomegaWithT(t) + + for i := 0; i < 2; i++ { + xform, src, acc := setup(g, i) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(xform.Inputs()[0])) + src.Handlers.Handle(event.Event{Kind: event.Reset}) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(xform.Outputs()[0]), + event.Event{Kind: event.Reset}, + )) + + xform.Stop() + } +} + +func TestAuthPolicy_InvalidEventKind(t *testing.T) { + g := NewGomegaWithT(t) + + for i := 0; i < 2; i++ { + xform, src, acc := setup(g, i) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(xform.Inputs()[0])) + src.Handlers.Handle(event.Event{Kind: 55}) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(xform.Outputs()[0]), + )) + + xform.Stop() + } +} + +func TestAuthPolicy_NoListeners(t *testing.T) { + g := NewGomegaWithT(t) + + for i := 0; i < 2; i++ { + xforms := Create() + g.Expect(xforms).To(HaveLen(2)) + + src := &fixtures.Source{} + xform := xforms[i] + src.Dispatch(xform) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(xform.Inputs()[0])) + src.Handlers.Handle(event.Event{Kind: event.Reset}) + src.Handlers.Handle(event.AddFor(xform.Inputs()[0], input())) + + // No crash + xform.Stop() + } +} + +func TestAuthPolicy_DoubleStart(t *testing.T) { + g := NewGomegaWithT(t) + + for i := 0; i < 2; i++ { + xform, src, acc := setup(g, i) + + xform.Start() + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(xform.Inputs()[0])) + src.Handlers.Handle(event.AddFor(xform.Inputs()[0], input())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(xform.Outputs()[0], output()), + event.FullSyncFor(xform.Outputs()[0]), + )) + xform.Stop() + } +} + +func TestAuthPolicy_DoubleStop(t *testing.T) { + g := NewGomegaWithT(t) + + for i := 0; i < 2; i++ { + xform, src, acc := setup(g, i) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(xform.Inputs()[0])) + src.Handlers.Handle(event.AddFor(xform.Inputs()[0], input())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(xform.Outputs()[0], output()), + event.FullSyncFor(xform.Outputs()[0]), + )) + + acc.Clear() + + xform.Stop() + xform.Stop() + + g.Consistently(acc.Events).Should(BeEmpty()) + } +} + +func TestAuthPolicy_StartStopStartStop(t *testing.T) { + g := NewGomegaWithT(t) + + for i := 0; i < 2; i++ { + xform, src, acc := setup(g, i) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(xform.Inputs()[0])) + src.Handlers.Handle(event.AddFor(xform.Inputs()[0], input())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(xform.Outputs()[0], output()), + event.FullSyncFor(xform.Outputs()[0]), + )) + + acc.Clear() + xform.Stop() + g.Consistently(acc.Events).Should(BeEmpty()) + + xform.Start() + src.Handlers.Handle(event.FullSyncFor(xform.Inputs()[0])) + src.Handlers.Handle(event.AddFor(xform.Inputs()[0], input())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(xform.Outputs()[0], output()), + event.FullSyncFor(xform.Outputs()[0]), + )) + + acc.Clear() + xform.Stop() + g.Consistently(acc.Events).Should(BeEmpty()) + } +} + +func TestAuthPolicy_InvalidEvent(t *testing.T) { + g := NewGomegaWithT(t) + + for i := 0; i < 2; i++ { + xform, src, acc := setup(g, i) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(xform.Outputs()[0])) // Send output events + src.Handlers.Handle(event.AddFor(xform.Outputs()[0], input())) + + g.Consistently(acc.Events).Should(BeEmpty()) + xform.Stop() + } +} + +func TestAuthPolicy_InvalidProto(t *testing.T) { + g := NewGomegaWithT(t) + + r := input() + r.Item = &types.Struct{} + + for i := 0; i < 2; i++ { + xform, src, acc := setup(g, i) + + acc.Clear() + xform.Start() + + src.Handlers.Handle(event.AddFor(xform.Inputs()[0], r)) + src.Handlers.Handle(event.FullSyncFor(xform.Inputs()[0])) + + g.Eventually(acc.Events).Should(ConsistOf( // No add event + event.FullSyncFor(xform.Outputs()[0]), + )) + xform.Stop() + } +} + +func setup(g *GomegaWithT, i int) (event.Transformer, *fixtures.Source, *fixtures.Accumulator) { + xforms := Create() + g.Expect(xforms).To(HaveLen(2)) + + src := &fixtures.Source{} + acc := &fixtures.Accumulator{} + src.Dispatch(xforms[i]) + xforms[i].DispatchFor(xforms[i].Outputs()[0], acc) + + return xforms[i], src, acc +} + +func input() *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns", "ap"), + }, + Item: &authn.Policy{ + PeerIsOptional: true, + Peers: []*authn.PeerAuthenticationMethod{ + { + Params: &authn.PeerAuthenticationMethod_Mtls{ + Mtls: nil, // This is what the conversion is all about... + }, + }, + }, + }, + } +} + +func output() *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns", "ap"), + }, + Item: &authn.Policy{ + PeerIsOptional: true, + Peers: []*authn.PeerAuthenticationMethod{ + { + Params: &authn.PeerAuthenticationMethod_Mtls{ + Mtls: &authn.MutualTls{}, + }, + }, + }, + }, + } +} diff --git a/galley/pkg/config/processor/transforms/direct/create.go b/galley/pkg/config/processor/transforms/direct/create.go new file mode 100644 index 000000000000..c0641075e901 --- /dev/null +++ b/galley/pkg/config/processor/transforms/direct/create.go @@ -0,0 +1,41 @@ +// 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 direct + +import ( + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" +) + +// Create a new Direct transformer. +func Create(mapping map[collection.Name]collection.Name) []event.Transformer { + var result []event.Transformer + + for k, v := range mapping { + from := k + to := v + xform := event.NewFnTransform( + collection.Names{from}, + collection.Names{to}, + nil, + nil, + func(e event.Event, h event.Handler) { + e = e.WithSource(to) + h.Handle(e) + }) + result = append(result, xform) + } + return result +} diff --git a/galley/pkg/config/processor/transforms/direct/create_test.go b/galley/pkg/config/processor/transforms/direct/create_test.go new file mode 100644 index 000000000000..892e1f30adc7 --- /dev/null +++ b/galley/pkg/config/processor/transforms/direct/create_test.go @@ -0,0 +1,246 @@ +// 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 direct + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/testing/basicmeta" + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestDirect_Input_Output(t *testing.T) { + g := NewGomegaWithT(t) + + xform, _, _ := setup(g) + + g.Expect(xform.Inputs()).To(Equal(collection.Names{basicmeta.Collection1})) + g.Expect(xform.Outputs()).To(Equal(collection.Names{basicmeta.Collection2})) +} + +func TestDirect_AddSync(t *testing.T) { + g := NewGomegaWithT(t) + + xform, src, acc := setup(g) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.AddFor(basicmeta.Collection1, data.EntryN1I1V1)) + src.Handlers.Handle(event.FullSyncFor(basicmeta.Collection1)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(basicmeta.Collection2, data.EntryN1I1V1), // XForm to Collection2 + event.FullSyncFor(basicmeta.Collection2), + )) +} + +func TestDirect_SyncAdd(t *testing.T) { + g := NewGomegaWithT(t) + + xform, src, acc := setup(g) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(basicmeta.Collection1)) + src.Handlers.Handle(event.AddFor(basicmeta.Collection1, data.EntryN1I1V1)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(basicmeta.Collection2, data.EntryN1I1V1), // XForm to Collection2 + event.FullSyncFor(basicmeta.Collection2), + )) +} + +func TestDirect_AddUpdateDelete(t *testing.T) { + g := NewGomegaWithT(t) + + xform, src, acc := setup(g) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(basicmeta.Collection1)) + src.Handlers.Handle(event.AddFor(basicmeta.Collection1, data.EntryN1I1V1)) + src.Handlers.Handle(event.UpdateFor(basicmeta.Collection1, data.EntryN1I1V2)) + src.Handlers.Handle(event.DeleteForResource(basicmeta.Collection1, data.EntryN1I1V2)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection2), + event.AddFor(basicmeta.Collection2, data.EntryN1I1V1), + event.UpdateFor(basicmeta.Collection2, data.EntryN1I1V2), + event.DeleteForResource(basicmeta.Collection2, data.EntryN1I1V2), + )) +} + +func TestDirect_SyncReset(t *testing.T) { + g := NewGomegaWithT(t) + + xform, src, acc := setup(g) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(basicmeta.Collection1)) + src.Handlers.Handle(event.Event{Kind: event.Reset}) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection2), + event.Event{Kind: event.Reset}, + )) +} + +func TestDirect_InvalidEventKind(t *testing.T) { + g := NewGomegaWithT(t) + + xform, src, acc := setup(g) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(basicmeta.Collection1)) + src.Handlers.Handle(event.Event{Kind: 55}) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection2), + )) +} + +func TestDirect_NoListeners(t *testing.T) { + g := NewGomegaWithT(t) + + xforms := Create(basicmeta.MustGet().DirectTransform().Mapping()) + g.Expect(xforms).To(HaveLen(1)) + + src := &fixtures.Source{} + xform := xforms[0] + src.Dispatch(xform) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(basicmeta.Collection1)) + src.Handlers.Handle(event.Event{Kind: event.Reset}) + src.Handlers.Handle(event.AddFor(basicmeta.Collection1, data.EntryN1I1V1)) + + // No crash +} + +func TestDirect_DoubleStart(t *testing.T) { + g := NewGomegaWithT(t) + + xform, src, acc := setup(g) + + xform.Start() + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(basicmeta.Collection1)) + src.Handlers.Handle(event.AddFor(basicmeta.Collection1, data.EntryN1I1V1)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(basicmeta.Collection2, data.EntryN1I1V1), // XForm to Collection2 + event.FullSyncFor(basicmeta.Collection2), + )) +} + +func TestDirect_DoubleStop(t *testing.T) { + g := NewGomegaWithT(t) + + xform, src, acc := setup(g) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(basicmeta.Collection1)) + src.Handlers.Handle(event.AddFor(basicmeta.Collection1, data.EntryN1I1V1)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(basicmeta.Collection2, data.EntryN1I1V1), // XForm to Collection2 + event.FullSyncFor(basicmeta.Collection2), + )) + + acc.Clear() + + xform.Stop() + xform.Stop() + + g.Consistently(acc.Events).Should(BeEmpty()) +} + +func TestDirect_StartStopStartStop(t *testing.T) { + g := NewGomegaWithT(t) + + xform, src, acc := setup(g) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(basicmeta.Collection1)) + src.Handlers.Handle(event.AddFor(basicmeta.Collection1, data.EntryN1I1V1)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(basicmeta.Collection2, data.EntryN1I1V1), // XForm to Collection2 + event.FullSyncFor(basicmeta.Collection2), + )) + + acc.Clear() + xform.Stop() + g.Consistently(acc.Events).Should(BeEmpty()) + + xform.Start() + src.Handlers.Handle(event.FullSyncFor(basicmeta.Collection1)) + src.Handlers.Handle(event.AddFor(basicmeta.Collection1, data.EntryN1I1V1)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(basicmeta.Collection2, data.EntryN1I1V1), // XForm to Collection2 + event.FullSyncFor(basicmeta.Collection2), + )) + + acc.Clear() + xform.Stop() + g.Consistently(acc.Events).Should(BeEmpty()) +} + +func TestDirect_InvalidEvent(t *testing.T) { + g := NewGomegaWithT(t) + + xform, src, acc := setup(g) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(basicmeta.Collection2)) // Collection2 + src.Handlers.Handle(event.AddFor(basicmeta.Collection2, data.EntryN1I1V1)) + + g.Consistently(acc.Events).Should(BeEmpty()) +} + +func setup(g *GomegaWithT) (event.Transformer, *fixtures.Source, *fixtures.Accumulator) { + xforms := Create(basicmeta.MustGet().DirectTransform().Mapping()) + g.Expect(xforms).To(HaveLen(1)) + + src := &fixtures.Source{} + acc := &fixtures.Accumulator{} + xform := xforms[0] + src.Dispatch(xform) + xform.DispatchFor(xform.Outputs()[0], acc) + + return xform, src, acc +} diff --git a/galley/pkg/config/processor/transforms/doc.go b/galley/pkg/config/processor/transforms/doc.go new file mode 100644 index 000000000000..39230dcc0698 --- /dev/null +++ b/galley/pkg/config/processor/transforms/doc.go @@ -0,0 +1,17 @@ +// 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 transforms contains basic processing building blocks that can be incorporated into bigger/self-contained +// processing pipelines. +package transforms diff --git a/galley/pkg/config/processor/transforms/ingress/annotations/annotations.go b/galley/pkg/config/processor/transforms/ingress/annotations/annotations.go new file mode 100644 index 000000000000..05cb457b298b --- /dev/null +++ b/galley/pkg/config/processor/transforms/ingress/annotations/annotations.go @@ -0,0 +1,24 @@ +// 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 annotations + +import "istio.io/pkg/annotations" + +// TODO: Move annotations to istio/api and use from there + +// IngressClass is the annotation on Ingress resources for the class of controllers responsible for it +var IngressClass = annotations.Register( + "kubernetes.io/ingress.class", + "IngressClass is the annotation on Ingress resources for the class of controllers responsible for it") diff --git a/galley/pkg/config/processor/transforms/ingress/common.go b/galley/pkg/config/processor/transforms/ingress/common.go new file mode 100644 index 000000000000..2a27c9b44999 --- /dev/null +++ b/galley/pkg/config/processor/transforms/ingress/common.go @@ -0,0 +1,81 @@ +// 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 ingressAdapter 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 ingress + +import ( + meshconfig "istio.io/api/mesh/v1alpha1" + + "istio.io/istio/galley/pkg/config/processor/transforms/ingress/annotations" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/scope" +) + +const ( + // TODO: Consider moving protocol definitions to their own package, if needed. + + https = "HTTPS" + + http = "HTTP" + + // IngressCertsPath is the path location for ingress certificates + IngressCertsPath = "/etc/istio/ingress-certs/" + + // IngressCertFilename is the ingress cert file name + IngressCertFilename = "tls.crt" + + // IngressKeyFilename is the ingress private key file name + IngressKeyFilename = "tls.key" + + // RootCertFilename is mTLS root cert + RootCertFilename = "root-cert.pem" + + // IstioIngressGatewayName is the internal gateway name assigned to ingress + IstioIngressGatewayName = "istio-autogenerated-k8s-ingress" + + // IstioIngressNamespace is the namespace where Istio ingress controller is deployed + IstioIngressNamespace = "istio-system" +) + +var ( + // IstioIngressWorkloadLabels is the label assigned to Istio ingress pods + IstioIngressWorkloadLabels = map[string]string{"istio": "ingress"} +) + +// shouldProcessIngress determines whether the given ingress resource should be processed +// by the controller, based on its ingress class annotation. +// See https://github.com/kubernetes/ingress/blob/master/examples/PREREQUISITES.md#ingress-class +func shouldProcessIngress(m *meshconfig.MeshConfig, r *resource.Entry) bool { + class, exists := "", false + if r.Metadata.Annotations != nil { + class, exists = r.Metadata.Annotations[annotations.IngressClass.Name] + } + + switch m.IngressControllerMode { + case meshconfig.MeshConfig_OFF: + scope.Processing.Debugf("Skipping ingress due to Ingress Controller Mode OFF (%s)", r.Metadata.Name) + return false + case meshconfig.MeshConfig_STRICT: + result := exists && class == m.IngressClass + scope.Processing.Debugf("Checking ingress class w/ Strict (%s): %v", r.Metadata.Name, result) + return result + case meshconfig.MeshConfig_DEFAULT: + result := !exists || class == m.IngressClass + scope.Processing.Debugf("Checking ingress class w/ Default (%s): %v", r.Metadata.Name, result) + return result + default: + scope.Processing.Warnf("invalid ingress controller mode: %v", m.IngressControllerMode) + return false + } +} diff --git a/galley/pkg/config/processor/transforms/ingress/create.go b/galley/pkg/config/processor/transforms/ingress/create.go new file mode 100644 index 000000000000..4c325b029988 --- /dev/null +++ b/galley/pkg/config/processor/transforms/ingress/create.go @@ -0,0 +1,28 @@ +// 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 ingressAdapter 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 ingress + +import ( + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processing" +) + +// Create transformer for Ingress resource +func Create(options processing.ProcessorOptions) []event.Transformer { + return []event.Transformer{ + newGatewayXform(options), + newVirtualServiceXform(options), + } +} diff --git a/galley/pkg/config/processor/transforms/ingress/dataset_test.go b/galley/pkg/config/processor/transforms/ingress/dataset_test.go new file mode 100644 index 000000000000..d287dac85a28 --- /dev/null +++ b/galley/pkg/config/processor/transforms/ingress/dataset_test.go @@ -0,0 +1,240 @@ +// 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 ingress + +import ( + "bytes" + + "github.com/gogo/protobuf/jsonpb" + "github.com/gogo/protobuf/proto" + + "istio.io/api/mesh/v1alpha1" + "istio.io/api/networking/v1alpha3" + "istio.io/istio/galley/pkg/config/meshcfg" + "istio.io/istio/galley/pkg/config/resource" +) + +func ingress1() *resource.Entry { + return toIngressResource(` +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: foo + namespace: ns + annotations: + kubernetes.io/ingress.class: "cls" + resourceVersion: v1 +spec: + rules: + - host: foohost.bar.com + http: + paths: + - path: /foopath + backend: + serviceName: service1 + servicePort: 4200 +`) +} + +func ingress1v2() *resource.Entry { + return toIngressResource(` +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: foo + namespace: ns + annotations: + kubernetes.io/ingress.class: "cls" + resourceVersion: v2 +spec: + rules: + - host: foohost.bar.com + http: + paths: + - path: /foopath + backend: + serviceName: service2 + servicePort: 2400 +`) +} + +func gw1() *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("", "istio-system/foo-istio-autogenerated-k8s-ingress"), + Version: "$ing_O/wmlZTvZJIo6adLqwDwQu/JHVrMb77jGjgugNQjiP4", + Annotations: map[string]string{}, + }, + Item: parseGateway(` + { + "selector": { + "istio": "ingress" + }, + "servers": [ + { + "hosts": [ + "*" + ], + "port": { + "name": "http-80-i-foo-ns", + "number": 80, + "protocol": "HTTP" + } + } + ] + }, +`), + } +} + +func gw1v2() *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("", "istio-system/foo-istio-autogenerated-k8s-ingress"), + Version: "$ing_+wTctpcOTD0Yc95R/VpQ17tGszgxE2AmZcNQ7EC1+ZA", + Annotations: map[string]string{}, + }, + Item: parseGateway(` + { + "selector": { + "istio": "ingress" + }, + "servers": [ + { + "hosts": [ + "*" + ], + "port": { + "name": "http-80-i-foo-ns", + "number": 80, + "protocol": "HTTP" + } + } + ] + }, +`), + } +} + +func vs1() *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("istio-system", "foohost-bar-com-foo-istio-autogenerated-k8s-ingress"), + Version: "$ing_zW/HWlEZ6+A8Z2HIpAsaRVskHx9AgXAyTvL7UNl5vuU", + Annotations: map[string]string{}, + }, + Item: &v1alpha3.VirtualService{ + Hosts: []string{ + "foohost.bar.com", + }, + Gateways: []string{"istio-autogenerated-k8s-ingress"}, + Http: []*v1alpha3.HTTPRoute{ + { + Match: []*v1alpha3.HTTPMatchRequest{ + { + Uri: &v1alpha3.StringMatch{ + MatchType: &v1alpha3.StringMatch_Exact{ + Exact: "/foopath", + }, + }, + }, + }, + + Route: []*v1alpha3.HTTPRouteDestination{ + { + Destination: &v1alpha3.Destination{ + Host: "service1.ns.svc.cluster.local", + Port: &v1alpha3.PortSelector{ + Port: &v1alpha3.PortSelector_Number{ + Number: 4200, + }, + }, + }, + Weight: 100, + }, + }, + }, + }, + }, + } +} + +func vs1v2() *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("istio-system", "foohost-bar-com-foo-istio-autogenerated-k8s-ingress"), + Version: "$ing_HWr/Pv0tKjRCWxF3pL8DhUuXlBRbnBgfI7EsEMVXuSY", + Annotations: map[string]string{}, + }, + Item: &v1alpha3.VirtualService{ + Hosts: []string{ + "foohost.bar.com", + }, + Gateways: []string{"istio-autogenerated-k8s-ingress"}, + Http: []*v1alpha3.HTTPRoute{ + { + Match: []*v1alpha3.HTTPMatchRequest{ + { + Uri: &v1alpha3.StringMatch{ + MatchType: &v1alpha3.StringMatch_Exact{ + Exact: "/foopath", + }, + }, + }, + }, + + Route: []*v1alpha3.HTTPRouteDestination{ + { + Destination: &v1alpha3.Destination{ + Host: "service2.ns.svc.cluster.local", + Port: &v1alpha3.PortSelector{ + Port: &v1alpha3.PortSelector_Number{ + Number: 2400, + }, + }, + }, + Weight: 100, + }, + }, + }, + }, + }, + } +} + +func meshConfig() *v1alpha1.MeshConfig { + m := meshcfg.Default() + m.IngressClass = "cls" + m.IngressControllerMode = v1alpha1.MeshConfig_STRICT + return m +} + +func toIngressResource(s string) *resource.Entry { + r, err := ingressAdapter.JSONToEntry(s) + if err != nil { + panic(err) + } + return r +} + +func parseGateway(s string) proto.Message { + p := &v1alpha3.Gateway{} + b := bytes.NewReader([]byte(s)) + err := jsonpb.Unmarshal(b, p) + if err != nil { + panic(err) + } + return p +} diff --git a/galley/pkg/config/processor/transforms/ingress/gateway.go b/galley/pkg/config/processor/transforms/ingress/gateway.go new file mode 100644 index 000000000000..8e0b7701105b --- /dev/null +++ b/galley/pkg/config/processor/transforms/ingress/gateway.go @@ -0,0 +1,170 @@ +// 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 ingressAdapter 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 ingress + +import ( + "fmt" + "path" + + ingress "k8s.io/api/extensions/v1beta1" + + meshconfig "istio.io/api/mesh/v1alpha1" + "istio.io/api/networking/v1alpha3" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processing" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/processor/transforms/ingress/annotations" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/synthesize" +) + +type gatewayXform struct { + *event.FnTransform + + options processing.ProcessorOptions +} + +var _ event.Transformer = &gatewayXform{} + +func newGatewayXform(o processing.ProcessorOptions) event.Transformer { + xform := &gatewayXform{} + xform.FnTransform = event.NewFnTransform( + collection.Names{metadata.K8SExtensionsV1Beta1Ingresses}, + collection.Names{metadata.IstioNetworkingV1Alpha3Gateways}, + nil, nil, + xform.handle) + xform.options = o + + return xform +} + +func (g *gatewayXform) handle(e event.Event, h event.Handler) { + + if g.options.MeshConfig.IngressControllerMode == meshconfig.MeshConfig_OFF { + // short circuit and return + return + } + + switch e.Kind { + case event.Added, event.Updated: + if !shouldProcessIngress(g.options.MeshConfig, e.Entry) { + return + } + + gw := g.convertIngressToGateway(e.Entry) + evt := event.Event{ + Kind: e.Kind, + Source: metadata.IstioNetworkingV1Alpha3Gateways, + Entry: gw, + } + h.Handle(evt) + + case event.Deleted: + gw := g.convertIngressToGateway(e.Entry) + evt := event.Event{ + Kind: e.Kind, + Source: metadata.IstioNetworkingV1Alpha3Gateways, + Entry: gw, + } + evt.Entry.Metadata.Name = generateSyntheticGatewayName(e.Entry.Metadata.Name) + evt.Entry.Metadata.Version = generateSyntheticVersion(e.Entry.Metadata.Version) + + h.Handle(evt) + + default: + panic(fmt.Errorf("gatewayXform.handle: unknown event: %v", e)) + } +} + +func (g *gatewayXform) convertIngressToGateway(e *resource.Entry) *resource.Entry { + namespace, name := e.Metadata.Name.InterpretAsNamespaceAndName() + + var gateway *v1alpha3.Gateway + if e.Item != nil { + i := e.Item.(*ingress.IngressSpec) + + gateway = &v1alpha3.Gateway{ + Selector: IstioIngressWorkloadLabels, + } + + // FIXME this is ingressAdapter temporary hack until all test templates are updated + // for _, tls := range i.Spec.TLS { + if len(i.TLS) > 0 { + tls := i.TLS[0] // FIXME + // TODO validation when multiple wildcard tls secrets are given + if len(tls.Hosts) == 0 { + tls.Hosts = []string{"*"} + } + gateway.Servers = append(gateway.Servers, &v1alpha3.Server{ + Port: &v1alpha3.Port{ + Number: 443, + Protocol: https, + Name: fmt.Sprintf("https-443-i-%s-%s", name, namespace), + }, + Hosts: tls.Hosts, + // While we accept multiple certs, we expect them to be mounted in + // /etc/certs/namespace/secretname/tls.crt|tls.key + Tls: &v1alpha3.Server_TLSOptions{ + HttpsRedirect: false, + Mode: v1alpha3.Server_TLSOptions_SIMPLE, + // TODO this is no longer valid for the new v2 stuff + PrivateKey: path.Join(IngressCertsPath, IngressKeyFilename), + ServerCertificate: path.Join(IngressCertsPath, IngressCertFilename), + // TODO: make sure this is mounted + CaCertificates: path.Join(IngressCertsPath, RootCertFilename), + }, + }) + } + + gateway.Servers = append(gateway.Servers, &v1alpha3.Server{ + Port: &v1alpha3.Port{ + Number: 80, + Protocol: http, + Name: fmt.Sprintf("http-80-i-%s-%s", name, namespace), + }, + Hosts: []string{"*"}, + }) + } + + ann := e.Metadata.Annotations.Clone() + ann.Delete(annotations.IngressClass.Name) + + gw := &resource.Entry{ + Metadata: resource.Metadata{ + Name: generateSyntheticGatewayName(e.Metadata.Name), + Version: generateSyntheticVersion(e.Metadata.Version), + CreateTime: e.Metadata.CreateTime, + Annotations: ann, + Labels: e.Metadata.Labels, + }, + Item: gateway, + } + + return gw +} + +func generateSyntheticGatewayName(name resource.Name) resource.Name { + _, n := name.InterpretAsNamespaceAndName() + newName := n + "-" + IstioIngressGatewayName + newNamespace := IstioIngressNamespace + + return resource.NewName(newNamespace, newName) +} + +func generateSyntheticVersion(v resource.Version) resource.Version { + return synthesize.Version("ing", v) +} diff --git a/galley/pkg/config/processor/transforms/ingress/gateway_test.go b/galley/pkg/config/processor/transforms/ingress/gateway_test.go new file mode 100644 index 000000000000..92b2a877d53a --- /dev/null +++ b/galley/pkg/config/processor/transforms/ingress/gateway_test.go @@ -0,0 +1,300 @@ +// 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 ingressAdapter 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 ingress + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/meshcfg" + "istio.io/istio/galley/pkg/config/processing" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/source/kube/rt" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +var ( + ingressAdapter = rt.DefaultProvider().GetAdapter(metadata.MustGet().KubeSource().Resources().MustFind( + "extensions", "Ingress")) +) + +func TestGateway_Input_Output(t *testing.T) { + g := NewGomegaWithT(t) + + xform, _, _ := setupGW(g, processing.ProcessorOptions{}) + + g.Expect(xform.Inputs()).To(Equal(collection.Names{metadata.K8SExtensionsV1Beta1Ingresses})) + g.Expect(xform.Outputs()).To(Equal(collection.Names{metadata.IstioNetworkingV1Alpha3Gateways})) +} + +func TestGateway_AddSync(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "svc.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupGW(g, o) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(metadata.IstioNetworkingV1Alpha3Gateways, gw1()), + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Gateways), + )) +} + +func TestGateway_SyncAdd(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "svc.local", + MeshConfig: meshConfig(), + } + xform, src, acc := setupGW(g, o) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Gateways), + event.AddFor(metadata.IstioNetworkingV1Alpha3Gateways, gw1()), + )) +} + +func TestGateway_AddUpdateDelete(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "svc.local", + MeshConfig: meshConfig(), + } + xform, src, acc := setupGW(g, o) + + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + src.Handlers.Handle(event.UpdateFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1v2())) + src.Handlers.Handle(event.DeleteForResource(metadata.K8SExtensionsV1Beta1Ingresses, ingress1v2())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Gateways), + event.AddFor(metadata.IstioNetworkingV1Alpha3Gateways, gw1()), + event.UpdateFor(metadata.IstioNetworkingV1Alpha3Gateways, gw1v2()), + event.DeleteForResource(metadata.IstioNetworkingV1Alpha3Gateways, gw1v2()), + )) +} + +func TestGateway_SyncReset(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "svc.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupGW(g, o) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.Event{Kind: event.Reset}) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Gateways), + event.Event{Kind: event.Reset}, + )) +} + +func TestGateway_InvalidEventKind(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "svc.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupGW(g, o) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.Event{Kind: 55}) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Gateways), + )) +} + +func TestGateway_NoListeners(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "svc.local", + MeshConfig: meshcfg.Default(), + } + + xforms := Create(o) + g.Expect(xforms).To(HaveLen(2)) + + src := &fixtures.Source{} + xform := xforms[0] + src.Dispatch(xform) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.Event{Kind: event.Reset}) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + + // No crash +} + +func TestGateway_DoubleStart(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "svc.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupGW(g, o) + + xform.Start() + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(metadata.IstioNetworkingV1Alpha3Gateways, gw1()), + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Gateways), + )) +} + +func TestGateway_DoubleStop(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "svc.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupGW(g, o) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(metadata.IstioNetworkingV1Alpha3Gateways, gw1()), + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Gateways), + )) + + acc.Clear() + + xform.Stop() + xform.Stop() + + g.Consistently(acc.Events).Should(BeEmpty()) +} + +func TestGateway_StartStopStartStop(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "svc.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupGW(g, o) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(metadata.IstioNetworkingV1Alpha3Gateways, gw1()), + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Gateways), + )) + + acc.Clear() + xform.Stop() + g.Consistently(acc.Events).Should(BeEmpty()) + + xform.Start() + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(metadata.IstioNetworkingV1Alpha3Gateways, gw1()), + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Gateways), + )) + + acc.Clear() + xform.Stop() + g.Consistently(acc.Events).Should(BeEmpty()) +} + +func TestGateway_InvalidEvent(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "svc.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupGW(g, o) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Virtualservices)) + + g.Consistently(acc.Events).Should(BeEmpty()) +} + +func setupGW(g *GomegaWithT, o processing.ProcessorOptions) (event.Transformer, *fixtures.Source, *fixtures.Accumulator) { + xforms := Create(o) + g.Expect(xforms).To(HaveLen(2)) + + src := &fixtures.Source{} + acc := &fixtures.Accumulator{} + xform := xforms[0] + src.Dispatch(xform) + xform.DispatchFor(metadata.IstioNetworkingV1Alpha3Gateways, acc) + + xform.Start() + + return xform, src, acc +} diff --git a/galley/pkg/config/processor/transforms/ingress/syntheticVirtualService.go b/galley/pkg/config/processor/transforms/ingress/syntheticVirtualService.go new file mode 100644 index 000000000000..658e1c7f0641 --- /dev/null +++ b/galley/pkg/config/processor/transforms/ingress/syntheticVirtualService.go @@ -0,0 +1,181 @@ +// 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 ingress + +import ( + "sort" + "strings" + + "k8s.io/api/extensions/v1beta1" + + "istio.io/api/networking/v1alpha3" + "istio.io/istio/galley/pkg/config/processor/transforms/ingress/annotations" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/synthesize" +) + +// syntheticVirtualService represents an in-memory state that maps ingress resources to a synthesized Virtual Service. +type syntheticVirtualService struct { + // host is the key for the mapping. + host string + + // Keep track of resource name. Depending on the ingresses that participate, the name can change. + name resource.Name + version resource.Version + + // ingresses that are represented in this Virtual Service + ingresses []*resource.Entry +} + +func (s *syntheticVirtualService) attachIngress(e *resource.Entry) (resource.Name, resource.Version) { + var found bool + for i, existing := range s.ingresses { + if existing.Metadata.Name == e.Metadata.Name { + s.ingresses[i] = e + found = true + break + } + } + + if !found { + s.ingresses = append(s.ingresses, e) + } + + sort.SliceStable(s.ingresses, func(i, j int) bool { + return strings.Compare(s.ingresses[i].Metadata.Name.String(), s.ingresses[j].Metadata.Name.String()) < 0 + }) + + oldName := s.name + oldVersion := s.version + + s.name = generateSyntheticVirtualServiceName(s.host, s.ingresses[0].Metadata.Name) + s.version = s.generateVersion() + + return oldName, oldVersion +} + +func generateSyntheticVirtualServiceName(host string, ingressName resource.Name) resource.Name { + _, name := ingressName.InterpretAsNamespaceAndName() + + namePrefix := strings.Replace(host, ".", "-", -1) + + newName := namePrefix + "-" + name + "-" + IstioIngressGatewayName + newNamespace := IstioIngressNamespace + + return resource.NewName(newNamespace, newName) +} + +func (s *syntheticVirtualService) detachIngress(e *resource.Entry) (resource.Name, resource.Version) { + for i, existing := range s.ingresses { + if existing.Metadata.Name == e.Metadata.Name { + s.ingresses = append(s.ingresses[:i], s.ingresses[i+1:]...) + oldName := s.name + oldVersion := s.version + + if i == 0 { + if len(s.ingresses) == 0 { + s.name = resource.Name{} + s.version = resource.Version("") + } else { + s.name = generateSyntheticVirtualServiceName(s.host, s.ingresses[0].Metadata.Name) + s.version = s.generateVersion() + } + } + return oldName, oldVersion + } + } + + return s.name, s.version +} + +func (s *syntheticVirtualService) isEmpty() bool { + return len(s.ingresses) == 0 +} + +func (s *syntheticVirtualService) generateEntry(domainSuffix string) *resource.Entry { + // Ingress allows a single host - if missing '*' is assumed + // We need to merge all rules with a particular host across + // all ingresses, and return a separate VirtualService for each + // host. + + first := s.ingresses[0] + namespace, name := first.Metadata.Name.InterpretAsNamespaceAndName() + + meta := first.Metadata.Clone() + meta.Name = s.name + meta.Version = s.version + if meta.Annotations != nil { + delete(meta.Annotations, annotations.IngressClass.Name) + } + + virtualService := &v1alpha3.VirtualService{ + Hosts: []string{s.host}, + Gateways: []string{IstioIngressGatewayName}, + } + + for _, ing := range s.ingresses { + ingress := ing.Item.(*v1beta1.IngressSpec) + for _, rule := range ingress.Rules { + if rule.HTTP == nil { + scope.Processing.Errorf("invalid ingress rule %s:%s for host %q, no paths defined", namespace, name, rule.Host) + continue + } + + var httpRoutes []*v1alpha3.HTTPRoute + for _, path := range rule.HTTP.Paths { + httpMatch := &v1alpha3.HTTPMatchRequest{ + Uri: createStringMatch(path.Path), + } + + httpRoute := ingressBackendToHTTPRoute(&path.Backend, namespace, domainSuffix) + if httpRoute == nil { + scope.Processing.Errorf("invalid ingress rule %s:%s for host %q, no backend defined for path", namespace, name, rule.Host) + continue + } + httpRoute.Match = []*v1alpha3.HTTPMatchRequest{httpMatch} + httpRoutes = append(httpRoutes, httpRoute) + } + + virtualService.Http = append(virtualService.Http, httpRoutes...) + } + + // Matches * and "/". Currently not supported - would conflict + // with any other explicit VirtualService. + if ingress.Backend != nil { + scope.Processing.Infof("Ignore default wildcard ingress, use VirtualService %s:%s", + namespace, name) + } + } + + return &resource.Entry{ + Metadata: meta, + Item: virtualService, + } +} + +func (s *syntheticVirtualService) generateVersion() resource.Version { + i := 0 + return synthesize.VersionIter("ing", func() (n resource.Name, v resource.Version, ok bool) { + if i < len(s.ingresses) { + ing := s.ingresses[i] + i++ + n = ing.Metadata.Name + v = ing.Metadata.Version + ok = true + } + return + }) +} diff --git a/galley/pkg/config/processor/transforms/ingress/virtualService.go b/galley/pkg/config/processor/transforms/ingress/virtualService.go new file mode 100644 index 000000000000..ea3004dcc4d4 --- /dev/null +++ b/galley/pkg/config/processor/transforms/ingress/virtualService.go @@ -0,0 +1,296 @@ +// 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 ingressAdapter 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 ingress + +import ( + "fmt" + "strings" + "sync" + + "k8s.io/api/extensions/v1beta1" + ingress "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/util/intstr" + + meshconfig "istio.io/api/mesh/v1alpha1" + "istio.io/api/networking/v1alpha3" + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processing" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/scope" +) + +type virtualServiceXform struct { + *event.FnTransform + + options processing.ProcessorOptions + + mu sync.Mutex + + ingresses map[resource.Name]*resource.Entry + vsByHost map[string]*syntheticVirtualService +} + +func newVirtualServiceXform(o processing.ProcessorOptions) event.Transformer { + xform := &virtualServiceXform{ + options: o, + } + xform.FnTransform = event.NewFnTransform( + collection.Names{metadata.K8SExtensionsV1Beta1Ingresses}, + collection.Names{metadata.IstioNetworkingV1Alpha3Virtualservices}, + xform.start, + xform.stop, + xform.handle) + + return xform +} + +// Start implements processing.Transformer +func (g *virtualServiceXform) start() { + g.vsByHost = make(map[string]*syntheticVirtualService) + + g.ingresses = make(map[resource.Name]*resource.Entry) +} + +// Stop implements processing.Transformer +func (g *virtualServiceXform) stop() { + g.vsByHost = nil + + g.ingresses = nil +} + +// Handle implements event.Handler +func (g *virtualServiceXform) handle(e event.Event, h event.Handler) { + if g.options.MeshConfig.IngressControllerMode == meshconfig.MeshConfig_OFF { + // short circuit and return + return + } + + switch e.Kind { + case event.Added, event.Updated: + if !shouldProcessIngress(g.options.MeshConfig, e.Entry) { + scope.Processing.Debugf("virtualServiceXform: Skipping ingress event: %v", e) + return + } + + g.processIngress(e.Entry, h) + + case event.Deleted: + ing, exists := g.ingresses[e.Entry.Metadata.Name] + if exists { + g.removeIngress(ing, h) + delete(g.ingresses, e.Entry.Metadata.Name) + } + + default: + panic(fmt.Errorf("virtualServiceXForm.handle: unknown event: %v", e)) + } +} + +func (g *virtualServiceXform) processIngress(newIngress *resource.Entry, h event.Handler) { + g.mu.Lock() + defer g.mu.Unlock() + + g.ingresses[newIngress.Metadata.Name] = newIngress + + // Extract the hosts from Ingress and find all relevant Synthetic Virtual Service entries. + iterateHosts(newIngress, func(host string) { + svs, exists := g.vsByHost[host] + if !exists { + svs = &syntheticVirtualService{ + host: host, + } + g.vsByHost[host] = svs + } + + // Associate the Ingress resource with the Synthetic Virtual Service. This may or may not + // cause a change in the resource state. + oldName, oldVersion := svs.attachIngress(newIngress) + if oldName != svs.name { + if exists { + g.notifyDelete(h, oldName, oldVersion) + } + g.notifyUpdate(h, event.Added, svs) + } else { + if exists { + g.notifyUpdate(h, event.Updated, svs) + } else { + g.notifyUpdate(h, event.Added, svs) + } + } + }) + + // It is possible that the ingress may have been removed from a Synthetic Virtual Service. Find and + // update/remove those + oldIngress, found := g.ingresses[newIngress.Metadata.Name] + if found { + iterateRemovedHosts(oldIngress, newIngress, func(host string) { + svs := g.vsByHost[host] + oldName, oldVersion := svs.detachIngress(oldIngress) + if oldName != svs.name { + g.notifyDelete(h, oldName, oldVersion) + if svs.isEmpty() { + delete(g.vsByHost, host) + } else { + g.notifyUpdate(h, event.Added, svs) + } + } else { + if svs.isEmpty() { + delete(g.vsByHost, host) + g.notifyDelete(h, oldName, oldVersion) + } else { + g.notifyUpdate(h, event.Updated, svs) + } + } + }) + } +} + +func (g *virtualServiceXform) removeIngress(oldIngress *resource.Entry, h event.Handler) { + g.mu.Lock() + defer g.mu.Unlock() + + iterateRemovedHosts(oldIngress, nil, func(host string) { + svs := g.vsByHost[host] + oldName, oldVersion := svs.detachIngress(oldIngress) + if oldName != svs.name { + g.notifyDelete(h, oldName, oldVersion) + if svs.isEmpty() { + delete(g.vsByHost, host) + } else { + g.notifyUpdate(h, event.Added, svs) + } + } else { + if svs.isEmpty() { + delete(g.vsByHost, host) + g.notifyDelete(h, oldName, oldVersion) + } else { + g.notifyUpdate(h, event.Updated, svs) + } + } + }) +} + +func iterateHosts(i *resource.Entry, fn func(string)) { + spec := i.Item.(*v1beta1.IngressSpec) + for _, r := range spec.Rules { + host := getHost(&r) + fn(host) + } +} + +func iterateRemovedHosts(o, n *resource.Entry, fn func(string)) { + // Use N^2 algorithm, to avoid garbage generation. +loop: + for _, ro := range o.Item.(*v1beta1.IngressSpec).Rules { + if n != nil { + for _, rn := range n.Item.(*v1beta1.IngressSpec).Rules { + if getHost(&ro) == getHost(&rn) { + continue loop + } + } + } + + fn(getHost(&ro)) + } +} + +func (g *virtualServiceXform) notifyUpdate(h event.Handler, k event.Kind, svs *syntheticVirtualService) { + e := event.Event{ + Kind: k, + Source: metadata.IstioNetworkingV1Alpha3Virtualservices, + Entry: svs.generateEntry(g.options.DomainSuffix), + } + h.Handle(e) +} + +func (g *virtualServiceXform) notifyDelete(h event.Handler, name resource.Name, v resource.Version) { + e := event.Event{ + Kind: event.Deleted, + Source: metadata.IstioNetworkingV1Alpha3Virtualservices, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: name, + Version: v, + }, + }, + } + h.Handle(e) +} + +func getHost(r *v1beta1.IngressRule) string { + host := r.Host + if host == "" { + host = "*" + } + return host +} + +func createStringMatch(s string) *v1alpha3.StringMatch { + if s == "" { + return nil + } + + // Note that this implementation only converts prefix and exact matches, not regexps. + + // Replace e.g. "foo.*" with prefix match + if strings.HasSuffix(s, ".*") { + return &v1alpha3.StringMatch{ + MatchType: &v1alpha3.StringMatch_Prefix{Prefix: strings.TrimSuffix(s, ".*")}, + } + } + if strings.HasSuffix(s, "/*") { + return &v1alpha3.StringMatch{ + MatchType: &v1alpha3.StringMatch_Prefix{Prefix: strings.TrimSuffix(s, "/*")}, + } + } + + // Replace e.g. "foo" with ingressAdapter exact match + return &v1alpha3.StringMatch{ + MatchType: &v1alpha3.StringMatch_Exact{Exact: s}, + } +} + +func ingressBackendToHTTPRoute(backend *ingress.IngressBackend, namespace string, domainSuffix string) *v1alpha3.HTTPRoute { + if backend == nil { + return nil + } + + port := &v1alpha3.PortSelector{ + Port: nil, + } + + if backend.ServicePort.Type == intstr.Int { + port.Port = &v1alpha3.PortSelector_Number{ + Number: uint32(backend.ServicePort.IntVal), + } + } else { + // Port names are not allowed in destination rules. + return nil + } + + return &v1alpha3.HTTPRoute{ + Route: []*v1alpha3.HTTPRouteDestination{ + { + Destination: &v1alpha3.Destination{ + Host: fmt.Sprintf("%s.%s.svc.%s", backend.ServiceName, namespace, domainSuffix), + Port: port, + }, + Weight: 100, + }, + }, + } +} diff --git a/galley/pkg/config/processor/transforms/ingress/virtualService_test.go b/galley/pkg/config/processor/transforms/ingress/virtualService_test.go new file mode 100644 index 000000000000..2da9ca34c9ba --- /dev/null +++ b/galley/pkg/config/processor/transforms/ingress/virtualService_test.go @@ -0,0 +1,294 @@ +// 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 ingressAdapter 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 ingress + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/meshcfg" + "istio.io/istio/galley/pkg/config/processing" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestVirtualService_Input_Output(t *testing.T) { + g := NewGomegaWithT(t) + + xform, _, _ := setupVS(g, processing.ProcessorOptions{}) + + g.Expect(xform.Inputs()).To(Equal(collection.Names{metadata.K8SExtensionsV1Beta1Ingresses})) + g.Expect(xform.Outputs()).To(Equal(collection.Names{metadata.IstioNetworkingV1Alpha3Virtualservices})) +} + +func TestVirtualService_AddSync(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "cluster.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupVS(g, o) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(metadata.IstioNetworkingV1Alpha3Virtualservices, vs1()), + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Virtualservices))) +} + +func TestVirtualService_SyncAdd(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "cluster.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupVS(g, o) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Virtualservices), + event.AddFor(metadata.IstioNetworkingV1Alpha3Virtualservices, vs1()), + )) +} + +func TestVirtualService_AddUpdateDelete(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "cluster.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupVS(g, o) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + src.Handlers.Handle(event.UpdateFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1v2())) + src.Handlers.Handle(event.DeleteForResource(metadata.K8SExtensionsV1Beta1Ingresses, ingress1v2())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Virtualservices), + event.AddFor(metadata.IstioNetworkingV1Alpha3Virtualservices, vs1()), + event.UpdateFor(metadata.IstioNetworkingV1Alpha3Virtualservices, vs1v2()), + event.DeleteFor(metadata.IstioNetworkingV1Alpha3Virtualservices, vs1v2().Metadata.Name, vs1v2().Metadata.Version), + )) +} + +func TestVirtualService_SyncReset(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "cluster.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupVS(g, o) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.Event{Kind: event.Reset}) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Virtualservices), + event.Event{Kind: event.Reset}, + )) +} + +func TestVirtualService_InvalidEventKind(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "cluster.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupVS(g, o) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.Event{Kind: 55}) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Virtualservices), + )) +} + +func TestVirtualService_NoListeners(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "cluster.local", + MeshConfig: meshcfg.Default(), + } + + xforms := Create(o) + g.Expect(xforms).To(HaveLen(2)) + + src := &fixtures.Source{} + xform := xforms[0] + src.Dispatch(xform) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.Event{Kind: event.Reset}) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + + // No crash +} + +func TestVirtualService_DoubleStart(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "cluster.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupVS(g, o) + + xform.Start() + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(metadata.IstioNetworkingV1Alpha3Virtualservices, vs1()), + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Virtualservices), + )) +} + +func TestVirtualService_DoubleStop(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "cluster.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupVS(g, o) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(metadata.IstioNetworkingV1Alpha3Virtualservices, vs1()), + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Virtualservices), + )) + + acc.Clear() + + xform.Stop() + xform.Stop() + + g.Consistently(acc.Events).Should(BeEmpty()) +} + +func TestVirtualService_StartStopStartStop(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "cluster.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupVS(g, o) + + xform.Start() + + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(metadata.IstioNetworkingV1Alpha3Virtualservices, vs1()), + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Virtualservices), + )) + + acc.Clear() + xform.Stop() + g.Consistently(acc.Events).Should(BeEmpty()) + + xform.Start() + src.Handlers.Handle(event.FullSyncFor(metadata.K8SExtensionsV1Beta1Ingresses)) + src.Handlers.Handle(event.AddFor(metadata.K8SExtensionsV1Beta1Ingresses, ingress1())) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(metadata.IstioNetworkingV1Alpha3Virtualservices, vs1()), + event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Virtualservices), + )) + + acc.Clear() + xform.Stop() + g.Consistently(acc.Events).Should(BeEmpty()) +} + +func TestVirtualService_InvalidEvent(t *testing.T) { + g := NewGomegaWithT(t) + + o := processing.ProcessorOptions{ + DomainSuffix: "cluster.local", + MeshConfig: meshConfig(), + } + + xform, src, acc := setupVS(g, o) + + xform.Start() + defer xform.Stop() + + src.Handlers.Handle(event.FullSyncFor(metadata.IstioNetworkingV1Alpha3Virtualservices)) + + g.Consistently(acc.Events).Should(BeEmpty()) +} + +func setupVS(g *GomegaWithT, o processing.ProcessorOptions) (event.Transformer, *fixtures.Source, *fixtures.Accumulator) { + xforms := Create(o) + g.Expect(xforms).To(HaveLen(2)) + + src := &fixtures.Source{} + acc := &fixtures.Accumulator{} + xform := xforms[1] + src.Dispatch(xform) + xform.DispatchFor(metadata.IstioNetworkingV1Alpha3Virtualservices, acc) + + return xform, src, acc +} diff --git a/galley/pkg/config/processor/transforms/serviceentry/annotations/annotations.go b/galley/pkg/config/processor/transforms/serviceentry/annotations/annotations.go new file mode 100644 index 000000000000..0032f75505a2 --- /dev/null +++ b/galley/pkg/config/processor/transforms/serviceentry/annotations/annotations.go @@ -0,0 +1,30 @@ +// 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 annotations + +const ( + // TODO: Move annotations to istio/api and use from there + + // ServiceVersion provides the raw resource version from the most recent k8s Service update. This will always + // be available for synthetic service entries. + ServiceVersion = "networking.istio.io/serviceVersion" + + // EndpointsVersion provides the raw resource version of the most recent k8s Endpoints update (if available). + EndpointsVersion = "networking.istio.io/endpointsVersion" + + // NotReadyEndpoints is an annotation providing the "NotReadyAddresses" from the Kubernetes Endpoints + // resource. The value is a comma-separated list of IP:port. + NotReadyEndpoints = "networking.istio.io/notReadyEndpoints" +) diff --git a/galley/pkg/config/processor/transforms/serviceentry/converter/instance.go b/galley/pkg/config/processor/transforms/serviceentry/converter/instance.go new file mode 100644 index 000000000000..9fefbb28a98d --- /dev/null +++ b/galley/pkg/config/processor/transforms/serviceentry/converter/instance.go @@ -0,0 +1,255 @@ +// 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 converter + +import ( + "sort" + "strconv" + "strings" + + coreV1 "k8s.io/api/core/v1" + + "istio.io/api/annotation" + networking "istio.io/api/networking/v1alpha3" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/annotations" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/pod" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/pkg/config" + configKube "istio.io/istio/pkg/config/kube" +) + +// Instance of the converter. +type Instance struct { + domain string + pods pod.Cache +} + +// New creates a new instance of the converter. +func New(domain string, pods pod.Cache) *Instance { + return &Instance{ + domain: domain, + pods: pods, + } +} + +// Convert applies the conversion function from k8s Service and Endpoints to ServiceEntry. The +// ServiceEntry is passed as an argument (out) in order to enable object reuse in the future. +func (i *Instance) Convert(service *resource.Entry, endpoints *resource.Entry, outMeta *resource.Metadata, + out *networking.ServiceEntry) error { + i.convertService(service, outMeta, out) + i.convertEndpoints(endpoints, outMeta, out) + return nil +} + +// convertService applies the k8s Service to the output. +func (i *Instance) convertService(service *resource.Entry, outMeta *resource.Metadata, out *networking.ServiceEntry) { + if service == nil { + // For testing only. Production code will always provide a non-nil service. + return + } + + spec := service.Item.(*coreV1.ServiceSpec) + + resolution := networking.ServiceEntry_STATIC + location := networking.ServiceEntry_MESH_INTERNAL + endpoints := convertExternalServiceEndpoints(spec, service.Metadata) + + // Check for an external service + externalName := "" + if spec.Type == coreV1.ServiceTypeExternalName && spec.ExternalName != "" { + externalName = spec.ExternalName + resolution = networking.ServiceEntry_DNS + location = networking.ServiceEntry_MESH_EXTERNAL + } + + // Check for unspecified Cluster IP + addr := config.UnspecifiedIP + if spec.ClusterIP != "" && spec.ClusterIP != coreV1.ClusterIPNone { + addr = spec.ClusterIP + } + if addr == config.UnspecifiedIP && externalName == "" { + // Headless services should not be load balanced + resolution = networking.ServiceEntry_NONE + } + + ports := make([]*networking.Port, 0, len(spec.Ports)) + for _, port := range spec.Ports { + ports = append(ports, convertPort(port)) + } + + host := serviceHostname(service.Metadata.Name, i.domain) + + // Store everything in the ServiceEntry. + out.Hosts = []string{host} + out.Addresses = []string{addr} + out.Resolution = resolution + out.Location = location + out.Ports = ports + out.Endpoints = endpoints + out.ExportTo = convertExportTo(service.Metadata.Annotations) + + // Convert Metadata + outMeta.Name = service.Metadata.Name + outMeta.Labels = service.Metadata.Labels.Clone() + + // Convert the creation time. + outMeta.CreateTime = service.Metadata.CreateTime + + // Update the annotations. + outMeta.Annotations = service.Metadata.Annotations.CloneOrCreate() + + // Add an annotation for the version of the service resource. + outMeta.Annotations[annotations.ServiceVersion] = string(service.Metadata.Version) +} + +func convertExportTo(annotations resource.StringMap) []string { + var exportTo map[string]struct{} + if annotations[annotation.NetworkingExportTo.Name] != "" { + exportTo = make(map[string]struct{}) + for _, e := range strings.Split(annotations[annotation.NetworkingExportTo.Name], ",") { + exportTo[strings.TrimSpace(e)] = struct{}{} + } + } + if exportTo == nil { + return nil + } + + out := make([]string, 0, len(exportTo)) + for k := range exportTo { + out = append(out, k) + } + sort.Strings(out) + return out +} + +// convertEndpoints applies the k8s Endpoints to the output. +func (i *Instance) convertEndpoints(endpoints *resource.Entry, outMeta *resource.Metadata, out *networking.ServiceEntry) { + if endpoints == nil { + return + } + + spec := endpoints.Item.(*coreV1.Endpoints) + // Store the subject alternate names in a set to avoid duplicates. + subjectAltNameSet := make(map[string]struct{}) + eps := make([]*networking.ServiceEntry_Endpoint, 0) + + // TODO: Add pooling support for strings.Builder and put it in pkg/pool + + // A builder for the annotation for not-ready addresses. The ServiceEntry does not support not ready addresses, + // so we send them as an annotation instead. + var notReadyBuilder strings.Builder + + for _, subset := range spec.Subsets { + // Convert the ports for this subset. They will be re-used for each endpoint in the same subset. + ports := make(map[string]uint32) + for _, port := range subset.Ports { + ports[port.Name] = uint32(port.Port) + + // Process the not ready addresses. + portString := strconv.Itoa(int(port.Port)) + for _, address := range subset.NotReadyAddresses { + if notReadyBuilder.Len() > 0 { + // Add a separator between the addresses. + notReadyBuilder.WriteByte(',') + } + notReadyBuilder.WriteString(address.IP) + notReadyBuilder.WriteByte(':') + notReadyBuilder.WriteString(portString) + } + } + + // Convert the endpoints in this subset. + for _, address := range subset.Addresses { + locality := "" + var labels map[string]string + + ip := address.IP + p, hasPod := i.pods.GetPodByIP(ip) + if hasPod { + labels = p.Labels + locality = p.Locality + if p.ServiceAccountName != "" { + subjectAltNameSet[p.ServiceAccountName] = struct{}{} + } + } + + ep := &networking.ServiceEntry_Endpoint{ + Labels: labels, + Address: ip, + Ports: ports, + Locality: locality, + // TODO(nmittler): Network: "", + } + eps = append(eps, ep) + } + } + + // Convert the subject alternate names to an array. + subjectAltNames := make([]string, 0, len(subjectAltNameSet)) + for k := range subjectAltNameSet { + subjectAltNames = append(subjectAltNames, k) + } + sort.Strings(subjectAltNames) + + out.Endpoints = eps + out.SubjectAltNames = subjectAltNames + + // Add an annotation for the version of the Endpoints resource. + outMeta.Annotations = outMeta.Annotations.CloneOrCreate() + outMeta.Annotations[annotations.EndpointsVersion] = string(endpoints.Metadata.Version) + + // Add an annotation for any "not ready" endpoints. + if notReadyBuilder.Len() > 0 { + outMeta.Annotations[annotations.NotReadyEndpoints] = notReadyBuilder.String() + } +} + +func convertExternalServiceEndpoints( + svc *coreV1.ServiceSpec, + serviceMeta resource.Metadata) []*networking.ServiceEntry_Endpoint { + + endpoints := make([]*networking.ServiceEntry_Endpoint, 0) + if svc.Type == coreV1.ServiceTypeExternalName && svc.ExternalName != "" { + // Generate endpoints for the external service. + ports := make(map[string]uint32) + for _, port := range svc.Ports { + ports[port.Name] = uint32(port.Port) + } + addr := svc.ExternalName + endpoints = append(endpoints, &networking.ServiceEntry_Endpoint{ + Address: addr, + Ports: ports, + Labels: serviceMeta.Labels, + }) + } + return endpoints +} + +// serviceHostname produces FQDN for a k8s service +func serviceHostname(fullName resource.Name, domainSuffix string) string { + namespace, name := fullName.InterpretAsNamespaceAndName() + if namespace == "" { + namespace = coreV1.NamespaceDefault + } + return name + "." + namespace + ".svc." + domainSuffix +} + +func convertPort(port coreV1.ServicePort) *networking.Port { + return &networking.Port{ + Name: port.Name, + Number: uint32(port.Port), + Protocol: string(configKube.ConvertProtocol(port.Name, port.Protocol)), + } +} diff --git a/galley/pkg/config/processor/transforms/serviceentry/converter/instance_bench_test.go b/galley/pkg/config/processor/transforms/serviceentry/converter/instance_bench_test.go new file mode 100644 index 000000000000..bc08954985d1 --- /dev/null +++ b/galley/pkg/config/processor/transforms/serviceentry/converter/instance_bench_test.go @@ -0,0 +1,263 @@ +// 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 converter_test + +import ( + "testing" + "time" + + networking "istio.io/api/networking/v1alpha3" + + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/converter" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/pod" + "istio.io/istio/galley/pkg/config/resource" + + coreV1 "k8s.io/api/core/v1" +) + +const ( + benchNamespace = "benchmarkns" +) + +// BenchmarkService tests the performance of converting a single k8s Service into a networking.ServiceEntry. +func BenchmarkService(b *testing.B) { + benchmarkService(b, false) +} + +// benchmarkService performs work for the Service benchmark. If reuse==true the same output networking.ServiceEntry +// will be used in each iteration. This will enable the benchmark to compare reuse/non-reuse once some form of object +// pooling is supported. +func benchmarkService(b *testing.B, reuse bool) { + b.Helper() + + b.StopTimer() + + service := &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName(benchNamespace, "someservice"), + Version: resource.Version("v1"), + CreateTime: time.Now(), + Annotations: resource.StringMap{ + "Annotation1": "AnnotationValue1", + "Annotation2": "AnnotationValue2", + "Annotation3": "AnnotationValue3", + "Annotation4": "AnnotationValue4", + "Annotation5": "AnnotationValue5", + }, + Labels: resource.StringMap{ + "Label1": "LabelValue1", + "Label2": "LabelValue2", + "Label3": "LabelValue3", + "Label4": "LabelValue4", + "Label5": "LabelValue5", + }, + }, + Item: &coreV1.ServiceSpec{ + ClusterIP: "10.0.0.1", + Ports: []coreV1.ServicePort{ + { + Name: "http", + Port: 80, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "https", + Port: 443, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "grpc", + Port: 8088, + Protocol: coreV1.ProtocolTCP, + }, + }, + }, + } + + c := converter.New(domainSuffix, nil) + + // Create/init the output ServiceEntry if reuse is enabled. + var outMeta *resource.Metadata + var out *networking.ServiceEntry + if reuse { + outMeta = newMetadata() + out = newServiceEntry() + if err := convertService(c, service, outMeta, out); err != nil { + b.Fatal(err) + } + } + + b.StartTimer() + + for i := 0; i < b.N; i++ { + if err := convertService(c, service, outMeta, out); err != nil { + b.Fatal(err) + } + } +} + +func convertService(c *converter.Instance, service *resource.Entry, outMeta *resource.Metadata, out *networking.ServiceEntry) error { + if outMeta == nil { + outMeta = newMetadata() + } + if out == nil { + out = newServiceEntry() + } + return c.Convert(service, nil, outMeta, out) +} + +// BenchmarkEndpoints tests the performance of converting a single k8s Endpoints resource into a networking.ServiceEntry. +func BenchmarkEndpoints(b *testing.B) { + benchmarkEndpoints(b, false) +} + +// benchmarkEndpoints performs the work for the Endpoints benchmark. If reuse==true the same output networking.ServiceEntry +// will be used in each iteration. This will enable the benchmark to compare reuse/non-reuse once some form of object +// pooling is supported. +func benchmarkEndpoints(b *testing.B, reuse bool) { + b.Helper() + b.StopTimer() + + // Establish the list of IPs and service accounts to be used. + ips := []string{ + "10.0.0.1", + "10.0.0.2", + "10.0.0.3", + "10.0.0.4", + "10.0.0.5", + "10.0.0.6", + "10.0.0.7", + "10.0.0.8", + "10.0.0.9", + "10.0.0.10", + } + serviceAccounts := []string{ + "serviceAccount1", + "serviceAccount2", + "serviceAccount3", + } + + // Create the pod/node cache, that will map the IPs to service accounts. + pods := newPodCache() + saIndex := 0 + for _, ip := range ips { + addPod(pods, ip, serviceAccounts[saIndex]) + saIndex = (saIndex + 1) % len(serviceAccounts) + } + + // Create the k8s Endpoints, splitting the available IPs between the number of subsets. + numSubsets := 2 + ipsPerSubset := len(ips) / numSubsets + ipIndex := 0 + endpoints := &coreV1.Endpoints{} + for subsetIndex := 0; subsetIndex < numSubsets; subsetIndex++ { + subset := coreV1.EndpointSubset{ + Ports: []coreV1.EndpointPort{ + { + Name: "http", + Port: 80, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "https", + Port: 443, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "grpc", + Port: 8088, + Protocol: coreV1.ProtocolTCP, + }, + }, + } + endIndex := min(ipIndex+ipsPerSubset, len(ips)) + for ; ipIndex < endIndex; ipIndex++ { + subset.Addresses = append(subset.Addresses, coreV1.EndpointAddress{ + IP: ips[ipIndex], + }) + } + endpoints.Subsets = append(endpoints.Subsets, subset) + } + + entry := &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName(benchNamespace, "someservice"), + Version: resource.Version("v1"), + CreateTime: time.Now(), + Annotations: resource.StringMap{ + "Annotation1": "AnnotationValue1", + "Annotation2": "AnnotationValue2", + "Annotation3": "AnnotationValue3", + "Annotation4": "AnnotationValue4", + "Annotation5": "AnnotationValue5", + }, + Labels: resource.StringMap{ + "Label1": "LabelValue1", + "Label2": "LabelValue2", + "Label3": "LabelValue3", + "Label4": "LabelValue4", + "Label5": "LabelValue5", + }, + }, + Item: endpoints, + } + + c := converter.New(domainSuffix, pods) + + // Create/init the output ServiceEntry if reuse is enabled. + var outMeta *resource.Metadata + var out *networking.ServiceEntry + if reuse { + outMeta = newMetadata() + out = newServiceEntry() + if err := convertEndpoints(c, entry, outMeta, out); err != nil { + b.Fatal(err) + } + } + + b.StartTimer() + + for i := 0; i < b.N; i++ { + if err := convertEndpoints(c, entry, outMeta, out); err != nil { + b.Fatal(err) + } + } +} + +func convertEndpoints(c *converter.Instance, endpoints *resource.Entry, outMeta *resource.Metadata, out *networking.ServiceEntry) error { + if outMeta == nil { + outMeta = newMetadata() + } + if out == nil { + out = newServiceEntry() + } + return c.Convert(nil, endpoints, outMeta, out) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func addPod(pods fakePodCache, ip, serviceAccountName string) { + pods[ip] = pod.Info{ + FullName: resource.NewName(benchNamespace, "SomePod"), + NodeName: "SomeNode", + Locality: "locality", + ServiceAccountName: serviceAccountName, + } +} diff --git a/galley/pkg/config/processor/transforms/serviceentry/converter/instance_test.go b/galley/pkg/config/processor/transforms/serviceentry/converter/instance_test.go new file mode 100644 index 000000000000..d438900972b0 --- /dev/null +++ b/galley/pkg/config/processor/transforms/serviceentry/converter/instance_test.go @@ -0,0 +1,697 @@ +// 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 converter_test + +import ( + "fmt" + "testing" + "time" + + . "github.com/onsi/gomega" + + "istio.io/api/annotation" + networking "istio.io/api/networking/v1alpha3" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/annotations" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/converter" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/pod" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/pkg/config" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + domainSuffix = "company.com" + namespace = "ns" + serviceName = "svc1" + ip = "10.0.0.1" + version = "v1" +) + +var ( + fullName = resource.NewName(namespace, serviceName) + + tnow = time.Now() + + podLabels = map[string]string{ + "pl1": "v1", + "pl2": "v2", + } +) + +func TestServiceDefaults(t *testing.T) { + g := NewGomegaWithT(t) + + service := &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + Version: version, + + CreateTime: tnow, + Labels: resource.StringMap{ + "l1": "v1", + "l2": "v2", + }, + Annotations: resource.StringMap{ + "a1": "v1", + "a2": "v2", + }, + }, + Item: &coreV1.ServiceSpec{ + ClusterIP: ip, + Ports: []coreV1.ServicePort{ + { + Name: "http", + Port: 8080, + Protocol: coreV1.ProtocolTCP, + }, + }, + }, + } + + expectedMeta := resource.Metadata{ + Name: service.Metadata.Name, + CreateTime: tnow, + Labels: resource.StringMap{ + "l1": "v1", + "l2": "v2", + }, + Annotations: resource.StringMap{ + "a1": "v1", + "a2": "v2", + annotations.ServiceVersion: version, + }, + } + expected := networking.ServiceEntry{ + Hosts: []string{hostForNamespace(namespace)}, + Addresses: []string{ip}, + Resolution: networking.ServiceEntry_STATIC, + Location: networking.ServiceEntry_MESH_INTERNAL, + Ports: []*networking.Port{ + { + Name: "http", + Number: 8080, + Protocol: "HTTP", + }, + }, + Endpoints: []*networking.ServiceEntry_Endpoint{}, + } + actualMeta, actual := doConvert(t, service, nil, newPodCache()) + g.Expect(actualMeta).To(Equal(expectedMeta)) + g.Expect(actual).To(Equal(expected)) +} + +func TestServiceExportTo(t *testing.T) { + g := NewGomegaWithT(t) + + service := &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + Version: resource.Version("v1"), + CreateTime: tnow, + Annotations: resource.StringMap{ + annotation.NetworkingExportTo.Name: "c, a, b", + }, + }, + Item: &coreV1.ServiceSpec{ + ClusterIP: ip, + }, + } + + expectedMeta := resource.Metadata{ + Name: fullName, + CreateTime: tnow, + Annotations: resource.StringMap{ + annotation.NetworkingExportTo.Name: "c, a, b", + annotations.ServiceVersion: "v1", + }, + } + + expected := networking.ServiceEntry{ + Hosts: []string{hostForNamespace(namespace)}, + Addresses: []string{ip}, + Resolution: networking.ServiceEntry_STATIC, + Location: networking.ServiceEntry_MESH_INTERNAL, + Ports: []*networking.Port{}, + Endpoints: []*networking.ServiceEntry_Endpoint{}, + ExportTo: []string{"a", "b", "c"}, + } + actualMeta, actual := doConvert(t, service, nil, newPodCache()) + g.Expect(actualMeta).To(Equal(expectedMeta)) + g.Expect(actual).To(Equal(expected)) +} + +func TestNoNamespaceShouldUseDefault(t *testing.T) { + g := NewGomegaWithT(t) + + ip := "10.0.0.1" + service := &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("", serviceName), + Version: resource.Version("v1"), + CreateTime: tnow, + }, + Item: &coreV1.ServiceSpec{ + ClusterIP: ip, + }, + } + + expectedMeta := resource.Metadata{ + Name: service.Metadata.Name, + CreateTime: tnow, + Annotations: resource.StringMap{ + annotations.ServiceVersion: "v1", + }, + } + + expected := networking.ServiceEntry{ + Hosts: []string{hostForNamespace(coreV1.NamespaceDefault)}, + Addresses: []string{ip}, + Resolution: networking.ServiceEntry_STATIC, + Location: networking.ServiceEntry_MESH_INTERNAL, + Ports: []*networking.Port{}, + Endpoints: []*networking.ServiceEntry_Endpoint{}, + } + + actualMeta, actual := doConvert(t, service, nil, newPodCache()) + g.Expect(actualMeta).To(Equal(expectedMeta)) + g.Expect(actual).To(Equal(expected)) +} + +func TestServicePorts(t *testing.T) { + cases := []struct { + name string + proto coreV1.Protocol + out config.Protocol + }{ + {"", coreV1.ProtocolTCP, config.ProtocolTCP}, + {"http", coreV1.ProtocolTCP, config.ProtocolHTTP}, + {"http-test", coreV1.ProtocolTCP, config.ProtocolHTTP}, + {"http", coreV1.ProtocolUDP, config.ProtocolUDP}, + {"httptest", coreV1.ProtocolTCP, config.ProtocolTCP}, + {"https", coreV1.ProtocolTCP, config.ProtocolHTTPS}, + {"https-test", coreV1.ProtocolTCP, config.ProtocolHTTPS}, + {"http2", coreV1.ProtocolTCP, config.ProtocolHTTP2}, + {"http2-test", coreV1.ProtocolTCP, config.ProtocolHTTP2}, + {"grpc", coreV1.ProtocolTCP, config.ProtocolGRPC}, + {"grpc-test", coreV1.ProtocolTCP, config.ProtocolGRPC}, + {"grpc-web", coreV1.ProtocolTCP, config.ProtocolGRPCWeb}, + {"grpc-web-test", coreV1.ProtocolTCP, config.ProtocolGRPCWeb}, + {"mongo", coreV1.ProtocolTCP, config.ProtocolMongo}, + {"mongo-test", coreV1.ProtocolTCP, config.ProtocolMongo}, + {"redis", coreV1.ProtocolTCP, config.ProtocolRedis}, + {"redis-test", coreV1.ProtocolTCP, config.ProtocolRedis}, + {"mysql", coreV1.ProtocolTCP, config.ProtocolMySQL}, + {"mysql-test", coreV1.ProtocolTCP, config.ProtocolMySQL}, + } + + ip := "10.0.0.1" + for _, c := range cases { + t.Run(fmt.Sprintf("%s_[%s]", c.proto, c.name), func(t *testing.T) { + g := NewGomegaWithT(t) + + service := &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + Version: resource.Version("v1"), + CreateTime: tnow, + }, + Item: &coreV1.ServiceSpec{ + ClusterIP: ip, + Ports: []coreV1.ServicePort{ + { + Name: c.name, + Port: 8080, + Protocol: c.proto, + }, + }, + }, + } + + expectedMeta := resource.Metadata{ + Name: service.Metadata.Name, + CreateTime: tnow, + Annotations: resource.StringMap{ + annotations.ServiceVersion: version, + }, + } + expected := networking.ServiceEntry{ + Hosts: []string{hostForNamespace(namespace)}, + Addresses: []string{ip}, + Resolution: networking.ServiceEntry_STATIC, + Location: networking.ServiceEntry_MESH_INTERNAL, + Ports: []*networking.Port{ + { + Name: c.name, + Number: 8080, + Protocol: string(c.out), + }, + }, + Endpoints: []*networking.ServiceEntry_Endpoint{}, + } + + actualMeta, actual := doConvert(t, service, nil, newPodCache()) + g.Expect(actualMeta).To(Equal(expectedMeta)) + g.Expect(actual).To(Equal(expected)) + }) + } +} + +func TestClusterIPWithNoResolution(t *testing.T) { + cases := []struct { + name string + clusterIP string + }{ + { + name: "Unspecified", + clusterIP: "", + }, + { + name: "None", + clusterIP: coreV1.ClusterIPNone, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + service := &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + Version: resource.Version("v1"), + CreateTime: tnow, + }, + Item: &coreV1.ServiceSpec{ + ClusterIP: c.clusterIP, + }, + } + + expectedMeta := resource.Metadata{ + Name: service.Metadata.Name, + CreateTime: tnow, + Annotations: resource.StringMap{ + annotations.ServiceVersion: version, + }, + } + expected := networking.ServiceEntry{ + Hosts: []string{hostForNamespace(namespace)}, + Addresses: []string{config.UnspecifiedIP}, + Resolution: networking.ServiceEntry_NONE, + Location: networking.ServiceEntry_MESH_INTERNAL, + Ports: []*networking.Port{}, + Endpoints: []*networking.ServiceEntry_Endpoint{}, + } + + actualMeta, actual := doConvert(t, service, nil, newPodCache()) + g.Expect(actualMeta).To(Equal(expectedMeta)) + g.Expect(actual).To(Equal(expected)) + }) + } +} + +func TestExternalService(t *testing.T) { + g := NewGomegaWithT(t) + + externalName := "myexternalsvc" + service := &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + Version: resource.Version("v1"), + CreateTime: tnow, + }, + Item: &coreV1.ServiceSpec{ + Type: coreV1.ServiceTypeExternalName, + ExternalName: externalName, + Ports: []coreV1.ServicePort{ + { + Name: "http", + Port: 8080, + Protocol: coreV1.ProtocolTCP, + }, + }, + }, + } + + expectedMeta := resource.Metadata{ + Name: service.Metadata.Name, + CreateTime: tnow, + Annotations: resource.StringMap{ + annotations.ServiceVersion: version, + }, + } + expected := networking.ServiceEntry{ + Hosts: []string{hostForNamespace(namespace)}, + Addresses: []string{config.UnspecifiedIP}, + Resolution: networking.ServiceEntry_DNS, + Location: networking.ServiceEntry_MESH_EXTERNAL, + Ports: []*networking.Port{ + { + Name: "http", + Number: 8080, + Protocol: "HTTP", + }, + }, + Endpoints: []*networking.ServiceEntry_Endpoint{ + { + Address: externalName, + Ports: map[string]uint32{ + "http": 8080, + }, + }, + }, + } + + actualMeta, actual := doConvert(t, service, nil, newPodCache()) + g.Expect(actualMeta).To(Equal(expectedMeta)) + g.Expect(actual).To(Equal(expected)) +} + +func TestEndpointsWithNoSubsets(t *testing.T) { + g := NewGomegaWithT(t) + + endpoints := &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + Version: resource.Version("v1"), + CreateTime: tnow, + }, + Item: &coreV1.Endpoints{}, + } + + expectedMeta := resource.Metadata{ + Annotations: resource.StringMap{ + annotations.EndpointsVersion: version, + }, + } + expected := networking.ServiceEntry{ + Endpoints: []*networking.ServiceEntry_Endpoint{}, + SubjectAltNames: []string{}, + } + + actualMeta, actual := doConvert(t, nil, endpoints, newPodCache()) + g.Expect(actualMeta).To(Equal(expectedMeta)) + g.Expect(actual).To(Equal(expected)) +} + +func TestEndpoints(t *testing.T) { + g := NewGomegaWithT(t) + + ip1 := "10.0.0.1" + ip2 := "10.0.0.2" + ip3 := "10.0.0.3" + l1 := "locality1" + l2 := "locality2" + cache := fakePodCache{ + ip1: { + NodeName: "node1", + Locality: l1, + FullName: resource.NewName(namespace, "pod1"), + ServiceAccountName: "sa1", + Labels: podLabels, + }, + ip2: { + NodeName: "node2", + Locality: l2, + FullName: resource.NewName(namespace, "pod2"), + ServiceAccountName: "sa2", + Labels: podLabels, + }, + ip3: { + NodeName: "node1", // Also on node1 + Locality: l1, + FullName: resource.NewName(namespace, "pod3"), + ServiceAccountName: "sa1", // Same service account as pod1 to test duplicates. + Labels: podLabels, + }, + } + + endpoints := &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + Version: resource.Version("v1"), + CreateTime: tnow, + }, + Item: &coreV1.Endpoints{ + ObjectMeta: metaV1.ObjectMeta{}, + Subsets: []coreV1.EndpointSubset{ + { + NotReadyAddresses: []coreV1.EndpointAddress{ + { + IP: ip1, + }, + { + IP: ip2, + }, + { + IP: ip3, + }, + }, + Addresses: []coreV1.EndpointAddress{ + { + IP: ip1, + }, + { + IP: ip2, + }, + { + IP: ip3, + }, + }, + Ports: []coreV1.EndpointPort{ + { + Name: "http", + Protocol: coreV1.ProtocolTCP, + Port: 80, + }, + { + Name: "https", + Protocol: coreV1.ProtocolTCP, + Port: 443, + }, + }, + }, + }, + }, + } + + expectedMeta := resource.Metadata{ + Annotations: resource.StringMap{ + annotations.EndpointsVersion: version, + annotations.NotReadyEndpoints: fmt.Sprintf("%s:%d,%s:%d,%s:%d,%s:%d,%s:%d,%s:%d", + ip1, 80, + ip2, 80, + ip3, 80, + ip1, 443, + ip2, 443, + ip3, 443), + }, + } + expected := networking.ServiceEntry{ + Endpoints: []*networking.ServiceEntry_Endpoint{ + { + Labels: podLabels, + Address: ip1, + Locality: l1, + Ports: map[string]uint32{ + "http": 80, + "https": 443, + }, + }, + { + Labels: podLabels, + Address: ip2, + Locality: l2, + Ports: map[string]uint32{ + "http": 80, + "https": 443, + }, + }, + { + Labels: podLabels, + Address: ip3, + Locality: l1, + Ports: map[string]uint32{ + "http": 80, + "https": 443, + }, + }, + }, + SubjectAltNames: []string{ + "sa1", + "sa2", + }, + } + + actualMeta, actual := doConvert(t, nil, endpoints, cache) + g.Expect(actualMeta).To(Equal(expectedMeta)) + g.Expect(actual).To(Equal(expected)) +} + +func TestEndpointsPodNotFound(t *testing.T) { + g := NewGomegaWithT(t) + + endpoints := &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + Version: resource.Version("v1"), + CreateTime: tnow, + }, + Item: &coreV1.Endpoints{ + ObjectMeta: metaV1.ObjectMeta{}, + Subsets: []coreV1.EndpointSubset{ + { + Addresses: []coreV1.EndpointAddress{ + { + IP: ip, + }, + }, + Ports: []coreV1.EndpointPort{ + { + Name: "http", + Protocol: coreV1.ProtocolTCP, + Port: 80, + }, + }, + }, + }, + }, + } + + expectedMeta := resource.Metadata{ + Annotations: resource.StringMap{ + annotations.EndpointsVersion: version, + }, + } + expected := networking.ServiceEntry{ + Endpoints: []*networking.ServiceEntry_Endpoint{ + { + Address: ip, + Locality: "", + Ports: map[string]uint32{ + "http": 80, + }, + }, + }, + SubjectAltNames: []string{}, + } + + actualMeta, actual := doConvert(t, nil, endpoints, newPodCache()) + g.Expect(actualMeta).To(Equal(expectedMeta)) + g.Expect(actual).To(Equal(expected)) +} + +func TestEndpointsNodeNotFound(t *testing.T) { + g := NewGomegaWithT(t) + + cache := fakePodCache{ + ip: { + NodeName: "node1", + FullName: resource.NewName(namespace, "pod1"), + ServiceAccountName: "sa1", + Labels: podLabels, + }, + } + + endpoints := &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + Version: resource.Version("v1"), + CreateTime: tnow, + }, + Item: &coreV1.Endpoints{ + Subsets: []coreV1.EndpointSubset{ + { + Addresses: []coreV1.EndpointAddress{ + { + IP: ip, + }, + }, + Ports: []coreV1.EndpointPort{ + { + Name: "http", + Protocol: coreV1.ProtocolTCP, + Port: 80, + }, + }, + }, + }, + }, + } + + expectedMeta := resource.Metadata{ + Annotations: resource.StringMap{ + annotations.EndpointsVersion: version, + }, + } + expected := networking.ServiceEntry{ + Endpoints: []*networking.ServiceEntry_Endpoint{ + { + Address: ip, + Locality: "", + Ports: map[string]uint32{ + "http": 80, + }, + Labels: podLabels, + }, + }, + SubjectAltNames: []string{"sa1"}, + } + + actualMeta, actual := doConvert(t, nil, endpoints, cache) + g.Expect(actualMeta).To(Equal(expectedMeta)) + g.Expect(actual).To(Equal(expected)) +} + +func doConvert(t *testing.T, service *resource.Entry, endpoints *resource.Entry, pods pod.Cache) (resource.Metadata, networking.ServiceEntry) { + actualMeta := newMetadata() + actual := newServiceEntry() + c := newInstance(pods) + if err := c.Convert(service, endpoints, actualMeta, actual); err != nil { + t.Fatal(err) + } + return *actualMeta, *actual +} + +func newInstance(pods pod.Cache) *converter.Instance { + return converter.New(domainSuffix, pods) +} + +func newServiceEntry() *networking.ServiceEntry { + return &networking.ServiceEntry{} +} + +func newMetadata() *resource.Metadata { + return &resource.Metadata{ + Annotations: make(map[string]string), + } +} + +func hostForNamespace(namespace string) string { + return fmt.Sprintf("%s.%s.svc.%s", serviceName, namespace, domainSuffix) +} + +var _ pod.Cache = newPodCache() + +type fakePodCache map[string]pod.Info + +func newPodCache() fakePodCache { + return make(fakePodCache) +} + +func (c fakePodCache) GetPodByIP(ip string) (pod.Info, bool) { + p, ok := c[ip] + return p, ok +} diff --git a/galley/pkg/config/processor/transforms/serviceentry/create.go b/galley/pkg/config/processor/transforms/serviceentry/create.go new file mode 100644 index 000000000000..52fad2965e19 --- /dev/null +++ b/galley/pkg/config/processor/transforms/serviceentry/create.go @@ -0,0 +1,29 @@ +// 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 serviceentry + +import ( + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processing" +) + +// Create transformer for Synthetic Service entries +func Create(o processing.ProcessorOptions) []event.Transformer { + return []event.Transformer{ + &transformer{ + options: o, + }, + } +} diff --git a/galley/pkg/config/processor/transforms/serviceentry/create_test.go b/galley/pkg/config/processor/transforms/serviceentry/create_test.go new file mode 100644 index 000000000000..2de215f4c6b2 --- /dev/null +++ b/galley/pkg/config/processor/transforms/serviceentry/create_test.go @@ -0,0 +1,1305 @@ +// 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 serviceentry_test + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + "testing" + "time" + + "github.com/gogo/protobuf/types" + . "github.com/onsi/gomega" + + mcp "istio.io/api/mcp/v1alpha1" + networking "istio.io/api/networking/v1alpha3" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/meshcfg" + "istio.io/istio/galley/pkg/config/processing" + "istio.io/istio/galley/pkg/config/processing/snapshotter" + "istio.io/istio/galley/pkg/config/processing/snapshotter/strategy" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/annotations" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/pod" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/testing/fixtures" + "istio.io/istio/pkg/config" + "istio.io/istio/pkg/mcp/snapshot" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + domain = "company.com" + clusterIP = "10.0.0.10" + pod1IP = "10.0.0.1" + pod2IP = "10.0.0.2" + namespace = "fakeNamespace" + nodeName = "node1" + region = "region1" + zone = "zone1" +) + +var ( + serviceName = resource.NewName(namespace, "svc1") + createTime = time.Now() + + nodeCollection = metadata.K8SCoreV1Nodes + podCollection = metadata.K8SCoreV1Pods + serviceCollection = metadata.K8SCoreV1Services + endpointsCollection = metadata.K8SCoreV1Endpoints + serviceEntryCollection = metadata.IstioNetworkingV1Alpha3SyntheticServiceentries + serviceAnnotations = resource.StringMap{ + "ak1": "av1", + } + serviceLabels = resource.StringMap{ + "lk1": "lv1", + } + podLabels = resource.StringMap{ + "pk1": "pv1", + } +) + +func TestInvalidCollectionShouldNotPanic(t *testing.T) { + rt, src, _, _ := newHandler() + defer rt.Stop() + src.Handlers.Handle(event.Event{ + Kind: event.Added, + Source: metadata.IstioNetworkingV1Alpha3Gateways, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns", "svc1"), + Version: resource.Version("123"), + }, + }, + }) +} + +func TestLifecycle(t *testing.T) { + expectedVersion := 0 + var service *resource.Entry + var endpoints *resource.Entry + + stages := []stage{ + { + name: "NodeSync", + event: event.FullSyncFor(metadata.K8SCoreV1Nodes), + }, + { + name: "PodSync", + event: event.FullSyncFor(metadata.K8SCoreV1Pods), + }, + { + name: "ServiceSync", + event: event.FullSyncFor(metadata.K8SCoreV1Services), + }, + { + name: "EndpointSync", + event: event.FullSyncFor(metadata.K8SCoreV1Endpoints), + validator: func(ctx pipelineContext) { + expectNotifications(ctx.t, ctx.acc, 1) + }, + }, + { + name: "AddNode", + event: event.Event{ + Kind: event.Added, + Source: nodeCollection, + Entry: nodeEntry(), + }, + validator: func(ctx pipelineContext) { + expectNotifications(ctx.t, ctx.acc, 0) + }, + }, + { + name: "AddPod1", + event: event.Event{ + Kind: event.Added, + Source: podCollection, + Entry: podEntry(resource.NewName(namespace, "pod1"), pod1IP, "sa1"), + }, + validator: func(ctx pipelineContext) { + expectNotifications(ctx.t, ctx.acc, 0) + }, + }, + { + name: "AddPod2", + event: event.Event{ + Kind: event.Added, + Source: podCollection, + Entry: podEntry(resource.NewName(namespace, "pod2"), pod2IP, "sa2"), + }, + validator: func(ctx pipelineContext) { + expectNotifications(ctx.t, ctx.acc, 0) + }, + }, + { + name: "AddService", + event: event.Event{ + Kind: event.Added, + Source: serviceCollection, + Entry: entryForService(serviceName, createTime, "v1"), + }, + validator: func(ctx pipelineContext) { + service = entryForService(serviceName, createTime, "v1") + + expectNotifications(ctx.t, ctx.acc, 1) + expectedVersion++ + expectedMetadata := newMetadataBuilder(service, endpoints). + CreateTime(createTime). + Version(expectedVersion). + Labels(serviceLabels). + Build() + expectedBody := newServiceEntryBuilder(). + ServiceName(serviceName). + Region(region). + Zone(zone). + PodLabels(podLabels). + Build() + expectResource(ctx.t, ctx.dst, expectedVersion, expectedMetadata, expectedBody) + }, + }, + { + name: "UpdateService", + event: event.Event{ + Kind: event.Updated, + Source: serviceCollection, + Entry: entryForService(serviceName, createTime, "v2"), + }, + validator: func(ctx pipelineContext) { + service = entryForService(serviceName, createTime, "v2") + + expectNotifications(ctx.t, ctx.acc, 1) + expectedVersion++ + expectedMetadata := newMetadataBuilder(service, endpoints). + CreateTime(createTime). + Version(expectedVersion). + Labels(serviceLabels). + Build() + expectedBody := newServiceEntryBuilder(). + ServiceName(serviceName). + Region(region). + Zone(zone). + PodLabels(podLabels). + Build() + expectResource(ctx.t, ctx.dst, expectedVersion, expectedMetadata, expectedBody) + }, + }, + { + name: "AddEndpoints", + event: event.Event{ + Kind: event.Added, + Source: endpointsCollection, + Entry: newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v1"). + IPs(pod1IP). + NotReadyIPs(pod2IP). + Build(), + }, + validator: func(ctx pipelineContext) { + entry := newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v1"). + IPs(pod1IP). + NotReadyIPs(pod2IP). + Build() + endpoints = entry + + expectedVersion++ + expectedMetadata := newMetadataBuilder(service, endpoints). + CreateTime(createTime). + Version(expectedVersion). + Labels(serviceLabels). + NotReadyIPs(pod2IP). + Build() + expectedBody := newServiceEntryBuilder(). + ServiceName(serviceName). + Region(region). + Zone(zone). + IPs(pod1IP). + ServiceAccounts("sa1"). + PodLabels(podLabels). + Build() + expectResource(ctx.t, ctx.dst, expectedVersion, expectedMetadata, expectedBody) + }, + }, + { + name: "ExpandEndpoints", + event: event.Event{ + Kind: event.Updated, + Source: endpointsCollection, + Entry: newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v2"). + IPs(pod1IP, pod2IP). + Build(), + }, + validator: func(ctx pipelineContext) { + entry := newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v2"). + IPs(pod1IP, pod2IP). + Build() + endpoints = entry + + expectNotifications(ctx.t, ctx.acc, 1) + expectedVersion++ + expectedMetadata := newMetadataBuilder(service, endpoints). + CreateTime(createTime). + Version(expectedVersion). + Labels(serviceLabels). + Build() + expectedBody := newServiceEntryBuilder(). + ServiceName(serviceName). + Region(region). + Zone(zone). + IPs(pod1IP, pod2IP). + ServiceAccounts("sa1", "sa2"). + PodLabels(podLabels). + Build() + expectResource(ctx.t, ctx.dst, expectedVersion, expectedMetadata, expectedBody) + }, + }, + { + name: "ContractEndpoints", + event: event.Event{ + Kind: event.Updated, + Source: endpointsCollection, + Entry: newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v3"). + IPs(pod2IP). + NotReadyIPs(pod1IP). + Build(), + }, + validator: func(ctx pipelineContext) { + entry := newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v3"). + IPs(pod2IP). + NotReadyIPs(pod1IP). + Build() + endpoints = entry + + expectNotifications(ctx.t, ctx.acc, 1) + expectedVersion++ + expectedMetadata := newMetadataBuilder(service, endpoints). + CreateTime(createTime). + Version(expectedVersion). + Labels(serviceLabels). + NotReadyIPs(pod1IP). + Build() + expectedBody := newServiceEntryBuilder(). + ServiceName(serviceName). + Region(region). + Zone(zone). + IPs(pod2IP). + ServiceAccounts("sa2"). + PodLabels(podLabels). + Build() + expectResource(ctx.t, ctx.dst, expectedVersion, expectedMetadata, expectedBody) + }, + }, + { + name: "DeleteEndpoints", + event: event.Event{ + Kind: event.Deleted, + Source: endpointsCollection, + Entry: newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v3"). + IPs(pod2IP). + Build(), + }, + validator: func(ctx pipelineContext) { + endpoints = nil + expectNotifications(ctx.t, ctx.acc, 1) + expectedVersion++ + expectedMetadata := newMetadataBuilder(service, endpoints). + CreateTime(createTime). + Version(expectedVersion). + Labels(serviceLabels). + Build() + expectedBody := newServiceEntryBuilder(). + ServiceName(serviceName). + Region(region). + Zone(zone). + PodLabels(podLabels). + Build() + expectResource(ctx.t, ctx.dst, expectedVersion, expectedMetadata, expectedBody) + }, + }, + { + name: "DeleteService", + event: event.Event{ + Kind: event.Deleted, + Source: serviceCollection, + Entry: entryForService(serviceName, createTime, "v2"), + }, + validator: func(ctx pipelineContext) { + expectNotifications(ctx.t, ctx.acc, 1) + + expectedVersion++ + expectEmptySnapshot(ctx.t, ctx.dst, expectedVersion) + }, + }, + } + + newPipeline(stages).run(t, nil) +} + +func TestAddOrder(t *testing.T) { + initialStages := []stage{ + { + name: "NodeSync", + event: event.FullSyncFor(metadata.K8SCoreV1Nodes), + }, + { + name: "PodSync", + event: event.FullSyncFor(metadata.K8SCoreV1Pods), + }, + { + name: "ServiceSync", + event: event.FullSyncFor(metadata.K8SCoreV1Services), + }, + { + name: "EndpointSync", + event: event.FullSyncFor(metadata.K8SCoreV1Endpoints), + }, + } + + stages := []stage{ + { + name: "Node", + event: event.Event{ + Kind: event.Added, + Source: nodeCollection, + Entry: nodeEntry(), + }, + }, + { + name: "Pod", + event: event.Event{ + Kind: event.Added, + Source: podCollection, + Entry: podEntry(resource.NewName(namespace, "pod1"), pod1IP, "sa1"), + }, + }, + { + name: "Service", + event: event.Event{ + Kind: event.Added, + Source: serviceCollection, + Entry: entryForService(serviceName, createTime, "v1"), + }, + }, + { + name: "Endpoints", + event: event.Event{ + Kind: event.Added, + Source: endpointsCollection, + Entry: newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v1"). + IPs(pod1IP). + Build(), + }, + }, + } + + // Iterate over all permutations of the events + for _, stageOrder := range getStagePermutations(stages) { + p := newPipeline(append(initialStages, stageOrder...)) + defer p.rt.Stop() + + t.Run(p.name(), func(t *testing.T) { + var service *resource.Entry + var endpoints *resource.Entry + hasPod := false + hasNode := false + expectedVersion := 0 + + p.run(t, func(ctx pipelineContext) { + // Determine whether or not an update is expected. + entry := ctx.s.event.Entry + updateExpected := false + switch ctx.s.name { + case "Service": + service = entry + updateExpected = true + case "Endpoints": + endpoints = entry + updateExpected = service != nil + case "Pod": + hasPod = true + updateExpected = service != nil && endpoints != nil + case "Node": + hasNode = true + updateExpected = service != nil && endpoints != nil && hasPod + case "EndpointSync": + expectNotifications(ctx.t, ctx.acc, 1) + return + } + + if !updateExpected { + expectNotifications(ctx.t, ctx.acc, 0) + } else { + expectNotifications(ctx.t, ctx.acc, 1) + expectedVersion++ + expectedMetadata := newMetadataBuilder(service.Clone(), endpoints). + CreateTime(createTime). + Version(expectedVersion). + Labels(serviceLabels). + Build() + + seBuilder := newServiceEntryBuilder().ServiceName(serviceName) + + if endpoints != nil { + seBuilder.IPs(pod1IP) + + if hasPod { + seBuilder.PodLabels(podLabels).ServiceAccounts("sa1") + if hasNode { + seBuilder.Region(region).Zone(zone) + } + } + } + + expectedBody := seBuilder.Build() + expectResource(ctx.t, ctx.dst, expectedVersion, expectedMetadata, expectedBody) + } + }) + }) + } +} + +func TestDeleteOrder(t *testing.T) { + stages := []stage{ + { + name: "Node", + event: event.Event{ + Kind: event.Deleted, + Source: nodeCollection, + Entry: nodeEntry(), + }, + }, + { + name: "Pod", + event: event.Event{ + Kind: event.Deleted, + Source: podCollection, + Entry: podEntry(resource.NewName(namespace, "pod1"), pod1IP, "sa1"), + }, + }, + { + name: "Endpoints", + event: event.Event{ + Kind: event.Deleted, + Source: endpointsCollection, + Entry: newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v1"). + IPs(pod1IP). + Build(), + }, + }, + { + name: "Service", + event: event.Event{ + Kind: event.Deleted, + Source: serviceCollection, + Entry: entryForService(serviceName, createTime, "v1"), + }, + }, + } + + // Create the initialization stages, which will add all of the resources we're about to delete. + initStages := append([]stage{}, stages...) + for i, s := range initStages { + s.event.Kind = event.Added + initStages[i] = s + } + + syncStages := []stage{ + { + name: "NodeSync", + event: event.FullSyncFor(metadata.K8SCoreV1Nodes), + }, + { + name: "PodSync", + event: event.FullSyncFor(metadata.K8SCoreV1Pods), + }, + { + name: "ServiceSync", + event: event.FullSyncFor(metadata.K8SCoreV1Services), + }, + { + name: "EndpointSync", + event: event.FullSyncFor(metadata.K8SCoreV1Endpoints), + validator: func(ctx pipelineContext) { + expectNotifications(ctx.t, ctx.acc, 1) + }, + }, + } + + initStages = append(syncStages, initStages...) + + for _, orderedStages := range getStagePermutations(stages) { + p := newPipeline(orderedStages) + defer p.rt.Stop() + + t.Run(p.name(), func(t *testing.T) { + // Add all of the resources to the handler. + initPipeline := &pipeline{ + stages: initStages, + rt: p.rt, + src: p.src, + dst: p.dst, // Use the same handler + acc: p.acc, + } + t.Run("Initialize", func(t *testing.T) { + initPipeline.run(t, nil) + expectNotifications(t, p.acc, 1) + }) + + t.Run("Delete", func(t *testing.T) { + entry := entryForService(serviceName, createTime, "v1") + service := &entry + + entry = newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v1"). + IPs(pod1IP). + Build() + endpoints := entry + + hasPod := true + hasNode := true + expectedVersion := 1 + + // Re-run the pipeline, but deleting the resources. + p.run(t, func(ctx pipelineContext) { + // Determine whether or not an update is expected. + updateExpected := false + switch ctx.s.name { + case "Service": + service = nil + updateExpected = true + case "Endpoints": + endpoints = nil + updateExpected = service != nil + case "Pod": + hasPod = false + updateExpected = service != nil && endpoints != nil + case "Node": + hasNode = false + updateExpected = service != nil && endpoints != nil && hasPod + } + + if !updateExpected { + expectNotifications(ctx.t, ctx.acc, 0) + } else { + expectNotifications(ctx.t, ctx.acc, 1) + + expectedVersion++ + if service == nil { + expectEmptySnapshot(t, ctx.dst, expectedVersion) + } else { + expectedMetadata := newMetadataBuilder(*service, endpoints). + CreateTime(createTime). + Version(expectedVersion). + Labels(serviceLabels). + Build() + + seBuilder := newServiceEntryBuilder().ServiceName(serviceName) + + if endpoints != nil { + seBuilder.IPs(pod1IP) + + if hasPod { + seBuilder.PodLabels(podLabels).ServiceAccounts("sa1") + if hasNode { + seBuilder.Region(region).Zone(zone) + } + } + } + + expectedBody := seBuilder.Build() + expectResource(ctx.t, ctx.dst, expectedVersion, expectedMetadata, expectedBody) + } + } + }) + }) + }) + } +} + +func TestReceiveEndpointsBeforeService(t *testing.T) { + rt, src, dst, acc := newHandler() + defer rt.Stop() + + syncEvents := []event.Event{ + event.FullSyncFor(metadata.K8SCoreV1Nodes), + event.FullSyncFor(metadata.K8SCoreV1Pods), + event.FullSyncFor(metadata.K8SCoreV1Services), + event.FullSyncFor(metadata.K8SCoreV1Endpoints), + } + + for _, e := range syncEvents { + src.Handlers.Handle(e) + } + expectNotifications(t, acc, 1) + + expectedVersion := 0 + t.Run("AddNode", func(t *testing.T) { + src.Handlers.Handle(event.Event{ + Kind: event.Added, + Source: nodeCollection, + Entry: nodeEntry(), + }) + expectNotifications(t, acc, 0) + }) + + t.Run("AddPod", func(t *testing.T) { + src.Handlers.Handle(event.Event{ + Kind: event.Added, + Source: podCollection, + Entry: podEntry(resource.NewName(namespace, "pod1"), pod1IP, "sa1"), + }) + expectNotifications(t, acc, 0) + }) + + var endpoints *resource.Entry + t.Run("AddEndpoints", func(t *testing.T) { + endpoints = newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v1"). + IPs(pod1IP). + Build() + src.Handlers.Handle(event.Event{ + Kind: event.Added, + Source: endpointsCollection, + Entry: endpoints, + }) + expectNotifications(t, acc, 0) + expectEmptySnapshot(t, dst, expectedVersion) + }) + + t.Run("AddService", func(t *testing.T) { + service := entryForService(serviceName, createTime, "v1") + src.Handlers.Handle(event.Event{ + Kind: event.Added, + Source: serviceCollection, + Entry: service, + }) + expectNotifications(t, acc, 1) + expectedVersion++ + expectedMetadata := newMetadataBuilder(service, endpoints). + CreateTime(createTime). + Version(expectedVersion). + Labels(serviceLabels). + Build() + expectedBody := newServiceEntryBuilder(). + ServiceName(serviceName). + Region(region). + Zone(zone). + IPs(pod1IP). + ServiceAccounts("sa1"). + PodLabels(podLabels). + Build() + expectResource(t, dst, expectedVersion, expectedMetadata, expectedBody) + }) +} + +func TestAddEndpointsWithUnknownEventKindShouldNotPanic(t *testing.T) { + rt, src, _, acc := newHandler() + defer rt.Stop() + + src.Handlers.Handle(event.Event{ + Kind: event.None, + Entry: newEndpointsEntryBuilder(). + ServiceName(serviceName). + CreateTime(createTime). + Version("v1"). + IPs(pod1IP). + Build(), + }) + expectNotifications(t, acc, 0) +} + +func newHandler() (*processing.Runtime, *fixtures.Source, *snapshotter.InMemoryDistributor, *fixtures.Accumulator) { + a := &fixtures.Accumulator{} + + src := &fixtures.Source{} + meshSrc := meshcfg.NewInmemory() + meshSrc.Set(meshcfg.Default()) + + dst := snapshotter.NewInMemoryDistributor() + o := processing.RuntimeOptions{ + DomainSuffix: domain, + Source: event.CombineSources(src, meshSrc), + ProcessorProvider: func(o processing.ProcessorOptions) event.Processor { + xforms := serviceentry.Create(o) + xforms[0].DispatchFor(metadata.IstioNetworkingV1Alpha3SyntheticServiceentries, a) + settings := []snapshotter.SnapshotOptions{ + { + Group: "syntheticServiceEntry", + Collections: []collection.Name{metadata.IstioNetworkingV1Alpha3SyntheticServiceentries}, + Strategy: strategy.NewImmediate(), + Distributor: dst, + }, + } + s, _ := snapshotter.NewSnapshotter(xforms, settings) + return s + }, + } + p := processing.NewRuntime(o) + p.Start() + + return p, src, dst, a +} + +func nodeEntry() *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("", nodeName), + Version: resource.Version("v1"), + Labels: localityLabels(region, zone), + }, + Item: &coreV1.NodeSpec{}, + } +} + +func podEntry(podName resource.Name, ip, saName string) *resource.Entry { + ns, name := podName.InterpretAsNamespaceAndName() + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: podName, + Version: resource.Version("v1"), + }, + Item: &coreV1.Pod{ + ObjectMeta: metaV1.ObjectMeta{ + Name: name, + Namespace: ns, + Labels: podLabels, + }, + Spec: coreV1.PodSpec{ + NodeName: nodeName, + ServiceAccountName: saName, + }, + Status: coreV1.PodStatus{ + PodIP: ip, + Phase: coreV1.PodRunning, + }, + }, + } +} + +func entryForService(serviceName resource.Name, createTime time.Time, version string) *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: serviceName, + Version: resource.Version(version), + CreateTime: createTime, + Annotations: serviceAnnotations, + Labels: serviceLabels, + }, + Item: &coreV1.ServiceSpec{ + ClusterIP: clusterIP, + Ports: []coreV1.ServicePort{ + { + Name: "http", + Protocol: coreV1.ProtocolTCP, + Port: 80, + }, + }, + }, + } +} + +type endpointsEntryBuilder struct { + serviceName resource.Name + createTime time.Time + version string + ips []string + notReadyIPs []string +} + +func newEndpointsEntryBuilder() *endpointsEntryBuilder { + return &endpointsEntryBuilder{} +} + +func (b *endpointsEntryBuilder) ServiceName(serviceName resource.Name) *endpointsEntryBuilder { + b.serviceName = serviceName + return b +} + +func (b *endpointsEntryBuilder) CreateTime(createTime time.Time) *endpointsEntryBuilder { + b.createTime = createTime + return b +} + +func (b *endpointsEntryBuilder) Version(version string) *endpointsEntryBuilder { + b.version = version + return b +} + +func (b *endpointsEntryBuilder) IPs(ips ...string) *endpointsEntryBuilder { + b.ips = ips + return b +} + +func (b *endpointsEntryBuilder) NotReadyIPs(ips ...string) *endpointsEntryBuilder { + b.notReadyIPs = ips + return b +} + +func (b *endpointsEntryBuilder) Build() *resource.Entry { + ns, n := b.serviceName.InterpretAsNamespaceAndName() + + eps := &coreV1.Endpoints{ + ObjectMeta: metaV1.ObjectMeta{ + CreationTimestamp: metaV1.Time{Time: b.createTime}, + Name: n, + Namespace: ns, + }, + Subsets: []coreV1.EndpointSubset{ + { + Ports: []coreV1.EndpointPort{ + { + Name: "http", + Port: 80, + Protocol: coreV1.ProtocolTCP, + }, + }, + }, + }, + } + + for _, ip := range b.ips { + eps.Subsets[0].Addresses = append(eps.Subsets[0].Addresses, coreV1.EndpointAddress{ + IP: ip, + }) + } + + for _, ip := range b.notReadyIPs { + eps.Subsets[0].NotReadyAddresses = append(eps.Subsets[0].NotReadyAddresses, coreV1.EndpointAddress{ + IP: ip, + }) + } + + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: b.serviceName, + Version: resource.Version(b.version), + CreateTime: b.createTime, + Annotations: serviceAnnotations, + }, + Item: eps, + } +} + +func host(namespace, serviceName string) string { + return fmt.Sprintf("%s.%s.svc.%s", serviceName, namespace, domain) +} + +func localityLabels(region, zone string) resource.StringMap { + labels := make(resource.StringMap) + if region != "" { + labels[pod.LabelZoneRegion] = region + } + if zone != "" { + labels[pod.LabelZoneFailureDomain] = zone + } + return labels +} + +type metadataBuilder struct { + service *resource.Entry + endpoints *resource.Entry + notReadyIPs []string + + version int + createTime time.Time + labels map[string]string +} + +func newMetadataBuilder(service *resource.Entry, endpoints *resource.Entry) *metadataBuilder { + return &metadataBuilder{ + service: service, + endpoints: endpoints, + } +} + +func (b *metadataBuilder) NotReadyIPs(notReadyIPs ...string) *metadataBuilder { + b.notReadyIPs = notReadyIPs + return b +} + +func (b *metadataBuilder) Version(version int) *metadataBuilder { + b.version = version + return b +} + +func (b *metadataBuilder) CreateTime(createTime time.Time) *metadataBuilder { + b.createTime = createTime + return b +} + +func (b *metadataBuilder) Labels(labels map[string]string) *metadataBuilder { + b.labels = labels + return b +} + +func (b *metadataBuilder) Build() *mcp.Metadata { + protoTime, _ := types.TimestampProto(b.createTime) + + annos := make(map[string]string) + for k, v := range b.service.Metadata.Annotations { + annos[k] = v + } + annos[annotations.ServiceVersion] = string(b.service.Metadata.Version) + if b.endpoints != nil { + annos[annotations.EndpointsVersion] = string(b.endpoints.Metadata.Version) + if len(b.notReadyIPs) > 0 { + annos[annotations.NotReadyEndpoints] = notReadyAnnotation(b.notReadyIPs...) + } + } + + return &mcp.Metadata{ + Name: serviceName.String(), + Version: strconv.Itoa(b.version), + CreateTime: protoTime, + Labels: b.labels, + Annotations: annos, + } +} + +type serviceEntryBuilder struct { + serviceName resource.Name + region string + zone string + ips []string + serviceAccounts []string + podLabels map[string]string +} + +func newServiceEntryBuilder() *serviceEntryBuilder { + return &serviceEntryBuilder{} +} + +func (b *serviceEntryBuilder) ServiceName(serviceName resource.Name) *serviceEntryBuilder { + b.serviceName = serviceName + return b +} + +func (b *serviceEntryBuilder) Region(region string) *serviceEntryBuilder { + b.region = region + return b +} + +func (b *serviceEntryBuilder) Zone(zone string) *serviceEntryBuilder { + b.zone = zone + return b +} + +func (b *serviceEntryBuilder) IPs(ips ...string) *serviceEntryBuilder { + b.ips = ips + return b +} + +func (b *serviceEntryBuilder) ServiceAccounts(serviceAccounts ...string) *serviceEntryBuilder { + b.serviceAccounts = serviceAccounts + return b +} + +func (b *serviceEntryBuilder) PodLabels(podLabels map[string]string) *serviceEntryBuilder { + b.podLabels = podLabels + return b +} + +func (b *serviceEntryBuilder) Build() *networking.ServiceEntry { + ns, n := b.serviceName.InterpretAsNamespaceAndName() + entry := &networking.ServiceEntry{ + Hosts: []string{host(ns, n)}, + Addresses: []string{clusterIP}, + Resolution: networking.ServiceEntry_STATIC, + Location: networking.ServiceEntry_MESH_INTERNAL, + Ports: []*networking.Port{ + { + Name: "http", + Number: 80, + Protocol: string(config.ProtocolHTTP), + }, + }, + SubjectAltNames: expectedSubjectAltNames(ns, b.serviceAccounts), + } + + for _, ip := range b.ips { + entry.Endpoints = append(entry.Endpoints, &networking.ServiceEntry_Endpoint{ + Address: ip, + Ports: map[string]uint32{ + "http": 80, + }, + + Locality: localityFor(b.region, b.zone), + Labels: b.podLabels, + }) + } + + return entry +} + +type validatorFunc func(ctx pipelineContext) + +type stage struct { + name string + event event.Event + validator validatorFunc +} + +type pipelineContext struct { + t *testing.T + acc *fixtures.Accumulator + src *fixtures.Source + dst *snapshotter.InMemoryDistributor + s stage +} + +type pipeline struct { + stages []stage + rt *processing.Runtime + acc *fixtures.Accumulator + src *fixtures.Source + dst *snapshotter.InMemoryDistributor +} + +func newPipeline(stages []stage) *pipeline { + rt, src, dst, acc := newHandler() + return &pipeline{ + stages: append([]stage{}, stages...), + rt: rt, + src: src, + dst: dst, + acc: acc, + } +} + +func (p *pipeline) name() string { + name := "" + for i, s := range p.stages { + if i > 0 { + name += "_" + } + name += s.name + } + return name +} + +func (p *pipeline) run(t *testing.T, globalValidator validatorFunc) { + t.Helper() + failed := false + for _, s := range p.stages { + success := t.Run(s.name, func(t *testing.T) { + if failed { + t.Fatal("previous stage failed") + } + + // Clear the accumulator + p.acc.Clear() + + // Handle the event. + p.src.Handlers.Handle(s.event) + + // If a global validator was supplied, use it. Otherwise use the stage validator. + v := globalValidator + if v == nil { + v = s.validator + } + if v != nil { + v(pipelineContext{ + t: t, + src: p.src, + dst: p.dst, + acc: p.acc, + s: s, + }) + } + }) + failed = failed || !success + } +} + +func getStagePermutations(values []stage) [][]stage { + var helper func([]stage, int) + res := make([][]stage, 0) + + helper = func(arr []stage, n int) { + if n == 1 { + tmp := make([]stage, len(arr)) + copy(tmp, arr) + res = append(res, tmp) + } else { + for i := 0; i < n; i++ { + helper(arr, n-1) + if n%2 == 1 { + arr[i], arr[n-1] = arr[n-1], arr[i] + } else { + arr[0], arr[n-1] = arr[n-1], arr[0] + } + } + } + } + helper(values, len(values)) + return res +} + +func localityFor(region, zone string) string { + if region != "" || zone != "" { + return fmt.Sprintf("%s/%s", region, zone) + } + return "" +} + +func notReadyAnnotation(ips ...string) string { + for i := range ips { + ips[i] += ":80" + } + + return strings.Join(ips, ",") +} + +func expectNotifications(t *testing.T, a *fixtures.Accumulator, count int) { + t.Helper() + g := NewGomegaWithT(t) + + if count == 0 { + g.Consistently(a.Events).Should(HaveLen(count)) + } else { + g.Eventually(a.Events).Should(HaveLen(count)) + } + for _, e := range a.Events() { + g.Expect(e.Source).To(Equal(serviceEntryCollection)) + } + a.Clear() +} + +func toJSON(t *testing.T, obj interface{}) string { + t.Helper() + if obj == nil { + return "nil" + } + + out, err := json.MarshalIndent(obj, "", " ") + if err != nil { + t.Fatal(err) + } + return string(out) +} + +func expectResource( + t *testing.T, + dst *snapshotter.InMemoryDistributor, + expectedVersion int, + expectedMetadata *mcp.Metadata, + expectedBody *networking.ServiceEntry) { + t.Helper() + g := NewGomegaWithT(t) + + g.Eventually(func() snapshot.Snapshot { + return dst.GetSnapshot("syntheticServiceEntry") + }).ShouldNot(BeNil()) + + expectedVersionStr := fmt.Sprintf("istio/networking/v1alpha3/synthetic/serviceentries/%d", expectedVersion) + g.Eventually(func() string { + sn := dst.GetSnapshot("syntheticServiceEntry") + return sn.Version(serviceEntryCollection.String()) + }).Should(Equal(expectedVersionStr)) + + sn := dst.GetSnapshot("syntheticServiceEntry") + // Extract out the resource. + rs := sn.Resources(serviceEntryCollection.String()) + if len(rs) != 1 { + t.Fatalf("expected snapshot resource count %d to equal %d", len(rs), 1) + } + actual := rs[0] + + // Verify the content. + actualMetadata := actual.Metadata + actualBody := &networking.ServiceEntry{} + if err := types.UnmarshalAny(actual.Body, actualBody); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(actualMetadata, expectedMetadata) { + t.Fatalf("expected:\n%s\nto equal:\n%s\n", toJSON(t, actualMetadata), toJSON(t, expectedMetadata)) + } + if !reflect.DeepEqual(actualBody, expectedBody) { + t.Fatalf("expected:\n%s\nto equal:\n%s\n", toJSON(t, actualBody), toJSON(t, expectedBody)) + } +} + +func expectEmptySnapshot(t *testing.T, dst *snapshotter.InMemoryDistributor, expectedVersion int) { + t.Helper() + g := NewGomegaWithT(t) + + g.Eventually(func() snapshot.Snapshot { return dst.GetSnapshot("syntheticServiceEntry") }).ShouldNot(BeNil()) + expectedVersionStr := fmt.Sprintf("istio/networking/v1alpha3/synthetic/serviceentries/%d", expectedVersion) + g.Eventually(func() string { + sn := dst.GetSnapshot("syntheticServiceEntry") + return sn.Version(serviceEntryCollection.String()) + }).Should(Equal(expectedVersionStr)) + + sn := dst.GetSnapshot("syntheticServiceEntry") + + // Verify there are no resources in the snapshot. + rs := sn.Resources(serviceEntryCollection.String()) + if len(rs) != 0 { + t.Fatalf("expected snapshot resource count %d to equal %d", len(rs), 0) + } +} + +func expectedSubjectAltNames(ns string, serviceAccountNames []string) []string { + if serviceAccountNames == nil { + return nil + } + out := make([]string, 0, len(serviceAccountNames)) + for _, serviceAccountName := range serviceAccountNames { + out = append(out, expectedSubjectAltName(ns, serviceAccountName)) + } + return out +} + +func expectedSubjectAltName(ns, serviceAccountName string) string { + return fmt.Sprintf("spiffe://cluster.local/ns/%s/sa/%s", ns, serviceAccountName) +} diff --git a/galley/pkg/config/processor/transforms/serviceentry/handler_bench_test.go b/galley/pkg/config/processor/transforms/serviceentry/handler_bench_test.go new file mode 100644 index 000000000000..554d88277e15 --- /dev/null +++ b/galley/pkg/config/processor/transforms/serviceentry/handler_bench_test.go @@ -0,0 +1,311 @@ +// 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 serviceentry_test + +import ( + "strconv" + "testing" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/meshcfg" + "istio.io/istio/galley/pkg/config/processing" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/pod" + "istio.io/istio/galley/pkg/config/resource" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + ips = []string{ + "10.0.0.1", + "10.0.0.2", + "10.0.0.3", + "10.0.0.4", + "10.0.0.5", + "10.0.0.6", + "10.0.0.7", + "10.0.0.8", + "10.0.0.9", + "10.0.0.10", + } + serviceAccounts = []string{ + "serviceAccount1", + "serviceAccount2", + "serviceAccount3", + } + annos = resource.StringMap{ + "Annotation1": "AnnotationValue1", + "Annotation2": "AnnotationValue2", + "Annotation3": "AnnotationValue3", + "Annotation4": "AnnotationValue4", + "Annotation5": "AnnotationValue5", + } + labels = resource.StringMap{ + "Label1": "LabelValue1", + "Label2": "LabelValue2", + "Label3": "LabelValue3", + "Label4": "LabelValue4", + "Label5": "LabelValue5", + } + benchServiceName = "service1" +) + +func BenchmarkEndpointNoChange(b *testing.B) { + b.StopTimer() + + handler := newBenchHandler() + + // Initialize the node and pod caches. + loadNodesAndPods(handler) + + // Add the service. + handler.Handle(event.Event{ + Kind: event.Added, + Entry: newService(), + }) + + // Add the endpoints for all IPs. + handler.Handle(event.Event{ + Kind: event.Added, + Entry: newEndpoints(ips...), + }) + + // Create an update event with no changes to the endpoints. + updateEvent := event.Event{ + Kind: event.Updated, + Entry: newEndpoints(ips...), + } + + version := uint64(1) + + b.StartTimer() + + for i := 0; i < b.N; i++ { + updateEvent.Entry.Metadata.Version = resource.Version(strconv.FormatUint(version, 10)) + version++ + handler.Handle(updateEvent) + } +} + +func BenchmarkEndpointChurn(b *testing.B) { + b.StopTimer() + + handler := newBenchHandler() + + // Initialize the node and pod caches. + loadNodesAndPods(handler) + + // Add the service. + handler.Handle(event.Event{ + Kind: event.Added, + Entry: newService(), + }) + + // Add the endpoints for all IPs. + handler.Handle(event.Event{ + Kind: event.Added, + Entry: newEndpoints(ips...), + }) + + // Create a sequence of endpoint updates to simulate pod churn. + updateEntries := []*resource.Entry{ + // Slowly take away a few (the even indices). + newEndpoints(ips[1], ips[2], ips[3], ips[4], ips[5], ips[6], ips[7], ips[8], ips[9]), + newEndpoints(ips[1], ips[3], ips[4], ips[5], ips[6], ips[7], ips[8], ips[9]), + newEndpoints(ips[1], ips[3], ips[5], ips[6], ips[7], ips[8], ips[9]), + newEndpoints(ips[1], ips[3], ips[5], ips[7], ips[8], ips[9]), + newEndpoints(ips[1], ips[3], ips[5], ips[7], ips[9]), + + // Slowly rebuild the endpoints until we get back to the original list. + newEndpoints(ips[0], ips[1], ips[3], ips[5], ips[7], ips[9]), + newEndpoints(ips[0], ips[1], ips[2], ips[3], ips[5], ips[7], ips[9]), + newEndpoints(ips[0], ips[1], ips[2], ips[3], ips[4], ips[5], ips[7], ips[9]), + newEndpoints(ips[0], ips[1], ips[2], ips[3], ips[4], ips[5], ips[6], ips[7], ips[9]), + newEndpoints(ips...), + } + + // Convert the entries to a list of update events. + updateEvents := make([]event.Event, 0, len(updateEntries)) + for _, entry := range updateEntries { + updateEvents = append(updateEvents, event.Event{ + Kind: event.Updated, + Entry: entry, + }) + } + + lenUpdateEvents := len(updateEvents) + updateIndex := 0 + version := uint64(1) + + b.StartTimer() + + for i := 0; i < b.N; i++ { + // Get the next update event. + update := updateEvents[updateIndex] + updateIndex = (updateIndex + 1) % lenUpdateEvents + update.Entry.Metadata.Version = resource.Version(strconv.FormatUint(version, 10)) + version++ + + handler.Handle(update) + } +} + +func loadNodesAndPods(handler event.Handler) { + saIndex := 0 + for i, ip := range ips { + + // Build the node. + nodeName := "node" + strconv.Itoa(i) + handler.Handle(event.Event{ + Kind: event.Added, + Source: metadata.K8SCoreV1Nodes, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("", nodeName), + Version: resource.Version("0"), + CreateTime: createTime, + Labels: resource.StringMap{ + pod.LabelZoneRegion: region, + pod.LabelZoneFailureDomain: zone, + }, + }, + Item: &coreV1.NodeSpec{}, + }, + }) + + // Build the pod for this node. + podName := "pod" + strconv.Itoa(i) + serviceAccount := serviceAccounts[saIndex] + saIndex = (saIndex + 1) % len(serviceAccounts) + handler.Handle(event.Event{ + Kind: event.Added, + Source: metadata.K8SCoreV1Pods, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName(namespace, podName), + Version: resource.Version("0"), + CreateTime: createTime, + }, + Item: &coreV1.Pod{ + ObjectMeta: metaV1.ObjectMeta{ + Name: podName, + Namespace: namespace, + }, + Spec: coreV1.PodSpec{ + NodeName: nodeName, + ServiceAccountName: serviceAccount, + }, + Status: coreV1.PodStatus{ + PodIP: ip, + Phase: coreV1.PodRunning, + }, + }, + }, + }) + } +} + +func newService() *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName(namespace, benchServiceName), + Version: resource.Version("0"), + CreateTime: createTime, + Labels: labels, + Annotations: annos, + }, + Item: &coreV1.ServiceSpec{ + Type: coreV1.ServiceTypeClusterIP, + ClusterIP: "10.0.0.0", + Ports: []coreV1.ServicePort{ + { + Name: "http1", + Port: 80, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "http2", + Port: 8088, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "udp", + Port: 90, + Protocol: coreV1.ProtocolUDP, + }, + }, + }, + } +} + +func newEndpoints(ips ...string) *resource.Entry { + addresses := make([]coreV1.EndpointAddress, 0, len(ips)) + for _, ip := range ips { + addresses = append(addresses, coreV1.EndpointAddress{ + IP: ip, + }) + } + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName(namespace, benchServiceName), + Version: resource.Version("0"), + CreateTime: createTime, + Labels: labels, + Annotations: annos, + }, + Item: &coreV1.Endpoints{ + ObjectMeta: metaV1.ObjectMeta{ + Name: benchServiceName, + Namespace: namespace, + CreationTimestamp: metaV1.Time{Time: createTime}, + Labels: labels, + Annotations: annos, + }, + Subsets: []coreV1.EndpointSubset{ + { + Addresses: addresses, + Ports: []coreV1.EndpointPort{ + { + Name: "http1", + Port: 80, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "http2", + Port: 8088, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "udp", + Port: 90, + Protocol: coreV1.ProtocolUDP, + }, + }, + }, + }, + }, + } +} + +func newBenchHandler() event.Transformer { + o := processing.ProcessorOptions{ + DomainSuffix: domain, + MeshConfig: meshcfg.Default(), + } + return serviceentry.Create(o)[0] +} diff --git a/galley/pkg/config/processor/transforms/serviceentry/integration/integration_bench_test.go b/galley/pkg/config/processor/transforms/serviceentry/integration/integration_bench_test.go new file mode 100644 index 000000000000..69ba92a3d392 --- /dev/null +++ b/galley/pkg/config/processor/transforms/serviceentry/integration/integration_bench_test.go @@ -0,0 +1,366 @@ +// 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 integration + +import ( + "strconv" + "sync" + "testing" + "time" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processor" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/pod" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/source/kube" + "istio.io/istio/galley/pkg/config/source/kube/apiserver" + "istio.io/istio/galley/pkg/testing/mock" + "istio.io/istio/pkg/mcp/snapshot" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +const ( + domainSuffix = "company.com" + namespace = "fakeNamespace" + region = "region1" + zone = "zone1" +) + +var ( + createTime = time.Now() + + ips = []string{ + "10.0.0.1", + "10.0.0.2", + "10.0.0.3", + "10.0.0.4", + "10.0.0.5", + "10.0.0.6", + "10.0.0.7", + "10.0.0.8", + "10.0.0.9", + "10.0.0.10", + } + serviceAccounts = []string{ + "serviceAccount1", + "serviceAccount2", + "serviceAccount3", + } + annos = resource.StringMap{ + "Annotation1": "AnnotationValue1", + "Annotation2": "AnnotationValue2", + "Annotation3": "AnnotationValue3", + "Annotation4": "AnnotationValue4", + "Annotation5": "AnnotationValue5", + } + labels = resource.StringMap{ + "Label1": "LabelValue1", + "Label2": "LabelValue2", + "Label3": "LabelValue3", + "Label4": "LabelValue4", + "Label5": "LabelValue5", + } + benchServiceName = "service1" +) + +// BenchmarkEndpointChurn is an integration-level benchmark for the entire runtime pipeline. This tests the performance +// of the ServiceEntry synthesis for a single service undergoing constant endpoint churn (i.e. endpoints come up and +// down constantly). +func BenchmarkEndpointChurn(b *testing.B) { + b.StopTimer() + + ki := mock.NewKube() + kubeClient := newKubeClient(b, ki) + + // Create all of the k8s resources. + loadNodesAndPods(b, kubeClient) + loadService(b, kubeClient) + loadEndpoints(b, kubeClient) + + // Create a sequence of endpoint updates to simulate pod churn. + updateEntries := []coreV1.Endpoints{ + // Slowly take away a few (the even indices). + newEndpoints(ips[1], ips[2], ips[3], ips[4], ips[5], ips[6], ips[7], ips[8], ips[9]), + newEndpoints(ips[1], ips[3], ips[4], ips[5], ips[6], ips[7], ips[8], ips[9]), + newEndpoints(ips[1], ips[3], ips[5], ips[6], ips[7], ips[8], ips[9]), + newEndpoints(ips[1], ips[3], ips[5], ips[7], ips[8], ips[9]), + newEndpoints(ips[1], ips[3], ips[5], ips[7], ips[9]), + + // Slowly rebuild the endpoints until we get back to the original list. + newEndpoints(ips[0], ips[1], ips[3], ips[5], ips[7], ips[9]), + newEndpoints(ips[0], ips[1], ips[2], ips[3], ips[5], ips[7], ips[9]), + newEndpoints(ips[0], ips[1], ips[2], ips[3], ips[4], ips[5], ips[7], ips[9]), + newEndpoints(ips[0], ips[1], ips[2], ips[3], ips[4], ips[5], ips[6], ips[7], ips[9]), + newEndpoints(ips...), + } + + m := metadata.MustGet() + src := newSource(b, ki, m.KubeSource().Resources()) + distributor := newFakeDistributor(b.N) + processor, err := processor.Initialize(m, domainSuffix, src, distributor) + if err != nil { + b.Fatal(err) + } + + distributor.waitForSnapshot() + go processor.Start() + + lenUpdateEvents := len(updateEntries) + updateIndex := 0 + version := uint64(1) + + endpoints := make([]*coreV1.Endpoints, b.N) + for i := 0; i < b.N; i++ { + update := updateEntries[updateIndex] + updateIndex = (updateIndex + 1) % lenUpdateEvents + + update.ResourceVersion = strconv.FormatUint(version, 10) + version++ + + endpoints[i] = &update + } + + b.StartTimer() + + for _, eps := range endpoints { + if _, err := kubeClient.CoreV1().Endpoints(namespace).Update(eps); err != nil { + b.Fatal(err) + } + } + + distributor.await() + + b.StopTimer() + processor.Stop() + b.StartTimer() +} + +func loadNodesAndPods(b *testing.B, kubeClient kubernetes.Interface) { + b.Helper() + saIndex := 0 + for i, ip := range ips { + + // Build the node. + nodeName := "node" + strconv.Itoa(i) + if _, err := kubeClient.CoreV1().Nodes().Create(&coreV1.Node{ + ObjectMeta: metaV1.ObjectMeta{ + Name: nodeName, + ResourceVersion: "0", + CreationTimestamp: metaV1.Time{ + Time: createTime, + }, + Labels: map[string]string{ + pod.LabelZoneRegion: region, + pod.LabelZoneFailureDomain: zone, + }, + }, + Spec: coreV1.NodeSpec{ + PodCIDR: "10.40.0.0/24", + }, + }); err != nil { + b.Fatal(err) + } + + // Build the pod for this node. + podName := "pod" + strconv.Itoa(i) + serviceAccount := serviceAccounts[saIndex] + saIndex = (saIndex + 1) % len(serviceAccounts) + if _, err := kubeClient.CoreV1().Pods(namespace).Create(&coreV1.Pod{ + ObjectMeta: metaV1.ObjectMeta{ + Name: podName, + Namespace: namespace, + ResourceVersion: "0", + CreationTimestamp: metaV1.Time{ + Time: createTime, + }, + Labels: map[string]string{ + pod.LabelZoneRegion: region, + pod.LabelZoneFailureDomain: zone, + }, + }, + Spec: coreV1.PodSpec{ + NodeName: nodeName, + ServiceAccountName: serviceAccount, + }, + Status: coreV1.PodStatus{ + PodIP: ip, + Phase: coreV1.PodRunning, + }, + }); err != nil { + b.Fatal(err) + } + } +} + +func loadService(b *testing.B, kubeClient kubernetes.Interface) { + b.Helper() + if _, err := kubeClient.CoreV1().Services(namespace).Create(&coreV1.Service{ + ObjectMeta: metaV1.ObjectMeta{ + Name: benchServiceName, + Namespace: namespace, + ResourceVersion: "0", + CreationTimestamp: metaV1.Time{ + Time: createTime, + }, + Labels: labels, + Annotations: annos, + }, + Spec: coreV1.ServiceSpec{ + Type: coreV1.ServiceTypeClusterIP, + ClusterIP: "10.0.0.0", + Ports: []coreV1.ServicePort{ + { + Name: "http1", + Port: 80, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "http2", + Port: 8088, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "udp", + Port: 90, + Protocol: coreV1.ProtocolUDP, + }, + }, + }, + }); err != nil { + b.Fatal(err) + } +} + +func loadEndpoints(b *testing.B, kubeClient kubernetes.Interface) { + b.Helper() + endpoints := newEndpoints(ips...) + if _, err := kubeClient.CoreV1().Endpoints(namespace).Create(&endpoints); err != nil { + b.Fatal(err) + } +} + +type fakeDistributor struct { + cond *sync.Cond + serviceCreation int + endpointsCreation int + complete int + counter int + + snapshotCond *sync.Cond +} + +func newFakeDistributor(numUpdates int) *fakeDistributor { + return &fakeDistributor{ + serviceCreation: 1, + endpointsCreation: 2, + complete: 2 + numUpdates, + cond: sync.NewCond(&sync.Mutex{}), + snapshotCond: sync.NewCond(&sync.Mutex{}), + } +} + +func (d *fakeDistributor) waitForSnapshot() { + d.snapshotCond.L.Lock() + d.snapshotCond.Wait() + d.snapshotCond.L.Unlock() +} + +func (d *fakeDistributor) SetSnapshot(name string, s snapshot.Snapshot) { + d.cond.Broadcast() + + d.counter++ + if d.counter == d.serviceCreation || d.counter == d.endpointsCreation || d.counter == d.complete { + d.cond.L.Lock() + d.cond.Signal() + d.cond.L.Unlock() + } +} + +func (d *fakeDistributor) await() { + d.cond.L.Lock() + defer d.cond.L.Unlock() + d.cond.Wait() +} + +func (d *fakeDistributor) ClearSnapshot(name string) { + // Do nothing. +} + +func newEndpoints(ips ...string) coreV1.Endpoints { + addresses := make([]coreV1.EndpointAddress, 0, len(ips)) + for _, ip := range ips { + addresses = append(addresses, coreV1.EndpointAddress{ + IP: ip, + }) + } + return coreV1.Endpoints{ + ObjectMeta: metaV1.ObjectMeta{ + Name: benchServiceName, + Namespace: namespace, + CreationTimestamp: metaV1.Time{Time: createTime}, + Labels: labels, + Annotations: annos, + }, + Subsets: []coreV1.EndpointSubset{ + { + Addresses: addresses, + Ports: []coreV1.EndpointPort{ + { + Name: "http1", + Port: 80, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "http2", + Port: 8088, + Protocol: coreV1.ProtocolTCP, + }, + { + Name: "udp", + Port: 90, + Protocol: coreV1.ProtocolUDP, + }, + }, + }, + }, + } +} + +func newKubeClient(b *testing.B, ki kube.Interfaces) kubernetes.Interface { + b.Helper() + kubeClient, err := ki.KubeClient() + if err != nil { + b.Fatal(err) + } + return kubeClient +} + +func newSource(b *testing.B, ifaces kube.Interfaces, resources schema.KubeResources) event.Source { + o := apiserver.Options{ + Client: ifaces, + ResyncPeriod: 0, + Resources: resources, + } + src := apiserver.New(o) + if src == nil { + b.Fatal("Expected non nil source") + } + return src +} diff --git a/galley/pkg/config/processor/transforms/serviceentry/pod/cache.go b/galley/pkg/config/processor/transforms/serviceentry/pod/cache.go new file mode 100644 index 000000000000..1427289c66f0 --- /dev/null +++ b/galley/pkg/config/processor/transforms/serviceentry/pod/cache.go @@ -0,0 +1,235 @@ +// 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 pod + +import ( + "fmt" + "reflect" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/pkg/spiffe" + + coreV1 "k8s.io/api/core/v1" +) + +var _ Cache = &cacheImpl{} +var _ event.Handler = &cacheImpl{} + +// k8s well known labels +const ( + LabelZoneRegion = "failure-domain.beta.kubernetes.io/region" + LabelZoneFailureDomain = "failure-domain.beta.kubernetes.io/zone" +) + +// Info for a Pod. +type Info struct { + IP string + FullName resource.Name + Labels map[string]string + Locality string + + // ServiceAccountName the Spiffe name for the Pod service account. + ServiceAccountName string + + NodeName string +} + +// Listener is an observer of updates to the pod cache. +type Listener struct { + PodAdded func(info Info) + PodUpdated func(info Info) + PodDeleted func(info Info) +} + +// Cache for pod Info. +type Cache interface { + GetPodByIP(ip string) (Info, bool) +} + +// NewCache creates a cache and its update handler +func NewCache(listener Listener) (Cache, event.Handler) { + c := &cacheImpl{ + pods: make(map[string]Info), + nodeNameToLocality: make(map[string]string), + listener: listener, + } + return c, c +} + +type cacheImpl struct { + listener Listener + pods map[string]Info + nodeNameToLocality map[string]string +} + +// GetPodByIP looks up and returns pod info based on ip. +func (pc *cacheImpl) GetPodByIP(ip string) (Info, bool) { + pod, ok := pc.pods[ip] + return pod, ok +} + +// Handle implmenets event.Handler +func (pc *cacheImpl) Handle(e event.Event) { + switch e.Source { + case metadata.K8SCoreV1Nodes: + pc.handleNode(e) + case metadata.K8SCoreV1Pods: + pc.handlePod(e) + default: + return + } +} + +func (pc *cacheImpl) handleNode(e event.Event) { + // Nodes don't have namespaces. + _, nodeName := e.Entry.Metadata.Name.InterpretAsNamespaceAndName() + + switch e.Kind { + case event.Added, event.Updated: + // Just update the node information directly + labels := e.Entry.Metadata.Labels + + region := labels[LabelZoneRegion] + zone := labels[LabelZoneFailureDomain] + + newLocality := getLocality(region, zone) + oldLocality := pc.nodeNameToLocality[nodeName] + if newLocality != oldLocality { + pc.nodeNameToLocality[nodeName] = getLocality(region, zone) + + // Update the pods. + pc.updatePodLocality(nodeName, newLocality) + } + case event.Deleted: + if _, ok := pc.nodeNameToLocality[nodeName]; ok { + delete(pc.nodeNameToLocality, nodeName) + + // Update the pods. + pc.updatePodLocality(nodeName, "") + } + } +} + +func (pc *cacheImpl) handlePod(e event.Event) { + switch e.Kind { + case event.Added, event.Updated: + pod := e.Entry.Item.(*coreV1.Pod) + + ip := pod.Status.PodIP + if ip == "" { + // PodIP will be empty when pod is just created, but before the IP is assigned + // via UpdateStatus. + return + } + + switch pod.Status.Phase { + case coreV1.PodPending, coreV1.PodRunning: + // add to cache if the pod is running or pending + nodeName := pod.Spec.NodeName + locality := pc.nodeNameToLocality[nodeName] + serviceAccountName := kubeToIstioServiceAccount(pod.Spec.ServiceAccountName, pod.Namespace) + pod := Info{ + IP: ip, + FullName: e.Entry.Metadata.Name, + NodeName: nodeName, + Locality: locality, + Labels: pod.Labels, + ServiceAccountName: serviceAccountName, + } + + pc.updatePod(pod) + default: + // delete if the pod switched to other states and is in the cache + pc.deletePod(ip) + } + case event.Deleted: + var ip string + if pod, ok := e.Entry.Item.(*coreV1.Pod); ok { + ip = pod.Status.PodIP + } else { + // The resource was either not available or failed parsing. Look it up by brute force. + for podIP, info := range pc.pods { + if info.FullName == e.Entry.Metadata.Name { + ip = podIP + break + } + } + } + + // delete only if this pod was in the cache + pc.deletePod(ip) + } +} + +func (pc *cacheImpl) updatePod(pod Info) { + // Store the pod. + prevPod, exists := pc.pods[pod.IP] + if exists && reflect.DeepEqual(prevPod, pod) { + // Nothing changed - just return. + return + } + + // Store the updated pod. + pc.pods[pod.IP] = pod + + // Notify the listeners. + if exists { + pc.listener.PodUpdated(pod) + } else { + pc.listener.PodAdded(pod) + } + +} + +func (pc *cacheImpl) deletePod(ip string) { + if pod, exists := pc.pods[ip]; exists { + delete(pc.pods, ip) + pc.listener.PodDeleted(pod) + } +} + +func (pc *cacheImpl) updatePodLocality(nodeName string, locality string) { + updatedPods := make([]Info, 0) + for ip, pod := range pc.pods { + if pod.NodeName == nodeName { + // Update locality and store the change back into the map. + pod.Locality = locality + pc.pods[ip] = pod + + // Mark this pod as updated. + updatedPods = append(updatedPods, pod) + } + } + + // Notify the listener that the pods have been updated. + for _, pod := range updatedPods { + pc.listener.PodUpdated(pod) + } +} + +func getLocality(region, zone string) string { + if region == "" && zone == "" { + return "" + } + + return fmt.Sprintf("%v/%v", region, zone) +} + +// kubeToIstioServiceAccount converts a K8s service account to an Istio service account +func kubeToIstioServiceAccount(saname string, ns string) string { + return spiffe.MustGenSpiffeURI(ns, saname) +} diff --git a/galley/pkg/config/processor/transforms/serviceentry/pod/cache_test.go b/galley/pkg/config/processor/transforms/serviceentry/pod/cache_test.go new file mode 100644 index 000000000000..ebdebc3e6f31 --- /dev/null +++ b/galley/pkg/config/processor/transforms/serviceentry/pod/cache_test.go @@ -0,0 +1,689 @@ +// // 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 pod_test + +import ( + "reflect" + "testing" + + . "github.com/onsi/gomega" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/pod" + "istio.io/istio/galley/pkg/config/resource" +) + +const ( + ip = "1.2.3.4" + nodeName = "node1" + podName = "pod1" + namespace = "ns" + region = "region1" + zone = "zone1" + expectedLocality = "region1/zone1" + serviceAccountName = "myServiceAccount" + expectedServiceAccountName = "spiffe://cluster.local/ns/ns/sa/myServiceAccount" +) + +var ( + fullName = resource.NewName(namespace, podName) + + labels = map[string]string{ + "l1": "v1", + "l2": "v2", + } +) + +func TestPodLifecycle(t *testing.T) { + l := &listener{} + c, h := pod.NewCache(l.asListener()) + + labels := map[string]string{ + "l2": "v2", + } + + // Add the node. + h.Handle(event.Event{ + Kind: event.Added, + Source: metadata.K8SCoreV1Nodes, + Entry: nodeEntry(region, zone), + }) + + t.Run("Add", func(t *testing.T) { + g := NewGomegaWithT(t) + h.Handle(event.Event{ + Kind: event.Added, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + IP(ip). + Labels(labels). + Phase(coreV1.PodPending). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }) + p, _ := c.GetPodByIP(ip) + expected := pod.Info{ + FullName: fullName, + IP: ip, + Locality: expectedLocality, + NodeName: nodeName, + ServiceAccountName: expectedServiceAccountName, + Labels: labels, + } + g.Expect(p).To(Equal(expected)) + l.assertAdded(t, expected) + }) + + l.reset() + + t.Run("NoChange", func(t *testing.T) { + g := NewGomegaWithT(t) + h.Handle(event.Event{ + Kind: event.Updated, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + IP(ip). + Labels(labels). + Phase(coreV1.PodRunning). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }) + p, _ := c.GetPodByIP(ip) + expected := pod.Info{ + FullName: fullName, + IP: ip, + Locality: expectedLocality, + NodeName: nodeName, + ServiceAccountName: expectedServiceAccountName, + Labels: labels, + } + g.Expect(p).To(Equal(expected)) + l.assertNone(t) + }) + + l.reset() + + t.Run("ChangeLabel", func(t *testing.T) { + g := NewGomegaWithT(t) + + labels = map[string]string{ + "l3": "v3", + "l4": "v4", + } + h.Handle(event.Event{ + Kind: event.Updated, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + IP(ip). + Labels(labels). + Phase(coreV1.PodRunning). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }) + p, _ := c.GetPodByIP(ip) + expected := pod.Info{ + FullName: fullName, + IP: ip, + Locality: expectedLocality, + NodeName: nodeName, + ServiceAccountName: expectedServiceAccountName, + Labels: labels, + } + g.Expect(p).To(Equal(expected)) + l.assertUpdated(t, expected) + }) + + l.reset() + + t.Run("Delete", func(t *testing.T) { + g := NewGomegaWithT(t) + h.Handle(event.Event{ + Kind: event.Deleted, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + IP(ip). + Labels(labels). + Phase(coreV1.PodRunning). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }) + _, ok := c.GetPodByIP(ip) + g.Expect(ok).To(BeFalse()) + l.assertDeleted(t, pod.Info{ + FullName: fullName, + IP: ip, + Locality: expectedLocality, + NodeName: nodeName, + ServiceAccountName: expectedServiceAccountName, + Labels: labels, + }) + }) +} + +func TestNodeLifecycle(t *testing.T) { + g := NewGomegaWithT(t) + + l := &listener{} + c, h := pod.NewCache(l.asListener()) + + applyEvents(l, h, []event.Event{ + { + Kind: event.Added, + Source: metadata.K8SCoreV1Nodes, + Entry: nodeEntry(region, zone), + }, + { + Kind: event.Added, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + IP(ip). + Labels(labels). + Phase(coreV1.PodPending). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }, + { + Kind: event.Deleted, + Source: metadata.K8SCoreV1Nodes, + Entry: nodeEntry(region, zone), + }, + }) + + p, _ := c.GetPodByIP(ip) + expected := pod.Info{ + FullName: fullName, + IP: ip, + Locality: "", + NodeName: nodeName, + ServiceAccountName: expectedServiceAccountName, + Labels: labels, + } + g.Expect(p).To(Equal(expected)) + l.assertUpdated(t, expected) +} + +func TestNodeAddedAfterPod(t *testing.T) { + g := NewGomegaWithT(t) + + l := &listener{} + c, h := pod.NewCache(l.asListener()) + + applyEvents(l, h, []event.Event{ + { + Kind: event.Added, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + IP(ip). + Labels(labels). + Phase(coreV1.PodPending). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }, + { + Kind: event.Added, + Source: metadata.K8SCoreV1Nodes, + Entry: nodeEntry(region, zone), + }, + }) + + p, _ := c.GetPodByIP(ip) + expected := pod.Info{ + FullName: fullName, + IP: ip, + Locality: expectedLocality, + NodeName: nodeName, + ServiceAccountName: expectedServiceAccountName, + Labels: labels, + } + g.Expect(p).To(Equal(expected)) + l.assertUpdated(t, expected) +} + +func TestNodeWithOnlyRegion(t *testing.T) { + g := NewGomegaWithT(t) + + l := &listener{} + c, h := pod.NewCache(l.asListener()) + + applyEvents(l, h, []event.Event{ + { + Kind: event.Added, + Source: metadata.K8SCoreV1Nodes, + Entry: nodeEntry(region, ""), + }, + { + Kind: event.Added, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + IP(ip). + Phase(coreV1.PodPending). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }, + }) + + p, _ := c.GetPodByIP(ip) + expected := pod.Info{ + FullName: fullName, + IP: ip, + Locality: "region1/", + NodeName: nodeName, + ServiceAccountName: expectedServiceAccountName, + } + g.Expect(p).To(Equal(expected)) + l.assertAdded(t, expected) +} + +func TestNodeWithNoLocality(t *testing.T) { + g := NewGomegaWithT(t) + + l := &listener{} + c, h := pod.NewCache(l.asListener()) + + applyEvents(l, h, []event.Event{ + { + Kind: event.Added, + Source: metadata.K8SCoreV1Nodes, + Entry: nodeEntry("", ""), + }, + { + Kind: event.Added, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + IP(ip). + Phase(coreV1.PodPending). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }, + }) + + p, _ := c.GetPodByIP(ip) + expected := pod.Info{ + FullName: fullName, + IP: ip, + Locality: "", + NodeName: nodeName, + ServiceAccountName: expectedServiceAccountName, + } + g.Expect(p).To(Equal(expected)) + l.assertAdded(t, expected) +} + +func TestNoNamespaceAndNoServiceAccount(t *testing.T) { + l := &listener{} + c, h := pod.NewCache(l.asListener()) + + g := NewGomegaWithT(t) + h.Handle(event.Event{ + Kind: event.Added, + Source: metadata.K8SCoreV1Pods, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + Version: resource.Version("v1"), + }, + Item: &coreV1.Pod{ + ObjectMeta: metaV1.ObjectMeta{ + Name: podName, + Namespace: "", + }, + Spec: coreV1.PodSpec{ + NodeName: nodeName, + ServiceAccountName: "", + }, + Status: coreV1.PodStatus{ + PodIP: "1.2.3.4", + Phase: coreV1.PodRunning, + }, + }, + }, + }) + p, _ := c.GetPodByIP(ip) + expected := pod.Info{ + IP: ip, + FullName: fullName, + NodeName: nodeName, + ServiceAccountName: "spiffe://cluster.local/ns//sa/", + } + g.Expect(p).To(Equal(expected)) + l.assertAdded(t, expected) +} + +func TestWrongCollectionShouldNotPanic(t *testing.T) { + l := &listener{} + _, h := pod.NewCache(l.asListener()) + + h.Handle(event.Event{ + Kind: event.Added, + Source: metadata.K8SCoreV1Services, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns", "myservice"), + Version: resource.Version("v1"), + }, + Item: &coreV1.Service{}, + }, + }) + l.assertNone(t) +} + +func TestInvalidPodPhase(t *testing.T) { + l := &listener{} + c, h := pod.NewCache(l.asListener()) + + for _, phase := range []coreV1.PodPhase{coreV1.PodSucceeded, coreV1.PodFailed, coreV1.PodUnknown} { + t.Run(string(phase), func(t *testing.T) { + g := NewGomegaWithT(t) + h.Handle(event.Event{ + Kind: event.Added, + Source: metadata.K8SCoreV1Services, + Entry: newPodEntryBuilder(). + IP(ip). + Labels(labels). + Phase(phase). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }) + _, ok := c.GetPodByIP(ip) + g.Expect(ok).To(BeFalse()) + }) + } +} + +func TestUpdateWithInvalidPhaseShouldDelete(t *testing.T) { + g := NewGomegaWithT(t) + + l := &listener{} + c, h := pod.NewCache(l.asListener()) + + applyEvents(l, h, []event.Event{ + { + Kind: event.Added, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + IP(ip). + Labels(labels). + Phase(coreV1.PodPending). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }, + { + Kind: event.Updated, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + IP(ip). + Labels(labels). + Phase(coreV1.PodUnknown). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }, + }) + + _, ok := c.GetPodByIP(ip) + g.Expect(ok).To(BeFalse()) + l.assertDeleted(t, pod.Info{ + IP: ip, + FullName: fullName, + NodeName: nodeName, + Labels: labels, + ServiceAccountName: expectedServiceAccountName, + }) +} + +func TestDeleteWithNoItemShouldUseFullName(t *testing.T) { + g := NewGomegaWithT(t) + + l := &listener{} + c, h := pod.NewCache(l.asListener()) + + applyEvents(l, h, []event.Event{ + { + Kind: event.Added, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + IP(ip). + Labels(labels). + Phase(coreV1.PodPending). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }, + { + Kind: event.Deleted, + Source: metadata.K8SCoreV1Pods, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + Version: resource.Version("v1"), + }, + }, + }, + }) + + _, ok := c.GetPodByIP(ip) + g.Expect(ok).To(BeFalse()) +} + +func TestDeleteNotFoundShouldNotPanic(t *testing.T) { + l := &listener{} + _, h := pod.NewCache(l.asListener()) + + // Delete it, but with a nil Item to force a lookup by fullName. + h.Handle(event.Event{ + Kind: event.Deleted, + Source: metadata.K8SCoreV1Services, + Entry: newPodEntryBuilder(). + IP(ip). + Labels(labels). + Phase(coreV1.PodPending). + NodeName(nodeName). + ServiceAccountName(serviceAccountName).Build(), + }) +} + +func TestDeleteNotFoundWithMissingItemShouldNotPanic(t *testing.T) { + l := &listener{} + _, h := pod.NewCache(l.asListener()) + + // Delete it, but with a nil Item to force a lookup by fullName. + h.Handle(event.Event{ + Kind: event.Deleted, + Source: metadata.K8SCoreV1Pods, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + }, + }, + }) +} + +func TestPodWithNoIPShouldBeIgnored(t *testing.T) { + l := &listener{} + _, h := pod.NewCache(l.asListener()) + + h.Handle(event.Event{ + Kind: event.Added, + Source: metadata.K8SCoreV1Pods, + Entry: newPodEntryBuilder(). + Phase(coreV1.PodPending).Build(), + }) + l.assertNone(t) +} + +func applyEvents(l *listener, h event.Handler, events []event.Event) { + for _, event := range events { + l.reset() + h.Handle(event) + } +} + +type podEntryBuilder struct { + ip string + nodeName string + labels map[string]string + serviceAccountName string + phase coreV1.PodPhase +} + +func newPodEntryBuilder() *podEntryBuilder { + return &podEntryBuilder{} +} + +func (b *podEntryBuilder) IP(ip string) *podEntryBuilder { + b.ip = ip + return b +} + +func (b *podEntryBuilder) NodeName(nodeName string) *podEntryBuilder { + b.nodeName = nodeName + return b +} + +func (b *podEntryBuilder) Labels(labels map[string]string) *podEntryBuilder { + b.labels = labels + return b +} + +func (b *podEntryBuilder) ServiceAccountName(serviceAccountName string) *podEntryBuilder { + b.serviceAccountName = serviceAccountName + return b +} + +func (b *podEntryBuilder) Phase(phase coreV1.PodPhase) *podEntryBuilder { + b.phase = phase + return b +} + +func (b *podEntryBuilder) Build() *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: fullName, + }, + Item: &coreV1.Pod{ + ObjectMeta: metaV1.ObjectMeta{ + Name: podName, + Namespace: namespace, + Labels: b.labels, + }, + Spec: coreV1.PodSpec{ + NodeName: b.nodeName, + ServiceAccountName: b.serviceAccountName, + }, + Status: coreV1.PodStatus{ + PodIP: b.ip, + Phase: b.phase, + }, + }, + } +} + +func nodeEntry(region, zone string) *resource.Entry { + labels := make(resource.StringMap) + if region != "" { + labels[pod.LabelZoneRegion] = region + } + if zone != "" { + labels[pod.LabelZoneFailureDomain] = zone + } + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("", nodeName), + Labels: labels, + }, + } +} + +type listener struct { + added []pod.Info + updated []pod.Info + deleted []pod.Info +} + +func (l *listener) reset() { + l.added = l.added[:0] + l.updated = l.updated[:0] + l.deleted = l.deleted[:0] +} + +func (l *listener) onAdded(p pod.Info) { + l.added = append(l.added, p) +} + +func (l *listener) onUpdated(p pod.Info) { + l.updated = append(l.updated, p) +} + +func (l *listener) onDeleted(p pod.Info) { + l.deleted = append(l.deleted, p) +} + +func (l *listener) asListener() pod.Listener { + return pod.Listener{ + PodAdded: l.onAdded, + PodUpdated: l.onUpdated, + PodDeleted: l.onDeleted, + } +} + +func (l *listener) assertNone(t *testing.T) { + t.Helper() + assertNone(t, "added", l.added) + assertNone(t, "updated", l.updated) + assertNone(t, "deleted", l.deleted) +} + +func (l *listener) assertAdded(t *testing.T, expected pod.Info) { + t.Helper() + assertOne(t, "added", l.added, expected) + assertNone(t, "updated", l.updated) + assertNone(t, "deleted", l.deleted) + +} + +func (l *listener) assertUpdated(t *testing.T, expected pod.Info) { + t.Helper() + assertNone(t, "added", l.added) + assertOne(t, "updated", l.updated, expected) + assertNone(t, "deleted", l.deleted) + +} + +func (l *listener) assertDeleted(t *testing.T, expected pod.Info) { + t.Helper() + assertNone(t, "added", l.added) + assertNone(t, "updated", l.updated) + assertOne(t, "deleted", l.deleted, expected) +} + +func assertNone(t *testing.T, name string, result []pod.Info) { + t.Helper() + if len(result) > 0 { + t.Fatalf("%s: expected 0, found %d", name, len(result)) + } +} + +func assertOne(t *testing.T, name string, result []pod.Info, expected pod.Info) { + t.Helper() + if len(result) != 1 { + t.Fatalf("%s: expected 1, found %d", name, len(result)) + } + actual := result[0] + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("%s: expected\n%v+\nto equal\n%v+", name, actual, expected) + } +} diff --git a/galley/pkg/config/processor/transforms/serviceentry/transformer.go b/galley/pkg/config/processor/transforms/serviceentry/transformer.go new file mode 100644 index 000000000000..b8fc1bba5612 --- /dev/null +++ b/galley/pkg/config/processor/transforms/serviceentry/transformer.go @@ -0,0 +1,342 @@ +// 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 serviceentry + +import ( + "context" + "fmt" + "strconv" + + "go.opencensus.io/tag" + coreV1 "k8s.io/api/core/v1" + + networking "istio.io/api/networking/v1alpha3" + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processing" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/converter" + "istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/pod" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/runtime/monitoring" +) + +type transformer struct { + options processing.ProcessorOptions + + converter *converter.Instance + + services map[resource.Name]*resource.Entry + endpoints map[resource.Name]*resource.Entry + ipToName map[string]map[resource.Name]struct{} + + podHandler event.Handler + nodeHandler event.Handler + + handler event.Handler + statsCtx context.Context + + fullSyncCtr int + + // The version number for the current State of the object. Every time mcpResources or versions change, + // the version number also changes + version int64 +} + +var _ event.Transformer = &transformer{} + +// Start implements event.Transformer +func (t *transformer) Start() { + t.ipToName = make(map[string]map[resource.Name]struct{}) + t.services = make(map[resource.Name]*resource.Entry) + t.endpoints = make(map[resource.Name]*resource.Entry) + + podCache, cacheHandler := pod.NewCache(pod.Listener{ + PodAdded: t.podUpdated, + PodUpdated: t.podUpdated, + PodDeleted: t.podUpdated, + }) + + t.podHandler = cacheHandler + t.nodeHandler = cacheHandler + + t.converter = converter.New(t.options.DomainSuffix, podCache) + + statsCtx, err := tag.New(context.Background(), tag.Insert(monitoring.CollectionTag, + metadata.IstioNetworkingV1Alpha3SyntheticServiceentries.String())) + if err != nil { + scope.Processing.Errorf("Error creating monitoring context for counting state: %v", err) + statsCtx = nil + } + t.statsCtx = statsCtx + + t.fullSyncCtr = len(t.Inputs()) +} + +// Stop implements event.Transformer +func (t *transformer) Stop() { + t.ipToName = nil + t.services = nil + t.endpoints = nil +} + +// DispatchFor implements event.Transformer +func (t *transformer) DispatchFor(c collection.Name, h event.Handler) { + switch c { + case metadata.IstioNetworkingV1Alpha3SyntheticServiceentries: + t.handler = event.CombineHandlers(t.handler, h) + } +} + +// Inputs implements event.Transformer +func (t *transformer) Inputs() collection.Names { + return collection.Names{ + metadata.K8SCoreV1Endpoints, + metadata.K8SCoreV1Nodes, + metadata.K8SCoreV1Pods, + metadata.K8SCoreV1Services, + } +} + +// Outputs implements event.Transformer +func (t *transformer) Outputs() collection.Names { + return collection.Names{ + metadata.IstioNetworkingV1Alpha3SyntheticServiceentries, + } +} + +// Handle implements event.Transformer +func (t *transformer) Handle(e event.Event) { + switch e.Kind { + case event.FullSync: + t.fullSyncCtr-- + if t.fullSyncCtr == 0 { + t.dispatch(event.FullSyncFor(t.Outputs()[0])) + } + return + + case event.Reset: + t.dispatch(event.Event{Kind: event.Reset}) + return + + case event.Added, event.Updated, event.Deleted: + // fallthrough + + default: + panic(fmt.Errorf("transformer.Handle: Unexpected event received: %v", e)) + } + + switch e.Source { + case metadata.K8SCoreV1Endpoints: + // Update the projections + t.handleEndpointsEvent(e) + case metadata.K8SCoreV1Services: + // Update the projections + t.handleServiceEvent(e) + case metadata.K8SCoreV1Nodes: + // Update the pod cache. + t.nodeHandler.Handle(e) + case metadata.K8SCoreV1Pods: + // Update the pod cache. + t.podHandler.Handle(e) + default: + panic(fmt.Errorf("received event with unexpected collection: %v", e.Source)) + } +} + +func (t *transformer) handleEndpointsEvent(e event.Event) { + endpoints := e.Entry + name := e.Entry.Metadata.Name + + switch e.Kind { + case event.Added, event.Updated: + // Update the IPs for this endpoint. + t.updateEndpointIPs(name, endpoints) + + // Store the endpoints. + t.endpoints[name] = endpoints + + t.doUpdate(name) + case event.Deleted: + // Remove the IPs for this endpoint. + t.deleteEndpointIPs(name, endpoints) + + // The lifecycle of the ServiceEntry is bound to the service, so only delete the endpoints entry here. + delete(t.endpoints, name) + + t.doUpdate(name) + default: + panic(fmt.Errorf("unknown event kind: %v", e.Kind)) + } +} + +func (t *transformer) handleServiceEvent(e event.Event) { + service := e.Entry + name := e.Entry.Metadata.Name + + switch e.Kind { + case event.Added, event.Updated: + // Store the service. + t.services[name] = service + + t.doUpdate(name) + case event.Deleted: + // Delete the Service and ServiceEntry + delete(t.services, name) + t.sendDelete(name) + + default: + panic(fmt.Errorf("unknown event kind: %v", e.Kind)) + } +} + +func (t *transformer) doUpdate(name resource.Name) { + // Look up the service associated with the endpoints. + service, ok := t.services[name] + if !ok { + // No service, nothing to update. + return + } + + // Get the associated endpoints, if available. + endpoints := t.endpoints[name] + + // Convert to an MCP resource to be used in the snapshot. + mcpEntry, ok := t.toMcpResource(service, endpoints) + if !ok { + return + } + // TODO: Distinguish between add/update. + t.sendUpdate(mcpEntry) +} + +func (t *transformer) dispatch(e event.Event) { + if t.handler != nil { + t.handler.Handle(e) + } +} + +func (t *transformer) sendDelete(name resource.Name) { + e := event.Event{ + Kind: event.Deleted, + Source: metadata.IstioNetworkingV1Alpha3SyntheticServiceentries, + Entry: &resource.Entry{ + Metadata: resource.Metadata{ + Name: name, + }, + }, + } + + t.dispatch(e) +} + +func (t *transformer) sendUpdate(r *resource.Entry) { + e := event.Event{ + Kind: event.Updated, + Source: metadata.IstioNetworkingV1Alpha3SyntheticServiceentries, + Entry: r, + } + + t.dispatch(e) +} + +func (t *transformer) podUpdated(p pod.Info) { + // Update the endpoints associated with this IP. + for name := range t.ipToName[p.IP] { + t.doUpdate(name) + } +} + +func (t *transformer) updateEndpointIPs(name resource.Name, newRE *resource.Entry) { + newIPs := getEndpointIPs(newRE) + var prevIPs map[string]struct{} + + if prev, exists := t.endpoints[name]; exists { + prevIPs = getEndpointIPs(prev) + + // Delete any IPs missing from the new endpoints. + for prevIP := range prevIPs { + if _, exists := newIPs[prevIP]; !exists { + t.deleteEndpointIP(name, prevIP) + } + } + } + + // Add/update + for newIP := range newIPs { + names := t.ipToName[newIP] + if names == nil { + names = make(map[resource.Name]struct{}) + t.ipToName[newIP] = names + } + names[name] = struct{}{} + } +} + +func (t *transformer) deleteEndpointIPs(name resource.Name, endpoints *resource.Entry) { + ips := getEndpointIPs(endpoints) + for ip := range ips { + t.deleteEndpointIP(name, ip) + } +} + +func (t *transformer) deleteEndpointIP(name resource.Name, ip string) { + if names := t.ipToName[ip]; names != nil { + // Remove the name from the names map for this IP. + delete(names, name) + if len(names) == 0 { + // There are no more endpoints using this IP. Delete the map. + delete(t.ipToName, ip) + } + } +} + +func getEndpointIPs(entry *resource.Entry) map[string]struct{} { + ips := make(map[string]struct{}) + endpoints := entry.Item.(*coreV1.Endpoints) + for _, subset := range endpoints.Subsets { + for _, address := range subset.Addresses { + ips[address.IP] = struct{}{} + } + } + return ips +} + +func (t *transformer) toMcpResource(service *resource.Entry, endpoints *resource.Entry) (*resource.Entry, bool) { + meta := resource.Metadata{ + Annotations: make(map[string]string), + Labels: make(map[string]string), + } + se := networking.ServiceEntry{} + if err := t.converter.Convert(service, endpoints, &meta, &se); err != nil { + scope.Processing.Errorf("error converting to ServiceEntry: %v", err) + return nil, false + } + + // Set the version on the metadata. + meta.Version = resource.Version(t.versionString()) + + entry := &resource.Entry{ + Metadata: meta, + Item: &se, + } + return entry, true +} + +func (t *transformer) versionString() string { + t.version++ + return strconv.FormatInt(t.version, 10) +} diff --git a/galley/pkg/config/resource/entry.go b/galley/pkg/config/resource/entry.go new file mode 100644 index 000000000000..1864bfcfb123 --- /dev/null +++ b/galley/pkg/config/resource/entry.go @@ -0,0 +1,41 @@ +// 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 resource contains core abstract types for representing configuration resources. +package resource + +import ( + "github.com/gogo/protobuf/proto" +) + +// Entry is the abstract representation of a versioned config resource in Istio. +type Entry struct { + Metadata Metadata + Item proto.Message +} + +// IsEmpty returns true if the resource Entry.Item is nil. +func (r *Entry) IsEmpty() bool { + return r.Item == nil +} + +// Clone returns a deep-copy of this entry. Warning, this is expensive! +func (r *Entry) Clone() *Entry { + result := &Entry{} + if r.Item != nil { + result.Item = proto.Clone(r.Item) + } + result.Metadata = r.Metadata.Clone() + return result +} diff --git a/galley/pkg/config/resource/entry_test.go b/galley/pkg/config/resource/entry_test.go new file mode 100644 index 000000000000..aa9fba4542d1 --- /dev/null +++ b/galley/pkg/config/resource/entry_test.go @@ -0,0 +1,58 @@ +// 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 resource + +import ( + "testing" + + "github.com/gogo/protobuf/types" + . "github.com/onsi/gomega" +) + +func TestEntry_IsEmpty_False(t *testing.T) { + g := NewGomegaWithT(t) + + e := Entry{ + Item: &types.Empty{}, + } + + g.Expect(e.IsEmpty()).To(BeFalse()) +} + +func TestEntry_IsEmpty_True(t *testing.T) { + g := NewGomegaWithT(t) + e := Entry{} + + g.Expect(e.IsEmpty()).To(BeTrue()) +} + +func TestEntry_Clone_Empty(t *testing.T) { + g := NewGomegaWithT(t) + e := &Entry{} + + c := e.Clone() + g.Expect(c).To(Equal(e)) +} + +func TestEntry_Clone_NonEmpty(t *testing.T) { + g := NewGomegaWithT(t) + + e := &Entry{ + Item: &types.Empty{}, + } + + c := e.Clone() + g.Expect(c).To(Equal(e)) +} diff --git a/galley/pkg/config/resource/metadata.go b/galley/pkg/config/resource/metadata.go new file mode 100644 index 000000000000..f3b542e8dc68 --- /dev/null +++ b/galley/pkg/config/resource/metadata.go @@ -0,0 +1,36 @@ +// 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 resource + +import ( + "time" +) + +// Metadata about a resource. +type Metadata struct { + Name Name + CreateTime time.Time + Version Version + Labels StringMap + Annotations StringMap +} + +// Clone Metadata. Warning, this is expensive! +func (m *Metadata) Clone() Metadata { + result := *m + result.Annotations = m.Annotations.Clone() + result.Labels = m.Labels.Clone() + return result +} diff --git a/galley/pkg/config/resource/metadata_test.go b/galley/pkg/config/resource/metadata_test.go new file mode 100644 index 000000000000..1d9e00252814 --- /dev/null +++ b/galley/pkg/config/resource/metadata_test.go @@ -0,0 +1,47 @@ +// 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 resource + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestMetadata_Clone_NilMaps(t *testing.T) { + g := NewGomegaWithT(t) + + m := Metadata{ + Name: NewName("ns1", "rs1"), + Version: Version("v1"), + } + + c := m.Clone() + g.Expect(m).To(Equal(c)) +} + +func TestMetadata_Clone_NonNilMaps(t *testing.T) { + g := NewGomegaWithT(t) + + m := Metadata{ + Name: NewName("ns1", "rs1"), + Version: Version("v1"), + Annotations: map[string]string{"foo": "bar"}, + Labels: map[string]string{"l1": "l2"}, + } + + c := m.Clone() + g.Expect(m).To(Equal(c)) +} diff --git a/galley/pkg/config/resource/name.go b/galley/pkg/config/resource/name.go new file mode 100644 index 000000000000..7e0a8db988eb --- /dev/null +++ b/galley/pkg/config/resource/name.go @@ -0,0 +1,44 @@ +// 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 resource + +import "strings" + +// Name of the resource. It is unique within a given set of resource of the same collection. +type Name struct{ string } + +// NewName returns a Name from namespace and name. +func NewName(namespace, local string) Name { + if namespace == "" { + return Name{string: local} + } + + return Name{string: namespace + "/" + local} +} + +// String inteface implementation. +func (n Name) String() string { + return n.string +} + +// InterpretAsNamespaceAndName tries to split the name as namespace and name +func (n Name) InterpretAsNamespaceAndName() (string, string) { + parts := strings.SplitN(n.string, "/", 2) + if len(parts) == 1 { + return "", parts[0] + } + + return parts[0], parts[1] +} diff --git a/galley/pkg/config/resource/name_test.go b/galley/pkg/config/resource/name_test.go new file mode 100644 index 000000000000..1957f647313b --- /dev/null +++ b/galley/pkg/config/resource/name_test.go @@ -0,0 +1,56 @@ +// 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 resource + +import ( + "testing" +) + +func TestNewName(t *testing.T) { + n := NewName("ns1", "l1") + if n.string != "ns1/l1" { + t.Fatalf("unexpected name: %v", n.string) + } +} + +func TestNewName_NoNamespace(t *testing.T) { + n := NewName("", "l1") + if n.string != "l1" { + t.Fatalf("unexpected name: %v", n.string) + } +} + +func TestName_String(t *testing.T) { + n := NewName("ns1", "l1") + if n.String() != "ns1/l1" { + t.Fatalf("unexpected string: %v", n.string) + } +} + +func TestInterpretAsNamespaceAndName(t *testing.T) { + n := NewName("ns1", "l1") + ns, l := n.InterpretAsNamespaceAndName() + if ns != "ns1" || l != "l1" { + t.Fatalf("unexpected %q, %q", ns, l) + } +} + +func TestInterpretAsNamespaceAndName_NoNamespace(t *testing.T) { + n := NewName("", "l1") + ns, l := n.InterpretAsNamespaceAndName() + if ns != "" || l != "l1" { + t.Fatalf("unexpected %q, %q", ns, l) + } +} diff --git a/galley/pkg/config/resource/serialization.go b/galley/pkg/config/resource/serialization.go new file mode 100644 index 000000000000..c25ef6d12bca --- /dev/null +++ b/galley/pkg/config/resource/serialization.go @@ -0,0 +1,127 @@ +// 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 resource + +import ( + "fmt" + + "github.com/gogo/protobuf/types" + + mcp "istio.io/api/mcp/v1alpha1" + "istio.io/pkg/log" +) + +var scope = log.RegisterScope("resource", "Core resource model scope", 0) + +// Serialize converts a resource entry into its enveloped form. +func Serialize(e *Entry) (*mcp.Resource, error) { + + a, err := types.MarshalAny(e.Item) + if err != nil { + scope.Errorf("Error serializing proto from source e: %v:", e) + return nil, err + } + + createTime, err := types.TimestampProto(e.Metadata.CreateTime) + if err != nil { + scope.Errorf("Error parsing resource create_time for event (%v): %v", e, err) + return nil, err + } + + entry := &mcp.Resource{ + Metadata: &mcp.Metadata{ + Name: e.Metadata.Name.String(), + CreateTime: createTime, + Version: string(e.Metadata.Version), + Annotations: e.Metadata.Annotations, + Labels: e.Metadata.Labels, + }, + Body: a, + } + + return entry, nil +} + +// MustSerialize converts a resource entry into its enveloped form or panics if it cannot. +func MustSerialize(e *Entry) *mcp.Resource { + m, err := Serialize(e) + if err != nil { + panic(fmt.Sprintf("resource.MustSerialize: %v", err)) + } + return m +} + +// SerializeAll envelopes and returns all the entries. +func SerializeAll(entries []*Entry) ([]*mcp.Resource, error) { + result := make([]*mcp.Resource, len(entries)) + for i, e := range entries { + r, err := Serialize(e) + if err != nil { + return nil, err + } + result[i] = r + } + return result, nil +} + +// Deserialize an entry from an envelope. +func Deserialize(e *mcp.Resource) (*Entry, error) { + p, err := types.EmptyAny(e.Body) + if err != nil { + return nil, fmt.Errorf("error unmarshaling proto: %v", err) + } + + createTime, err := types.TimestampFromProto(e.Metadata.CreateTime) + if err != nil { + return nil, fmt.Errorf("error unmarshaling create time: %v", err) + } + + if err = types.UnmarshalAny(e.Body, p); err != nil { + return nil, fmt.Errorf("error unmarshaling body: %v", err) + } + + return &Entry{ + Metadata: Metadata{ + Name: Name{e.Metadata.Name}, + CreateTime: createTime, + Version: Version(e.Metadata.Version), + Annotations: e.Metadata.Annotations, + Labels: e.Metadata.Labels, + }, + Item: p, + }, nil +} + +// MustDeserialize deserializes an entry from an envelope or panics. +func MustDeserialize(e *mcp.Resource) *Entry { + m, err := Deserialize(e) + if err != nil { + panic(fmt.Sprintf("resource.MustDeserialize: %v", err)) + } + return m +} + +// DeserializeAll extracts all entries from the given envelopes and returns. +func DeserializeAll(es []*mcp.Resource) ([]*Entry, error) { + result := make([]*Entry, len(es)) + for i, e := range es { + r, err := Deserialize(e) + if err != nil { + return nil, err + } + result[i] = r + } + return result, nil +} diff --git a/galley/pkg/config/resource/serialization_test.go b/galley/pkg/config/resource/serialization_test.go new file mode 100644 index 000000000000..d8d9c8f6514f --- /dev/null +++ b/galley/pkg/config/resource/serialization_test.go @@ -0,0 +1,377 @@ +// 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 resource + +import ( + "bytes" + "errors" + "fmt" + "math" + "reflect" + "testing" + "time" + + "github.com/gogo/protobuf/jsonpb" + "github.com/gogo/protobuf/proto" + "github.com/gogo/protobuf/types" +) + +func TestSerialization_Basic(t *testing.T) { + e := Entry{ + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: parseStruct(`{ "foo": "bar" }`), + } + + env, err := Serialize(&e) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if env.Metadata.Name != e.Metadata.Name.String() { + t.Fatalf("unexpected name: %v", env.Metadata.Name) + } + + if env.Metadata.Version != string(e.Metadata.Version) { + t.Fatalf("unexpected version: %v", env.Metadata.Version) + } + + if env.Metadata.CreateTime == nil { + t.Fatal("CreateTime is nil") + } + + expected := boxAny(parseStruct(`{ "foo": "bar" }`)) + if !reflect.DeepEqual(env.Body, expected) { + t.Fatalf("Resources are not equal %v != %v", env.Body, expected) + } + + ext, err := Deserialize(env) + if err != nil { + t.Fatalf("Unexpected error when extracting: %v", err) + } + + if !reflect.DeepEqual(ext.Metadata, e.Metadata) { + t.Fatalf("mismatch: got:%v, wanted: %v", ext, e) + } +} + +func TestSerialize_Error(t *testing.T) { + e := Entry{ + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: &invalidProto{}, + } + + _, err := Serialize(&e) + if err == nil { + t.Fatal("expected error not found") + } +} + +func TestMustSerialize(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("Should not have panicked %v", r) + } + }() + + e := Entry{ + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: &types.Empty{}, + } + + _ = MustSerialize(&e) +} + +func TestMustSerialize_Panic(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatalf("Should have panicked %v", r) + } + }() + + e := Entry{ + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: &invalidProto{}, + } + + _ = MustSerialize(&e) +} + +func TestSerialize_InvalidTimestamp_Error(t *testing.T) { + e := Entry{ + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(math.MinInt64, math.MinInt64).UTC(), + Version: "v1", + }, + Item: &types.Empty{}, + } + _, err := Serialize(&e) + if err == nil { + t.Fatal("expected error not found") + } +} + +func TestDeserialize_Error(t *testing.T) { + e := Entry{ + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: &types.Empty{}, + } + + env, err := Serialize(&e) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + env.Body.TypeUrl += ".foo" + + if _, err = Deserialize(env); err == nil { + t.Fatalf("expected error not found") + } +} + +func TestDeserialize_InvalidTimestamp_Error(t *testing.T) { + e := Entry{ + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: &types.Empty{}, + } + + env, err := Serialize(&e) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + env.Metadata.CreateTime.Seconds = 253402300800 + 1 + + if _, err = Deserialize(env); err == nil { + t.Fatalf("expected error not found") + } +} + +func TestDeserialize_Any_Error(t *testing.T) { + e := Entry{ + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: &types.Empty{}, + } + + env, err := Serialize(&e) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + b := make([]byte, len(env.Body.Value)+1) + b[0] = 0xFA + copy(b[1:], env.Body.Value) + env.Body.Value = b + + if _, err = Deserialize(env); err == nil { + t.Fatalf("expected error not found") + } +} + +func TestMustDeserialize(t *testing.T) { + e := Entry{ + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: &types.Empty{}, + } + + s := MustSerialize(&e) + + defer func() { + if r := recover(); r != nil { + t.Fatalf("Should not have panicked %v", r) + } + }() + + _ = MustDeserialize(s) +} + +func TestMustDeserialize_Panic(t *testing.T) { + e := Entry{ + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: &types.Empty{}, + } + + s := MustSerialize(&e) + + defer func() { + if r := recover(); r == nil { + t.Fatalf("Should have panicked %v", r) + } + }() + + s.Metadata.CreateTime.Seconds = 253402300800 + 1 + + _ = MustDeserialize(s) +} + +func TestDeserializeAll(t *testing.T) { + entries := []*Entry{ + { + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: parseStruct(`{"foo": "bar"}`), + }, + { + Metadata: Metadata{ + Name: NewName("ns2", "res2"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v2", + }, + Item: parseStruct(`{"bar": "foo"}`), + }, + } + + envs, err := SerializeAll(entries) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + actual, err := DeserializeAll(envs) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(entries, actual) { + t.Fatalf("mismatch: got:%+v, wanted:%+v", actual, entries) + } +} + +func TestSerializeAll_Error(t *testing.T) { + entries := []*Entry{ + { + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: &invalidProto{}, + }, + { + Metadata: Metadata{ + Name: NewName("ns2", "res2"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v2", + }, + Item: &types.Empty{}, + }, + } + + if _, err := SerializeAll(entries); err == nil { + t.Fatal("expected error not found") + } +} + +func TestDeserializeAll_Error(t *testing.T) { + entries := []*Entry{ + { + Metadata: Metadata{ + Name: NewName("ns1", "res1"), + CreateTime: time.Unix(1, 1).UTC(), + Version: "v1", + }, + Item: &types.Empty{}, + }, + { + Metadata: Metadata{ + Name: NewName("ns2", "res2"), + CreateTime: time.Unix(2, 2).UTC(), + Version: "v2", + }, + Item: &types.Empty{}, + }, + } + + env, err := SerializeAll(entries) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + env[1].Metadata.CreateTime.Seconds = 253402300800 + 1 + + if _, err = DeserializeAll(env); err == nil { + t.Fatal("expected error not found") + } +} + +type invalidProto struct { +} + +var _ proto.Message = &invalidProto{} +var _ proto.Marshaler = &invalidProto{} +var _ proto.Unmarshaler = &invalidProto{} + +func (i *invalidProto) Reset() {} +func (i *invalidProto) String() string { return "" } +func (i *invalidProto) ProtoMessage() {} +func (i *invalidProto) Unmarshal([]byte) error { return errors.New("unmarshal error") } +func (i *invalidProto) Marshal() ([]byte, error) { return nil, errors.New("marshal error") } + +func parseStruct(s string) *types.Struct { + p := &types.Struct{} + + b := bytes.NewReader([]byte(s)) + if err := jsonpb.Unmarshal(b, p); err != nil { + panic(fmt.Errorf("invalid struct JSON: %v", err)) + } + + return p +} + +func boxAny(p *types.Struct) *types.Any { // nolint:interfacer + a, err := types.MarshalAny(p) + if err != nil { + panic(fmt.Errorf("unable to marshal to any: %v", err)) + } + return a +} diff --git a/galley/pkg/config/resource/stringmap.go b/galley/pkg/config/resource/stringmap.go new file mode 100644 index 000000000000..fdf930f14abf --- /dev/null +++ b/galley/pkg/config/resource/stringmap.go @@ -0,0 +1,48 @@ +// 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 resource + +// StringMap is used to store labels and annotations. +type StringMap map[string]string + +// Clone the StringMap +func (s *StringMap) Clone() StringMap { + if *s == nil { + return nil + } + + m := make(map[string]string, len(*s)) + for k, v := range *s { + m[k] = v + } + + return m +} + +// CloneOrCreate clones a StringMap. It creates the map if it doesn't exist. +func (s *StringMap) CloneOrCreate() StringMap { + m := s.Clone() + if m == nil { + m = make(map[string]string) + } + return m +} + +// Remove the given name from the string map +func (s *StringMap) Delete(name string) { + if s != nil { + delete(*s, name) + } +} diff --git a/galley/pkg/config/resource/stringmap_test.go b/galley/pkg/config/resource/stringmap_test.go new file mode 100644 index 000000000000..60e0eb42d563 --- /dev/null +++ b/galley/pkg/config/resource/stringmap_test.go @@ -0,0 +1,87 @@ +// 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 resource + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestStringMap_Clone_Nil(t *testing.T) { + g := NewGomegaWithT(t) + + var s StringMap + + c := s.Clone() + g.Expect(c).To(BeNil()) +} + +func TestStringMap_Clone_NonNil(t *testing.T) { + g := NewGomegaWithT(t) + + var s StringMap = map[string]string{ + "foo": "bar", + } + + c := s.Clone() + g.Expect(c).NotTo(BeNil()) + g.Expect(c).To(HaveLen(1)) + g.Expect(c["foo"]).To(Equal("bar")) +} + +func TestStringMap_CloneOrCreate_Nil(t *testing.T) { + g := NewGomegaWithT(t) + + var s StringMap + + c := s.CloneOrCreate() + g.Expect(c).NotTo(BeNil()) + g.Expect(c).To(HaveLen(0)) +} + +func TestStringMap_CloneOrCreate_NonNil(t *testing.T) { + g := NewGomegaWithT(t) + + var s StringMap = map[string]string{ + "foo": "bar", + } + + c := s.CloneOrCreate() + g.Expect(c).NotTo(BeNil()) + g.Expect(c).To(HaveLen(1)) + g.Expect(c["foo"]).To(Equal("bar")) +} + +func TestStringMap_Delete_NonNil(t *testing.T) { + g := NewGomegaWithT(t) + + var s StringMap = map[string]string{ + "foo": "bar", + } + + s.Delete("foo") + g.Expect(s).NotTo(BeNil()) + g.Expect(s).To(HaveLen(0)) +} + +func TestStringMap_Delete_Nil(t *testing.T) { + g := NewGomegaWithT(t) + + var s StringMap + + s.Delete("foo") + g.Expect(s).To(BeNil()) +} diff --git a/galley/pkg/config/resource/version.go b/galley/pkg/config/resource/version.go new file mode 100644 index 000000000000..7bb22ac34a86 --- /dev/null +++ b/galley/pkg/config/resource/version.go @@ -0,0 +1,18 @@ +// 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 resource + +// Version is the version identifier of a resource. +type Version string diff --git a/galley/pkg/config/schema/ast/ast.go b/galley/pkg/config/schema/ast/ast.go new file mode 100644 index 000000000000..236a35882afd --- /dev/null +++ b/galley/pkg/config/schema/ast/ast.go @@ -0,0 +1,143 @@ +// 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 ast + +import ( + "encoding/json" + "fmt" + + "github.com/ghodss/yaml" +) + +// Metadata is the top-level container. +type Metadata struct { + Collections []*Collection `json:"collections"` + Snapshots []*Snapshot `json:"snapshots"` + Sources []Source `json:"sources"` + Transforms []Transform `json:"transforms"` +} + +var _ json.Unmarshaler = &Metadata{} + +// Collection metadata. Describes basic structure of collections. +type Collection struct { + Name string `json:"name"` + Proto string `json:"proto"` + ProtoPackage string `json:"protoPackage"` +} + +// Snapshot metadata. Describes the snapshots that should be produced. +type Snapshot struct { + Name string `json:"name"` + Strategy string `json:"strategy"` + Collections []string `json:"collections"` +} + +// Source configuration metadata. +type Source interface { +} + +// Transform configuration metadata. +type Transform interface { +} + +// KubeSource is configuration for K8s based input sources. +type KubeSource struct { + Resources []*Resource `json:"resources"` +} + +var _ Source = &KubeSource{} + +// Resource metadata for a Kubernetes Resource. +type Resource struct { + Collection string `json:"collection"` + Group string `json:"group"` + Version string `json:"version"` + Kind string `json:"kind"` + Plural string `json:"plural"` + Optional bool `json:"optional"` + Disabled bool `json:"disabled"` +} + +// DirectTransform configuration +type DirectTransform struct { + Mapping map[string]string `json:"mapping"` +} + +// for testing purposes +var jsonUnmarshal = json.Unmarshal + +// UnmarshalJSON implements json.Unmarshaler +func (s *Metadata) UnmarshalJSON(data []byte) error { + var in struct { + Collections []*Collection `json:"collections"` + Snapshots []*Snapshot `json:"snapshots"` + Sources []json.RawMessage `json:"sources"` + Transforms []json.RawMessage `json:"transforms"` + } + + if err := jsonUnmarshal(data, &in); err != nil { + return err + } + + s.Collections = in.Collections + s.Snapshots = in.Snapshots + + for _, src := range in.Sources { + m := make(map[string]interface{}) + if err := jsonUnmarshal(src, &m); err != nil { + return err + } + + if m["type"] == "kubernetes" { + ks := &KubeSource{} + if err := jsonUnmarshal(src, &ks); err != nil { + return err + } + s.Sources = append(s.Sources, ks) + } else { + return fmt.Errorf("unable to parse source: %v", string([]byte(src))) + } + } + + for _, xform := range in.Transforms { + m := make(map[string]interface{}) + if err := jsonUnmarshal(xform, &m); err != nil { + return err + } + + if m["type"] == "direct" { + dt := &DirectTransform{} + if err := jsonUnmarshal(xform, &dt); err != nil { + return err + } + s.Transforms = append(s.Transforms, dt) + } else { + return fmt.Errorf("unable to parse transform: %v", string([]byte(xform))) + } + } + + return nil +} + +// Parse and return a yaml representation of Metadata +func Parse(yamlText string) (*Metadata, error) { + var s Metadata + err := yaml.Unmarshal([]byte(yamlText), &s) + if err != nil { + return nil, err + } + return &s, nil +} diff --git a/galley/pkg/config/schema/ast/ast_test.go b/galley/pkg/config/schema/ast/ast_test.go new file mode 100644 index 000000000000..d50aad2b7334 --- /dev/null +++ b/galley/pkg/config/schema/ast/ast_test.go @@ -0,0 +1,216 @@ +// 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 ast + +import ( + "encoding/json" + "fmt" + "testing" + + . "github.com/onsi/gomega" +) + +func TestParse(t *testing.T) { + var cases = []struct { + input string + expected *Metadata + }{ + { + input: ``, + expected: &Metadata{}, + }, + { + input: ` +collections: + - name: "istio/meshconfig" + proto: "istio.mesh.v1alpha1.MeshConfig" + protoPackage: "istio.io/api/mesh/v1alpha1" + +snapshots: + - name: "default" + collections: + - "istio/meshconfig" + +sources: + - type: kubernetes + resources: + - collection: "k8s/networking.istio.io/v1alpha3/virtualservices" + kind: "VirtualService" + group: "networking.istio.io" + version: "v1alpha3" + +transforms: + - type: direct + mapping: + "k8s/networking.istio.io/v1alpha3/destinationrules": "istio/networking/v1alpha3/destinationrules" +`, + expected: &Metadata{ + Collections: []*Collection{ + { + Name: "istio/meshconfig", + Proto: "istio.mesh.v1alpha1.MeshConfig", + ProtoPackage: "istio.io/api/mesh/v1alpha1", + }, + }, + Snapshots: []*Snapshot{ + { + Name: "default", + Collections: []string{ + "istio/meshconfig", + }, + }, + }, + Sources: []Source{ + &KubeSource{ + Resources: []*Resource{ + { + Collection: "k8s/networking.istio.io/v1alpha3/virtualservices", + Kind: "VirtualService", + Group: "networking.istio.io", + Version: "v1alpha3", + }, + }, + }, + }, + Transforms: []Transform{ + &DirectTransform{ + Mapping: map[string]string{ + "k8s/networking.istio.io/v1alpha3/destinationrules": "istio/networking/v1alpha3/destinationrules", + }, + }, + }, + }, + }, + } + + for _, c := range cases { + t.Run("", func(t *testing.T) { + g := NewGomegaWithT(t) + actual, err := Parse(c.input) + g.Expect(err).To((BeNil())) + g.Expect(actual).To(Equal(c.expected)) + }) + } +} + +func TestParseErrors(t *testing.T) { + var cases = []string{ + ` +collections: + - name: "istio/meshconfig" + proto: "istio.mesh.v1alpha1.MeshConfig" + protoPackage: "istio.io/api/mesh/v1alpha1" + +snapshots: + - name: "default" + collections: + - "istio/meshconfig" + +sources: + - type: foo + resources: + - collection: "k8s/networking.istio.io/v1alpha3/virtualservices" + kind: "VirtualService" + group: "networking.istio.io" + version: "v1alpha3" + +transforms: + - type: direct + mapping: + "k8s/networking.istio.io/v1alpha3/destinationrules": "istio/networking/v1alpha3/destinationrules" +`, + ` +collections: + - name: "istio/meshconfig" + proto: "istio.mesh.v1alpha1.MeshConfig" + protoPackage: "istio.io/api/mesh/v1alpha1" + +snapshots: + - name: "default" + collections: + - "istio/meshconfig" + +sources: + - type: kubernetes + resources: + - collection: "k8s/networking.istio.io/v1alpha3/virtualservices" + kind: "VirtualService" + group: "networking.istio.io" + version: "v1alpha3" + +transforms: + - type: foo + mapping: + "k8s/networking.istio.io/v1alpha3/destinationrules": "istio/networking/v1alpha3/destinationrules" +`, + } + + for _, c := range cases { + t.Run("", func(t *testing.T) { + g := NewGomegaWithT(t) + _, err := Parse(c) + g.Expect(err).NotTo((BeNil())) + }) + } +} + +func TestParseErrors_Unmarshal(t *testing.T) { + input := ` +collections: + - name: "istio/meshconfig" + proto: "istio.mesh.v1alpha1.MeshConfig" + protoPackage: "istio.io/api/mesh/v1alpha1" + +snapshots: + - name: "default" + collections: + - "istio/meshconfig" + +sources: + - type: kubernetes + resources: + - collection: "k8s/networking.istio.io/v1alpha3/virtualservices" + kind: "VirtualService" + group: "networking.istio.io" + version: "v1alpha3" + +transforms: + - type: direct + mapping: + "k8s/networking.istio.io/v1alpha3/destinationrules": "istio/networking/v1alpha3/destinationrules" +` + + for i := 0; i < 5; i++ { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + g := NewGomegaWithT(t) + + var cur int + jsonUnmarshal = func(data []byte, v interface{}) error { + if cur >= i { + return fmt.Errorf("err") + } + cur++ + return json.Unmarshal(data, v) + } + + defer func() { + jsonUnmarshal = json.Unmarshal + }() + + _, err := Parse(input) + g.Expect(err).NotTo((BeNil())) + }) + } +} diff --git a/galley/pkg/config/schema/codegen/collections.go b/galley/pkg/config/schema/codegen/collections.go new file mode 100644 index 000000000000..b8eeb527d0ca --- /dev/null +++ b/galley/pkg/config/schema/codegen/collections.go @@ -0,0 +1,135 @@ +// 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 codegen + +import ( + "sort" + "strings" +) + +const staticCollectionsTemplate = ` +// GENERATED FILE -- DO NOT EDIT +// + +package {{.PackageName}} + +import ( + "istio.io/istio/galley/pkg/config/collection" +) + +var ( +{{range .Entries}} + // {{.VarName}} is the name of collection {{.Name}} + {{.VarName}} = collection.NewName("{{.Name}}") +{{end}} +) + +// CollectionNames returns the collection names declared in this package. +func CollectionNames() []collection.Name { + return []collection.Name { + {{range .Entries}}{{.VarName}}, + {{end}} + } +} +` + +type entry struct { + Name string + VarName string +} + +// StaticCollections generates a Go file for static-importing Proto packages, so that they get registered statically. +func StaticCollections(packageName string, collections []string) (string, error) { + var entries []entry + + for _, col := range collections { + entries = append(entries, entry{Name: col, VarName: asColVarName(col)}) + } + sort.Slice(entries, func(i, j int) bool { + return strings.Compare(entries[i].Name, entries[j].Name) < 0 + }) + + context := struct { + Entries []entry + PackageName string + }{Entries: entries, PackageName: packageName} + + // Calculate the Go packages that needs to be imported for the proto types to be registered. + return applyTemplate(staticCollectionsTemplate, context) +} + +func asColVarName(n string) string { + n = camelCase(n, "/") + n = camelCase(n, ".") + return n +} + +// CamelCase converts the string into camel case string +func CamelCase(s string) string { + if s == "" { + return "" + } + t := make([]byte, 0, 32) + i := 0 + if s[0] == '_' { + // Need a capital letter; drop the '_'. + t = append(t, 'X') + i++ + } + // Invariant: if the next letter is lower case, it must be converted + // to upper case. + // That is, we process a word at a time, where words are marked by _ or + // upper case letter. Digits are treated as words. + for ; i < len(s); i++ { + c := s[i] + if c == '_' && i+1 < len(s) && isASCIILower(s[i+1]) { + continue // Skip the underscore in s. + } + if isASCIIDigit(c) { + t = append(t, c) + continue + } + // Assume we have a letter now - if not, it's a bogus identifier. + // The next word is a sequence of characters that must start upper case. + if isASCIILower(c) { + c ^= ' ' // Make it a capital letter. + } + t = append(t, c) // Guaranteed not lower case. + // Accept lower case sequence that follows. + for i+1 < len(s) && isASCIILower(s[i+1]) { + i++ + t = append(t, s[i]) + } + } + return string(t) +} + +func camelCase(n string, sep string) string { + p := strings.Split(n, sep) + for i := 0; i < len(p); i++ { + p[i] = CamelCase(p[i]) + } + return strings.Join(p, "") +} + +// Is c an ASCII lower-case letter? +func isASCIILower(c byte) bool { + return 'a' <= c && c <= 'z' +} + +// Is c an ASCII digit? +func isASCIIDigit(c byte) bool { + return '0' <= c && c <= '9' +} diff --git a/galley/pkg/config/schema/codegen/collections_test.go b/galley/pkg/config/schema/codegen/collections_test.go new file mode 100644 index 000000000000..5e65456ee429 --- /dev/null +++ b/galley/pkg/config/schema/codegen/collections_test.go @@ -0,0 +1,103 @@ +// 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 codegen + +import ( + "strings" + "testing" + + . "github.com/onsi/gomega" +) + +func TestStaticCollections(t *testing.T) { + var cases = []struct { + packageName string + collections []string + err string + output string + }{ + { + packageName: "pkg", + collections: []string{"foo", "bar"}, + output: ` +// GENERATED FILE -- DO NOT EDIT +// + +package pkg + +import ( + "istio.io/istio/galley/pkg/config/collection" +) + +var ( + + // Bar is the name of collection bar + Bar = collection.NewName("bar") + + // Foo is the name of collection foo + Foo = collection.NewName("foo") + +) + +// CollectionNames returns the collection names declared in this package. +func CollectionNames() []collection.Name { + return []collection.Name { + Bar, + Foo, + + } +}`, + }, + } + + for _, c := range cases { + t.Run("", func(t *testing.T) { + g := NewGomegaWithT(t) + + s, err := StaticCollections(c.packageName, c.collections) + if c.err != "" { + g.Expect(err).NotTo(BeNil()) + g.Expect(err.Error()).To(Equal(s)) + } else { + g.Expect(err).To(BeNil()) + g.Expect(strings.TrimSpace(s)).To(Equal(strings.TrimSpace(c.output))) + } + }) + } +} + +func TestCamelCase(t *testing.T) { + cases := map[string]string{ + "": "", + "foo": "Foo", + "foobar": "Foobar", + "fooBar": "FooBar", + "foo_bar": "FooBar", + "foo_Bar": "Foo_Bar", // TODO: This seems like a bug. + "foo9bar": "Foo9Bar", + "_foo": "XFoo", + "_Foo": "XFoo", + } + + for k, v := range cases { + t.Run(k, func(t *testing.T) { + g := NewGomegaWithT(t) + + a := CamelCase(k) + g.Expect(a).To(Equal(v)) + }) + } + +} diff --git a/galley/pkg/config/schema/codegen/staticinit.go b/galley/pkg/config/schema/codegen/staticinit.go new file mode 100644 index 000000000000..b87845322637 --- /dev/null +++ b/galley/pkg/config/schema/codegen/staticinit.go @@ -0,0 +1,75 @@ +// 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 codegen + +import ( + "bytes" + "sort" + "text/template" +) + +const importInitTemplate = ` +// GENERATED FILE -- DO NOT EDIT +// + +package {{.PackageName}} + +import ( + // Pull in all the known proto types to ensure we get their types registered. + +{{range .Packages}} + // Register protos in "{{.}}" + _ "{{.}}" +{{end}} +) +` + +// StaticInit generates a Go file for static-importing Proto packages, so that they get registered statically. +func StaticInit(packageName string, packages []string) (string, error) { + // Single instance and sort names + names := make(map[string]struct{}) + + for _, p := range packages { + if p != "" { + names[p] = struct{}{} + } + } + + sorted := make([]string, 0, len(names)) + for p := range names { + sorted = append(sorted, p) + } + sort.Strings(sorted) + + context := struct { + Packages []string + PackageName string + }{Packages: sorted, PackageName: packageName} + + return applyTemplate(importInitTemplate, context) +} + +func applyTemplate(tmpl string, i interface{}) (string, error) { + t := template.New("tmpl") + + t2 := template.Must(t.Parse(tmpl)) + + var b bytes.Buffer + if err := t2.Execute(&b, i); err != nil { + return "", err + } + + return b.String(), nil +} diff --git a/galley/pkg/config/schema/codegen/staticinit_test.go b/galley/pkg/config/schema/codegen/staticinit_test.go new file mode 100644 index 000000000000..1ff6b647e600 --- /dev/null +++ b/galley/pkg/config/schema/codegen/staticinit_test.go @@ -0,0 +1,75 @@ +// 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 codegen + +import ( + "strings" + "testing" + + . "github.com/onsi/gomega" +) + +func TestStaticInit(t *testing.T) { + var cases = []struct { + packageName string + packages []string + err string + output string + }{ + { + packageName: "pkg", + packages: []string{"foo", "bar"}, + output: ` +// GENERATED FILE -- DO NOT EDIT +// + +package pkg + +import ( + // Pull in all the known proto types to ensure we get their types registered. + + + // Register protos in "bar" + _ "bar" + + // Register protos in "foo" + _ "foo" + +)`, + }, + } + + for _, c := range cases { + t.Run("", func(t *testing.T) { + g := NewGomegaWithT(t) + + s, err := StaticInit(c.packageName, c.packages) + if c.err != "" { + g.Expect(err).NotTo(BeNil()) + g.Expect(err.Error()).To(Equal(s)) + } else { + g.Expect(err).To(BeNil()) + g.Expect(strings.TrimSpace(s)).To(Equal(strings.TrimSpace(c.output))) + } + }) + } +} + +func TestApplyTemplate_Error(t *testing.T) { + g := NewGomegaWithT(t) + + _, err := applyTemplate(staticCollectionsTemplate, struct{}{}) + g.Expect(err).ToNot(BeNil()) +} diff --git a/galley/pkg/config/schema/codegen/tools/collections.main.go b/galley/pkg/config/schema/codegen/tools/collections.main.go new file mode 100644 index 000000000000..69af4da2e560 --- /dev/null +++ b/galley/pkg/config/schema/codegen/tools/collections.main.go @@ -0,0 +1,68 @@ +// 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. + +// +build ignore + +package main + +import ( + "fmt" + "io/ioutil" + "os" + + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/schema/codegen" +) + +// Utility for generating collections.gen.go. Called from gen.go +func main() { + if len(os.Args) != 4 { + fmt.Printf("Invalid args: %v", os.Args) + os.Exit(-1) + } + + pkg := os.Args[1] + input := os.Args[2] + output := os.Args[3] + + c, err := readMetadata(input) + if err != nil { + fmt.Printf("Error reading metadata: %v", err) + os.Exit(-2) + } + + var names []string + for _, r := range c.Collections().All() { + names = append(names, r.Name.String()) + } + contents, err := codegen.StaticCollections(pkg, names) + if err != nil { + fmt.Printf("Error applying static init template: %v", err) + os.Exit(-3) + } + + if err = ioutil.WriteFile(output, []byte(contents), os.ModePerm); err != nil { + fmt.Printf("Error writing output file: %v", err) + os.Exit(-4) + } +} + +func readMetadata(path string) (*schema.Metadata, error) { + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("unable to read input file: %v", err) + } + + return schema.ParseAndBuild(string(b)) +} diff --git a/galley/pkg/config/schema/codegen/tools/staticinit.main.go b/galley/pkg/config/schema/codegen/tools/staticinit.main.go new file mode 100644 index 000000000000..2706e673dc62 --- /dev/null +++ b/galley/pkg/config/schema/codegen/tools/staticinit.main.go @@ -0,0 +1,68 @@ +// 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. + +// +build ignore + +package main + +import ( + "fmt" + "io/ioutil" + "os" + + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/schema/codegen" +) + +// Utility for generating staticinit.gen.go. Called from gen.go +func main() { + if len(os.Args) != 4 { + fmt.Printf("Invalid args: %v", os.Args) + os.Exit(-1) + } + + pkg := os.Args[1] + input := os.Args[2] + output := os.Args[3] + + c, err := readMetadata(input) + if err != nil { + fmt.Printf("Error reading metadata: %v", err) + os.Exit(-2) + } + + var packages []string + for _, r := range c.Collections().All() { + packages = append(packages, r.ProtoPackage) + } + contents, err := codegen.StaticInit(pkg, packages) + if err != nil { + fmt.Printf("Error applying static init template: %v", err) + os.Exit(-3) + } + + if err = ioutil.WriteFile(output, []byte(contents), os.ModePerm); err != nil { + fmt.Printf("Error writing output file: %v", err) + os.Exit(-4) + } +} + +func readMetadata(path string) (*schema.Metadata, error) { + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("unable to read input file: %v", err) + } + + return schema.ParseAndBuild(string(b)) +} diff --git a/galley/pkg/config/schema/schema.go b/galley/pkg/config/schema/schema.go new file mode 100644 index 000000000000..52c50e8ff06f --- /dev/null +++ b/galley/pkg/config/schema/schema.go @@ -0,0 +1,313 @@ +// 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 schema + +import ( + "fmt" + "sort" + "strings" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/schema/ast" +) + +// Metadata is the top-level container. +type Metadata struct { + collections collection.Specs + snapshots map[string]*Snapshot + sources []Source + transforms []Transform +} + +// Collections is all known collections +func (m *Metadata) Collections() collection.Specs { return m.collections } + +// Snapshots returns all known snapshots +func (m *Metadata) Snapshots() []*Snapshot { + result := make([]*Snapshot, 0, len(m.snapshots)) + for _, s := range m.snapshots { + result = append(result, s) + } + return result +} + +// Sources is all known sources +func (m *Metadata) Sources() []Source { + result := make([]Source, len(m.sources)) + copy(result, m.sources) + return result +} + +// KubeSource is a temporary convenience function for getting the Kubernetes Source. As the infrastructure +// is generified, then this method should disappear. +func (m *Metadata) KubeSource() *KubeSource { + for _, s := range m.sources { + if ks, ok := s.(*KubeSource); ok { + return ks + } + } + + panic("Metadata.KubeSource: KubeSource not found") +} + +// Transforms is all known transforms +func (m *Metadata) Transforms() []Transform { + result := make([]Transform, len(m.transforms)) + copy(result, m.transforms) + return result +} + +// DirectTransform is a temporary convenience function for getting the Direct Transform config. As the +// infrastructure is generified, then this method should disappear. +func (m *Metadata) DirectTransform() *DirectTransform { + for _, s := range m.transforms { + if ks, ok := s.(*DirectTransform); ok { + return ks + } + } + + panic("Metadata.DirectTransform: DirectTransform not found") +} + +// AllCollectionsInSnapshots returns an aggregate list of names of collections that will appear in snapshots. +func (m *Metadata) AllCollectionsInSnapshots() []string { + names := make(map[collection.Name]struct{}) + + for _, s := range m.snapshots { + for _, c := range s.Collections { + names[c] = struct{}{} + } + } + + var result = make([]string, 0, len(names)) + for name := range names { + result = append(result, name.String()) + } + + sort.SliceStable(result, func(i, j int) bool { + return strings.Compare(result[i], result[j]) < 0 + }) + + return result +} + +// Snapshot metadata. Describes the snapshots that should be produced. +type Snapshot struct { + Name string + Collections []collection.Name + Strategy string +} + +// Source configuration metadata. +type Source interface { +} + +// Transform configuration metadata. +type Transform interface { +} + +// KubeSource is configuration for K8s based input sources. +type KubeSource struct { + resources []*KubeResource +} + +// KubeResources is all known resources +func (k *KubeSource) Resources() KubeResources { + result := make([]KubeResource, len(k.resources)) + for i, r := range k.resources { + result[i] = *r + } + return result +} + +var _ Source = &KubeSource{} + +// KubeResource metadata for a Kubernetes KubeResource. +type KubeResource struct { + Collection collection.Spec + Group string + Version string + Kind string + Plural string + Disabled bool + Optional bool +} + +// KubeResources is an array of resources +type KubeResources []KubeResource + +// CanonicalResourceName of the resource. +func (i KubeResource) CanonicalResourceName() string { + if i.Group == "" { + return "core/" + i.Version + "/" + i.Kind + } + return i.Group + "/" + i.Version + "/" + i.Kind +} + +// Collections returns the name of collections for this set of resources +func (k KubeResources) Collections() []collection.Name { + result := make([]collection.Name, 0, len(k)) + for _, res := range k { + result = append(result, res.Collection.Name) + } + + return result +} + +// Find searches and returns the resource spec with the given group/kind +func (k KubeResources) Find(group, kind string) (KubeResource, bool) { + for _, rs := range k { + if rs.Group == group && rs.Kind == kind { + return rs, true + } + } + + return KubeResource{}, false +} + +// MustFind calls Find and panics if not found. +func (k KubeResources) MustFind(group, kind string) KubeResource { + r, found := k.Find(group, kind) + if !found { + panic(fmt.Sprintf("KubeSource.MustFind: unable to find %s/%s", group, kind)) + } + return r +} + +// DirectTransform configuration +type DirectTransform struct { + mapping map[collection.Name]collection.Name +} + +// Mapping from source to destination +func (d *DirectTransform) Mapping() map[collection.Name]collection.Name { + m := make(map[collection.Name]collection.Name) + for k, v := range d.mapping { + m[k] = v + } + + return m +} + +// ParseAndBuild parses the given metadata file and returns the strongly typed schema. +func ParseAndBuild(yamlText string) (*Metadata, error) { + mast, err := ast.Parse(yamlText) + if err != nil { + return nil, err + } + + return Build(mast) +} + +// Build strongly-typed Metadata from parsed AST. +func Build(astm *ast.Metadata) (*Metadata, error) { + b := collection.NewSpecsBuilder() + for _, c := range astm.Collections { + s, err := collection.NewSpec(c.Name, c.ProtoPackage, c.Proto) + if err != nil { + return nil, err + } + + if err = b.Add(s); err != nil { + return nil, err + } + } + collections := b.Build() + + snapshots := make(map[string]*Snapshot) + for _, s := range astm.Snapshots { + sn := &Snapshot{ + Name: s.Name, + Strategy: s.Strategy, + } + + for _, c := range s.Collections { + col, found := collections.Lookup(c) + if !found { + return nil, fmt.Errorf("collection not found: %v", c) + } + sn.Collections = append(sn.Collections, col.Name) + } + snapshots[sn.Name] = sn + } + + var sources []Source + for _, s := range astm.Sources { + switch v := s.(type) { + case *ast.KubeSource: + var resources []*KubeResource + for i, r := range v.Resources { + if r == nil { + return nil, fmt.Errorf("invalid KubeResource entry at position: %d", i) + } + col, ok := collections.Lookup(r.Collection) + if !ok { + return nil, fmt.Errorf("collection not found: %v", r.Collection) + } + res := &KubeResource{ + Collection: col, + Kind: r.Kind, + Plural: r.Plural, + Version: r.Version, + Group: r.Group, + Optional: r.Optional, + Disabled: r.Disabled, + } + + resources = append(resources, res) + } + src := &KubeSource{ + resources: resources, + } + sources = append(sources, src) + + default: + return nil, fmt.Errorf("unrecognized source type: %T", s) + } + } + + var transforms []Transform + for _, t := range astm.Transforms { + switch v := t.(type) { + case *ast.DirectTransform: + mapping := make(map[collection.Name]collection.Name) + for k, v := range v.Mapping { + from, ok := collections.Lookup(k) + if !ok { + return nil, fmt.Errorf("collection not found: %v", k) + } + to, ok := collections.Lookup(v) + if !ok { + return nil, fmt.Errorf("collection not found: %v", v) + } + mapping[from.Name] = to.Name + } + tr := &DirectTransform{ + mapping: mapping, + } + transforms = append(transforms, tr) + + default: + return nil, fmt.Errorf("unrecognized transform type: %T", t) + } + } + + return &Metadata{ + collections: collections, + snapshots: snapshots, + sources: sources, + transforms: transforms, + }, nil +} diff --git a/galley/pkg/config/schema/schema_test.go b/galley/pkg/config/schema/schema_test.go new file mode 100644 index 000000000000..0464766918fb --- /dev/null +++ b/galley/pkg/config/schema/schema_test.go @@ -0,0 +1,435 @@ +// 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 schema + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/schema/ast" +) + +func TestSchema_ParseAndBuild(t *testing.T) { + var cases = []struct { + Input string + Expected *Metadata + }{ + { + Input: ``, + Expected: &Metadata{ + collections: collection.NewSpecsBuilder().Build(), + snapshots: map[string]*Snapshot{}, + }, + }, + { + Input: ` +collections: + - name: "k8s/networking.istio.io/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking.istio.io/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" + +snapshots: + - name: "default" + strategy: debounce + collections: + - "istio/networking.istio.io/v1alpha3/virtualservices" + + +sources: + - type: kubernetes + resources: + - collection: "k8s/networking.istio.io/v1alpha3/virtualservices" + kind: "VirtualService" + group: "networking.istio.io" + version: "v1alpha3" + +transforms: + - type: direct + mapping: + "k8s/networking.istio.io/v1alpha3/virtualservices": "istio/networking.istio.io/v1alpha3/virtualservices" +`, + Expected: &Metadata{ + collections: func() collection.Specs { + b := collection.NewSpecsBuilder() + b.MustAdd( + collection.MustNewSpec( + "k8s/networking.istio.io/v1alpha3/virtualservices", + "istio.io/api/networking/v1alpha3", + "istio.networking.v1alpha3.VirtualService"), + ) + b.MustAdd( + collection.MustNewSpec( + "istio/networking.istio.io/v1alpha3/virtualservices", + "istio.io/api/networking/v1alpha3", + "istio.networking.v1alpha3.VirtualService"), + ) + return b.Build() + }(), + snapshots: map[string]*Snapshot{ + "default": { + Name: "default", + Strategy: "debounce", + Collections: []collection.Name{ + collection.NewName("istio/networking.istio.io/v1alpha3/virtualservices"), + }, + }, + }, + sources: []Source{ + &KubeSource{ + resources: []*KubeResource{ + { + Collection: collection.MustNewSpec( + "k8s/networking.istio.io/v1alpha3/virtualservices", + "istio.io/api/networking/v1alpha3", + "istio.networking.v1alpha3.VirtualService"), + Version: "v1alpha3", + Kind: "VirtualService", + Group: "networking.istio.io", + }, + }, + }, + }, + transforms: []Transform{ + &DirectTransform{ + mapping: map[collection.Name]collection.Name{ + collection.NewName("k8s/networking.istio.io/v1alpha3/virtualservices"): collection.NewName("istio/networking.istio.io/v1alpha3/virtualservices"), + }, + }, + }, + }, + }, + } + + for _, c := range cases { + t.Run("", func(t *testing.T) { + g := NewGomegaWithT(t) + + actual, err := ParseAndBuild(c.Input) + g.Expect(err).To(BeNil()) + g.Expect(actual).To(Equal(c.Expected)) + }) + } +} + +func TestSchema_ParseAndBuild_Error(t *testing.T) { + var cases = []string{ + ` + $$$ +`, + + ` +collections: + - name: "$$$" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" +`, + ` +collections: + - name: "k8s/networking.istio.io/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" + - name: "k8s/networking.istio.io/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" +`, + + ` +collections: + - name: "k8s/networking.istio.io/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" +snapshots: +- name: "default" + strategy: debounce + collections: + - "istio/networking.istio.io/v1alpha3/virtualservices" +`, + ` +collections: +sources: + - type: kubernetes + resources: + - collection: "k8s/networking.istio.io/v1alpha3/virtualservices" + kind: "VirtualService" + group: "networking.istio.io" + version: "v1alpha3" +`, + ` +collections: + - name: "k8s/networking.istio.io/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" +transforms: + - type: direct + mapping: + "k8s/networking.istio.io/v1alpha3/virtualservices": "istio/networking.istio.io/v1alpha3/virtualservices" +`, + ` +collections: + - name: "istio/networking.istio.io/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" +transforms: + - type: direct + mapping: + "k8s/networking.istio.io/v1alpha3/virtualservices": "istio/networking.istio.io/v1alpha3/virtualservices" +`, + } + + for _, c := range cases { + t.Run("", func(t *testing.T) { + g := NewGomegaWithT(t) + + _, err := ParseAndBuild(c) + g.Expect(err).NotTo(BeNil()) + }) + } +} + +var input = ` +collections: + - name: "k8s/networking.istio.io/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" + + - name: "istio/networking.istio.io/v1alpha3/virtualservices" + proto: "istio.networking.v1alpha3.VirtualService" + protoPackage: "istio.io/api/networking/v1alpha3" + +snapshots: + - name: "default" + strategy: debounce + collections: + - "istio/networking.istio.io/v1alpha3/virtualservices" + + +sources: + - type: kubernetes + resources: + - collection: "k8s/networking.istio.io/v1alpha3/virtualservices" + kind: "VirtualService" + group: "networking.istio.io" + version: "v1alpha3" + +transforms: + - type: direct + mapping: + "k8s/networking.istio.io/v1alpha3/virtualservices": "istio/networking.istio.io/v1alpha3/virtualservices" +` + +func TestSchemaBasic(t *testing.T) { + g := NewGomegaWithT(t) + + s, err := ParseAndBuild(input) + g.Expect(err).To(BeNil()) + + b := collection.NewSpecsBuilder() + b.MustAdd(collection.MustNewSpec("k8s/networking.istio.io/v1alpha3/virtualservices", + "istio.io/api/networking/v1alpha3", + "istio.networking.v1alpha3.VirtualService")) + b.MustAdd(collection.MustNewSpec("istio/networking.istio.io/v1alpha3/virtualservices", + "istio.io/api/networking/v1alpha3", + "istio.networking.v1alpha3.VirtualService")) + g.Expect(s.Collections()).To(Equal(b.Build())) + + g.Expect(s.Transforms()).To(HaveLen(1)) + g.Expect(s.Transforms()[0]).To(Equal( + &DirectTransform{ + mapping: map[collection.Name]collection.Name{ + collection.NewName("k8s/networking.istio.io/v1alpha3/virtualservices"): collection.NewName("istio/networking.istio.io/v1alpha3/virtualservices"), + }, + })) + g.Expect(s.DirectTransform()).To(Equal( + &DirectTransform{ + mapping: map[collection.Name]collection.Name{ + collection.NewName("k8s/networking.istio.io/v1alpha3/virtualservices"): collection.NewName("istio/networking.istio.io/v1alpha3/virtualservices"), + }, + })) + + g.Expect(s.DirectTransform().Mapping()).To(Equal( + map[collection.Name]collection.Name{ + collection.NewName("k8s/networking.istio.io/v1alpha3/virtualservices"): collection.NewName("istio/networking.istio.io/v1alpha3/virtualservices"), + })) + + g.Expect(s.Sources()).To(HaveLen(1)) + g.Expect(s.Sources()[0]).To(Equal( + &KubeSource{ + resources: []*KubeResource{ + { + Collection: collection.MustNewSpec("k8s/networking.istio.io/v1alpha3/virtualservices", + "istio.io/api/networking/v1alpha3", + "istio.networking.v1alpha3.VirtualService"), + Group: "networking.istio.io", + Version: "v1alpha3", + Kind: "VirtualService", + }, + }, + })) + + g.Expect(s.Snapshots()).To(HaveLen(1)) + g.Expect(s.Snapshots()[0]).To(Equal( + &Snapshot{ + Name: "default", + Strategy: "debounce", + Collections: []collection.Name{collection.NewName("istio/networking.istio.io/v1alpha3/virtualservices")}, + })) + + g.Expect(s.KubeSource()).To(Equal(&KubeSource{ + resources: []*KubeResource{ + { + Collection: collection.MustNewSpec("k8s/networking.istio.io/v1alpha3/virtualservices", + "istio.io/api/networking/v1alpha3", + "istio.networking.v1alpha3.VirtualService"), + Group: "networking.istio.io", + Version: "v1alpha3", + Kind: "VirtualService", + }, + }, + })) + + g.Expect(s.KubeSource().Resources()).To(Equal(KubeResources{ + { + Collection: collection.MustNewSpec("k8s/networking.istio.io/v1alpha3/virtualservices", + "istio.io/api/networking/v1alpha3", + "istio.networking.v1alpha3.VirtualService"), + Group: "networking.istio.io", + Version: "v1alpha3", + Kind: "VirtualService", + }, + })) + + g.Expect(s.KubeSource().Resources().Collections()).To(Equal([]collection.Name{ + collection.NewName("k8s/networking.istio.io/v1alpha3/virtualservices"), + })) + + g.Expect(s.KubeSource().Resources()[0].CanonicalResourceName()).To(Equal("networking.istio.io/v1alpha3/VirtualService")) +} + +func TestSchema_Find(t *testing.T) { + g := NewGomegaWithT(t) + + s, err := ParseAndBuild(input) + g.Expect(err).To(BeNil()) + + k, b := s.KubeSource().Resources().Find("networking.istio.io", "VirtualService") + g.Expect(b).To(BeTrue()) + g.Expect(k).To(Equal(KubeResource{ + Collection: collection.MustNewSpec("k8s/networking.istio.io/v1alpha3/virtualservices", + "istio.io/api/networking/v1alpha3", + "istio.networking.v1alpha3.VirtualService"), + Group: "networking.istio.io", + Version: "v1alpha3", + Kind: "VirtualService", + })) + + _, b = s.KubeSource().Resources().Find("foo", "bar") + g.Expect(b).To(BeFalse()) +} + +func TestSchema_MustFind(t *testing.T) { + g := NewGomegaWithT(t) + + defer func() { + r := recover() + g.Expect(r).To(BeNil()) + }() + + s, err := ParseAndBuild(input) + g.Expect(err).To(BeNil()) + + k := s.KubeSource().Resources().MustFind("networking.istio.io", "VirtualService") + g.Expect(k).To(Equal(KubeResource{ + Collection: collection.MustNewSpec("k8s/networking.istio.io/v1alpha3/virtualservices", + "istio.io/api/networking/v1alpha3", + "istio.networking.v1alpha3.VirtualService"), + Group: "networking.istio.io", + Version: "v1alpha3", + Kind: "VirtualService", + })) +} + +func TestSchema_MustFind_Panic(t *testing.T) { + g := NewGomegaWithT(t) + + defer func() { + r := recover() + g.Expect(r).NotTo(BeNil()) + }() + + s, err := ParseAndBuild(input) + g.Expect(err).To(BeNil()) + + _ = s.KubeSource().Resources().MustFind("foo.istio.io", "bar") +} + +func TestSchema_KubeResource_Panic(t *testing.T) { + g := NewGomegaWithT(t) + + defer func() { + r := recover() + g.Expect(r).NotTo(BeNil()) + }() + + s, err := ParseAndBuild(``) + g.Expect(err).To(BeNil()) + + _ = s.KubeSource() +} + +func TestSchema_DirectTransform_Panic(t *testing.T) { + g := NewGomegaWithT(t) + + defer func() { + r := recover() + g.Expect(r).NotTo(BeNil()) + }() + + s, err := ParseAndBuild(``) + g.Expect(err).To(BeNil()) + + _ = s.DirectTransform() +} + +func TestBuild_UnknownSource(t *testing.T) { + g := NewGomegaWithT(t) + + a := &ast.Metadata{ + Sources: []ast.Source{ + &struct{}{}, + }, + } + + _, err := Build(a) + g.Expect(err).NotTo(BeNil()) +} + +func TestBuild_UnknownTransform(t *testing.T) { + g := NewGomegaWithT(t) + + a := &ast.Metadata{ + Transforms: []ast.Transform{ + &struct{}{}, + }, + } + + _, err := Build(a) + g.Expect(err).NotTo(BeNil()) +} diff --git a/galley/pkg/config/scope/scopes.go b/galley/pkg/config/scope/scopes.go new file mode 100644 index 000000000000..d5cc03fc74b2 --- /dev/null +++ b/galley/pkg/config/scope/scopes.go @@ -0,0 +1,25 @@ +// 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 scope + +import "istio.io/pkg/log" + +var ( + // Processing is a logging scope used by configuration processing pipeline. + Processing = log.RegisterScope("processing", "Scope for configuration processing runtime", 0) + + // Source is a logging scope for config event sources. + Source = log.RegisterScope("source", "Scope for configuration event sources", 0) +) diff --git a/galley/pkg/config/source/inmemory/collection.go b/galley/pkg/config/source/inmemory/collection.go new file mode 100644 index 000000000000..753bc9d53f8d --- /dev/null +++ b/galley/pkg/config/source/inmemory/collection.go @@ -0,0 +1,169 @@ +// 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 inmemory + +import ( + "sort" + "strings" + "sync" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/scope" +) + +// Collection is an in-memory collection that implements event.Source +type Collection struct { + mu sync.RWMutex // TODO: We should be able to get rid of this mutex. + collection collection.Name + handler event.Handler + resources map[resource.Name]*resource.Entry + synced bool +} + +var _ event.Source = &Collection{} + +// NewCollection returns a new in-memory collection. +func NewCollection(c collection.Name) *Collection { + scope.Source.Debuga(" Creating in-memory collection: ", c) + + return &Collection{ + collection: c, + resources: make(map[resource.Name]*resource.Entry), + } +} + +// Start dispatching events for the collection. +func (c *Collection) Start() { + c.mu.Lock() + defer c.mu.Unlock() + c.synced = true + + for _, e := range c.resources { + c.dispatchFor(e, event.Added) + } + + c.dispatchEvent(event.FullSyncFor(c.collection)) +} + +// Stop dispatching events and reset internal state. +func (c *Collection) Stop() { + c.mu.Lock() + defer c.mu.Unlock() + + c.synced = false +} + +// Dispatch an event handler to receive resource events. +func (c *Collection) Dispatch(handler event.Handler) { + if scope.Source.DebugEnabled() { + scope.Source.Debugf("Collection.Dispatch: (collection: %-50v, handler: %T)", c.collection, handler) + } + + c.handler = event.CombineHandlers(c.handler, handler) +} + +// Set the entry in the collection +func (c *Collection) Set(entry *resource.Entry) { + c.mu.Lock() + defer c.mu.Unlock() + + kind := event.Added + _, found := c.resources[entry.Metadata.Name] + if found { + kind = event.Updated + } + + c.resources[entry.Metadata.Name] = entry + + if c.synced { + c.dispatchFor(entry, kind) + } +} + +// Clear the contents of this collection. +func (c *Collection) Clear() { + c.mu.Lock() + defer c.mu.Unlock() + + if c.synced { + for _, entry := range c.resources { + e := event.Event{ + Kind: event.Deleted, + Source: c.collection, + Entry: entry, + } + + c.dispatchEvent(e) + } + } + + c.resources = make(map[resource.Name]*resource.Entry) +} + +func (c *Collection) dispatchEvent(e event.Event) { + if scope.Source.DebugEnabled() { + scope.Source.Debugf(">>> Collection.dispatchEvent: (col: %-50s): %v", c.collection, e) + } + if c.handler != nil { + c.handler.Handle(e) + } +} + +func (c *Collection) dispatchFor(entry *resource.Entry, kind event.Kind) { + e := event.Event{ + Source: c.collection, + Entry: entry, + Kind: kind, + } + c.dispatchEvent(e) +} + +// Remove the entry from the collection +func (c *Collection) Remove(n resource.Name) { + c.mu.Lock() + defer c.mu.Unlock() + + entry, found := c.resources[n] + if found { + e := event.Event{ + Kind: event.Deleted, + Source: c.collection, + Entry: entry, + } + + delete(c.resources, n) + c.dispatchEvent(e) + } +} + +// AllSorted returns all entries in this collection, in sort order. +// Warning: This is not performant! +func (c *Collection) AllSorted() []*resource.Entry { + c.mu.Lock() + defer c.mu.Unlock() + + var result []*resource.Entry + for _, e := range c.resources { + result = append(result, e) + } + + sort.Slice(result, func(i, j int) bool { + return strings.Compare(result[i].Metadata.Name.String(), result[j].Metadata.Name.String()) < 0 + }) + + return result +} diff --git a/galley/pkg/config/source/inmemory/collection_test.go b/galley/pkg/config/source/inmemory/collection_test.go new file mode 100644 index 000000000000..476f2b1a5798 --- /dev/null +++ b/galley/pkg/config/source/inmemory/collection_test.go @@ -0,0 +1,207 @@ +// 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 inmemory + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/pkg/log" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestCollection_Start_Empty(t *testing.T) { + g := NewGomegaWithT(t) + + col := NewCollection(data.Collection1) + acc := &fixtures.Accumulator{} + col.Dispatch(acc) + + col.Start() + + expected := []event.Event{event.FullSyncFor(data.Collection1)} + actual := acc.Events() + g.Expect(actual).To(Equal(expected)) +} + +func TestCollection_Start_Element(t *testing.T) { + g := NewGomegaWithT(t) + + old := scope.Source.GetOutputLevel() + defer func() { + scope.Source.SetOutputLevel(old) + }() + scope.Source.SetOutputLevel(log.DebugLevel) + + col := NewCollection(data.Collection1) + acc := &fixtures.Accumulator{} + col.Dispatch(acc) + + col.Set(data.Event1Col1AddItem1.Entry) + col.Start() + + expected := []event.Event{data.Event1Col1AddItem1, event.FullSyncFor(data.Collection1)} + actual := acc.Events() + g.Expect(actual).To(Equal(expected)) +} + +func TestCollection_Update(t *testing.T) { + g := NewGomegaWithT(t) + + col := NewCollection(data.Collection1) + acc := &fixtures.Accumulator{} + col.Dispatch(acc) + + col.Set(data.Event1Col1AddItem1.Entry) + col.Start() + + col.Set(data.Event1Col1UpdateItem1.Entry) + + expected := []event.Event{ + data.Event1Col1AddItem1, + event.FullSyncFor(data.Collection1), + data.Event1Col1UpdateItem1} + + actual := acc.Events() + g.Expect(actual).To(Equal(expected)) +} + +func TestCollection_Delete(t *testing.T) { + g := NewGomegaWithT(t) + + col := NewCollection(data.Collection1) + acc := &fixtures.Accumulator{} + col.Dispatch(acc) + + col.Set(data.Event1Col1AddItem1.Entry) + col.Start() + + col.Remove(data.Event1Col1AddItem1.Entry.Metadata.Name) + + expected := []event.Event{ + data.Event1Col1AddItem1, + event.FullSyncFor(data.Collection1), + data.Event1Col1DeleteItem1} + + actual := acc.Events() + g.Expect(actual).To(Equal(expected)) +} + +func TestCollection_Delete_NoItem(t *testing.T) { + g := NewGomegaWithT(t) + + col := NewCollection(data.Collection1) + acc := &fixtures.Accumulator{} + col.Dispatch(acc) + + col.Set(data.EntryN1I1V1) + col.Start() + + col.Remove(data.EntryN2I2V2.Metadata.Name) + + expected := []event.Event{ + data.Event1Col1AddItem1, + event.FullSyncFor(data.Collection1)} + + actual := acc.Events() + g.Expect(actual).To(Equal(expected)) +} + +func TestCollection_Clear_BeforeStart(t *testing.T) { + g := NewGomegaWithT(t) + + col := NewCollection(data.Collection1) + acc := &fixtures.Accumulator{} + col.Dispatch(acc) + + col.Set(data.EntryN1I1V1) + col.Set(data.EntryN2I2V2) + col.Clear() + + col.Start() + + expected := []event.Event{event.FullSyncFor(data.Collection1)} + actual := acc.Events() + g.Expect(actual).To(Equal(expected)) +} + +func TestCollection_Clear_AfterStart(t *testing.T) { + g := NewGomegaWithT(t) + + col := NewCollection(data.Collection1) + acc := &fixtures.Accumulator{} + col.Dispatch(acc) + + col.Set(data.EntryN1I1V1) + col.Set(data.EntryN2I2V2) + col.Start() + col.Clear() + + expected := []interface{}{ + data.Event1Col1AddItem1, + data.Event2Col1AddItem2, + event.FullSyncFor(data.Collection1), + data.Event1Col1DeleteItem1, + data.Event1Col1DeleteItem2, + } + + actual := acc.Events() + g.Expect(actual).To(ConsistOf(expected...)) +} + +func TestCollection_StopStart(t *testing.T) { + g := NewGomegaWithT(t) + + col := NewCollection(data.Collection1) + acc := &fixtures.Accumulator{} + col.Dispatch(acc) + + col.Set(data.Event1Col1AddItem1.Entry) + col.Start() + + expected := []event.Event{ + data.Event1Col1AddItem1, + event.FullSyncFor(data.Collection1)} + + g.Eventually(acc.Events).Should(Equal(expected)) + + col.Stop() + acc.Clear() + col.Start() + + g.Eventually(acc.Events).Should(Equal(expected)) +} + +func TestCollection_AllSorted(t *testing.T) { + g := NewGomegaWithT(t) + + col := NewCollection(data.Collection1) + + col.Set(data.EntryN1I1V1) + col.Set(data.EntryN2I2V2) + + expected := []*resource.Entry{ + data.EntryN1I1V1, + data.EntryN2I2V2, + } + + g.Expect(col.AllSorted()).To(Equal(expected)) +} diff --git a/galley/pkg/config/source/inmemory/source.go b/galley/pkg/config/source/inmemory/source.go new file mode 100644 index 000000000000..fdd98913b494 --- /dev/null +++ b/galley/pkg/config/source/inmemory/source.go @@ -0,0 +1,116 @@ +// 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 inmemory + +import ( + "fmt" + "sync" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/scope" +) + +var inMemoryNameDiscriminator int64 + +// Source is an in-memory processor.Source implementation. +type Source struct { + mu sync.Mutex + started bool + + collections map[collection.Name]*Collection + name string +} + +var _ event.Source = &Source{} + +// New returns a new in-memory source, based on given collections. +func New(collections collection.Names) *Source { + name := fmt.Sprintf("inmemory-%d", inMemoryNameDiscriminator) + inMemoryNameDiscriminator++ + + scope.Source.Debugf("Creating new in-memory source (collections: %d)", len(collections)) + + s := &Source{ + collections: make(map[collection.Name]*Collection), + name: name, + } + + for _, c := range collections { + s.collections[c] = NewCollection(c) + } + + return s +} + +// Dispatch implements event.Source +func (s *Source) Dispatch(h event.Handler) { + s.mu.Lock() + defer s.mu.Unlock() + + for _, c := range s.collections { + c.Dispatch(h) + } +} + +// Start implements processor.Source +func (s *Source) Start() { + s.mu.Lock() + defer s.mu.Unlock() + + if s.started { + return + } + + for _, c := range s.collections { + c.Start() + } + + s.started = true +} + +// Stop implements processor.Source +func (s *Source) Stop() { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.started { + return + } + + s.started = false + + for _, c := range s.collections { + c.Stop() + } +} + +// Clear contents of this source +func (s *Source) Clear() { + s.mu.Lock() + defer s.mu.Unlock() + + for _, c := range s.collections { + c.Clear() + } +} + +// Get returns the named collection. +func (s *Source) Get(collection collection.Name) *Collection { + s.mu.Lock() + defer s.mu.Unlock() + + return s.collections[collection] +} diff --git a/galley/pkg/config/source/inmemory/source_test.go b/galley/pkg/config/source/inmemory/source_test.go new file mode 100644 index 000000000000..13e49c51454c --- /dev/null +++ b/galley/pkg/config/source/inmemory/source_test.go @@ -0,0 +1,273 @@ +// 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 inmemory + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/fixtures" + + "github.com/gogo/protobuf/types" +) + +func TestInMemory_Register_Empty(t *testing.T) { + g := NewGomegaWithT(t) + + i := New(data.CollectionNames[:1]) + h := &fixtures.Accumulator{} + i.Dispatch(h) + i.Start() + defer i.Stop() + + expected := []event.Event{ + { + Kind: event.FullSync, + Source: data.Collection1, + }, + } + + g.Expect(h.Events()).To(Equal(expected)) +} + +func TestInMemory_Set_BeforeSync(t *testing.T) { + g := NewGomegaWithT(t) + + r := &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns1", "l1"), + Version: "v1", + }, + Item: &types.Empty{}, + } + + i := New(data.CollectionNames[:1]) + i.Get(data.Collection1).Set(r) + + h := &fixtures.Accumulator{} + i.Dispatch(h) + i.Start() + defer i.Stop() + + expected := []event.Event{ + { + Kind: event.Added, + Source: data.Collection1, + Entry: r, + }, + { + Kind: event.FullSync, + Source: data.Collection1, + }, + } + + g.Expect(h.Events()).To(Equal(expected)) +} + +func TestInMemory_Set_Add(t *testing.T) { + g := NewGomegaWithT(t) + + r := &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns1", "l1"), + Version: "v1", + }, + Item: &types.Empty{}, + } + + i := New(data.CollectionNames[:1]) + + h := &fixtures.Accumulator{} + i.Dispatch(h) + i.Start() + defer i.Stop() + + expected := []event.Event{ + { + Kind: event.FullSync, + Source: data.Collection1, + }, + } + + g.Expect(h.Events()).To(Equal(expected)) + + i.Get(data.Collection1).Set(r) + + expected = []event.Event{ + { + Kind: event.FullSync, + Source: data.Collection1, + }, + { + Kind: event.Added, + Source: data.Collection1, + Entry: r, + }, + } + + g.Expect(h.Events()).To(Equal(expected)) +} + +func TestInMemory_Set_Update(t *testing.T) { + g := NewGomegaWithT(t) + + r1 := &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns1", "l1"), + Version: "v1", + }, + Item: &types.Empty{}, + } + r2 := &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("ns1", "l1"), + Version: "v2", + }, + Item: &types.Empty{}, + } + + i := New(data.CollectionNames[:1]) + + h := &fixtures.Accumulator{} + i.Dispatch(h) + i.Start() + defer i.Stop() + + expected := []event.Event{ + { + Kind: event.FullSync, + Source: data.Collection1, + }, + } + + g.Expect(h.Events()).To(Equal(expected)) + + i.Get(data.Collection1).Set(r1) + i.Get(data.Collection1).Set(r2) + + expected = []event.Event{ + { + Kind: event.FullSync, + Source: data.Collection1, + }, + { + Kind: event.Added, + Source: data.Collection1, + Entry: r1, + }, + { + Kind: event.Updated, + Source: data.Collection1, + Entry: r2, + }, + } + + g.Expect(h.Events()).To(Equal(expected)) +} + +func TestInMemory_Clear_BeforeSync(t *testing.T) { + g := NewGomegaWithT(t) + + i := New(data.CollectionNames[:1]) + i.Get(data.Collection1).Set(data.EntryN1I1V1) + + h := &fixtures.Accumulator{} + i.Dispatch(h) + + i.Clear() + + i.Start() + defer i.Stop() + + expected := []event.Event{ + { + Kind: event.FullSync, + Source: data.Collection1, + }, + } + + g.Expect(h.Events()).To(Equal(expected)) +} + +func TestInMemory_Clear_AfterSync(t *testing.T) { + g := NewGomegaWithT(t) + + i := New(data.CollectionNames[:1]) + i.Get(data.Collection1).Set(data.EntryN1I1V1) + + h := &fixtures.Accumulator{} + i.Dispatch(h) + + i.Start() + defer i.Stop() + + i.Clear() + + expected := []event.Event{ + data.Event1Col1AddItem1, + { + Kind: event.FullSync, + Source: data.Collection1, + }, + data.Event1Col1DeleteItem1, + } + + g.Expect(h.Events()).To(Equal(expected)) +} + +func TestInMemory_DoubleStart(t *testing.T) { + g := NewGomegaWithT(t) + + i := New(data.CollectionNames[:1]) + h := &fixtures.Accumulator{} + i.Dispatch(h) + i.Start() + i.Start() + defer i.Stop() + + expected := []event.Event{ + { + Kind: event.FullSync, + Source: data.Collection1, + }, + } + + g.Expect(h.Events()).To(Equal(expected)) +} + +func TestInMemory_DoubleStop(t *testing.T) { + g := NewGomegaWithT(t) + + i := New(data.CollectionNames[:1]) + h := &fixtures.Accumulator{} + i.Dispatch(h) + i.Start() + + expected := []event.Event{ + { + Kind: event.FullSync, + Source: data.Collection1, + }, + } + + g.Expect(h.Events()).To(Equal(expected)) + + i.Stop() + i.Stop() +} diff --git a/galley/pkg/config/source/kube/apiserver/options.go b/galley/pkg/config/source/kube/apiserver/options.go new file mode 100644 index 000000000000..606bb9dfe3f2 --- /dev/null +++ b/galley/pkg/config/source/kube/apiserver/options.go @@ -0,0 +1,34 @@ +// 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 apiserver + +import ( + "time" + + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/source/kube" +) + +// Options for the kube controller +type Options struct { + // The Client interfaces to use for connecting to the API server. + Client kube.Interfaces + + ResyncPeriod time.Duration + + Resources schema.KubeResources + + // TODO: Add target namespaces here when we do namespace specific listeners. +} diff --git a/galley/pkg/config/source/kube/apiserver/source.go b/galley/pkg/config/source/kube/apiserver/source.go new file mode 100644 index 000000000000..2eece96c4471 --- /dev/null +++ b/galley/pkg/config/source/kube/apiserver/source.go @@ -0,0 +1,247 @@ +// 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 apiserver + +import ( + "fmt" + "sort" + "strings" + "sync" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/source/kube/rt" +) + +var ( + // crdKubeResource is metadata for listening to CRD resource on the API Server. + crdKubeResource = schema.KubeResource{ + Group: "apiextensions.k8s.io", + Version: "v1beta1", + Plural: "customresourcedefinitions", + Kind: "CustomResourceDefinition", + } +) + +// Source is an implementation of processing.KubeSource +type Source struct { // nolint:maligned + mu sync.Mutex + options Options + + // Keep the handlers that are registered by this Source. As we're recreating watchers, we need to seed them correctly + // with each incarnation. + handlers *event.Handlers + + // Indicates whether this source is started or not. + started bool + + // Set of resources that we're waiting CRD events for. As CRD events arrive, if they match to entries in expectedResources, + // the watchers for those resources will be created. + expectedResources map[string]schema.KubeResource + + // publishing indicates that the CRD discovery phase is over and actual data events are being published. Until + // publishing set, the incoming CRD events will cause new watchers to come online. Once the publishing is set, + // any new CRD event will cause an event.RESET to be published. + publishing bool + + provider *rt.Provider + + // crdWatcher is a specialized watcher just for listening to CRDs. + crdWatcher *watcher + + // watchers for each collection that were created as part of CRD discovery. + watchers map[collection.Name]*watcher +} + +var _ event.Source = &Source{} + +// New returns a new kube.Source. +func New(o Options) *Source { + s := &Source{ + options: o, + handlers: &event.Handlers{}, + } + + return s +} + +// Dispatch implements processor.Source +func (s *Source) Dispatch(h event.Handler) { + s.mu.Lock() + defer s.mu.Unlock() + s.handlers.Add(h) +} + +// Start implements processor.Source +func (s *Source) Start() { + s.mu.Lock() + defer s.mu.Unlock() + + if s.started { + scope.Source.Warn("Source.Start: already started") + return + } + s.started = true + + // Create a set of pending resources. These will be matched up with incoming CRD events for creating watchers for + // each resource that we expect. + s.expectedResources = make(map[string]schema.KubeResource) + for _, r := range s.options.Resources { + // If we received the metadata with the resource marked as disabled, simply ignore it. + if r.Disabled { + continue + } + + // Use the disabled bit to track whether we received an event for this resource. + r.Disabled = true + s.expectedResources[asKey(r.Group, r.Kind)] = r + } + + // Start the CRD listener. When the listener is fully-synced, the listening of actual resources will start. + scope.Source.Infof("Beginning CRD Discovery, to figure out resources that are available...") + s.provider = rt.NewProvider(s.options.Client, s.options.ResyncPeriod) + a := s.provider.GetAdapter(crdKubeResource) + s.crdWatcher = newWatcher(crdKubeResource, a) + s.crdWatcher.dispatch(event.HandlerFromFn(s.onCrdEvent)) + s.crdWatcher.start() +} + +func (s *Source) onCrdEvent(e event.Event) { + scope.Source.Debuga("onCrdEvent: ", e) + + s.mu.Lock() + defer s.mu.Unlock() + if !s.started { + // Avoid any potential timings with .Stop() being called while an event being received. + return + } + + if s.publishing { + // Any event in publishing state causes a reset + scope.Source.Infof("Detected a CRD change while processing configuration events. Sending Reset event.") + s.handlers.Handle(event.Event{Kind: event.Reset}) + return + } + + switch e.Kind { + case event.Added: + crd := e.Entry.Item.(*v1beta1.CustomResourceDefinitionSpec) + g := crd.Group + k := crd.Names.Kind + key := asKey(g, k) + r, ok := s.expectedResources[key] + if ok { + scope.Source.Debugf("Marking resource as available: %v", r.CanonicalResourceName()) + r.Disabled = false + s.expectedResources[key] = r + } + + case event.FullSync: + scope.Source.Infof("CRD Discovery complete, starting listening to resources...") + s.startWatchers() + s.publishing = true + + case event.Updated, event.Deleted, event.Reset: + // The code is currently not equipped to deal with this. Simply publish a reset event to get everything restarted later. + s.handlers.Handle(event.Event{Kind: event.Reset}) + + default: + panic(fmt.Errorf("onCrdEvent: unrecognized event: %v", e)) + } +} + +func (s *Source) startWatchers() { + // must be called under lock + + // sort resources by name for consistent logging + resources := make([]schema.KubeResource, 0, len(s.expectedResources)) + for _, r := range s.expectedResources { + resources = append(resources, r) + } + + sort.Slice(resources, func(i, j int) bool { + return strings.Compare(resources[i].CanonicalResourceName(), resources[j].CanonicalResourceName()) < 0 + }) + + scope.Source.Info("Creating watchers for Kubernetes CRDs") + s.watchers = make(map[collection.Name]*watcher) + for i, r := range resources { + a := s.provider.GetAdapter(r) + + scope.Source.Infof("[%d]", i) + scope.Source.Infof(" Source: %s", r.CanonicalResourceName()) + scope.Source.Infof(" Name: %s", r.Collection) + scope.Source.Infof(" Built-in: %v", a.IsBuiltIn()) + if !a.IsBuiltIn() { + scope.Source.Infof(" Found: %v", !r.Disabled) + } + + if a.IsBuiltIn() || !r.Disabled { + col := newWatcher(r, a) + col.dispatch(s.handlers) + s.watchers[r.Collection.Name] = col + } + } + + for c, w := range s.watchers { + scope.Source.Debuga("Source.Start: starting watcher: ", c) + w.start() + } +} + +// Stop implements processor.Source +func (s *Source) Stop() { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.started { + scope.Source.Warn("Source.Stop: Already stopped") + return + } + + s.stop() +} + +func (s *Source) stop() { + // must be called under lock + + if s.watchers != nil { + for c, w := range s.watchers { + scope.Source.Debuga("Source.Stop: stopping watcher: ", c) + w.stop() + } + s.watchers = nil + } + + if s.crdWatcher != nil { + s.crdWatcher.stop() + s.crdWatcher = nil + } + + s.provider = nil + s.publishing = false + s.expectedResources = nil + + s.started = false + +} + +func asKey(group, kind string) string { + return group + "/" + kind +} diff --git a/galley/pkg/config/source/kube/apiserver/source_builtin_test.go b/galley/pkg/config/source/kube/apiserver/source_builtin_test.go new file mode 100644 index 000000000000..66d7bc1b6abe --- /dev/null +++ b/galley/pkg/config/source/kube/apiserver/source_builtin_test.go @@ -0,0 +1,418 @@ +// 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 apiserver_test + +import ( + "testing" + "time" + + "github.com/gogo/protobuf/proto" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "istio.io/pkg/log" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/testing/k8smeta" + "istio.io/istio/galley/pkg/testing/mock" +) + +const ( + name = "fakeResource" + namespace = "fakeNamespace" +) + +var ( + fakeCreateTime, _ = time.Parse(time.RFC3339, "2009-02-04T21:00:57-08:00") + fakeObjectMeta = metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + CreationTimestamp: metav1.Time{ + Time: fakeCreateTime, + }, + Labels: map[string]string{ + "lk1": "lv1", + }, + Annotations: map[string]string{ + "ak1": "av1", + }, + ResourceVersion: "rv1", + } +) + +func TestBasic(t *testing.T) { + g := NewGomegaWithT(t) + + // Set the log level to debug for codecov. + prevLevel := setDebugLogLevel() + defer restoreLogLevel(prevLevel) + + k := mock.NewKube() + client, err := k.KubeClient() + g.Expect(err).To(BeNil()) + + // Start the source. + s := newOrFail(t, k, k8smeta.MustGet().KubeSource().Resources()) + acc := start(s) + defer s.Stop() + + g.Eventually(acc.Events).Should(HaveLen(6)) + for i := 0; i < 6; i++ { + g.Expect(acc.Events()[i].Kind).Should(Equal(event.FullSync)) + } + + acc.Clear() + + node := &corev1.Node{ + ObjectMeta: fakeObjectMeta, + Spec: corev1.NodeSpec{ + PodCIDR: "10.40.0.0/24", + }, + } + node.Namespace = "" // nodes don't have namespaces. + + // Add the resource. + if node, err = client.CoreV1().Nodes().Create(node); err != nil { + t.Fatalf("failed creating node: %v", err) + } + + expected := event.AddFor(k8smeta.K8SCoreV1Nodes, toResource(node, &node.Spec)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) +} + +func TestNodes(t *testing.T) { + g := NewGomegaWithT(t) + + // Set the log level to debug for codecov. + prevLevel := setDebugLogLevel() + defer restoreLogLevel(prevLevel) + + k := mock.NewKube() + client, err := k.KubeClient() + g.Expect(err).To(BeNil()) + + // Start the source. + s := newOrFail(t, k, k8smeta.MustGet().KubeSource().Resources()) + acc := start(s) + defer s.Stop() + + g.Eventually(acc.Events).Should(HaveLen(6)) + for i := 0; i < 6; i++ { + g.Expect(acc.Events()[i].Kind).Should(Equal(event.FullSync)) + } + acc.Clear() + + node := &corev1.Node{ + ObjectMeta: fakeObjectMeta, + Spec: corev1.NodeSpec{ + PodCIDR: "10.40.0.0/24", + }, + } + node.Namespace = "" // nodes don't have namespaces. + + // Add the resource. + if node, err = client.CoreV1().Nodes().Create(node); err != nil { + t.Fatalf("failed creating node: %v", err) + } + + expected := event.AddFor(k8smeta.K8SCoreV1Nodes, toResource(node, &node.Spec)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) + + acc.Clear() + + // Update the resource. + node = node.DeepCopy() + node.Spec.PodCIDR = "10.20.0.0/32" + node.ResourceVersion = "rv2" + if _, err := client.CoreV1().Nodes().Update(node); err != nil { + t.Fatalf("failed updating node: %v", err) + } + + expected = event.UpdateFor(k8smeta.K8SCoreV1Nodes, toResource(node, &node.Spec)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) + + acc.Clear() + + if _, err = client.CoreV1().Nodes().Update(node); err != nil { + t.Fatalf("failed updating node: %v", err) + } + g.Consistently(acc.Events).Should(BeEmpty()) + + acc.Clear() + + // Delete the resource. + if err := client.CoreV1().Nodes().Delete(node.Name, nil); err != nil { + t.Fatalf("failed deleting node: %v", err) + } + expected = event.DeleteForResource(k8smeta.K8SCoreV1Nodes, toResource(node, &node.Spec)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) +} + +func TestPods(t *testing.T) { + g := NewGomegaWithT(t) + + // Set the log level to debug for codecov. + prevLevel := setDebugLogLevel() + defer restoreLogLevel(prevLevel) + + k := mock.NewKube() + client, err := k.KubeClient() + g.Expect(err).To(BeNil()) + + // Start the source. + s := newOrFail(t, k, k8smeta.MustGet().KubeSource().Resources()) + acc := start(s) + defer s.Stop() + + g.Eventually(acc.Events).Should(HaveLen(6)) + for i := 0; i < 6; i++ { + g.Expect(acc.Events()[i].Kind).Should(Equal(event.FullSync)) + } + acc.Clear() + + pod := &corev1.Pod{ + ObjectMeta: fakeObjectMeta, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "c1", + Image: "someImage", + ImagePullPolicy: corev1.PullIfNotPresent, + Ports: []corev1.ContainerPort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + HostPort: 80, + }, + }, + }, + }, + }, + } + + if pod, err = client.CoreV1().Pods(namespace).Create(pod); err != nil { + t.Fatalf("failed creating pod: %v", err) + } + expected := event.AddFor(k8smeta.K8SCoreV1Pods, toResource(pod, pod)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) + + acc.Clear() + + // Update the resource. + pod = pod.DeepCopy() + pod.Spec.Containers[0].Name = "c2" + pod.ResourceVersion = "rv2" + if _, err := client.CoreV1().Pods(namespace).Update(pod); err != nil { + t.Fatalf("failed updating pod: %v", err) + } + expected = event.UpdateFor(k8smeta.K8SCoreV1Pods, toResource(pod, pod)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) + + acc.Clear() + + // Update event with no changes, should yield no events. + if _, err = client.CoreV1().Pods(namespace).Update(pod); err != nil { + t.Fatalf("failed updating pod: %v", err) + } + g.Consistently(acc.Events).Should(BeEmpty()) + + acc.Clear() + + // Delete the resource. + if err = client.CoreV1().Pods(namespace).Delete(pod.Name, nil); err != nil { + t.Fatalf("failed deleting pod: %v", err) + } + expected = event.DeleteForResource(k8smeta.K8SCoreV1Pods, toResource(pod, pod)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) +} + +func TestServices(t *testing.T) { + g := NewGomegaWithT(t) + + // Set the log level to debug for codecov. + prevLevel := setDebugLogLevel() + defer restoreLogLevel(prevLevel) + + k := mock.NewKube() + client, err := k.KubeClient() + g.Expect(err).To(BeNil()) + + // Start the source. + s := newOrFail(t, k, k8smeta.MustGet().KubeSource().Resources()) + acc := start(s) + defer s.Stop() + + g.Eventually(acc.Events).Should(HaveLen(6)) + for i := 0; i < 6; i++ { + g.Expect(acc.Events()[i].Kind).Should(Equal(event.FullSync)) + } + acc.Clear() + + svc := &corev1.Service{ + ObjectMeta: fakeObjectMeta, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + Port: 80, + }, + }, + }, + } + + // Add the resource. + if svc, err = client.CoreV1().Services(namespace).Create(svc); err != nil { + t.Fatalf("failed creating service: %v", err) + } + expected := event.AddFor(k8smeta.K8SCoreV1Services, toResource(svc, &svc.Spec)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) + + acc.Clear() + + // Update the resource. + svc = svc.DeepCopy() + svc.Spec.Ports[0].Port = 8080 + svc.ResourceVersion = "rv2" + if _, err = client.CoreV1().Services(namespace).Update(svc); err != nil { + t.Fatalf("failed updating service: %v", err) + } + expected = event.UpdateFor(k8smeta.K8SCoreV1Services, toResource(svc, &svc.Spec)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) + + acc.Clear() + + // Update event with no changes, should yield no events. + if _, err = client.CoreV1().Services(namespace).Update(svc); err != nil { + t.Fatalf("failed updating service: %v", err) + } + g.Consistently(acc.Events).Should(BeEmpty()) + + acc.Clear() + + // Delete the resource. + if err := client.CoreV1().Services(namespace).Delete(svc.Name, nil); err != nil { + t.Fatalf("failed deleting service: %v", err) + } + expected = event.DeleteForResource(k8smeta.K8SCoreV1Services, toResource(svc, &svc.Spec)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) +} + +func TestEndpoints(t *testing.T) { + g := NewGomegaWithT(t) + + // Set the log level to debug for codecov. + prevLevel := setDebugLogLevel() + defer restoreLogLevel(prevLevel) + + k := mock.NewKube() + client, err := k.KubeClient() + g.Expect(err).To(BeNil()) + + // Start the source. + s := newOrFail(t, k, k8smeta.MustGet().KubeSource().Resources()) + acc := start(s) + defer s.Stop() + + g.Eventually(acc.Events).Should(HaveLen(6)) + for i := 0; i < 6; i++ { + g.Expect(acc.Events()[i].Kind).Should(Equal(event.FullSync)) + } + acc.Clear() + + eps := &corev1.Endpoints{ + ObjectMeta: fakeObjectMeta, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + Hostname: "fake.host.com", + IP: "10.40.0.0", + }, + }, + Ports: []corev1.EndpointPort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + Port: 80, + }, + }, + }, + }, + } + + // Add the resource. + if eps, err = client.CoreV1().Endpoints(namespace).Create(eps); err != nil { + t.Fatalf("failed creating endpoints: %v", err) + } + expected := event.AddFor(k8smeta.K8SCoreV1Endpoints, toResource(eps, eps)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) + + acc.Clear() + + // Update the resource. + eps = eps.DeepCopy() + eps.Subsets[0].Ports[0].Port = 8080 + eps.ResourceVersion = "rv2" + if _, err = client.CoreV1().Endpoints(namespace).Update(eps); err != nil { + t.Fatalf("failed updating endpoints: %v", err) + } + expected = event.UpdateFor(k8smeta.K8SCoreV1Endpoints, toResource(eps, eps)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) + + acc.Clear() + + // Update event with no changes, should yield no events. + // Changing only the resource version, should have not result in an update. + eps = eps.DeepCopy() + eps.ResourceVersion = "rv3" + if _, err = client.CoreV1().Endpoints(namespace).Update(eps); err != nil { + t.Fatalf("failed updating endpoints: %v", err) + } + g.Consistently(acc.Events).Should(BeEmpty()) + + // Delete the resource. + if err = client.CoreV1().Endpoints(namespace).Delete(eps.Name, nil); err != nil { + t.Fatalf("failed deleting endpoints: %v", err) + } + expected = event.DeleteForResource(k8smeta.K8SCoreV1Endpoints, toResource(eps, eps)) + g.Eventually(acc.Events).Should(ConsistOf(expected)) +} + +func toResource(objectMeta metav1.Object, item proto.Message) *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName(objectMeta.GetNamespace(), objectMeta.GetName()), + Version: resource.Version(objectMeta.GetResourceVersion()), + CreateTime: fakeCreateTime, + Labels: objectMeta.GetLabels(), + Annotations: objectMeta.GetAnnotations(), + }, + Item: item, + } +} + +func setDebugLogLevel() log.Level { + prev := scope.Source.GetOutputLevel() + scope.Source.SetOutputLevel(log.DebugLevel) + return prev +} + +func restoreLogLevel(level log.Level) { + scope.Source.SetOutputLevel(level) +} diff --git a/galley/pkg/config/source/kube/apiserver/source_dynamic_test.go b/galley/pkg/config/source/kube/apiserver/source_dynamic_test.go new file mode 100644 index 000000000000..9bd260b591f5 --- /dev/null +++ b/galley/pkg/config/source/kube/apiserver/source_dynamic_test.go @@ -0,0 +1,380 @@ +// 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 apiserver_test + +import ( + "errors" + "testing" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/source/kube" + "istio.io/istio/galley/pkg/config/source/kube/apiserver" + "istio.io/istio/galley/pkg/config/testing/basicmeta" + "istio.io/istio/galley/pkg/config/testing/fixtures" + "istio.io/istio/galley/pkg/testing/mock" + + "github.com/gogo/protobuf/types" + . "github.com/onsi/gomega" + extfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8sRuntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic/fake" + k8sTesting "k8s.io/client-go/testing" +) + +func TestNewSource(t *testing.T) { + k := &mock.Kube{} + for i := 0; i < 100; i++ { + _ = fakeClient(k) + } + + r := basicmeta.MustGet().KubeSource().Resources() + + _ = newOrFail(t, k, r) +} + +func TestStartTwice(t *testing.T) { + // Create the source + w, _, cl := createMocks() + defer w.Stop() + + r := basicmeta.MustGet().KubeSource().Resources() + s := newOrFail(t, cl, r) + + // Start it once. + _ = start(s) + defer s.Stop() + + // Start again should fail + s.Start() +} + +func TestStopTwiceShouldSucceed(t *testing.T) { + // Create the source + w, _, cl := createMocks() + defer w.Stop() + r := basicmeta.MustGet().KubeSource().Resources() + s := newOrFail(t, cl, r) + + // Start it once. + _ = start(s) + + s.Stop() + s.Stop() +} + +func TestEvents(t *testing.T) { + g := NewGomegaWithT(t) + + w, wcrd, cl := createMocks() + defer wcrd.Stop() + defer w.Stop() + + r := basicmeta.MustGet().KubeSource().Resources() + addCrdEvents(wcrd, r) + + // Create and start the source + s := newOrFail(t, cl, r) + acc := start(s) + defer s.Stop() + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection1), + )) + acc.Clear() + + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "testdata.istio.io/v1alpha1", + "kind": "Kind1", + "metadata": map[string]interface{}{ + "name": "i1", + "namespace": "ns", + "resourceVersion": "v1", + }, + "spec": map[string]interface{}{}, + }, + } + + obj = obj.DeepCopy() + w.Send(watch.Event{Type: watch.Added, Object: obj}) + + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(basicmeta.Collection1, toEntry(obj)), + )) + + acc.Clear() + + obj = obj.DeepCopy() + obj.SetResourceVersion("rv2") + + w.Send(watch.Event{Type: watch.Modified, Object: obj}) + + g.Eventually(acc.Events).Should(ConsistOf( + event.UpdateFor(basicmeta.Collection1, toEntry(obj)))) + + acc.Clear() + + // Make a copy so we can change it without affecting the original. + objCopy := obj.DeepCopy() + objCopy.SetResourceVersion("rv2") + + w.Send(watch.Event{Type: watch.Modified, Object: objCopy}) + g.Consistently(acc.Events).Should(BeEmpty()) + + w.Send(watch.Event{Type: watch.Deleted, Object: obj}) + + g.Eventually(acc.Events).Should(ConsistOf( + event.DeleteForResource(basicmeta.Collection1, toEntry(obj)))) +} + +func TestEvents_CRDEventAfterFullSync(t *testing.T) { + g := NewGomegaWithT(t) + + w, wcrd, cl := createMocks() + defer wcrd.Stop() + defer w.Stop() + + r := basicmeta.MustGet().KubeSource().Resources() + addCrdEvents(wcrd, r) + + // Create and start the source + s := newOrFail(t, cl, r) + acc := start(s) + defer s.Stop() + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection1), + )) + + acc.Clear() + c := toCrd(r[0]) + c.ResourceVersion = "v2" + wcrd.Send(watch.Event{ + Type: watch.Modified, + Object: c, + }) + + g.Eventually(acc.Events).Should(ContainElement( + event.Event{Kind: event.Reset}, + )) +} + +func TestEvents_NonAddEvent(t *testing.T) { + g := NewGomegaWithT(t) + + w, wcrd, cl := createMocks() + defer wcrd.Stop() + defer w.Stop() + + r := basicmeta.MustGet().KubeSource().Resources() + addCrdEvents(wcrd, r) + c := toCrd(r[0]) + c.ResourceVersion = "v2" + wcrd.Send(watch.Event{ + Type: watch.Modified, + Object: c, + }) + + // Create and start the source + s := newOrFail(t, cl, r) + acc := start(s) + defer s.Stop() + + g.Eventually(acc.Events).Should(ContainElement( + event.Event{Kind: event.Reset}, + )) +} + +func TestEvents_NoneForDisabled(t *testing.T) { + g := NewGomegaWithT(t) + + w, wcrd, cl := createMocks() + defer wcrd.Stop() + defer w.Stop() + + r := basicmeta.MustGet().KubeSource().Resources() + addCrdEvents(wcrd, r) + + // Create and start the source + s := newOrFail(t, cl, r) + acc := start(s) + defer s.Stop() + + g.Eventually(acc.Events).Should(BeEmpty()) +} + +func TestSource_WatcherFailsCreatingInformer(t *testing.T) { + g := NewGomegaWithT(t) + + k := mock.NewKube() + wcrd := mockCrdWatch(k.APIExtClientSet) + + r := basicmeta.MustGet().KubeSource().Resources() + addCrdEvents(wcrd, r) + + k.AddResponse(nil, errors.New("no cheese found")) + + // Create and start the source + s := newOrFail(t, k, r) + // Start/stop when informer is not created. It should not crash or cause errors. + acc := start(s) + + // we should get a full sync event, even if the watcher doesn't properly start. + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection1), + )) + + s.Stop() + + acc.Clear() + wcrd.Stop() + + wcrd = mockCrdWatch(k.APIExtClientSet) + addCrdEvents(wcrd, r) + + // Now start properly and get events + cl := fake.NewSimpleDynamicClient(k8sRuntime.NewScheme()) + k.AddResponse(cl, nil) + w := mockWatch(cl) + + s.Start() + + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "testdata.istio.io/v1alpha1", + "kind": "Kind1", + "metadata": map[string]interface{}{ + "name": "i1", + "namespace": "ns", + "resourceVersion": "v1", + }, + "spec": map[string]interface{}{}, + }, + } + obj = obj.DeepCopy() + + w.Send(watch.Event{Type: watch.Added, Object: obj}) + + defer s.Stop() + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection1), + event.AddFor(basicmeta.Collection1, toEntry(obj)), + )) +} + +func newOrFail(t *testing.T, ifaces kube.Interfaces, r schema.KubeResources) *apiserver.Source { + t.Helper() + o := apiserver.Options{ + Resources: r, + ResyncPeriod: 0, + Client: ifaces, + } + s := apiserver.New(o) + if s == nil { + t.Fatal("Expected non nil source") + } + return s +} + +func start(s *apiserver.Source) *fixtures.Accumulator { + acc := &fixtures.Accumulator{} + s.Dispatch(acc) + + s.Start() + return acc +} + +func createMocks() (*mock.Watch, *mock.Watch, *mock.Kube) { + k := mock.NewKube() + cl := fakeClient(k) + w := mockWatch(cl) + wcrd := mockCrdWatch(k.APIExtClientSet) + return w, wcrd, k +} + +func addCrdEvents(w *mock.Watch, res []schema.KubeResource) { + for _, r := range res { + w.Send(watch.Event{ + Object: toCrd(r), + Type: watch.Added, + }) + } +} + +func fakeClient(k *mock.Kube) *fake.FakeDynamicClient { + cl := fake.NewSimpleDynamicClient(k8sRuntime.NewScheme()) + k.AddResponse(cl, nil) + return cl +} + +func mockWatch(cl *fake.FakeDynamicClient) *mock.Watch { + w := mock.NewWatch() + cl.PrependWatchReactor("*", func(_ k8sTesting.Action) (handled bool, ret watch.Interface, err error) { + return true, w, nil + }) + return w +} + +func mockCrdWatch(cl *extfake.Clientset) *mock.Watch { + w := mock.NewWatch() + cl.PrependWatchReactor("*", func(_ k8sTesting.Action) (handled bool, ret watch.Interface, err error) { + return true, w, nil + }) + return w +} + +func toEntry(obj *unstructured.Unstructured) *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName(obj.GetNamespace(), obj.GetName()), + Labels: obj.GetLabels(), + Annotations: obj.GetAnnotations(), + Version: resource.Version(obj.GetResourceVersion()), + }, + Item: &types.Struct{ + Fields: make(map[string]*types.Value), + }, + } +} + +func toCrd(r schema.KubeResource) *v1beta1.CustomResourceDefinition { + return &v1beta1.CustomResourceDefinition{ + ObjectMeta: v1.ObjectMeta{ + Name: r.Plural + "." + r.Group, + ResourceVersion: "v1", + }, + + Spec: v1beta1.CustomResourceDefinitionSpec{ + Group: r.Group, + Names: v1beta1.CustomResourceDefinitionNames{ + Plural: r.Plural, + Kind: r.Kind, + }, + Versions: []v1beta1.CustomResourceDefinitionVersion{ + { + Name: r.Version, + }, + }, + Scope: v1beta1.NamespaceScoped, + }, + } +} diff --git a/galley/pkg/config/source/kube/apiserver/stats/stats.go b/galley/pkg/config/source/kube/apiserver/stats/stats.go new file mode 100644 index 000000000000..bd0d5cb6d7a2 --- /dev/null +++ b/galley/pkg/config/source/kube/apiserver/stats/stats.go @@ -0,0 +1,118 @@ +// 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 stats + +import ( + "context" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + + "istio.io/istio/galley/pkg/config/scope" +) + +const ( + apiVersion = "apiVersion" + group = "group" + kind = "kind" + errorStr = "error" +) + +var ( + // APIVersionTag holds the API version of the resource. + APIVersionTag tag.Key + // GroupTag holds the group of the resource. + GroupTag tag.Key + // KindTag holds the kind of the resource. + KindTag tag.Key + // ErrorTag holds the error message of a handleEvent failure. + ErrorTag tag.Key +) + +var ( + sourceEventError = stats.Int64( + "galley/source/kube/event_error_total", + "The number of times a kubernetes source encountered errored while handling an event", + stats.UnitDimensionless) + sourceEventSuccess = stats.Int64( + "galley/source/kube/event_success_total", + "The number of times a kubernetes source successfully handled an event", + stats.UnitDimensionless) + + sourceConversionSuccess = stats.Int64( + "galley/source/kube/dynamic/converter_success_total", + "The number of times a dynamic kubernetes source successfully converted a resource", + stats.UnitDimensionless) + sourceConversionFailure = stats.Int64( + "galley/source/kube/dynamic/converter_failure_total", + "The number of times a dynamnic kubernetes source failed converting a resources", + stats.UnitDimensionless) +) + +// RecordEventError records an error handling a kube event. +func RecordEventError(msg string) { + ctx, ctxErr := tag.New(context.Background(), tag.Insert(ErrorTag, msg)) + if ctxErr != nil { + scope.Source.Errorf("error creating context to record handleEvent error") + } else { + stats.Record(ctx, sourceEventError.M(1)) + } +} + +// RecordEventSuccess records successfully handling a kube event. +func RecordEventSuccess() { + stats.Record(context.Background(), sourceEventSuccess.M(1)) +} + +func newTagKey(label string) tag.Key { + if t, err := tag.NewKey(label); err != nil { + panic(err) + } else { + return t + } +} + +func newView(measure stats.Measure, keys []tag.Key, aggregation *view.Aggregation) *view.View { + return &view.View{ + Name: measure.Name(), + Description: measure.Description(), + Measure: measure, + TagKeys: keys, + Aggregation: aggregation, + } +} + +func init() { + APIVersionTag = newTagKey(apiVersion) + GroupTag = newTagKey(group) + KindTag = newTagKey(kind) + ErrorTag = newTagKey(errorStr) + + errorKey := []tag.Key{ErrorTag} + conversionKeys := []tag.Key{APIVersionTag, GroupTag, KindTag} + var noKeys []tag.Key + + err := view.Register( + newView(sourceEventError, errorKey, view.Count()), + newView(sourceEventSuccess, noKeys, view.Count()), + newView(sourceConversionSuccess, conversionKeys, view.Count()), + newView(sourceConversionFailure, conversionKeys, view.Count()), + ) + + if err != nil { + panic(err) + } +} diff --git a/galley/pkg/config/source/kube/apiserver/tombstone/recover.go b/galley/pkg/config/source/kube/apiserver/tombstone/recover.go new file mode 100644 index 000000000000..53aec886a9ca --- /dev/null +++ b/galley/pkg/config/source/kube/apiserver/tombstone/recover.go @@ -0,0 +1,51 @@ +// 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 tombstone + +import ( + "fmt" + "reflect" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" + + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/source/kube/apiserver/stats" +) + +// RecoverResource from a kubernetes tombstone (cache.DeletedFinalStateUnknown). Returns the resource or nil if +// recovery failed. +func RecoverResource(obj interface{}) metav1.Object { + var tombstone cache.DeletedFinalStateUnknown + var ok bool + if tombstone, ok = obj.(cache.DeletedFinalStateUnknown); !ok { + msg := fmt.Sprintf("error decoding object, invalid type: %v", reflect.TypeOf(obj)) + scope.Source.Error(msg) + stats.RecordEventError(msg) + return nil + } + + var objectMeta metav1.Object + if objectMeta, ok = tombstone.Obj.(metav1.Object); !ok { + msg := fmt.Sprintf("error decoding object tombstone, invalid type: %v", + reflect.TypeOf(tombstone.Obj)) + scope.Source.Error(msg) + stats.RecordEventError(msg) + return nil + } + + scope.Source.Infof("Recovered deleted object '%s' from tombstone", objectMeta.GetName()) + return objectMeta +} diff --git a/galley/pkg/config/source/kube/apiserver/tombstone/recover_test.go b/galley/pkg/config/source/kube/apiserver/tombstone/recover_test.go new file mode 100644 index 000000000000..20ef83f1c17d --- /dev/null +++ b/galley/pkg/config/source/kube/apiserver/tombstone/recover_test.go @@ -0,0 +1,54 @@ +// 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 tombstone_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/source/kube/apiserver/tombstone" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" +) + +func TestRecoverySuccessful(t *testing.T) { + g := NewGomegaWithT(t) + expected := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mynode", + }, + } + obj := tombstone.RecoverResource(cache.DeletedFinalStateUnknown{ + Obj: expected, + }) + g.Expect(obj).To(Equal(expected)) +} + +func TestUnkownTypeShouldFail(t *testing.T) { + g := NewGomegaWithT(t) + obj := tombstone.RecoverResource(&struct{}{}) + g.Expect(obj).To(BeNil()) +} + +func TestUnkownTombstoneObjectShouldFail(t *testing.T) { + g := NewGomegaWithT(t) + obj := tombstone.RecoverResource(cache.DeletedFinalStateUnknown{ + Obj: &struct{}{}, + }) + g.Expect(obj).To(BeNil()) +} diff --git a/galley/pkg/config/source/kube/apiserver/watcher.go b/galley/pkg/config/source/kube/apiserver/watcher.go new file mode 100644 index 000000000000..c506027a5253 --- /dev/null +++ b/galley/pkg/config/source/kube/apiserver/watcher.go @@ -0,0 +1,138 @@ +// 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 apiserver + +import ( + "sync" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/source/kube/apiserver/stats" + "istio.io/istio/galley/pkg/config/source/kube/apiserver/tombstone" + "istio.io/istio/galley/pkg/config/source/kube/rt" +) + +type watcher struct { + mu sync.Mutex + + adapter *rt.Adapter + resource schema.KubeResource + + handler event.Handler + + done chan struct{} +} + +func newWatcher(r schema.KubeResource, a *rt.Adapter) *watcher { + return &watcher{ + resource: r, + adapter: a, + handler: event.SentinelHandler(), + } +} + +func (w *watcher) start() { + w.mu.Lock() + defer w.mu.Unlock() + if w.done != nil { + panic("watcher.start: already started") + } + + scope.Source.Debugf("Starting watcher for %q (%q)", w.resource.Collection.Name, w.resource.CanonicalResourceName()) + + informer, err := w.adapter.NewInformer() + if err != nil { + scope.Source.Errorf("unable to start watcher for %q: %v", w.resource.CanonicalResourceName(), err) + // Send a FullSync event, even if the informer is not available. This will ensure that the processing backend + // will still work, in absence of CRDs. + w.handler.Handle(event.FullSyncFor(w.resource.Collection.Name)) + return + } + + informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { w.handleEvent(event.Added, obj) }, + UpdateFunc: func(old, new interface{}) { + if w.adapter.IsEqual(old, new) { + // Periodic resync will send update events for all known resources. + // Two different versions of the same resource will always have different RVs. + return + } + + w.handleEvent(event.Updated, new) + }, + DeleteFunc: func(obj interface{}) { w.handleEvent(event.Deleted, obj) }, + }) + + done := make(chan struct{}) + w.done = done + + // Send the FullSync event after the cache syncs. + go func() { + if cache.WaitForCacheSync(done, informer.HasSynced) { + w.handler.Handle(event.FullSyncFor(w.resource.Collection.Name)) + } + }() + + // Start CRD shared informer and wait for it to exit. + go informer.Run(done) +} + +func (w *watcher) stop() { + w.mu.Lock() + defer w.mu.Unlock() + if w.done != nil { + close(w.done) + w.done = nil + } +} + +func (w *watcher) dispatch(h event.Handler) { + w.handler = event.CombineHandlers(w.handler, h) +} + +func (w *watcher) handleEvent(c event.Kind, obj interface{}) { + object, ok := obj.(metav1.Object) + if !ok { + if obj = tombstone.RecoverResource(obj); object != nil { + // Tombstone recovery failed. + scope.Source.Warnf("Unable to extract object for event: %v", obj) + return + } + obj = object + } + + object = w.adapter.ExtractObject(obj) + res, err := w.adapter.ExtractResource(obj) + if err != nil { + scope.Source.Warnf("unable to extract resource: %v: %e", obj, err) + return + } + + r := rt.ToResourceEntry(object, res) + + e := event.Event{ + Kind: c, + Source: w.resource.Collection.Name, + Entry: r, + } + + w.handler.Handle(e) + + stats.RecordEventSuccess() +} diff --git a/galley/pkg/config/source/kube/check/check.go b/galley/pkg/config/source/kube/check/check.go new file mode 100644 index 000000000000..f994794f897a --- /dev/null +++ b/galley/pkg/config/source/kube/check/check.go @@ -0,0 +1,103 @@ +// 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 check + +import ( + "fmt" + "time" + + configSchema "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/source/kube" + + "github.com/hashicorp/go-multierror" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" +) + +var ( + pollInterval = time.Second + pollTimeout = time.Minute +) + +// ResourceTypesPresence verifies that all expected k8s resources types are +// present in the k8s apiserver. +func ResourceTypesPresence(k kube.Interfaces, resources configSchema.KubeResources) error { + cs, err := k.APIExtensionsClientset() + if err != nil { + return err + } + return resourceTypesPresence(cs, resources) +} + +func resourceTypesPresence(cs clientset.Interface, resources configSchema.KubeResources) error { + required := make(map[string]configSchema.KubeResource, len(resources)) + for _, spec := range resources { + if !spec.Optional { + required[spec.Plural] = spec + } + } + + err := wait.Poll(pollInterval, pollTimeout, func() (bool, error) { + var errs error + nextResource: + for _, spec := range resources { + if spec.Optional { + continue + } + + gv := schema.GroupVersion{Group: spec.Group, Version: spec.Version}.String() + list, err := cs.Discovery().ServerResourcesForGroupVersion(gv) + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("could not find %v: %v", gv, err)) + continue nextResource + } + found := false + for _, r := range list.APIResources { + if r.Name == spec.Plural { + delete(required, spec.Plural) + found = true + break + } + } + if !found { + scope.Source.Warnf("%s resource type not found", spec.CanonicalResourceName()) + } + } + if len(required) == 0 { + return true, nil + } + // entire search failed + if errs != nil { + return true, errs + } + // check again next poll + return false, nil + }) + + if err != nil { + var notFound []string + for _, spec := range required { + notFound = append(notFound, spec.Kind) + } + scope.Source.Errorf("Expected resources (CRDs) not found: %v", notFound) + scope.Source.Error("To stop Galley from waiting for these resources (CRDs), consider using the --excludedResourceKinds flag") + + return fmt.Errorf("could not find resource type(s) %v: %v", notFound, err) + } + + return nil +} diff --git a/galley/pkg/config/source/kube/check/check_test.go b/galley/pkg/config/source/kube/check/check_test.go new file mode 100644 index 000000000000..01207a9eb796 --- /dev/null +++ b/galley/pkg/config/source/kube/check/check_test.go @@ -0,0 +1,140 @@ +// 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 check + +import ( + "fmt" + "testing" + "time" + + "github.com/pkg/errors" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/testing/basicmeta" + "istio.io/istio/galley/pkg/config/testing/k8smeta" + + extfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestCheckCRDPresence(t *testing.T) { + prevInterval, prevTimeout := pollInterval, pollTimeout + pollInterval = time.Nanosecond + pollTimeout = time.Millisecond + defer func() { + pollInterval, pollTimeout = prevInterval, prevTimeout + }() + + k8sSpecs := k8smeta.MustGet().KubeSource().Resources() + testSpecs := basicmeta.MustGet2().KubeSource().Resources() + specs := append(k8sSpecs, testSpecs...) + + cases := []struct { + name string + missing map[string]bool + wantErr bool + }{ + { + name: "all present", + wantErr: false, + }, + { + name: "none ready", + missing: func() map[string]bool { + m := make(map[string]bool) + for _, spec := range specs { + m[spec.Plural] = true + } + return m + }(), + wantErr: true, + }, + { + name: "first missing", + missing: map[string]bool{"Kind1s": true}, + wantErr: true, + }, + { + name: "pod not ready", + missing: map[string]bool{"pods": true}, + wantErr: true, + }, + { + name: "optional not ready", + missing: map[string]bool{"Kind2s": true}, + wantErr: false, + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("[%v] %v", i, c.name), func(tt *testing.T) { + cs := extfake.NewSimpleClientset() + + byGroupVersion := map[string][]meta_v1.APIResource{} + for _, spec := range specs { + if c.missing[spec.Plural] { + continue + } + gv := meta_v1.GroupVersion{Group: spec.Group, Version: spec.Version}.String() + byGroupVersion[gv] = append(byGroupVersion[gv], meta_v1.APIResource{Name: spec.Plural}) + } + for gv, resources := range byGroupVersion { + resourceList := &meta_v1.APIResourceList{ + GroupVersion: gv, + APIResources: resources, + } + cs.Resources = append(cs.Resources, resourceList) + } + + err := resourceTypesPresence(cs, specs) + if c.wantErr { + if err == nil { + tt.Fatal("expected error but got success") + } + } else { + if err != nil { + tt.Fatalf("expected success but got error: %v", err) + } + } + }) + } +} + +type mockExtensionClient struct{ err error } + +func (m mockExtensionClient) DynamicInterface() (dynamic.Interface, error) { return nil, m.err } +func (m mockExtensionClient) APIExtensionsClientset() (clientset.Interface, error) { return nil, m.err } +func (m mockExtensionClient) KubeClient() (kubernetes.Interface, error) { return nil, m.err } + +func TestExportedFunctions(t *testing.T) { + var m mockExtensionClient + + // provide an empty list of specs so the calling code doesn't + // invoke the mockExtensionClient's unimplemented discovery API. Those + // functions are tested covered by TestCheckCRDPresence and TestFindSupportedResourceSchemas + var emptySpecs []schema.KubeResource + + if got := ResourceTypesPresence(m, emptySpecs); got != nil { + t.Errorf("ResourceTypesPresence() returned unexpected error: %v", got) + } + + m.err = errors.New("oops") + if got := ResourceTypesPresence(m, emptySpecs); got == nil { + t.Error("ResourceTypesPresence() returned unexpected success") + } +} diff --git a/galley/pkg/config/source/kube/fs/source.go b/galley/pkg/config/source/kube/fs/source.go new file mode 100644 index 000000000000..6621cf4a3c5a --- /dev/null +++ b/galley/pkg/config/source/kube/fs/source.go @@ -0,0 +1,159 @@ +// 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 fs + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + "syscall" + + "istio.io/pkg/appsignals" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/source/kube/inmemory" +) + +var ( + supportedExtensions = map[string]bool{ + ".yaml": true, + ".yml": true, + } +) + +var nameDiscriminator int64 + +type source struct { + mu sync.Mutex + name string + s *inmemory.KubeSource + root string + done chan struct{} +} + +var _ event.Source = &source{} + +// New returns a new filesystem based processor.Source. +func New(root string, resources schema.KubeResources) (event.Source, error) { + src := inmemory.NewKubeSource(resources) + name := fmt.Sprintf("fs-%d", nameDiscriminator) + nameDiscriminator++ + + s := &source{ + name: name, + root: root, + s: src, + } + + return s, nil +} + +// Start implements processor.Source +func (s *source) Start() { + s.mu.Lock() + defer s.mu.Unlock() + + if s.done != nil { + return + } + s.done = make(chan struct{}) + + c := make(chan appsignals.Signal, 1) + appsignals.Watch(c) + go func() { + s.reload() + s.s.Start() + for { + select { + case trigger := <-c: + if trigger.Signal == syscall.SIGUSR1 { + scope.Source.Infof("[%s] Triggering reload in response to: %v", s.name, trigger.Source) + s.reload() + } + case <-s.done: + return + } + } + }() +} + +// Stop implements processor.Source. +func (s *source) Stop() { + scope.Source.Debugf("fs.Source.Stop >>>") + defer scope.Source.Debugf("fs.Source.Stop <<<") + s.mu.Lock() + defer s.mu.Unlock() + if s.done == nil { + return + } + close(s.done) + s.s.Stop() + s.s.Clear() + s.done = nil +} + +// Dispatch implements event.Source +func (s *source) Dispatch(h event.Handler) { + s.s.Dispatch(h) +} + +func (s *source) reload() { + s.mu.Lock() + defer s.mu.Unlock() + + scope.Source.Debugf("[%s] Begin reloading files...", s.name) + names := s.s.ContentNames() + + err := filepath.Walk(s.root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if mode := info.Mode() & os.ModeType; !supportedExtensions[filepath.Ext(path)] || (mode != 0 && mode != os.ModeSymlink) { + return nil + } + + scope.Source.Infof("[%s] Discovered file: %q", s.name, path) + + data, err := ioutil.ReadFile(path) + if err != nil { + scope.Source.Infof("[%s] Error reading file %q: %v", s.name, path, err) + return err + } + + if err := s.s.ApplyContent(path, string(data)); err != nil { + scope.Source.Errorf("[%s] Error applying file contents(%q): %v", s.name, path, err) + } + delete(names, path) + return nil + }) + + if err != nil { + scope.Source.Errorf("Error walking path during reload: %v", err) + return + } + + for n := range names { + scope.Source.Infof("Removing the contents of the file %q", n) + + s.s.RemoveContent(n) + } + + scope.Source.Debugf("[%s] Completed reloading files...", s.name) +} diff --git a/galley/pkg/config/source/kube/fs/source_test.go b/galley/pkg/config/source/kube/fs/source_test.go new file mode 100644 index 000000000000..8bdd8e09ffb9 --- /dev/null +++ b/galley/pkg/config/source/kube/fs/source_test.go @@ -0,0 +1,321 @@ +// 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 fs_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "syscall" + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/source/kube/fs" + "istio.io/istio/galley/pkg/config/testing/basicmeta" + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/data/builtin" + "istio.io/istio/galley/pkg/config/testing/fixtures" + "istio.io/istio/galley/pkg/config/testing/k8smeta" + "istio.io/pkg/appsignals" +) + +func TestNew(t *testing.T) { + dir := createTempDir(t) + defer deleteTempDir(t, dir) + + _ = newOrFail(t, dir) +} + +func TestInvalidDirShouldSucceed(t *testing.T) { + s := newOrFail(t, "somebaddir") + + acc := startOrFail(t, s) + defer s.Stop() + + fixtures.ExpectFilter(t, acc, fixtures.NoFullSync) +} + +func TestInitialFile(t *testing.T) { + g := NewGomegaWithT(t) + + dir := createTempDir(t) + defer deleteTempDir(t, dir) + + copyFile(t, dir, "foo.yaml", data.YamlN1I1V1) + + // Start the source. + s := newOrFail(t, dir) + acc := startOrFail(t, s) + defer s.Stop() + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection1), + event.AddFor(data.Collection1, data.EntryN1I1V1))) + + acc.Clear() + + deleteFiles(t, dir, "foo.yaml") + appsignals.Notify("test", syscall.SIGUSR1) + + g.Eventually(acc.Events).Should(ConsistOf( + event.DeleteForResource(data.Collection1, data.EntryN1I1V1))) +} + +func TestAddDeleteMultipleTimes(t *testing.T) { + g := NewGomegaWithT(t) + + dir := createTempDir(t) + defer deleteTempDir(t, dir) + + s := newOrFail(t, dir) + acc := startOrFail(t, s) + defer s.Stop() + + copyFile(t, dir, "foo.yaml", data.YamlN1I1V1) + appsignals.Notify("test", syscall.SIGUSR1) + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection1), + event.AddFor(data.Collection1, data.EntryN1I1V1))) + + acc.Clear() + deleteFiles(t, dir, "foo.yaml") + appsignals.Notify("test", syscall.SIGUSR1) + g.Eventually(acc.Events).Should(ConsistOf( + event.DeleteForResource(data.Collection1, data.EntryN1I1V1))) + + acc.Clear() + copyFile(t, dir, "foo.yaml", data.YamlN1I1V1) + appsignals.Notify("test", syscall.SIGUSR1) + g.Eventually(acc.Events).Should(ConsistOf( + event.AddFor(data.Collection1, withVersion(data.EntryN1I1V1, "v2")))) + + acc.Clear() + deleteFiles(t, dir, "foo.yaml") + appsignals.Notify("test", syscall.SIGUSR1) + g.Eventually(acc.Events).Should(ConsistOf( + event.DeleteForResource(data.Collection1, withVersion(data.EntryN1I1V1, "v2")))) +} + +func TestAddDeleteMultipleTimes1(t *testing.T) { + g := NewGomegaWithT(t) + + dir := createTempDir(t) + defer deleteTempDir(t, dir) + + s := newOrFail(t, dir) + acc := startOrFail(t, s) + defer s.Stop() + + copyFile(t, dir, "foo.yaml", data.YamlN1I1V1) + appsignals.Notify("test", syscall.SIGUSR1) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection1), + event.AddFor(data.Collection1, data.EntryN1I1V1))) + + acc.Clear() + deleteFiles(t, dir, "foo.yaml") + appsignals.Notify("test", syscall.SIGUSR1) + + g.Eventually(acc.Events).Should(ConsistOf( + event.DeleteForResource(data.Collection1, data.EntryN1I1V1))) +} + +func TestAddUpdateDelete(t *testing.T) { + g := NewGomegaWithT(t) + + dir := createTempDir(t) + defer deleteTempDir(t, dir) + + s := newOrFail(t, dir) + acc := startOrFail(t, s) + defer s.Stop() + + copyFile(t, dir, "foo.yaml", data.YamlN1I1V1) + appsignals.Notify("test", syscall.SIGUSR1) + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(basicmeta.Collection1), + event.AddFor(data.Collection1, data.EntryN1I1V1))) + + acc.Clear() + copyFile(t, dir, "foo.yaml", data.YamlN1I1V2) + appsignals.Notify("test", syscall.SIGUSR1) + + g.Eventually(acc.Events).Should(ConsistOf( + event.UpdateFor(data.Collection1, withVersion(data.EntryN1I1V2, "v2")))) + + acc.Clear() + copyFile(t, dir, "foo.yaml", "") + appsignals.Notify("test", syscall.SIGUSR1) + + g.Eventually(acc.Events).Should(ConsistOf( + event.DeleteForResource(data.Collection1, withVersion(data.EntryN1I1V2, "v2")))) +} + +func TestAddUpdateDelete_K8sResources(t *testing.T) { + g := NewGomegaWithT(t) + + dir := createTempDir(t) + defer deleteTempDir(t, dir) + + s := newWithMetadataOrFail(t, dir, k8smeta.MustGet()) + acc := startOrFail(t, s) + defer s.Stop() + + g.Eventually(acc.Events).Should(ConsistOf( + event.FullSyncFor(k8smeta.K8SCoreV1Endpoints), + event.FullSyncFor(k8smeta.K8SExtensionsV1Beta1Ingresses), + event.FullSyncFor(k8smeta.K8SCoreV1Namespaces), + event.FullSyncFor(k8smeta.K8SCoreV1Nodes), + event.FullSyncFor(k8smeta.K8SCoreV1Pods), + event.FullSyncFor(k8smeta.K8SCoreV1Services))) + + acc.Clear() + copyFile(t, dir, "bar.yaml", builtin.GetService()) + appsignals.Notify("test", syscall.SIGUSR1) + + g.Eventually(acc.Events).Should(HaveLen(1)) + g.Expect(acc.Events()[0].Source).To(Equal(k8smeta.K8SCoreV1Services)) + g.Expect(acc.Events()[0].Kind).To(Equal(event.Added)) + g.Expect(acc.Events()[0].Entry.Metadata.Name).To(Equal(resource.NewName("kube-system", "kube-dns"))) + + acc.Clear() + deleteFiles(t, dir, "bar.yaml") + appsignals.Notify("test", syscall.SIGUSR1) + + g.Eventually(acc.Events).Should(HaveLen(1)) + g.Expect(acc.Events()[0].Source).To(Equal(k8smeta.K8SCoreV1Services)) + g.Expect(acc.Events()[0].Kind).To(Equal(event.Deleted)) + g.Expect(acc.Events()[0].Entry.Metadata.Name).To(Equal(resource.NewName("kube-system", "kube-dns"))) +} + +func TestMultiStart(t *testing.T) { + dir := createTempDir(t) + defer deleteTempDir(t, dir) + + s := newOrFail(t, dir) + defer s.Stop() + + s.Start() + + s.Start() + // No crash +} + +func TestMultiStop(t *testing.T) { + dir := createTempDir(t) + defer deleteTempDir(t, dir) + + s := newOrFail(t, dir) + s.Start() + + s.Stop() + s.Stop() // Ensure that it does not crash +} + +func TestStartStopStart(t *testing.T) { + dir := createTempDir(t) + defer deleteTempDir(t, dir) + + s := newOrFail(t, dir) + defer s.Stop() + + s.Start() + + s.Stop() + + s.Start() + + s.Stop() + + s.Start() + + s.Stop() +} + +func createTempDir(t *testing.T) string { + t.Helper() + rootPath, err := ioutil.TempDir("", "configPath") + if err != nil { + t.Fatal(err) + } + t.Logf("Root dir: %v", rootPath) + return rootPath +} + +func deleteTempDir(t *testing.T, dir string) { + t.Helper() + err := os.RemoveAll(dir) + if err != nil { + t.Fatal(err) + } +} + +func copyFile(t *testing.T, dir string, name string, content string) { + t.Helper() + err := ioutil.WriteFile(filepath.Join(dir, name), []byte(content), 0600) + if err != nil { + t.Fatal(err) + } +} + +func deleteFiles(t *testing.T, dir string, files ...string) { + t.Helper() + for _, name := range files { + err := os.Remove(filepath.Join(dir, name)) + if err != nil { + t.Fatal(err) + } + } +} + +func newOrFail(t *testing.T, dir string) event.Source { + t.Helper() + return newWithMetadataOrFail(t, dir, basicmeta.MustGet()) +} + +func newWithMetadataOrFail(t *testing.T, dir string, m *schema.Metadata) event.Source { + t.Helper() + s, err := fs.New(dir, m.KubeSource().Resources()) + if err != nil { + t.Fatalf("Unexpected error found: %v", err) + } + + if s == nil { + t.Fatal("expected non-nil source") + } + return s +} + +func startOrFail(t *testing.T, s event.Source) *fixtures.Accumulator { + t.Helper() + + acc := &fixtures.Accumulator{} + s.Dispatch(acc) + s.Start() + + return acc +} + +func withVersion(r *resource.Entry, v string) *resource.Entry { // nolint:unparam + r = r.Clone() + r.Metadata.Version = resource.Version(v) + return r +} diff --git a/galley/pkg/config/source/kube/inmemory/kubesource.go b/galley/pkg/config/source/kube/inmemory/kubesource.go new file mode 100644 index 000000000000..3e900e6ea2c7 --- /dev/null +++ b/galley/pkg/config/source/kube/inmemory/kubesource.go @@ -0,0 +1,236 @@ +// 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 inmemory + +import ( + "bytes" + "crypto/sha1" + "fmt" + "sync" + + "github.com/ghodss/yaml" + kubeJson "k8s.io/apimachinery/pkg/runtime/serializer/json" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/source/inmemory" + "istio.io/istio/galley/pkg/config/source/kube/rt" + "istio.io/istio/galley/pkg/config/util/kubeyaml" +) + +var inMemoryKubeNameDiscriminator int64 + +// KubeSource is an in-memory source implementation that can handle K8s style resources. +type KubeSource struct { + mu sync.Mutex + + name string + resources schema.KubeResources + source *inmemory.Source + + versionCtr int64 + shas map[kubeResourceKey]resourceSha + byFile map[string]map[kubeResourceKey]collection.Name +} + +type resourceSha [sha1.Size]byte + +type kubeResource struct { + entry *resource.Entry + spec schema.KubeResource + sha resourceSha +} + +func (r *kubeResource) newKey() kubeResourceKey { + return kubeResourceKey{ + kind: r.spec.Kind, + fullName: r.entry.Metadata.Name, + } +} + +type kubeResourceKey struct { + fullName resource.Name + kind string +} + +var _ event.Source = &KubeSource{} + +// NewKubeSource returns a new in-memory Source that works with Kubernetes resources. +func NewKubeSource(resources schema.KubeResources) *KubeSource { + name := fmt.Sprintf("kube-inmemory-%d", inMemoryKubeNameDiscriminator) + inMemoryKubeNameDiscriminator++ + + s := inmemory.New(resources.Collections()) + + return &KubeSource{ + name: name, + resources: resources, + source: s, + shas: make(map[kubeResourceKey]resourceSha), + byFile: make(map[string]map[kubeResourceKey]collection.Name), + } +} + +// Start implements processor.Source +func (s *KubeSource) Start() { + s.source.Start() +} + +// Stop implements processor.Source +func (s *KubeSource) Stop() { + s.source.Stop() +} + +// Clear the contents of this source +func (s *KubeSource) Clear() { + s.versionCtr = 0 + s.shas = make(map[kubeResourceKey]resourceSha) + s.byFile = make(map[string]map[kubeResourceKey]collection.Name) + s.source.Clear() +} + +// Dispatch implements processor.Source +func (s *KubeSource) Dispatch(h event.Handler) { + s.source.Dispatch(h) +} + +// Get returns the named collection. +func (s *KubeSource) Get(collection collection.Name) *inmemory.Collection { + return s.source.Get(collection) +} + +// ContentNames returns the names known to this source. +func (s *KubeSource) ContentNames() map[string]struct{} { + s.mu.Lock() + defer s.mu.Unlock() + + result := make(map[string]struct{}) + for n := range s.byFile { + result[n] = struct{}{} + } + + return result +} + +// ApplyContent applies the given yamltext to this source. The content is tracked with the given name. If ApplyContent +// gets called multiple times with the same name, the contents applied by the previous incarnation will be overwritten +// or removed, depending on the new content. +func (s *KubeSource) ApplyContent(name, yamlText string) error { + s.mu.Lock() + defer s.mu.Unlock() + + resources := parseContent(s.resources, name, yamlText) + + oldKeys := s.byFile[name] + newKeys := make(map[kubeResourceKey]collection.Name) + + for _, r := range resources { + key := r.newKey() + + oldSha, found := s.shas[key] + if !found || oldSha != r.sha { + s.versionCtr++ + r.entry.Metadata.Version = resource.Version(fmt.Sprintf("v%d", s.versionCtr)) + scope.Source.Debuga("KubeSource.ApplyContent: Set: ", r.spec.Collection.Name, r.entry.Metadata.Name) + s.source.Get(r.spec.Collection.Name).Set(r.entry) + s.shas[key] = r.sha + } + newKeys[key] = r.spec.Collection.Name + if oldKeys != nil { + scope.Source.Debuga("KubeSource.ApplyContent: Delete: ", r.spec.Collection.Name, key) + delete(oldKeys, key) + } + } + + for k, col := range oldKeys { + s.source.Get(col).Remove(k.fullName) + } + s.byFile[name] = newKeys + + return nil +} + +// RemoveContent removes the content for the given name +func (s *KubeSource) RemoveContent(name string) { + s.mu.Lock() + defer s.mu.Unlock() + + keys := s.byFile[name] + if keys != nil { + for key, col := range keys { + s.source.Get(col).Remove(key.fullName) + delete(s.shas, key) + } + + delete(s.byFile, name) + } +} + +func parseContent(r schema.KubeResources, name, yamlText string) []kubeResource { + var resources []kubeResource + for i, chunk := range kubeyaml.Split([]byte(yamlText)) { + chunk = bytes.TrimSpace(chunk) + + r, err := parseChunk(r, chunk) + if err != nil { + scope.Source.Errorf("Error processing %s[%d]: %v", name, i, err) + scope.Source.Debugf("Offending Yaml chunk: %v", string(chunk)) + continue + } + + resources = append(resources, r) + } + return resources +} + +func parseChunk(r schema.KubeResources, yamlChunk []byte) (kubeResource, error) { + // Convert to JSON + jsonChunk, err := yaml.YAMLToJSON(yamlChunk) + if err != nil { + return kubeResource{}, fmt.Errorf("failed converting YAML to JSON") + } + + // Peek at the beginning of the JSON to + groupVersionKind, err := kubeJson.DefaultMetaFactory.Interpret(jsonChunk) + if err != nil { + return kubeResource{}, fmt.Errorf("failed interpreting jsonChunk: %v", err) + } + + resourceSpec, found := r.Find(groupVersionKind.Group, groupVersionKind.Kind) + if !found { + return kubeResource{}, fmt.Errorf("failed finding spec for group/kind: %s/%s", groupVersionKind.Group, groupVersionKind.Kind) + } + + t := rt.DefaultProvider().GetAdapter(resourceSpec) + obj, err := t.ParseJSON(jsonChunk) + if err != nil { + return kubeResource{}, fmt.Errorf("failed parsing JSON for built-in type: %v", err) + } + objMeta := t.ExtractObject(obj) + + item, err := t.ExtractResource(obj) + if err != nil { + return kubeResource{}, err + } + + return kubeResource{ + spec: resourceSpec, + sha: sha1.Sum(yamlChunk), + entry: rt.ToResourceEntry(objMeta, item), + }, nil +} diff --git a/galley/pkg/config/source/kube/inmemory/kubesource_test.go b/galley/pkg/config/source/kube/inmemory/kubesource_test.go new file mode 100644 index 000000000000..035061e23941 --- /dev/null +++ b/galley/pkg/config/source/kube/inmemory/kubesource_test.go @@ -0,0 +1,312 @@ +// 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 inmemory + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/testing/basicmeta" + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/data/builtin" + "istio.io/istio/galley/pkg/config/testing/fixtures" + "istio.io/istio/galley/pkg/config/testing/k8smeta" + "istio.io/istio/galley/pkg/config/util/kubeyaml" +) + +func TestKubeSource_ApplyContent(t *testing.T) { + g := NewGomegaWithT(t) + + s, acc := setupKubeSource() + s.Start() + defer s.Stop() + + err := s.ApplyContent("foo", data.YamlN1I1V1) + g.Expect(err).To(BeNil()) + + g.Expect(s.ContentNames()).To(Equal(map[string]struct{}{"foo": {}})) + + actual := s.Get(data.Collection1).AllSorted() + g.Expect(actual).To(HaveLen(1)) + + g.Expect(actual[0].Metadata.Name).To(Equal(data.EntryN1I1V1.Metadata.Name)) + + g.Expect(acc.Events()).To(HaveLen(2)) + g.Expect(acc.Events()[0].Kind).To(Equal(event.FullSync)) + g.Expect(acc.Events()[1].Kind).To(Equal(event.Added)) + g.Expect(acc.Events()[1].Entry.Metadata.Name).To(Equal(data.EntryN1I1V1.Metadata.Name)) +} + +func TestKubeSource_ApplyContent_BeforeStart(t *testing.T) { + g := NewGomegaWithT(t) + + s, acc := setupKubeSource() + defer s.Stop() + + err := s.ApplyContent("foo", data.YamlN1I1V1) + g.Expect(err).To(BeNil()) + s.Start() + + g.Expect(s.ContentNames()).To(Equal(map[string]struct{}{"foo": {}})) + + actual := s.Get(data.Collection1).AllSorted() + g.Expect(actual).To(HaveLen(1)) + + g.Expect(actual[0].Metadata.Name).To(Equal(data.EntryN1I1V1.Metadata.Name)) + + g.Expect(acc.Events()).To(HaveLen(2)) + g.Expect(acc.Events()[0].Kind).To(Equal(event.Added)) + g.Expect(acc.Events()[0].Entry.Metadata.Name).To(Equal(data.EntryN1I1V1.Metadata.Name)) + g.Expect(acc.Events()[1].Kind).To(Equal(event.FullSync)) +} + +func TestKubeSource_ApplyContent_Unchanged0Add1(t *testing.T) { + g := NewGomegaWithT(t) + + s, acc := setupKubeSource() + s.Start() + defer s.Stop() + + err := s.ApplyContent("foo", kubeyaml.JoinString(data.YamlN1I1V1, data.YamlN2I2V1)) + g.Expect(err).To(BeNil()) + + actual := s.Get(data.Collection1).AllSorted() + g.Expect(actual).To(HaveLen(2)) + g.Expect(actual[0].Metadata.Name).To(Equal(data.EntryN1I1V1.Metadata.Name)) + g.Expect(actual[1].Metadata.Name).To(Equal(data.EntryN2I2V1.Metadata.Name)) + + err = s.ApplyContent("foo", kubeyaml.JoinString(data.YamlN2I2V2, data.YamlN3I3V1)) + g.Expect(err).To(BeNil()) + + g.Expect(s.ContentNames()).To(Equal(map[string]struct{}{"foo": {}})) + + actual = s.Get(data.Collection1).AllSorted() + g.Expect(actual).To(HaveLen(2)) + g.Expect(actual[0].Metadata.Name).To(Equal(data.EntryN2I2V2.Metadata.Name)) + g.Expect(actual[1].Metadata.Name).To(Equal(data.EntryN3I3V1.Metadata.Name)) + + g.Expect(acc.Events()).To(HaveLen(6)) + g.Expect(acc.Events()[0].Kind).To(Equal(event.FullSync)) + g.Expect(acc.Events()[1].Kind).To(Equal(event.Added)) + g.Expect(acc.Events()[1].Entry).To(Equal(data.EntryN1I1V1)) + g.Expect(acc.Events()[2].Kind).To(Equal(event.Added)) + g.Expect(acc.Events()[2].Entry).To(Equal(withVersion(data.EntryN2I2V1, "v2"))) + g.Expect(acc.Events()[3].Kind).To(Equal(event.Updated)) + g.Expect(acc.Events()[3].Entry).To(Equal(withVersion(data.EntryN2I2V2, "v3"))) + g.Expect(acc.Events()[4].Kind).To(Equal(event.Added)) + g.Expect(acc.Events()[4].Entry).To(Equal(withVersion(data.EntryN3I3V1, "v4"))) + g.Expect(acc.Events()[5].Kind).To(Equal(event.Deleted)) + g.Expect(acc.Events()[5].Entry.Metadata.Name).To(Equal(data.EntryN1I1V1.Metadata.Name)) +} + +func TestKubeSource_RemoveContent(t *testing.T) { + g := NewGomegaWithT(t) + + s, acc := setupKubeSource() + s.Start() + defer s.Stop() + + err := s.ApplyContent("foo", kubeyaml.JoinString(data.YamlN1I1V1, data.YamlN2I2V1)) + g.Expect(err).To(BeNil()) + err = s.ApplyContent("bar", kubeyaml.JoinString(data.YamlN3I3V1)) + g.Expect(err).To(BeNil()) + + g.Expect(s.ContentNames()).To(Equal(map[string]struct{}{"bar": {}, "foo": {}})) + + s.RemoveContent("foo") + g.Expect(s.ContentNames()).To(Equal(map[string]struct{}{"bar": {}})) + + actual := s.Get(data.Collection1).AllSorted() + g.Expect(actual).To(HaveLen(1)) + + g.Expect(acc.Events()).To(HaveLen(6)) + g.Expect(acc.Events()[0:4]).To(ConsistOf( + event.FullSyncFor(data.Collection1), + event.AddFor(data.Collection1, data.EntryN1I1V1), + event.AddFor(data.Collection1, withVersion(data.EntryN2I2V1, "v2")), + event.AddFor(data.Collection1, withVersion(data.EntryN3I3V1, "v3")))) + + // Delete events can appear out of order. + g.Expect(acc.Events()[4].Kind).To(Equal(event.Deleted)) + g.Expect(acc.Events()[5].Kind).To(Equal(event.Deleted)) + + if acc.Events()[4].Entry.Metadata.Name == data.EntryN1I1V1.Metadata.Name { + g.Expect(acc.Events()[4:]).To(ConsistOf( + event.DeleteForResource(data.Collection1, data.EntryN1I1V1), + event.DeleteForResource(data.Collection1, withVersion(data.EntryN2I2V1, "v2")))) + } else { + g.Expect(acc.Events()[4:]).To(ConsistOf( + event.DeleteForResource(data.Collection1, withVersion(data.EntryN2I2V1, "v2")), + event.DeleteForResource(data.Collection1, data.EntryN1I1V1))) + } +} + +func TestKubeSource_Clear(t *testing.T) { + g := NewGomegaWithT(t) + + s, acc := setupKubeSource() + s.Start() + defer s.Stop() + + err := s.ApplyContent("foo", kubeyaml.JoinString(data.YamlN1I1V1, data.YamlN2I2V1)) + g.Expect(err).To(BeNil()) + + s.Clear() + + actual := s.Get(data.Collection1).AllSorted() + g.Expect(actual).To(HaveLen(0)) + + g.Expect(acc.Events()).To(HaveLen(5)) + g.Expect(acc.Events()[0].Kind).To(Equal(event.FullSync)) + g.Expect(acc.Events()[1].Kind).To(Equal(event.Added)) + g.Expect(acc.Events()[1].Entry).To(Equal(data.EntryN1I1V1)) + g.Expect(acc.Events()[2].Kind).To(Equal(event.Added)) + g.Expect(acc.Events()[2].Entry).To(Equal(withVersion(data.EntryN2I2V1, "v2"))) + + g.Expect(acc.Events()[3].Kind).To(Equal(event.Deleted)) + g.Expect(acc.Events()[4].Kind).To(Equal(event.Deleted)) + + if acc.Events()[3].Entry.Metadata.Name == data.EntryN1I1V1.Metadata.Name { + g.Expect(acc.Events()[3].Entry.Metadata.Name).To(Equal(data.EntryN1I1V1.Metadata.Name)) + g.Expect(acc.Events()[4].Entry.Metadata.Name).To(Equal(data.EntryN2I2V1.Metadata.Name)) + } else { + g.Expect(acc.Events()[3].Entry.Metadata.Name).To(Equal(data.EntryN2I2V1.Metadata.Name)) + g.Expect(acc.Events()[4].Entry.Metadata.Name).To(Equal(data.EntryN1I1V1.Metadata.Name)) + } +} + +func TestKubeSource_UnparseableSegment(t *testing.T) { + g := NewGomegaWithT(t) + + s, _ := setupKubeSource() + s.Start() + defer s.Stop() + + err := s.ApplyContent("foo", kubeyaml.JoinString(data.YamlN1I1V1, " \n", data.YamlN2I2V1)) + g.Expect(err).To(BeNil()) + + actual := s.Get(data.Collection1).AllSorted() + g.Expect(actual).To(HaveLen(2)) + g.Expect(actual[0]).To(Equal(data.EntryN1I1V1)) + g.Expect(actual[1]).To(Equal(withVersion(data.EntryN2I2V1, "v2"))) +} + +func TestKubeSource_Unrecognized(t *testing.T) { + g := NewGomegaWithT(t) + + s, _ := setupKubeSource() + s.Start() + defer s.Stop() + + err := s.ApplyContent("foo", kubeyaml.JoinString(data.YamlN1I1V1, data.YamlUnrecognized)) + g.Expect(err).To(BeNil()) + + actual := s.Get(data.Collection1).AllSorted() + g.Expect(actual).To(HaveLen(1)) + g.Expect(actual[0]).To(Equal(data.EntryN1I1V1)) +} + +func TestKubeSource_UnparseableResource(t *testing.T) { + g := NewGomegaWithT(t) + + s, _ := setupKubeSource() + s.Start() + defer s.Stop() + + err := s.ApplyContent("foo", kubeyaml.JoinString(data.YamlN1I1V1, data.YamlUnparseableResource)) + g.Expect(err).To(BeNil()) + + actual := s.Get(data.Collection1).AllSorted() + g.Expect(actual).To(HaveLen(1)) + g.Expect(actual[0]).To(Equal(data.EntryN1I1V1)) +} + +func TestKubeSource_NonStringKey(t *testing.T) { + g := NewGomegaWithT(t) + + s, _ := setupKubeSource() + s.Start() + defer s.Stop() + + err := s.ApplyContent("foo", kubeyaml.JoinString(data.YamlN1I1V1, data.YamlNonStringKey)) + g.Expect(err).To(BeNil()) + + actual := s.Get(data.Collection1).AllSorted() + g.Expect(actual).To(HaveLen(1)) + g.Expect(actual[0]).To(Equal(data.EntryN1I1V1)) +} + +func TestKubeSource_Service(t *testing.T) { + g := NewGomegaWithT(t) + + s, _ := setupKubeSourceWithK8sMeta() + s.Start() + defer s.Stop() + + err := s.ApplyContent("foo", builtin.GetService()) + g.Expect(err).To(BeNil()) + + actual := s.Get(k8smeta.K8SCoreV1Services).AllSorted() + g.Expect(actual).To(HaveLen(1)) + g.Expect(actual[0].Metadata.Name).To(Equal(resource.NewName("kube-system", "kube-dns"))) +} + +func TestSameNameDifferentKind(t *testing.T) { + g := NewGomegaWithT(t) + + s := NewKubeSource(basicmeta.MustGet2().KubeSource().Resources()) + acc := &fixtures.Accumulator{} + s.Dispatch(acc) + s.Start() + defer s.Stop() + + err := s.ApplyContent("foo", kubeyaml.JoinString(data.YamlN1I1V1, data.YamlN1I1V1Kind2)) + g.Expect(err).To(BeNil()) + + g.Expect(acc.Events()).To(HaveLen(4)) + g.Expect(acc.Events()).To(ConsistOf( + event.FullSyncFor(basicmeta.Collection1), + event.FullSyncFor(basicmeta.Collection2), + event.AddFor(basicmeta.Collection1, data.EntryN1I1V1), + event.AddFor(basicmeta.Collection2, withVersion(data.EntryN1I1V1, "v2")))) +} + +func setupKubeSource() (*KubeSource, *fixtures.Accumulator) { + s := NewKubeSource(basicmeta.MustGet().KubeSource().Resources()) + + acc := &fixtures.Accumulator{} + s.Dispatch(acc) + + return s, acc +} + +func setupKubeSourceWithK8sMeta() (*KubeSource, *fixtures.Accumulator) { + s := NewKubeSource(k8smeta.MustGet().KubeSource().Resources()) + + acc := &fixtures.Accumulator{} + s.Dispatch(acc) + + s.Start() + return s, acc +} + +func withVersion(r *resource.Entry, v string) *resource.Entry { + r = r.Clone() + r.Metadata.Version = resource.Version(v) + return r +} diff --git a/galley/pkg/config/source/kube/interfaces.go b/galley/pkg/config/source/kube/interfaces.go new file mode 100644 index 000000000000..aa2dc344f26d --- /dev/null +++ b/galley/pkg/config/source/kube/interfaces.go @@ -0,0 +1,70 @@ +// 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 kube + +import ( + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" // import GKE cluster authentication plugin + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +// Interfaces interface allows access to the Kubernetes API Service methods. It is mainly used for +// test/injection purposes. +type Interfaces interface { + DynamicInterface() (dynamic.Interface, error) + APIExtensionsClientset() (clientset.Interface, error) + KubeClient() (kubernetes.Interface, error) +} + +type interfaces struct { + cfg *rest.Config +} + +var _ Interfaces = &interfaces{} + +// NewInterfacesFromConfigFile returns a new instance of Interfaces. +func NewInterfacesFromConfigFile(kubeconfig string) (Interfaces, error) { + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, err + } + + return NewInterfaces(config), nil +} + +// NewInterfaces returns a new instance of Interfaces. +func NewInterfaces(cfg *rest.Config) Interfaces { + return &interfaces{ + cfg: cfg, + } +} + +// DynamicInterface returns a new dynamic.Interface for the specified API Group/Version. +func (k *interfaces) DynamicInterface() (dynamic.Interface, error) { + return dynamic.NewForConfig(k.cfg) +} + +// APIExtensionsClientset returns a new apiextensions clientset +func (k *interfaces) APIExtensionsClientset() (clientset.Interface, error) { + return clientset.NewForConfig(k.cfg) +} + +// KubeClient returns a new kubernetes Interface client. +func (k *interfaces) KubeClient() (kubernetes.Interface, error) { + return kubernetes.NewForConfig(k.cfg) +} diff --git a/galley/pkg/config/source/kube/interfaces_test.go b/galley/pkg/config/source/kube/interfaces_test.go new file mode 100644 index 000000000000..1abad89d80e2 --- /dev/null +++ b/galley/pkg/config/source/kube/interfaces_test.go @@ -0,0 +1,50 @@ +// 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 kube_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "k8s.io/client-go/rest" + + "istio.io/istio/galley/pkg/config/source/kube" +) + +func TestCreateConfig(t *testing.T) { + k := kube.NewInterfaces(&rest.Config{}) + + if _, err := k.DynamicInterface(); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if _, err := k.APIExtensionsClientset(); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if _, err := k.KubeClient(); err != nil { + t.Fatalf("Unexpected error: %v", err) + } +} + +func TestNewKubeWithInvalidConfigFileShouldFail(t *testing.T) { + g := NewGomegaWithT(t) + _, err := kube.NewInterfacesFromConfigFile("badconfigfile") + g.Expect(err).ToNot(BeNil()) +} + +func TestNewKube(t *testing.T) { + // Should not panic + _ = kube.NewInterfaces(&rest.Config{}) +} diff --git a/galley/pkg/config/source/kube/rt/adapter.go b/galley/pkg/config/source/kube/rt/adapter.go new file mode 100644 index 000000000000..444f8f19485a --- /dev/null +++ b/galley/pkg/config/source/kube/rt/adapter.go @@ -0,0 +1,107 @@ +// 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 rt + +import ( + "fmt" + "reflect" + + "github.com/gogo/protobuf/proto" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" + + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/source/kube/apiserver/stats" +) + +// Adapter provides core functions that are necessary to interact with a Kubernetes resource. +type Adapter struct { + extractObject extractObjectFn + extractResource extractResourceFn + newInformer newInformerFn + parseJSON parseJSONFn + isEqual isEqualFn + isBuiltIn bool +} + +// ExtractObject extracts the k8s object metadata from the given object of this type. +func (p *Adapter) ExtractObject(o interface{}) metav1.Object { + return p.extractObject(o) +} + +// ExtractResource extracts the resource proto from the given object of this type. +func (p *Adapter) ExtractResource(o interface{}) (proto.Message, error) { + return p.extractResource(o) +} + +// NewInformer creates a new k8s informer for resources of this type. +func (p *Adapter) NewInformer() (cache.SharedInformer, error) { + return p.newInformer() +} + +// ParseJSON parses the given JSON into a k8s object of this type. +func (p *Adapter) ParseJSON(input []byte) (interface{}, error) { + return p.parseJSON(input) +} + +// IsEqual checks whether the given two resources are equal +func (p *Adapter) IsEqual(o1, o2 interface{}) bool { + return p.isEqual(o1, o2) +} + +// IsBuiltIn returns true if the adapter uses built-in client libraries. +func (p *Adapter) IsBuiltIn() bool { + return p.isBuiltIn +} + +// JSONToEntry parses the K8s Resource in JSON form and converts it to resource entry. +func (p *Adapter) JSONToEntry(s string) (*resource.Entry, error) { + i, err := p.ParseJSON([]byte(s)) + if err != nil { + return nil, err + } + + obj := p.ExtractObject(i) + item, err := p.ExtractResource(i) + if err != nil { + return nil, err + } + + return ToResourceEntry(obj, item), nil + +} + +type extractObjectFn func(o interface{}) metav1.Object +type extractResourceFn func(o interface{}) (proto.Message, error) +type newInformerFn func() (cache.SharedIndexInformer, error) +type parseJSONFn func(input []byte) (interface{}, error) +type isEqualFn func(o1 interface{}, o2 interface{}) bool + +// resourceVersionsMatch is a resourceEqualFn that determines equality by the resource version. +func resourceVersionsMatch(o1 interface{}, o2 interface{}) bool { + r1, ok1 := o1.(metav1.Object) + r2, ok2 := o2.(metav1.Object) + if !ok1 || !ok2 { + msg := fmt.Sprintf("error decoding kube objects during update, o1 type: %v, o2 type: %v", + reflect.TypeOf(o1), + reflect.TypeOf(o2)) + scope.Source.Error(msg) + stats.RecordEventError(msg) + return false + } + return r1.GetResourceVersion() == r2.GetResourceVersion() +} diff --git a/galley/pkg/config/source/kube/rt/dynamic.go b/galley/pkg/config/source/kube/rt/dynamic.go new file mode 100644 index 000000000000..6ee5ad4a8a14 --- /dev/null +++ b/galley/pkg/config/source/kube/rt/dynamic.go @@ -0,0 +1,100 @@ +// 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 rt + +import ( + "encoding/json" + "fmt" + + "github.com/gogo/protobuf/proto" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/tools/cache" + + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/util/pb" +) + +func (p *Provider) getDynamicAdapter(r schema.KubeResource) *Adapter { + return &Adapter{ + extractObject: func(o interface{}) metav1.Object { + r, ok := o.(*unstructured.Unstructured) + if !ok { + return nil + } + return r + }, + + extractResource: func(o interface{}) (proto.Message, error) { + u, ok := o.(*unstructured.Unstructured) + if !ok { + return nil, fmt.Errorf("extractResource: not unstructured: %v", o) + } + + pr := r.Collection.NewProtoInstance() + if err := pb.UnmarshalData(pr, u.Object["spec"]); err != nil { + return nil, err + } + + return pr, nil + }, + + newInformer: func() (cache.SharedIndexInformer, error) { + d, err := p.dynamicResource(r) + if err != nil { + return nil, err + } + + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + return d.List(options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + options.Watch = true + return d.Watch(options) + }, + }, + &unstructured.Unstructured{}, + p.resyncPeriod, + cache.Indexers{}), nil + }, + + parseJSON: func(data []byte) (interface{}, error) { + u := &unstructured.Unstructured{} + if err := json.Unmarshal(data, u); err != nil { + return nil, fmt.Errorf("failed marshaling into unstructured: %v", err) + } + + if empty(u) { + return nil, nil + } + + return u, nil + }, + isEqual: resourceVersionsMatch, + isBuiltIn: false, + } +} + +// Check if the parsed resource is empty +func empty(r *unstructured.Unstructured) bool { + if r.Object == nil || len(r.Object) == 0 { + return true + } + return false +} diff --git a/galley/pkg/config/source/kube/rt/dynamic_test.go b/galley/pkg/config/source/kube/rt/dynamic_test.go new file mode 100644 index 000000000000..c5ab3a39c220 --- /dev/null +++ b/galley/pkg/config/source/kube/rt/dynamic_test.go @@ -0,0 +1,102 @@ +// 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 rt_test + +import ( + "testing" + + "github.com/gogo/protobuf/proto" + "github.com/gogo/protobuf/types" + . "github.com/onsi/gomega" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/yaml" + + "istio.io/istio/galley/pkg/config/source/kube/rt" + "istio.io/istio/galley/pkg/config/testing/basicmeta" + "istio.io/istio/galley/pkg/config/testing/data" +) + +func TestParseDynamic(t *testing.T) { + g := NewGomegaWithT(t) + input, err := yaml.ToJSON([]byte(data.YamlN1I1V1)) + g.Expect(err).To(BeNil()) + objMeta, objResource := parseDynamic(t, input, "Kind1") + + // Just validate a couple of things... + _, ok := objResource.(*types.Struct) + if !ok { + t.Fatal("failed casting item to Endpoints") + } + g.Expect(objMeta.GetNamespace()).To(Equal("n1")) + g.Expect(objMeta.GetName()).To(Equal("i1")) +} + +func TestExtractObjectDynamic(t *testing.T) { + for _, r := range basicmeta.MustGet().KubeSource().Resources() { + a := rt.DefaultProvider().GetAdapter(r) + + t.Run(r.Kind, func(t *testing.T) { + t.Run("WrongTypeShouldReturnNil", func(t *testing.T) { + out := a.ExtractObject(struct{}{}) + g := NewGomegaWithT(t) + g.Expect(out).To(BeNil()) + }) + + t.Run("Success", func(t *testing.T) { + out := a.ExtractObject(&unstructured.Unstructured{}) + g := NewGomegaWithT(t) + g.Expect(out).ToNot(BeNil()) + }) + }) + } +} + +func TestExtractResourceDynamic(t *testing.T) { + for _, r := range basicmeta.MustGet().KubeSource().Resources() { + a := rt.DefaultProvider().GetAdapter(r) + + t.Run(r.Kind, func(t *testing.T) { + t.Run("WrongTypeShouldReturnNil", func(t *testing.T) { + _, err := a.ExtractResource(struct{}{}) + g := NewGomegaWithT(t) + g.Expect(err).NotTo(BeNil()) + }) + + t.Run("Success", func(t *testing.T) { + out, err := a.ExtractResource(&unstructured.Unstructured{}) + g := NewGomegaWithT(t) + g.Expect(err).To(BeNil()) + g.Expect(out).ToNot(BeNil()) + }) + }) + } +} + +func parseDynamic(t *testing.T, input []byte, kind string) (metaV1.Object, proto.Message) { + t.Helper() + g := NewGomegaWithT(t) + + pr := rt.DefaultProvider() + a := pr.GetAdapter(basicmeta.MustGet().KubeSource().Resources().MustFind("testdata.istio.io", kind)) + + obj, err := a.ParseJSON(input) + g.Expect(err).To(BeNil()) + + p, err := a.ExtractResource(obj) + g.Expect(err).To(BeNil()) + + return a.ExtractObject(obj), p +} diff --git a/galley/pkg/config/source/kube/rt/extract.go b/galley/pkg/config/source/kube/rt/extract.go new file mode 100644 index 000000000000..cef12ddf8960 --- /dev/null +++ b/galley/pkg/config/source/kube/rt/extract.go @@ -0,0 +1,37 @@ +// 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 rt + +import ( + "github.com/gogo/protobuf/proto" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "istio.io/istio/galley/pkg/config/resource" +) + +// ToResourceEntry converts the given object and proto to a resource.Entry +func ToResourceEntry(object metav1.Object, item proto.Message) *resource.Entry { + return &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName(object.GetNamespace(), object.GetName()), + Version: resource.Version(object.GetResourceVersion()), + Annotations: object.GetAnnotations(), + Labels: object.GetLabels(), + CreateTime: object.GetCreationTimestamp().Time, + }, + Item: item, + } +} diff --git a/galley/pkg/config/source/kube/rt/known.go b/galley/pkg/config/source/kube/rt/known.go new file mode 100644 index 000000000000..a789cbaba93d --- /dev/null +++ b/galley/pkg/config/source/kube/rt/known.go @@ -0,0 +1,269 @@ +// 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 rt + +import ( + "fmt" + "reflect" + + "github.com/gogo/protobuf/proto" + v1 "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" + v1beta12 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/tools/cache" + + "istio.io/istio/galley/pkg/config/scope" + "istio.io/istio/galley/pkg/config/source/kube/apiserver/stats" +) + +func (p *Provider) initKnownAdapters() { + runtimeScheme := runtime.NewScheme() + codecs := serializer.NewCodecFactory(runtimeScheme) + deserializer := codecs.UniversalDeserializer() + + p.known = map[string]*Adapter{ + asTypesKey("", "Service"): { + extractObject: defaultExtractObject, + extractResource: func(o interface{}) (proto.Message, error) { + if obj, ok := o.(*v1.Service); ok { + return &obj.Spec, nil + } + return nil, fmt.Errorf("unable to convert to v1.Service: %T", o) + }, + newInformer: func() (cache.SharedIndexInformer, error) { + informer, err := p.sharedInformerFactory() + if err != nil { + return nil, err + } + + return informer.Core().V1().Services().Informer(), nil + }, + parseJSON: func(input []byte) (interface{}, error) { + out := &v1.Service{} + if _, _, err := deserializer.Decode(input, nil, out); err != nil { + return nil, err + } + return out, nil + }, + isEqual: resourceVersionsMatch, + isBuiltIn: true, + }, + + asTypesKey("", "Namespace"): { + extractObject: defaultExtractObject, + extractResource: func(o interface{}) (proto.Message, error) { + if obj, ok := o.(*v1.Namespace); ok { + return &obj.Spec, nil + } + return nil, fmt.Errorf("unable to convert to v1.Namespace: %T", o) + }, + newInformer: func() (cache.SharedIndexInformer, error) { + informer, err := p.sharedInformerFactory() + if err != nil { + return nil, err + } + + return informer.Core().V1().Namespaces().Informer(), nil + }, + parseJSON: func(input []byte) (interface{}, error) { + out := &v1.Namespace{} + if _, _, err := deserializer.Decode(input, nil, out); err != nil { + return nil, err + } + return out, nil + }, + isEqual: resourceVersionsMatch, + isBuiltIn: true, + }, + + asTypesKey("", "Node"): { + extractObject: defaultExtractObject, + extractResource: func(o interface{}) (proto.Message, error) { + if obj, ok := o.(*v1.Node); ok { + return &obj.Spec, nil + } + return nil, fmt.Errorf("unable to convert to v1.Node: %T", o) + }, + newInformer: func() (cache.SharedIndexInformer, error) { + informer, err := p.sharedInformerFactory() + if err != nil { + return nil, err + } + + return informer.Core().V1().Nodes().Informer(), nil + }, + parseJSON: func(input []byte) (interface{}, error) { + out := &v1.Node{} + if _, _, err := deserializer.Decode(input, nil, out); err != nil { + return nil, err + } + return out, nil + }, + isEqual: resourceVersionsMatch, + isBuiltIn: true, + }, + + asTypesKey("", "Pod"): { + extractObject: defaultExtractObject, + extractResource: func(o interface{}) (proto.Message, error) { + if obj, ok := o.(*v1.Pod); ok { + return obj, nil + } + return nil, fmt.Errorf("unable to convert to v1.Pod: %T", o) + }, + newInformer: func() (cache.SharedIndexInformer, error) { + informer, err := p.sharedInformerFactory() + if err != nil { + return nil, err + } + + return informer.Core().V1().Pods().Informer(), nil + }, + parseJSON: func(input []byte) (interface{}, error) { + out := &v1.Pod{} + if _, _, err := deserializer.Decode(input, nil, out); err != nil { + return nil, err + } + return out, nil + }, + isEqual: resourceVersionsMatch, + isBuiltIn: true, + }, + + asTypesKey("", "Endpoints"): { + extractObject: defaultExtractObject, + extractResource: func(o interface{}) (proto.Message, error) { + // TODO(nmittler): This copies ObjectMeta since Endpoints have no spec. + if obj, ok := o.(*v1.Endpoints); ok { + return obj, nil + } + return nil, fmt.Errorf("unable to convert to v1.Endpoints: %T", o) + }, + newInformer: func() (cache.SharedIndexInformer, error) { + informer, err := p.sharedInformerFactory() + if err != nil { + return nil, err + } + + return informer.Core().V1().Endpoints().Informer(), nil + }, + parseJSON: func(input []byte) (interface{}, error) { + out := &v1.Endpoints{} + if _, _, err := deserializer.Decode(input, nil, out); err != nil { + return nil, err + } + return out, nil + }, + isEqual: func(o1 interface{}, o2 interface{}) bool { + r1, ok1 := o1.(*v1.Endpoints) + r2, ok2 := o2.(*v1.Endpoints) + if !ok1 || !ok2 { + msg := fmt.Sprintf("error decoding kube endpoints during update, o1 type: %T, o2 type: %T", + o1, o2) + scope.Source.Error(msg) + stats.RecordEventError(msg) + return false + } + // Endpoint updates can be noisy. Make sure that the subsets have actually changed. + return reflect.DeepEqual(r1.Subsets, r2.Subsets) + }, + + isBuiltIn: true, + }, + asTypesKey("extensions", "Ingress"): { + extractObject: defaultExtractObject, + extractResource: func(o interface{}) (proto.Message, error) { + if obj, ok := o.(*v1beta1.Ingress); ok { + return &obj.Spec, nil + } + return nil, fmt.Errorf("unable to convert to v1beta1.Ingress: %T", o) + }, + newInformer: func() (cache.SharedIndexInformer, error) { + informer, err := p.sharedInformerFactory() + if err != nil { + return nil, err + } + + return informer.Extensions().V1beta1().Ingresses().Informer(), nil + }, + parseJSON: func(input []byte) (interface{}, error) { + out := &v1beta1.Ingress{} + if _, _, err := deserializer.Decode(input, nil, out); err != nil { + return nil, err + } + return out, nil + }, + isEqual: resourceVersionsMatch, + isBuiltIn: true, + }, + asTypesKey("apiextensions.k8s.io", "CustomResourceDefinition"): { + extractObject: defaultExtractObject, + extractResource: func(o interface{}) (proto.Message, error) { + if obj, ok := o.(*v1beta12.CustomResourceDefinition); ok { + return &obj.Spec, nil + } + return nil, fmt.Errorf("unable to convert to v1beta1.Ingress: %T", o) + }, + newInformer: func() (cache.SharedIndexInformer, error) { + ext, err := p.interfaces.APIExtensionsClientset() + if err != nil { + return nil, err + } + inf := cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + return ext.ApiextensionsV1beta1().CustomResourceDefinitions().List(options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + return ext.ApiextensionsV1beta1().CustomResourceDefinitions().Watch(options) + }, + }, + &v1beta12.CustomResourceDefinition{}, + 0, + cache.Indexers{}) + + return inf, nil + + }, + parseJSON: func(input []byte) (interface{}, error) { + out := &v1beta12.CustomResourceDefinition{} + if _, _, err := deserializer.Decode(input, nil, out); err != nil { + return nil, err + } + return out, nil + }, + isEqual: resourceVersionsMatch, + isBuiltIn: true, + }, + } +} + +func asTypesKey(group, kind string) string { + if group == "" { + return kind + } + return fmt.Sprintf("%s/%s", group, kind) +} + +func defaultExtractObject(o interface{}) metav1.Object { + if obj, ok := o.(metav1.Object); ok { + return obj + } + return nil +} diff --git a/galley/pkg/config/source/kube/rt/known_test.go b/galley/pkg/config/source/kube/rt/known_test.go new file mode 100644 index 000000000000..ee5598a310fc --- /dev/null +++ b/galley/pkg/config/source/kube/rt/known_test.go @@ -0,0 +1,192 @@ +// 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 rt_test + +import ( + "fmt" + "testing" + + "github.com/gogo/protobuf/proto" + . "github.com/onsi/gomega" + "k8s.io/api/extensions/v1beta1" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "istio.io/istio/galley/pkg/config/source/kube/rt" + "istio.io/istio/galley/pkg/config/testing/data/builtin" + "istio.io/istio/galley/pkg/config/testing/k8smeta" +) + +func TestParse(t *testing.T) { + t.Run("Endpoints", func(t *testing.T) { + g := NewGomegaWithT(t) + input := builtin.GetEndpoints() + + objMeta, objResource := parse(t, []byte(input), "", "Endpoints") + + // Just validate a couple of things... + _, ok := objResource.(*coreV1.Endpoints) + if !ok { + t.Fatal("failed casting item to Endpoints") + } + g.Expect(objMeta.GetName()).To(Equal("kube-dns")) + }) + + t.Run("Namespace", func(t *testing.T) { + g := NewGomegaWithT(t) + input := builtin.GetNamespace() + + objMeta, objResource := parse(t, []byte(input), "", "Namespace") + + // Just validate a couple of things... + _, ok := objResource.(*coreV1.NamespaceSpec) + if !ok { + t.Fatal("failed casting item to Namespace") + } + g.Expect(objMeta.GetName()).To(Equal("somens")) + }) + + t.Run("Ingress", func(t *testing.T) { + g := NewGomegaWithT(t) + input := builtin.GetIngress() + + objMeta, objResource := parse(t, []byte(input), "extensions", "Ingress") + + // Just validate a couple of things... + _, ok := objResource.(*v1beta1.IngressSpec) + if !ok { + t.Fatal("failed casting item to IngressSpec") + } + g.Expect(objMeta.GetName()).To(Equal("secured-ingress")) + }) + + t.Run("Node", func(t *testing.T) { + g := NewGomegaWithT(t) + input := builtin.GetNode() + + objMeta, objResource := parse(t, []byte(input), "", "Node") + + // Just validate a couple of things... + _, ok := objResource.(*coreV1.NodeSpec) + if !ok { + t.Fatal("failed casting item to NodeSpec") + } + g.Expect(objMeta.GetName()).To(Equal("gke-istio-test-default-pool-866a0405-420r")) + }) + + t.Run("Pod", func(t *testing.T) { + g := NewGomegaWithT(t) + input := builtin.GetPod() + + objMeta, objResource := parse(t, []byte(input), "", "Pod") + + // Just validate a couple of things... + _, ok := objResource.(*coreV1.Pod) + if !ok { + t.Fatal("failed casting item to Pod") + } + g.Expect(objMeta.GetName()).To(Equal("kube-dns-548976df6c-d9kkv")) + }) + + t.Run("Service", func(t *testing.T) { + g := NewGomegaWithT(t) + input := builtin.GetService() + + objMeta, objResource := parse(t, []byte(input), "", "Service") + + // Just validate a couple of things... + _, ok := objResource.(*coreV1.ServiceSpec) + if !ok { + t.Fatal("failed casting item to ServiceSpec") + } + g.Expect(objMeta.GetName()).To(Equal("kube-dns")) + }) +} + +func TestExtractObject(t *testing.T) { + for _, r := range k8smeta.MustGet().KubeSource().Resources() { + a := rt.DefaultProvider().GetAdapter(r) + + t.Run(r.Kind, func(t *testing.T) { + t.Run("WrongTypeShouldReturnNil", func(t *testing.T) { + out := a.ExtractObject(struct{}{}) + g := NewGomegaWithT(t) + g.Expect(out).To(BeNil()) + }) + + t.Run("Success", func(t *testing.T) { + out := a.ExtractObject(empty(r.Kind)) + g := NewGomegaWithT(t) + g.Expect(out).ToNot(BeNil()) + }) + }) + } +} + +func TestExtractResource(t *testing.T) { + for _, r := range k8smeta.MustGet().KubeSource().Resources() { + a := rt.DefaultProvider().GetAdapter(r) + + t.Run(r.Kind, func(t *testing.T) { + t.Run("WrongTypeShouldReturnNil", func(t *testing.T) { + _, err := a.ExtractResource(struct{}{}) + g := NewGomegaWithT(t) + g.Expect(err).NotTo(BeNil()) + }) + + t.Run("Success", func(t *testing.T) { + out, err := a.ExtractResource(empty(r.Kind)) + g := NewGomegaWithT(t) + g.Expect(err).To(BeNil()) + g.Expect(out).ToNot(BeNil()) + }) + }) + } +} + +func parse(t *testing.T, input []byte, group, kind string) (metaV1.Object, proto.Message) { + t.Helper() + g := NewGomegaWithT(t) + + pr := rt.DefaultProvider() + a := pr.GetAdapter(k8smeta.MustGet().KubeSource().Resources().MustFind(group, kind)) + obj, err := a.ParseJSON(input) + g.Expect(err).To(BeNil()) + + p, err := a.ExtractResource(obj) + g.Expect(err).To(BeNil()) + + return a.ExtractObject(obj), p +} + +func empty(kind string) metaV1.Object { + switch kind { + case "Node": + return &coreV1.Node{} + case "Service": + return &coreV1.Service{} + case "Pod": + return &coreV1.Pod{} + case "Endpoints": + return &coreV1.Endpoints{} + case "Namespace": + return &coreV1.Namespace{} + case "Ingress": + return &v1beta1.Ingress{} + default: + panic(fmt.Sprintf("unsupported kind: %v", kind)) + } +} diff --git a/galley/pkg/config/source/kube/rt/provider.go b/galley/pkg/config/source/kube/rt/provider.go new file mode 100644 index 000000000000..fe3baa92389f --- /dev/null +++ b/galley/pkg/config/source/kube/rt/provider.go @@ -0,0 +1,111 @@ +// 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 rt + +import ( + "errors" + "sync" + "time" + + kubeSchema "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/informers" + + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/source/kube" +) + +var ( + defaultProvider = NewProvider(nil, 0) +) + +// DefaultProvider returns a default provider that has no K8s connectivity enabled. +func DefaultProvider() *Provider { + return defaultProvider +} + +// Provider for adapters. It closes over K8s connection-related infrastructure. +type Provider struct { + mu sync.Mutex + + resyncPeriod time.Duration + interfaces kube.Interfaces + known map[string]*Adapter + + informers informers.SharedInformerFactory + dynamicInterface dynamic.Interface +} + +// NewProvider returns a new instance of Provider. +func NewProvider(interfaces kube.Interfaces, resyncPeriod time.Duration) *Provider { + p := &Provider{ + resyncPeriod: resyncPeriod, + interfaces: interfaces, + } + + p.initKnownAdapters() + + return p +} + +// GetAdapter returns a type for the group/kind. If the type is a well-known type, then the returned type will have +// a specialized implementation. Otherwise, it will be using the dynamic conversion logic. +func (p *Provider) GetAdapter(r schema.KubeResource) *Adapter { + if t, found := p.known[asTypesKey(r.Group, r.Kind)]; found { + return t + } + + return p.getDynamicAdapter(r) +} + +func (p *Provider) sharedInformerFactory() (informers.SharedInformerFactory, error) { + p.mu.Lock() + defer p.mu.Unlock() + + if p.informers == nil { + if p.interfaces == nil { + return nil, errors.New("client interfaces was not initialized") + } + cl, err := p.interfaces.KubeClient() + if err != nil { + return nil, err + } + p.informers = informers.NewSharedInformerFactory(cl, p.resyncPeriod) + } + + return p.informers, nil +} + +func (p *Provider) dynamicResource(r schema.KubeResource) (dynamic.NamespaceableResourceInterface, error) { + p.mu.Lock() + defer p.mu.Unlock() + + if p.dynamicInterface == nil { + if p.interfaces == nil { + return nil, errors.New("client interfaces was not initialized") + } + d, err := p.interfaces.DynamicInterface() + if err != nil { + return nil, err + } + p.dynamicInterface = d + } + + return p.dynamicInterface.Resource(kubeSchema.GroupVersionResource{ + Group: r.Group, + Version: r.Version, + Resource: r.Plural, + }), nil +} diff --git a/galley/pkg/config/synthesize/version.go b/galley/pkg/config/synthesize/version.go new file mode 100644 index 000000000000..9527de3835da --- /dev/null +++ b/galley/pkg/config/synthesize/version.go @@ -0,0 +1,61 @@ +// 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 synthesize + +import ( + "crypto/sha256" + "encoding/base64" + + "istio.io/istio/galley/pkg/config/resource" + "istio.io/pkg/pool" +) + +// Version synthesizes a new resource version from existing resource versions. There needs to be at least one version +// in versions, otherwise function panics. +func Version(prefix string, versions ...resource.Version) resource.Version { + i := 0 + return VersionIter(prefix, func() (n resource.Name, v resource.Version, ok bool) { + if i < len(versions) { + v = versions[i] + i++ + ok = true + } + return + }) +} + +// VersionIter synthesizes a new resource version from existing resource versions. +func VersionIter(prefix string, iter func() (resource.Name, resource.Version, bool)) resource.Version { + b := pool.GetBuffer() + + for { + n, v, ok := iter() + if !ok { + break + } + _, _ = b.WriteString(n.String()) + _, _ = b.WriteString(string(v)) + } + + if b.Len() == 0 { + panic("synthesize.VersionIter: at least one version is required") + } + + sgn := sha256.Sum256(b.Bytes()) + + pool.PutBuffer(b) + + return resource.Version("$" + prefix + "_" + base64.RawStdEncoding.EncodeToString(sgn[:])) +} diff --git a/galley/pkg/config/synthesize/version_test.go b/galley/pkg/config/synthesize/version_test.go new file mode 100644 index 000000000000..ba56978519b5 --- /dev/null +++ b/galley/pkg/config/synthesize/version_test.go @@ -0,0 +1,75 @@ +// 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 synthesize + +import ( + "fmt" + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/resource" +) + +func TestVersion(t *testing.T) { + cases := []struct { + prefix string + versions []resource.Version + expected resource.Version + }{ + { + prefix: "p", + versions: []resource.Version{ + resource.Version("v1"), + }, + expected: resource.Version("$p_O/wmlZTvZJIo6adLqwDwQu/JHVrMb77jGjgugNQjiP4"), + }, + { + prefix: "pfx", + versions: []resource.Version{ + resource.Version("v1"), + resource.Version("v2"), + }, + expected: resource.Version("$pfx_cMZ8rs1/SXZ1bn9ReGI47pGRRHokoNqC018mIwq2jNU"), + }, + { + prefix: "pfx", + versions: []resource.Version{ + resource.Version("v2"), + resource.Version("v1"), + }, + expected: resource.Version("$pfx_8nucyH5RFJixr3d2YLNRB+NAKutn+DRBfXx5GmOJSRI"), + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + g := NewGomegaWithT(t) + + v := Version(c.prefix, c.versions...) + g.Expect(v).To(Equal(c.expected)) + }) + } +} + +func TestVersion_Panic(t *testing.T) { + g := NewGomegaWithT(t) + defer func() { + r := recover() + g.Expect(r).NotTo(BeNil()) + }() + + _ = Version("aa") +} diff --git a/galley/pkg/config/testing/basicmeta/basicmeta.gen.go b/galley/pkg/config/testing/basicmeta/basicmeta.gen.go new file mode 100644 index 000000000000..045f3a23b0d7 --- /dev/null +++ b/galley/pkg/config/testing/basicmeta/basicmeta.gen.go @@ -0,0 +1,327 @@ +// Code generated by go-bindata. +// sources: +// basicmeta.yaml +// basicmeta2.yaml +// DO NOT EDIT! + +package basicmeta + +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 _basicmetaYaml = []byte(`# Copyright 2019 Istio Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in conformance 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. + + +collections: + - name: "collection1" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "collection2" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + +# Configuration for input sources +sources: + # Kubernetes specific configuration. + - type: kubernetes + resources: + # Test data sources + - collection: "collection1" + kind: "Kind1" + plural: "Kind1s" + group: "testdata.istio.io" + version: "v1alpha1" + +# Transform specific configurations +transforms: + - type: direct + mapping: + "collection1": "collection2" +`) + +func basicmetaYamlBytes() ([]byte, error) { + return _basicmetaYaml, nil +} + +func basicmetaYaml() (*asset, error) { + bytes, err := basicmetaYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "basicmeta.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _basicmeta2Yaml = []byte(`# Copyright 2019 Istio Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in conformance 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. + + +collections: + - name: "collection1" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "collection1out" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "collection2" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "collection2out" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + +# Configuration for input sources +sources: + # Kubernetes specific configuration. + - type: kubernetes + resources: + # Test data sources + - collection: "collection1" + kind: "Kind1" + plural: "Kind1s" + group: "testdata.istio.io" + version: "v1alpha1" + + - collection: "collection2" + kind: "Kind2" + plural: "Kind2s" + group: "testdata.istio.io" + version: "v1alpha1" + optional: true + + +# Transform specific configurations +transforms: + - type: direct + mapping: + "collection1": "collection1out" + "collection2": "collection2out" +`) + +func basicmeta2YamlBytes() ([]byte, error) { + return _basicmeta2Yaml, nil +} + +func basicmeta2Yaml() (*asset, error) { + bytes, err := basicmeta2YamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "basicmeta2.yaml", 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){ + "basicmeta.yaml": basicmetaYaml, + "basicmeta2.yaml": basicmeta2Yaml, +} + +// 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{ + "basicmeta.yaml": &bintree{basicmetaYaml, map[string]*bintree{}}, + "basicmeta2.yaml": &bintree{basicmeta2Yaml, 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/galley/pkg/config/testing/basicmeta/basicmeta.yaml b/galley/pkg/config/testing/basicmeta/basicmeta.yaml new file mode 100644 index 000000000000..95a9418d6c82 --- /dev/null +++ b/galley/pkg/config/testing/basicmeta/basicmeta.yaml @@ -0,0 +1,41 @@ +# Copyright 2019 Istio Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in conformance 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. + + +collections: + - name: "collection1" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "collection2" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + +# Configuration for input sources +sources: + # Kubernetes specific configuration. + - type: kubernetes + resources: + # Test data sources + - collection: "collection1" + kind: "Kind1" + plural: "Kind1s" + group: "testdata.istio.io" + version: "v1alpha1" + +# Transform specific configurations +transforms: + - type: direct + mapping: + "collection1": "collection2" diff --git a/galley/pkg/config/testing/basicmeta/basicmeta2.yaml b/galley/pkg/config/testing/basicmeta/basicmeta2.yaml new file mode 100644 index 000000000000..01612136f403 --- /dev/null +++ b/galley/pkg/config/testing/basicmeta/basicmeta2.yaml @@ -0,0 +1,58 @@ +# Copyright 2019 Istio Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in conformance 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. + + +collections: + - name: "collection1" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "collection1out" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "collection2" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + + - name: "collection2out" + proto: "google.protobuf.Struct" + protoPackage: "github.com/gogo/protobuf/types" + +# Configuration for input sources +sources: + # Kubernetes specific configuration. + - type: kubernetes + resources: + # Test data sources + - collection: "collection1" + kind: "Kind1" + plural: "Kind1s" + group: "testdata.istio.io" + version: "v1alpha1" + + - collection: "collection2" + kind: "Kind2" + plural: "Kind2s" + group: "testdata.istio.io" + version: "v1alpha1" + optional: true + + +# Transform specific configurations +transforms: + - type: direct + mapping: + "collection1": "collection1out" + "collection2": "collection2out" diff --git a/galley/pkg/config/testing/basicmeta/collections.gen.go b/galley/pkg/config/testing/basicmeta/collections.gen.go new file mode 100755 index 000000000000..02d0f504d0b6 --- /dev/null +++ b/galley/pkg/config/testing/basicmeta/collections.gen.go @@ -0,0 +1,25 @@ +// GENERATED FILE -- DO NOT EDIT +// + +package basicmeta + +import ( + "istio.io/istio/galley/pkg/config/collection" +) + +var ( + + // Collection1 is the name of collection collection1 + Collection1 = collection.NewName("collection1") + + // Collection2 is the name of collection collection2 + Collection2 = collection.NewName("collection2") +) + +// CollectionNames returns the collection names declared in this package. +func CollectionNames() []collection.Name { + return []collection.Name{ + Collection1, + Collection2, + } +} diff --git a/galley/pkg/config/testing/basicmeta/gen.go b/galley/pkg/config/testing/basicmeta/gen.go new file mode 100644 index 000000000000..8c55cccf4774 --- /dev/null +++ b/galley/pkg/config/testing/basicmeta/gen.go @@ -0,0 +1,27 @@ +// 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 basicmeta + +// Embed the core metadata file containing the collections as a resource +//go:generate go-bindata --nocompress --nometadata --pkg basicmeta -o basicmeta.gen.go basicmeta.yaml basicmeta2.yaml + +// Create static initializers file +//go:generate go run $GOPATH/src/istio.io/istio/galley/pkg/config/schema/codegen/tools/staticinit.main.go basicmeta basicmeta.yaml staticinit.gen.go + +// Create collection constants +//go:generate go run $GOPATH/src/istio.io/istio/galley/pkg/config/schema/codegen/tools/collections.main.go basicmeta basicmeta.yaml collections.gen.go + +//go:generate goimports -w -local istio.io "$GOPATH/src/istio.io/istio/galley/pkg/config/testing/basicmeta/collections.gen.go" +//go:generate goimports -w -local istio.io "$GOPATH/src/istio.io/istio/galley/pkg/config/testing/basicmeta/staticinit.gen.go" diff --git a/galley/pkg/config/testing/basicmeta/get.go b/galley/pkg/config/testing/basicmeta/get.go new file mode 100644 index 000000000000..00443d534f6b --- /dev/null +++ b/galley/pkg/config/testing/basicmeta/get.go @@ -0,0 +1,69 @@ +// 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 basicmeta + +import ( + "fmt" + + "istio.io/istio/galley/pkg/config/schema" +) + +// Get returns the contained baiscmeta.yaml file, in parsed form. +func Get() (*schema.Metadata, error) { + b, err := Asset("basicmeta.yaml") + if err != nil { + return nil, err + } + + m, err := schema.ParseAndBuild(string(b)) + if err != nil { + return nil, err + } + + return m, nil +} + +// Get returns the contained baiscmeta.yaml file, in parsed form. +func Get2() (*schema.Metadata, error) { + b, err := Asset("basicmeta2.yaml") + if err != nil { + return nil, err + } + + m, err := schema.ParseAndBuild(string(b)) + if err != nil { + return nil, err + } + + return m, nil +} + +// MustGet calls Get and panics if it returns and error. +func MustGet() *schema.Metadata { + s, err := Get() + if err != nil { + panic(fmt.Sprintf("testmeta.MustGet: %v", err)) + } + return s +} + +// MustGet2 calls Get2 and panics if it returns and error. +func MustGet2() *schema.Metadata { + s, err := Get2() + if err != nil { + panic(fmt.Sprintf("testmeta.MustGet2: %v", err)) + } + return s +} diff --git a/galley/pkg/config/testing/basicmeta/staticinit.gen.go b/galley/pkg/config/testing/basicmeta/staticinit.gen.go new file mode 100755 index 000000000000..e0569eb0665c --- /dev/null +++ b/galley/pkg/config/testing/basicmeta/staticinit.gen.go @@ -0,0 +1,11 @@ +// GENERATED FILE -- DO NOT EDIT +// + +package basicmeta + +import ( + // Pull in all the known proto types to ensure we get their types registered. + + // Register protos in "github.com/gogo/protobuf/types" + _ "github.com/gogo/protobuf/types" +) diff --git a/galley/pkg/config/testing/data/builtin.gen.go b/galley/pkg/config/testing/data/builtin.gen.go new file mode 100644 index 000000000000..7a00e64e8f94 --- /dev/null +++ b/galley/pkg/config/testing/data/builtin.gen.go @@ -0,0 +1,979 @@ +// Code generated by go-bindata. +// sources: +// builtin/endpoints.yaml +// builtin/get.go +// builtin/ingress.yaml +// builtin/namespace.yaml +// builtin/node.yaml +// builtin/pod.yaml +// builtin/service.yaml +// DO NOT EDIT! + +package data + +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 _builtinEndpointsYaml = []byte(`apiVersion: v1 +kind: Endpoints +metadata: + creationTimestamp: 2018-02-12T15:48:44Z + labels: + addonmanager.kubernetes.io/mode: Reconcile + k8s-app: kube-dns + kubernetes.io/cluster-service: "true" + kubernetes.io/name: KubeDNS + name: kube-dns + namespace: kube-system + resourceVersion: "50573380" + selfLink: /api/v1/namespaces/kube-system/endpoints/kube-dns + uid: 34991433-100c-11e8-a600-42010a8002c3 +subsets: +- addresses: + - ip: 10.40.0.5 + nodeName: gke-istio-test-default-pool-866a0405-420r + targetRef: + kind: Pod + name: kube-dns-548976df6c-kxnhb + namespace: kube-system + resourceVersion: "50573379" + uid: 66b0ca7d-f71d-11e8-af4f-42010a800072 + - ip: 10.40.1.4 + nodeName: gke-istio-test-default-pool-866a0405-ftch + targetRef: + kind: Pod + name: kube-dns-548976df6c-d9kkv + namespace: kube-system + resourceVersion: "50572715" + uid: dd4bbbd4-f71c-11e8-af4f-42010a800072 + ports: + - name: dns + port: 53 + protocol: UDP + - name: dns-tcp + port: 53 + protocol: TCP +`) + +func builtinEndpointsYamlBytes() ([]byte, error) { + return _builtinEndpointsYaml, nil +} + +func builtinEndpointsYaml() (*asset, error) { + bytes, err := builtinEndpointsYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "builtin/endpoints.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _builtinGetGo = []byte(`// 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. + +// Embed the core metadata file containing the collections as a resource +//go:generate go-bindata --nocompress --nometadata --pkg builtin -o builtin.gen.go testdata/ + +package builtin + +import ( + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/testing/data" +) + +var ( + // EndpointsCollection for testing + EndpointsCollection = collection.NewName("k8s/endpoints") + + // NodesCollection for testing + NodesCollection = collection.NewName("k8s/nodes") + + // PodsCollection for testing + PodsCollection = collection.NewName("k8s/pods") + + // ServicesCollection for testing + ServicesCollection = collection.NewName("k8s/services") +) + +// GetEndpoints returns Endpoints test data +func GetEndpoints() string { + return string(data.MustAsset("builtin/endpoints.yaml")) +} + +// GetNode returns Node test data +func GetNode() string { + return string(data.MustAsset("builtin/node.yaml")) +} + +// GetPod returns Pod test data +func GetPod() string { + return string(data.MustAsset("builtin/pod.yaml")) +} + +// GetService returns Service test data +func GetService() string { + return string(data.MustAsset("builtin/service.yaml")) +} + +// GetNamespace returns Namespace test data +func GetNamespace() string { + return string(data.MustAsset("builtin/namespace.yaml")) +} + +// GetIngress returns Ingress test data +func GetIngress() string { + return string(data.MustAsset("builtin/ingress.yaml")) +} +`) + +func builtinGetGoBytes() ([]byte, error) { + return _builtinGetGo, nil +} + +func builtinGetGo() (*asset, error) { + bytes, err := builtinGetGoBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "builtin/get.go", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _builtinIngressYaml = []byte(`apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: secured-ingress + annotations: + kubernetes.io/ingress.class: "istio" +spec: + tls: + - secretName: istio-ingress-certs + rules: + - http: + paths: + - path: /http + backend: + serviceName: a + servicePort: http + - path: /pasta + backend: + serviceName: b + servicePort: 8080 + - path: /.* + backend: + serviceName: a + servicePort: grpc +`) + +func builtinIngressYamlBytes() ([]byte, error) { + return _builtinIngressYaml, nil +} + +func builtinIngressYaml() (*asset, error) { + bytes, err := builtinIngressYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "builtin/ingress.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _builtinNamespaceYaml = []byte(`apiVersion: v1 +kind: Namespace +metadata: + labels: + istio-injection: disabled + name: somens +spec: + finalizers: + - kubernetes +status: + phase: Active +`) + +func builtinNamespaceYamlBytes() ([]byte, error) { + return _builtinNamespaceYaml, nil +} + +func builtinNamespaceYaml() (*asset, error) { + bytes, err := builtinNamespaceYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "builtin/namespace.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _builtinNodeYaml = []byte(`apiVersion: v1 +kind: Node +metadata: + annotations: + container.googleapis.com/instance_id: "2787417306096525587" + node.alpha.kubernetes.io/ttl: "0" + volumes.kubernetes.io/controller-managed-attach-detach: "true" + creationTimestamp: 2018-10-05T19:40:48Z + labels: + beta.kubernetes.io/arch: amd64 + beta.kubernetes.io/fluentd-ds-ready: "true" + beta.kubernetes.io/instance-type: n1-standard-4 + beta.kubernetes.io/os: linux + cloud.google.com/gke-nodepool: default-pool + cloud.google.com/gke-os-distribution: cos + failure-domain.beta.kubernetes.io/region: us-central1 + failure-domain.beta.kubernetes.io/zone: us-central1-a + kubernetes.io/hostname: gke-istio-test-default-pool-866a0405-420r + name: gke-istio-test-default-pool-866a0405-420r + resourceVersion: "60695251" + selfLink: /api/v1/nodes/gke-istio-test-default-pool-866a0405-420r + uid: 8f63dfef-c8d6-11e8-8901-42010a800278 +spec: + externalID: "1929748586650271976" + podCIDR: 10.40.0.0/24 + providerID: gce://nathanmittler-istio-test/us-central1-a/gke-istio-test-default-pool-866a0405-420r +status: + addresses: + - address: 10.128.0.4 + type: InternalIP + - address: 35.238.214.129 + type: ExternalIP + - address: gke-istio-test-default-pool-866a0405-420r + type: Hostname + allocatable: + cpu: 3920m + ephemeral-storage: "47093746742" + hugepages-2Mi: "0" + memory: 12699980Ki + pods: "110" + capacity: + cpu: "4" + ephemeral-storage: 98868448Ki + hugepages-2Mi: "0" + memory: 15399244Ki + pods: "110" + conditions: + - lastHeartbeatTime: 2019-01-15T17:36:51Z + lastTransitionTime: 2018-12-03T17:00:58Z + message: node is functioning properly + reason: UnregisterNetDevice + status: "False" + type: FrequentUnregisterNetDevice + - lastHeartbeatTime: 2019-01-15T17:36:51Z + lastTransitionTime: 2018-12-03T16:55:56Z + message: kernel has no deadlock + reason: KernelHasNoDeadlock + status: "False" + type: KernelDeadlock + - lastHeartbeatTime: 2018-10-05T19:40:58Z + lastTransitionTime: 2018-10-05T19:40:58Z + message: RouteController created a route + reason: RouteCreated + status: "False" + type: NetworkUnavailable + - lastHeartbeatTime: 2019-01-15T17:37:32Z + lastTransitionTime: 2018-12-03T16:55:57Z + message: kubelet has sufficient disk space available + reason: KubeletHasSufficientDisk + status: "False" + type: OutOfDisk + - lastHeartbeatTime: 2019-01-15T17:37:32Z + lastTransitionTime: 2018-12-03T16:55:57Z + message: kubelet has sufficient memory available + reason: KubeletHasSufficientMemory + status: "False" + type: MemoryPressure + - lastHeartbeatTime: 2019-01-15T17:37:32Z + lastTransitionTime: 2018-12-03T16:55:57Z + message: kubelet has no disk pressure + reason: KubeletHasNoDiskPressure + status: "False" + type: DiskPressure + - lastHeartbeatTime: 2019-01-15T17:37:32Z + lastTransitionTime: 2018-10-05T19:40:48Z + message: kubelet has sufficient PID available + reason: KubeletHasSufficientPID + status: "False" + type: PIDPressure + - lastHeartbeatTime: 2019-01-15T17:37:32Z + lastTransitionTime: 2018-12-03T16:56:07Z + message: kubelet is posting ready status. AppArmor enabled + reason: KubeletReady + status: "True" + type: Ready + daemonEndpoints: + kubeletEndpoint: + Port: 10250 + images: + - names: + - gcr.io/stackdriver-agents/stackdriver-logging-agent@sha256:a33f69d0034fdce835a1eb7df8a051ea74323f3fc30d911bbd2e3f2aef09fc93 + - gcr.io/stackdriver-agents/stackdriver-logging-agent:0.3-1.5.34-1-k8s-1 + sizeBytes: 554981103 + - names: + - istio/examples-bookinfo-reviews-v3@sha256:8c0385f0ca799e655d8770b52cb4618ba54e8966a0734ab1aeb6e8b14e171a3b + - istio/examples-bookinfo-reviews-v3:1.9.0 + sizeBytes: 525074812 + - names: + - istio/examples-bookinfo-reviews-v2@sha256:d2483dcb235b27309680177726e4e86905d66e47facaf1d57ed590b2bf95c8ad + - istio/examples-bookinfo-reviews-v2:1.9.0 + sizeBytes: 525074812 + - names: + - istio/examples-bookinfo-reviews-v1@sha256:920d46b3c526376b28b90d0e895ca7682d36132e6338301fcbcd567ef81bde05 + - istio/examples-bookinfo-reviews-v1:1.9.0 + sizeBytes: 525074812 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:8cea2c055dd3d3ab78f99584256efcc1cff7d8ddbed11cded404e9d164235502 + sizeBytes: 448337138 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:9949bc22667ef88e54ae91700a64bf1459e8c14ed92b870b7ec2f630e14cf3c1 + sizeBytes: 446407220 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:fc1f957cfa26673768be8fa865066f730f22fde98a6e80654d00f755a643b507 + sizeBytes: 446407220 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:23a52850819d5196d66e8e20f4f63f314f779716f830e1d109ad0e24b1f0df43 + sizeBytes: 446407220 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:e338c2c5cbc379db24c5b2d67a4acc9cca9a069c2927217fca0ce7cbc582d312 + - gcr.io/nathanmittler-istio-test/proxyv2:latest + sizeBytes: 446398900 + - names: + - gcr.io/istio-release/proxyv2@sha256:dec972eab4f46c974feec1563ea484ad4995edf55ea91d42e148c5db04b3d4d2 + - gcr.io/istio-release/proxyv2:master-latest-daily + sizeBytes: 353271308 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:cb4a29362ff9014bf1d96e0ce2bb6337bf034908bb4a8d48af0628a4d8d64413 + sizeBytes: 344543156 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:9d502fd29961bc3464f7906ac0e86b07edf01cf4892352ef780e55b3525fb0b8 + sizeBytes: 344257154 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:cdd2f527b4bd392b533d2d0e62c257c19d5a35a6b5fc3512aa327c560866aec1 + sizeBytes: 344257154 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:6ec1dced4cee8569c77817927938fa4341f939e0dddab511bc3ee8724d652ae2 + sizeBytes: 344257154 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:3f4115cd8c26a17f6bf8ea49f1ff5b875382bda5a6d46281c70c886e802666b0 + sizeBytes: 344257154 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:4e75c42518bb46376cfe0b4fbaa3da1d8f1cea99f706736f1b0b04a3ac554db2 + sizeBytes: 344201616 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:58a7511f549448f6f86280559069bc57f5c754877ebec69da5bbc7ad55e42162 + sizeBytes: 344201616 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:7f60a750d15cda9918e9172e529270ce78c670751d4027f6adc6bdc84ec2d884 + sizeBytes: 344201436 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:6fc25c08212652c7539caaf0f6d913d929f84c54767f20066657ce0f4e6a51e0 + sizeBytes: 344193424 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:4e93825950c831ce6d2b65c9a80921c8860035e39a4b384d38d40f7d2cb2a4ee + sizeBytes: 344185232 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:842216399613774640a4605202d446cf61bd48ff20e12391a0239cbc6a8f2c77 + sizeBytes: 344185052 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:8ee2bb6fc5484373227b17e377fc226d8d19be11d38d6dbc304970bd46bc929b + sizeBytes: 344159662 + - names: + - gcr.io/nathanmittler-istio-test/pilot@sha256:b62e9f12609b89892bb38c858936f76d81aa3ccdc91a3961309f900c1c4f574b + sizeBytes: 307722351 + - names: + - gcr.io/nathanmittler-istio-test/pilot@sha256:2445d3c2839825be2decbafcd3f2668bdf148ba9acbbb855810006a58899f320 + sizeBytes: 307722351 + - names: + - gcr.io/nathanmittler-istio-test/pilot@sha256:ea8e501811c06674bb4b4622862e2d12e700f5edadc01a050030a0b33a6435a6 + sizeBytes: 307722351 + nodeInfo: + architecture: amd64 + bootID: 8f772c7c-09eb-41eb-8bb5-76ef214eaaa1 + containerRuntimeVersion: docker://17.3.2 + kernelVersion: 4.14.65+ + kubeProxyVersion: v1.11.3-gke.18 + kubeletVersion: v1.11.3-gke.18 + machineID: f325f89cd295bdcda652fd40f0049e32 + operatingSystem: linux + osImage: Container-Optimized OS from Google + systemUUID: F325F89C-D295-BDCD-A652-FD40F0049E32 +`) + +func builtinNodeYamlBytes() ([]byte, error) { + return _builtinNodeYaml, nil +} + +func builtinNodeYaml() (*asset, error) { + bytes, err := builtinNodeYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "builtin/node.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _builtinPodYaml = []byte(`apiVersion: v1 +kind: Pod +metadata: + annotations: + scheduler.alpha.kubernetes.io/critical-pod: "" + seccomp.security.alpha.kubernetes.io/pod: docker/default + creationTimestamp: 2018-12-03T16:59:57Z + generateName: kube-dns-548976df6c- + labels: + k8s-app: kube-dns + pod-template-hash: "1045328927" + name: kube-dns-548976df6c-d9kkv + namespace: kube-system + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: ReplicaSet + name: kube-dns-548976df6c + uid: b589a851-f71b-11e8-af4f-42010a800072 + resourceVersion: "50572715" + selfLink: /api/v1/namespaces/kube-system/pods/kube-dns-548976df6c-d9kkv + uid: dd4bbbd4-f71c-11e8-af4f-42010a800072 +spec: + containers: + - args: + - --domain=cluster.local. + - --dns-port=10053 + - --config-dir=/kube-dns-config + - --v=2 + env: + - name: PROMETHEUS_PORT + value: "10055" + image: k8s.gcr.io/k8s-dns-kube-dns-amd64:1.14.13 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 5 + httpGet: + path: /healthcheck/kubedns + port: 10054 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: kubedns + ports: + - containerPort: 10053 + name: dns-local + protocol: UDP + - containerPort: 10053 + name: dns-tcp-local + protocol: TCP + - containerPort: 10055 + name: metrics + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /readiness + port: 8081 + scheme: HTTP + initialDelaySeconds: 3 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + memory: 170Mi + requests: + cpu: 100m + memory: 70Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /kube-dns-config + name: kube-dns-config + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-dns-token-lwn8l + readOnly: true + - args: + - -v=2 + - -logtostderr + - -configDir=/etc/k8s/dns/dnsmasq-nanny + - -restartDnsmasq=true + - -- + - -k + - --cache-size=1000 + - --no-negcache + - --log-facility=- + - --server=/cluster.local/127.0.0.1#10053 + - --server=/in-addr.arpa/127.0.0.1#10053 + - --server=/ip6.arpa/127.0.0.1#10053 + image: k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64:1.14.13 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 5 + httpGet: + path: /healthcheck/dnsmasq + port: 10054 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: dnsmasq + ports: + - containerPort: 53 + name: dns + protocol: UDP + - containerPort: 53 + name: dns-tcp + protocol: TCP + resources: + requests: + cpu: 150m + memory: 20Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /etc/k8s/dns/dnsmasq-nanny + name: kube-dns-config + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-dns-token-lwn8l + readOnly: true + - args: + - --v=2 + - --logtostderr + - --probe=kubedns,127.0.0.1:10053,kubernetes.default.svc.cluster.local,5,SRV + - --probe=dnsmasq,127.0.0.1:53,kubernetes.default.svc.cluster.local,5,SRV + image: k8s.gcr.io/k8s-dns-sidecar-amd64:1.14.13 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 5 + httpGet: + path: /metrics + port: 10054 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: sidecar + ports: + - containerPort: 10054 + name: metrics + protocol: TCP + resources: + requests: + cpu: 10m + memory: 20Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-dns-token-lwn8l + readOnly: true + - command: + - /monitor + - --component=kubedns + - --target-port=10054 + - --stackdriver-prefix=container.googleapis.com/internal/addons + - --api-override=https://monitoring.googleapis.com/ + - --whitelisted-metrics=probe_kubedns_latency_ms,probe_kubedns_errors,dnsmasq_misses,dnsmasq_hits + - --pod-id=$(POD_NAME) + - --namespace-id=$(POD_NAMESPACE) + - --v=2 + env: + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + image: gcr.io/google-containers/prometheus-to-sd:v0.2.3 + imagePullPolicy: IfNotPresent + name: prometheus-to-sd + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-dns-token-lwn8l + readOnly: true + dnsPolicy: Default + nodeName: gke-istio-test-default-pool-866a0405-ftch + priority: 2000000000 + priorityClassName: system-cluster-critical + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccount: kube-dns + serviceAccountName: kube-dns + terminationGracePeriodSeconds: 30 + tolerations: + - key: CriticalAddonsOnly + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 + volumes: + - configMap: + defaultMode: 420 + name: kube-dns + optional: true + name: kube-dns-config + - name: kube-dns-token-lwn8l + secret: + defaultMode: 420 + secretName: kube-dns-token-lwn8l +status: + conditions: + - lastProbeTime: null + lastTransitionTime: 2018-12-03T17:00:00Z + status: "True" + type: Initialized + - lastProbeTime: null + lastTransitionTime: 2018-12-03T17:00:20Z + status: "True" + type: Ready + - lastProbeTime: null + lastTransitionTime: null + status: "True" + type: ContainersReady + - lastProbeTime: null + lastTransitionTime: 2018-12-03T16:59:57Z + status: "True" + type: PodScheduled + containerStatuses: + - containerID: docker://676f6c98bfa136315c4ccf0fe40e7a56cbf9ac85128e94310eae82f191246b3e + image: k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64:1.14.13 + imageID: docker-pullable://k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64@sha256:45df3e8e0c551bd0c79cdba48ae6677f817971dcbd1eeed7fd1f9a35118410e4 + lastState: {} + name: dnsmasq + ready: true + restartCount: 0 + state: + running: + startedAt: 2018-12-03T17:00:14Z + - containerID: docker://93fd0664e150982dad0481c5260183308a7035a2f938ec50509d586ed586a107 + image: k8s.gcr.io/k8s-dns-kube-dns-amd64:1.14.13 + imageID: docker-pullable://k8s.gcr.io/k8s-dns-kube-dns-amd64@sha256:618a82fa66cf0c75e4753369a6999032372be7308866fc9afb381789b1e5ad52 + lastState: {} + name: kubedns + ready: true + restartCount: 0 + state: + running: + startedAt: 2018-12-03T17:00:10Z + - containerID: docker://e823b79a0a48af75f2eebb1c89ba4c31e8c1ee67ee0d917ac7b4891b67d2cd0f + image: gcr.io/google-containers/prometheus-to-sd:v0.2.3 + imageID: docker-pullable://gcr.io/google-containers/prometheus-to-sd@sha256:be220ec4a66275442f11d420033c106bb3502a3217a99c806eef3cf9858788a2 + lastState: {} + name: prometheus-to-sd + ready: true + restartCount: 0 + state: + running: + startedAt: 2018-12-03T17:00:18Z + - containerID: docker://74223c401a8dac04b8bd29cdfedcb216881791b4e84bb80a15714991dd18735e + image: k8s.gcr.io/k8s-dns-sidecar-amd64:1.14.13 + imageID: docker-pullable://k8s.gcr.io/k8s-dns-sidecar-amd64@sha256:cedc8fe2098dffc26d17f64061296b7aa54258a31513b6c52df271a98bb522b3 + lastState: {} + name: sidecar + ready: true + restartCount: 0 + state: + running: + startedAt: 2018-12-03T17:00:16Z + hostIP: 10.128.0.5 + phase: Running + podIP: 10.40.1.4 + qosClass: Burstable + startTime: 2018-12-03T17:00:00Z +`) + +func builtinPodYamlBytes() ([]byte, error) { + return _builtinPodYaml, nil +} + +func builtinPodYaml() (*asset, error) { + bytes, err := builtinPodYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "builtin/pod.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _builtinServiceYaml = []byte(`apiVersion: v1 +kind: Service +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"labels":{"addonmanager.kubernetes.io/mode":"Reconcile","k8s-app":"kube-dns","kubernetes.io/cluster-service":"true","kubernetes.io/name":"KubeDNS"},"name":"kube-dns","namespace":"kube-system"},"spec":{"clusterIP":"10.43.240.10","ports":[{"name":"dns","port":53,"protocol":"UDP"},{"name":"dns-tcp","port":53,"protocol":"TCP"}],"selector":{"k8s-app":"kube-dns"}}} + creationTimestamp: 2018-02-12T15:48:44Z + labels: + addonmanager.kubernetes.io/mode: Reconcile + k8s-app: kube-dns + kubernetes.io/cluster-service: "true" + kubernetes.io/name: KubeDNS + name: kube-dns + namespace: kube-system + resourceVersion: "274" + selfLink: /api/v1/namespaces/kube-system/services/kube-dns + uid: 3497d702-100c-11e8-a600-42010a8002c3 +spec: + clusterIP: 10.43.240.10 + ports: + - name: dns + port: 53 + protocol: UDP + targetPort: 53 + - name: dns-tcp + port: 53 + protocol: TCP + targetPort: 53 + selector: + k8s-app: kube-dns + sessionAffinity: None + type: ClusterIP +status: + loadBalancer: {} +`) + +func builtinServiceYamlBytes() ([]byte, error) { + return _builtinServiceYaml, nil +} + +func builtinServiceYaml() (*asset, error) { + bytes, err := builtinServiceYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "builtin/service.yaml", 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){ + "builtin/endpoints.yaml": builtinEndpointsYaml, + "builtin/get.go": builtinGetGo, + "builtin/ingress.yaml": builtinIngressYaml, + "builtin/namespace.yaml": builtinNamespaceYaml, + "builtin/node.yaml": builtinNodeYaml, + "builtin/pod.yaml": builtinPodYaml, + "builtin/service.yaml": builtinServiceYaml, +} + +// 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{ + "builtin": &bintree{nil, map[string]*bintree{ + "endpoints.yaml": &bintree{builtinEndpointsYaml, map[string]*bintree{}}, + "get.go": &bintree{builtinGetGo, map[string]*bintree{}}, + "ingress.yaml": &bintree{builtinIngressYaml, map[string]*bintree{}}, + "namespace.yaml": &bintree{builtinNamespaceYaml, map[string]*bintree{}}, + "node.yaml": &bintree{builtinNodeYaml, map[string]*bintree{}}, + "pod.yaml": &bintree{builtinPodYaml, map[string]*bintree{}}, + "service.yaml": &bintree{builtinServiceYaml, 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/galley/pkg/config/testing/data/builtin.go b/galley/pkg/config/testing/data/builtin.go new file mode 100644 index 000000000000..4c00c78e1782 --- /dev/null +++ b/galley/pkg/config/testing/data/builtin.go @@ -0,0 +1,43 @@ +// 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. + +// Embed the core metadata file containing the collections as a resource +//go:generate go-bindata --nocompress --nometadata --pkg data -o builtin.gen.go builtin/ + +package data + +// GetEndpoints returns Endpoints test data +func GetEndpoints() string { + return string(MustAsset("builtin/endpoints.yaml")) +} + +// GetNode returns Node test data +func GetNode() string { + return string(MustAsset("builtin/node.yaml")) +} + +// GetPod returns Pod test data +func GetPod() string { + return string(MustAsset("builtin/pod.yaml")) +} + +// GetService returns Service test data +func GetService() string { + return string(MustAsset("builtin/service.yaml")) +} + +// GetNamespace returns Namespace test data +func GetNamespace() string { + return string(MustAsset("builtin/namespace.yaml")) +} diff --git a/galley/pkg/config/testing/data/builtin/endpoints.yaml b/galley/pkg/config/testing/data/builtin/endpoints.yaml new file mode 100644 index 000000000000..e95c902ae1ef --- /dev/null +++ b/galley/pkg/config/testing/data/builtin/endpoints.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: Endpoints +metadata: + creationTimestamp: 2018-02-12T15:48:44Z + labels: + addonmanager.kubernetes.io/mode: Reconcile + k8s-app: kube-dns + kubernetes.io/cluster-service: "true" + kubernetes.io/name: KubeDNS + name: kube-dns + namespace: kube-system + resourceVersion: "50573380" + selfLink: /api/v1/namespaces/kube-system/endpoints/kube-dns + uid: 34991433-100c-11e8-a600-42010a8002c3 +subsets: +- addresses: + - ip: 10.40.0.5 + nodeName: gke-istio-test-default-pool-866a0405-420r + targetRef: + kind: Pod + name: kube-dns-548976df6c-kxnhb + namespace: kube-system + resourceVersion: "50573379" + uid: 66b0ca7d-f71d-11e8-af4f-42010a800072 + - ip: 10.40.1.4 + nodeName: gke-istio-test-default-pool-866a0405-ftch + targetRef: + kind: Pod + name: kube-dns-548976df6c-d9kkv + namespace: kube-system + resourceVersion: "50572715" + uid: dd4bbbd4-f71c-11e8-af4f-42010a800072 + ports: + - name: dns + port: 53 + protocol: UDP + - name: dns-tcp + port: 53 + protocol: TCP diff --git a/galley/pkg/config/testing/data/builtin/get.go b/galley/pkg/config/testing/data/builtin/get.go new file mode 100644 index 000000000000..2d47efa16a9e --- /dev/null +++ b/galley/pkg/config/testing/data/builtin/get.go @@ -0,0 +1,67 @@ +// 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. + +// Embed the core metadata file containing the collections as a resource +//go:generate go-bindata --nocompress --nometadata --pkg builtin -o builtin.gen.go testdata/ + +package builtin + +import ( + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/testing/data" +) + +var ( + // EndpointsCollection for testing + EndpointsCollection = collection.NewName("k8s/endpoints") + + // NodesCollection for testing + NodesCollection = collection.NewName("k8s/nodes") + + // PodsCollection for testing + PodsCollection = collection.NewName("k8s/pods") + + // ServicesCollection for testing + ServicesCollection = collection.NewName("k8s/services") +) + +// GetEndpoints returns Endpoints test data +func GetEndpoints() string { + return string(data.MustAsset("builtin/endpoints.yaml")) +} + +// GetNode returns Node test data +func GetNode() string { + return string(data.MustAsset("builtin/node.yaml")) +} + +// GetPod returns Pod test data +func GetPod() string { + return string(data.MustAsset("builtin/pod.yaml")) +} + +// GetService returns Service test data +func GetService() string { + return string(data.MustAsset("builtin/service.yaml")) +} + +// GetNamespace returns Namespace test data +func GetNamespace() string { + return string(data.MustAsset("builtin/namespace.yaml")) +} + +// GetIngress returns Ingress test data +func GetIngress() string { + return string(data.MustAsset("builtin/ingress.yaml")) +} diff --git a/galley/pkg/config/testing/data/builtin/ingress.yaml b/galley/pkg/config/testing/data/builtin/ingress.yaml new file mode 100644 index 000000000000..3eae1f1dd95d --- /dev/null +++ b/galley/pkg/config/testing/data/builtin/ingress.yaml @@ -0,0 +1,24 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: secured-ingress + annotations: + kubernetes.io/ingress.class: "istio" +spec: + tls: + - secretName: istio-ingress-certs + rules: + - http: + paths: + - path: /http + backend: + serviceName: a + servicePort: http + - path: /pasta + backend: + serviceName: b + servicePort: 8080 + - path: /.* + backend: + serviceName: a + servicePort: grpc diff --git a/galley/pkg/config/testing/data/builtin/namespace.yaml b/galley/pkg/config/testing/data/builtin/namespace.yaml new file mode 100644 index 000000000000..d7993f40f129 --- /dev/null +++ b/galley/pkg/config/testing/data/builtin/namespace.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + istio-injection: disabled + name: somens +spec: + finalizers: + - kubernetes +status: + phase: Active diff --git a/galley/pkg/config/testing/data/builtin/node.yaml b/galley/pkg/config/testing/data/builtin/node.yaml new file mode 100644 index 000000000000..0c7cb33d22b2 --- /dev/null +++ b/galley/pkg/config/testing/data/builtin/node.yaml @@ -0,0 +1,191 @@ +apiVersion: v1 +kind: Node +metadata: + annotations: + container.googleapis.com/instance_id: "2787417306096525587" + node.alpha.kubernetes.io/ttl: "0" + volumes.kubernetes.io/controller-managed-attach-detach: "true" + creationTimestamp: 2018-10-05T19:40:48Z + labels: + beta.kubernetes.io/arch: amd64 + beta.kubernetes.io/fluentd-ds-ready: "true" + beta.kubernetes.io/instance-type: n1-standard-4 + beta.kubernetes.io/os: linux + cloud.google.com/gke-nodepool: default-pool + cloud.google.com/gke-os-distribution: cos + failure-domain.beta.kubernetes.io/region: us-central1 + failure-domain.beta.kubernetes.io/zone: us-central1-a + kubernetes.io/hostname: gke-istio-test-default-pool-866a0405-420r + name: gke-istio-test-default-pool-866a0405-420r + resourceVersion: "60695251" + selfLink: /api/v1/nodes/gke-istio-test-default-pool-866a0405-420r + uid: 8f63dfef-c8d6-11e8-8901-42010a800278 +spec: + externalID: "1929748586650271976" + podCIDR: 10.40.0.0/24 + providerID: gce://nathanmittler-istio-test/us-central1-a/gke-istio-test-default-pool-866a0405-420r +status: + addresses: + - address: 10.128.0.4 + type: InternalIP + - address: 35.238.214.129 + type: ExternalIP + - address: gke-istio-test-default-pool-866a0405-420r + type: Hostname + allocatable: + cpu: 3920m + ephemeral-storage: "47093746742" + hugepages-2Mi: "0" + memory: 12699980Ki + pods: "110" + capacity: + cpu: "4" + ephemeral-storage: 98868448Ki + hugepages-2Mi: "0" + memory: 15399244Ki + pods: "110" + conditions: + - lastHeartbeatTime: 2019-01-15T17:36:51Z + lastTransitionTime: 2018-12-03T17:00:58Z + message: node is functioning properly + reason: UnregisterNetDevice + status: "False" + type: FrequentUnregisterNetDevice + - lastHeartbeatTime: 2019-01-15T17:36:51Z + lastTransitionTime: 2018-12-03T16:55:56Z + message: kernel has no deadlock + reason: KernelHasNoDeadlock + status: "False" + type: KernelDeadlock + - lastHeartbeatTime: 2018-10-05T19:40:58Z + lastTransitionTime: 2018-10-05T19:40:58Z + message: RouteController created a route + reason: RouteCreated + status: "False" + type: NetworkUnavailable + - lastHeartbeatTime: 2019-01-15T17:37:32Z + lastTransitionTime: 2018-12-03T16:55:57Z + message: kubelet has sufficient disk space available + reason: KubeletHasSufficientDisk + status: "False" + type: OutOfDisk + - lastHeartbeatTime: 2019-01-15T17:37:32Z + lastTransitionTime: 2018-12-03T16:55:57Z + message: kubelet has sufficient memory available + reason: KubeletHasSufficientMemory + status: "False" + type: MemoryPressure + - lastHeartbeatTime: 2019-01-15T17:37:32Z + lastTransitionTime: 2018-12-03T16:55:57Z + message: kubelet has no disk pressure + reason: KubeletHasNoDiskPressure + status: "False" + type: DiskPressure + - lastHeartbeatTime: 2019-01-15T17:37:32Z + lastTransitionTime: 2018-10-05T19:40:48Z + message: kubelet has sufficient PID available + reason: KubeletHasSufficientPID + status: "False" + type: PIDPressure + - lastHeartbeatTime: 2019-01-15T17:37:32Z + lastTransitionTime: 2018-12-03T16:56:07Z + message: kubelet is posting ready status. AppArmor enabled + reason: KubeletReady + status: "True" + type: Ready + daemonEndpoints: + kubeletEndpoint: + Port: 10250 + images: + - names: + - gcr.io/stackdriver-agents/stackdriver-logging-agent@sha256:a33f69d0034fdce835a1eb7df8a051ea74323f3fc30d911bbd2e3f2aef09fc93 + - gcr.io/stackdriver-agents/stackdriver-logging-agent:0.3-1.5.34-1-k8s-1 + sizeBytes: 554981103 + - names: + - istio/examples-bookinfo-reviews-v3@sha256:8c0385f0ca799e655d8770b52cb4618ba54e8966a0734ab1aeb6e8b14e171a3b + - istio/examples-bookinfo-reviews-v3:1.9.0 + sizeBytes: 525074812 + - names: + - istio/examples-bookinfo-reviews-v2@sha256:d2483dcb235b27309680177726e4e86905d66e47facaf1d57ed590b2bf95c8ad + - istio/examples-bookinfo-reviews-v2:1.9.0 + sizeBytes: 525074812 + - names: + - istio/examples-bookinfo-reviews-v1@sha256:920d46b3c526376b28b90d0e895ca7682d36132e6338301fcbcd567ef81bde05 + - istio/examples-bookinfo-reviews-v1:1.9.0 + sizeBytes: 525074812 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:8cea2c055dd3d3ab78f99584256efcc1cff7d8ddbed11cded404e9d164235502 + sizeBytes: 448337138 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:9949bc22667ef88e54ae91700a64bf1459e8c14ed92b870b7ec2f630e14cf3c1 + sizeBytes: 446407220 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:fc1f957cfa26673768be8fa865066f730f22fde98a6e80654d00f755a643b507 + sizeBytes: 446407220 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:23a52850819d5196d66e8e20f4f63f314f779716f830e1d109ad0e24b1f0df43 + sizeBytes: 446407220 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:e338c2c5cbc379db24c5b2d67a4acc9cca9a069c2927217fca0ce7cbc582d312 + - gcr.io/nathanmittler-istio-test/proxyv2:latest + sizeBytes: 446398900 + - names: + - gcr.io/istio-release/proxyv2@sha256:dec972eab4f46c974feec1563ea484ad4995edf55ea91d42e148c5db04b3d4d2 + - gcr.io/istio-release/proxyv2:master-latest-daily + sizeBytes: 353271308 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:cb4a29362ff9014bf1d96e0ce2bb6337bf034908bb4a8d48af0628a4d8d64413 + sizeBytes: 344543156 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:9d502fd29961bc3464f7906ac0e86b07edf01cf4892352ef780e55b3525fb0b8 + sizeBytes: 344257154 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:cdd2f527b4bd392b533d2d0e62c257c19d5a35a6b5fc3512aa327c560866aec1 + sizeBytes: 344257154 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:6ec1dced4cee8569c77817927938fa4341f939e0dddab511bc3ee8724d652ae2 + sizeBytes: 344257154 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:3f4115cd8c26a17f6bf8ea49f1ff5b875382bda5a6d46281c70c886e802666b0 + sizeBytes: 344257154 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:4e75c42518bb46376cfe0b4fbaa3da1d8f1cea99f706736f1b0b04a3ac554db2 + sizeBytes: 344201616 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:58a7511f549448f6f86280559069bc57f5c754877ebec69da5bbc7ad55e42162 + sizeBytes: 344201616 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:7f60a750d15cda9918e9172e529270ce78c670751d4027f6adc6bdc84ec2d884 + sizeBytes: 344201436 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:6fc25c08212652c7539caaf0f6d913d929f84c54767f20066657ce0f4e6a51e0 + sizeBytes: 344193424 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:4e93825950c831ce6d2b65c9a80921c8860035e39a4b384d38d40f7d2cb2a4ee + sizeBytes: 344185232 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:842216399613774640a4605202d446cf61bd48ff20e12391a0239cbc6a8f2c77 + sizeBytes: 344185052 + - names: + - gcr.io/nathanmittler-istio-test/proxyv2@sha256:8ee2bb6fc5484373227b17e377fc226d8d19be11d38d6dbc304970bd46bc929b + sizeBytes: 344159662 + - names: + - gcr.io/nathanmittler-istio-test/pilot@sha256:b62e9f12609b89892bb38c858936f76d81aa3ccdc91a3961309f900c1c4f574b + sizeBytes: 307722351 + - names: + - gcr.io/nathanmittler-istio-test/pilot@sha256:2445d3c2839825be2decbafcd3f2668bdf148ba9acbbb855810006a58899f320 + sizeBytes: 307722351 + - names: + - gcr.io/nathanmittler-istio-test/pilot@sha256:ea8e501811c06674bb4b4622862e2d12e700f5edadc01a050030a0b33a6435a6 + sizeBytes: 307722351 + nodeInfo: + architecture: amd64 + bootID: 8f772c7c-09eb-41eb-8bb5-76ef214eaaa1 + containerRuntimeVersion: docker://17.3.2 + kernelVersion: 4.14.65+ + kubeProxyVersion: v1.11.3-gke.18 + kubeletVersion: v1.11.3-gke.18 + machineID: f325f89cd295bdcda652fd40f0049e32 + operatingSystem: linux + osImage: Container-Optimized OS from Google + systemUUID: F325F89C-D295-BDCD-A652-FD40F0049E32 diff --git a/galley/pkg/config/testing/data/builtin/pod.yaml b/galley/pkg/config/testing/data/builtin/pod.yaml new file mode 100644 index 000000000000..790b656f5d5f --- /dev/null +++ b/galley/pkg/config/testing/data/builtin/pod.yaml @@ -0,0 +1,283 @@ +apiVersion: v1 +kind: Pod +metadata: + annotations: + scheduler.alpha.kubernetes.io/critical-pod: "" + seccomp.security.alpha.kubernetes.io/pod: docker/default + creationTimestamp: 2018-12-03T16:59:57Z + generateName: kube-dns-548976df6c- + labels: + k8s-app: kube-dns + pod-template-hash: "1045328927" + name: kube-dns-548976df6c-d9kkv + namespace: kube-system + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: ReplicaSet + name: kube-dns-548976df6c + uid: b589a851-f71b-11e8-af4f-42010a800072 + resourceVersion: "50572715" + selfLink: /api/v1/namespaces/kube-system/pods/kube-dns-548976df6c-d9kkv + uid: dd4bbbd4-f71c-11e8-af4f-42010a800072 +spec: + containers: + - args: + - --domain=cluster.local. + - --dns-port=10053 + - --config-dir=/kube-dns-config + - --v=2 + env: + - name: PROMETHEUS_PORT + value: "10055" + image: k8s.gcr.io/k8s-dns-kube-dns-amd64:1.14.13 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 5 + httpGet: + path: /healthcheck/kubedns + port: 10054 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: kubedns + ports: + - containerPort: 10053 + name: dns-local + protocol: UDP + - containerPort: 10053 + name: dns-tcp-local + protocol: TCP + - containerPort: 10055 + name: metrics + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /readiness + port: 8081 + scheme: HTTP + initialDelaySeconds: 3 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + memory: 170Mi + requests: + cpu: 100m + memory: 70Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /kube-dns-config + name: kube-dns-config + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-dns-token-lwn8l + readOnly: true + - args: + - -v=2 + - -logtostderr + - -configDir=/etc/k8s/dns/dnsmasq-nanny + - -restartDnsmasq=true + - -- + - -k + - --cache-size=1000 + - --no-negcache + - --log-facility=- + - --server=/cluster.local/127.0.0.1#10053 + - --server=/in-addr.arpa/127.0.0.1#10053 + - --server=/ip6.arpa/127.0.0.1#10053 + image: k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64:1.14.13 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 5 + httpGet: + path: /healthcheck/dnsmasq + port: 10054 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: dnsmasq + ports: + - containerPort: 53 + name: dns + protocol: UDP + - containerPort: 53 + name: dns-tcp + protocol: TCP + resources: + requests: + cpu: 150m + memory: 20Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /etc/k8s/dns/dnsmasq-nanny + name: kube-dns-config + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-dns-token-lwn8l + readOnly: true + - args: + - --v=2 + - --logtostderr + - --probe=kubedns,127.0.0.1:10053,kubernetes.default.svc.cluster.local,5,SRV + - --probe=dnsmasq,127.0.0.1:53,kubernetes.default.svc.cluster.local,5,SRV + image: k8s.gcr.io/k8s-dns-sidecar-amd64:1.14.13 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 5 + httpGet: + path: /metrics + port: 10054 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: sidecar + ports: + - containerPort: 10054 + name: metrics + protocol: TCP + resources: + requests: + cpu: 10m + memory: 20Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-dns-token-lwn8l + readOnly: true + - command: + - /monitor + - --component=kubedns + - --target-port=10054 + - --stackdriver-prefix=container.googleapis.com/internal/addons + - --api-override=https://monitoring.googleapis.com/ + - --whitelisted-metrics=probe_kubedns_latency_ms,probe_kubedns_errors,dnsmasq_misses,dnsmasq_hits + - --pod-id=$(POD_NAME) + - --namespace-id=$(POD_NAMESPACE) + - --v=2 + env: + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + image: gcr.io/google-containers/prometheus-to-sd:v0.2.3 + imagePullPolicy: IfNotPresent + name: prometheus-to-sd + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-dns-token-lwn8l + readOnly: true + dnsPolicy: Default + nodeName: gke-istio-test-default-pool-866a0405-ftch + priority: 2000000000 + priorityClassName: system-cluster-critical + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccount: kube-dns + serviceAccountName: kube-dns + terminationGracePeriodSeconds: 30 + tolerations: + - key: CriticalAddonsOnly + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 + volumes: + - configMap: + defaultMode: 420 + name: kube-dns + optional: true + name: kube-dns-config + - name: kube-dns-token-lwn8l + secret: + defaultMode: 420 + secretName: kube-dns-token-lwn8l +status: + conditions: + - lastProbeTime: null + lastTransitionTime: 2018-12-03T17:00:00Z + status: "True" + type: Initialized + - lastProbeTime: null + lastTransitionTime: 2018-12-03T17:00:20Z + status: "True" + type: Ready + - lastProbeTime: null + lastTransitionTime: null + status: "True" + type: ContainersReady + - lastProbeTime: null + lastTransitionTime: 2018-12-03T16:59:57Z + status: "True" + type: PodScheduled + containerStatuses: + - containerID: docker://676f6c98bfa136315c4ccf0fe40e7a56cbf9ac85128e94310eae82f191246b3e + image: k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64:1.14.13 + imageID: docker-pullable://k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64@sha256:45df3e8e0c551bd0c79cdba48ae6677f817971dcbd1eeed7fd1f9a35118410e4 + lastState: {} + name: dnsmasq + ready: true + restartCount: 0 + state: + running: + startedAt: 2018-12-03T17:00:14Z + - containerID: docker://93fd0664e150982dad0481c5260183308a7035a2f938ec50509d586ed586a107 + image: k8s.gcr.io/k8s-dns-kube-dns-amd64:1.14.13 + imageID: docker-pullable://k8s.gcr.io/k8s-dns-kube-dns-amd64@sha256:618a82fa66cf0c75e4753369a6999032372be7308866fc9afb381789b1e5ad52 + lastState: {} + name: kubedns + ready: true + restartCount: 0 + state: + running: + startedAt: 2018-12-03T17:00:10Z + - containerID: docker://e823b79a0a48af75f2eebb1c89ba4c31e8c1ee67ee0d917ac7b4891b67d2cd0f + image: gcr.io/google-containers/prometheus-to-sd:v0.2.3 + imageID: docker-pullable://gcr.io/google-containers/prometheus-to-sd@sha256:be220ec4a66275442f11d420033c106bb3502a3217a99c806eef3cf9858788a2 + lastState: {} + name: prometheus-to-sd + ready: true + restartCount: 0 + state: + running: + startedAt: 2018-12-03T17:00:18Z + - containerID: docker://74223c401a8dac04b8bd29cdfedcb216881791b4e84bb80a15714991dd18735e + image: k8s.gcr.io/k8s-dns-sidecar-amd64:1.14.13 + imageID: docker-pullable://k8s.gcr.io/k8s-dns-sidecar-amd64@sha256:cedc8fe2098dffc26d17f64061296b7aa54258a31513b6c52df271a98bb522b3 + lastState: {} + name: sidecar + ready: true + restartCount: 0 + state: + running: + startedAt: 2018-12-03T17:00:16Z + hostIP: 10.128.0.5 + phase: Running + podIP: 10.40.1.4 + qosClass: Burstable + startTime: 2018-12-03T17:00:00Z diff --git a/galley/pkg/config/testing/data/builtin/service.yaml b/galley/pkg/config/testing/data/builtin/service.yaml new file mode 100644 index 000000000000..7f205e1dcb83 --- /dev/null +++ b/galley/pkg/config/testing/data/builtin/service.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"labels":{"addonmanager.kubernetes.io/mode":"Reconcile","k8s-app":"kube-dns","kubernetes.io/cluster-service":"true","kubernetes.io/name":"KubeDNS"},"name":"kube-dns","namespace":"kube-system"},"spec":{"clusterIP":"10.43.240.10","ports":[{"name":"dns","port":53,"protocol":"UDP"},{"name":"dns-tcp","port":53,"protocol":"TCP"}],"selector":{"k8s-app":"kube-dns"}}} + creationTimestamp: 2018-02-12T15:48:44Z + labels: + addonmanager.kubernetes.io/mode: Reconcile + k8s-app: kube-dns + kubernetes.io/cluster-service: "true" + kubernetes.io/name: KubeDNS + name: kube-dns + namespace: kube-system + resourceVersion: "274" + selfLink: /api/v1/namespaces/kube-system/services/kube-dns + uid: 3497d702-100c-11e8-a600-42010a8002c3 +spec: + clusterIP: 10.43.240.10 + ports: + - name: dns + port: 53 + protocol: UDP + targetPort: 53 + - name: dns-tcp + port: 53 + protocol: TCP + targetPort: 53 + selector: + k8s-app: kube-dns + sessionAffinity: None + type: ClusterIP +status: + loadBalancer: {} diff --git a/galley/pkg/config/testing/data/collections.go b/galley/pkg/config/testing/data/collections.go new file mode 100644 index 000000000000..42bff9b779bc --- /dev/null +++ b/galley/pkg/config/testing/data/collections.go @@ -0,0 +1,42 @@ +// 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 data + +import ( + "istio.io/istio/galley/pkg/config/collection" +) + +var ( + // Collection1 is a testing collection + Collection1 = collection.NewName("collection1") + + // Collection2 is a testing collection + Collection2 = collection.NewName("collection2") + + // Collection3 is a testing collection + Collection3 = collection.NewName("collection3") + + // CollectionNames of all collections in the test data. + CollectionNames = collection.Names{Collection1, Collection2, Collection3} + + // Specs is the set of collection.Specs for all test data. + Specs = func() collection.Specs { + b := collection.NewSpecsBuilder() + b.MustAdd(collection.MustNewSpec(Collection1.String(), "google.protobuf", "google.protobuf.Empty")) + b.MustAdd(collection.MustNewSpec(Collection2.String(), "google.protobuf", "google.protobuf.Empty")) + b.MustAdd(collection.MustNewSpec(Collection3.String(), "google.protobuf", "google.protobuf.Empty")) + return b.Build() + }() +) diff --git a/galley/pkg/config/testing/data/events.go b/galley/pkg/config/testing/data/events.go new file mode 100644 index 000000000000..176023f183d2 --- /dev/null +++ b/galley/pkg/config/testing/data/events.go @@ -0,0 +1,76 @@ +// 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 data + +import ( + "istio.io/istio/galley/pkg/config/event" +) + +var ( + // Event1Col1AddItem1 is a testing event + Event1Col1AddItem1 = event.Event{ + Kind: event.Added, + Source: Collection1, + Entry: EntryN1I1V1, + } + + // Event1Col1AddItem1Broken is a testing event + Event1Col1AddItem1Broken = event.Event{ + Kind: event.Added, + Source: Collection1, + Entry: EntryN1I1V1Broken, + } + + // Event1Col1UpdateItem1 is a testing event + Event1Col1UpdateItem1 = event.Event{ + Kind: event.Updated, + Source: Collection1, + Entry: EntryN1I1V2, + } + + // Event1Col1DeleteItem1 is a testing event + Event1Col1DeleteItem1 = event.Event{ + Kind: event.Deleted, + Source: Collection1, + Entry: EntryN1I1V1, + } + + // Event1Col1DeleteItem2 is a testing event + Event1Col1DeleteItem2 = event.Event{ + Kind: event.Deleted, + Source: Collection1, + Entry: EntryN2I2V2, + } + + // Event1Col1Synced is a testing event + Event1Col1Synced = event.Event{ + Kind: event.FullSync, + Source: Collection1, + } + + // Event2Col1AddItem2 is a testing event + Event2Col1AddItem2 = event.Event{ + Kind: event.Added, + Source: Collection1, + Entry: EntryN2I2V2, + } + + // Event3Col2AddItem1 is a testing event + Event3Col2AddItem1 = event.Event{ + Kind: event.Added, + Source: Collection2, + Entry: EntryN1I1V1, + } +) diff --git a/galley/pkg/config/testing/data/resources.go b/galley/pkg/config/testing/data/resources.go new file mode 100644 index 000000000000..b6ce03105554 --- /dev/null +++ b/galley/pkg/config/testing/data/resources.go @@ -0,0 +1,105 @@ +// 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 data + +import ( + "bytes" + + "github.com/gogo/protobuf/jsonpb" + "github.com/gogo/protobuf/types" + + "istio.io/istio/galley/pkg/config/resource" +) + +var ( + // EntryN1I1V1 is a test resource.Entry + EntryN1I1V1 = &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("n1", "i1"), + Version: "v1", + }, + Item: parseStruct(` +{ + "n1_i1": "v1" +}`), + } + + // EntryN1I1V1Broken is a test resource.Entry + EntryN1I1V1Broken = &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("n1", "i1"), + Version: "v1", + }, + Item: nil, + } + + // EntryN1I1V2 is a test resource.Entry + EntryN1I1V2 = &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("n1", "i1"), + Version: "v2", + }, + Item: parseStruct(` +{ + "n1_i1": "v2" +}`), + } + + // EntryN2I2V1 is a test resource.Entry + EntryN2I2V1 = &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("n2", "i2"), + Version: "v1", + }, + Item: parseStruct(` +{ + "n2_i2": "v1" +}`), + } + + // EntryN2I2V2 is a test resource.Entry + EntryN2I2V2 = &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("n2", "i2"), + Version: "v2", + }, + Item: parseStruct(`{ + "n2_i2": "v2" +}`), + } + + // EntryN3I3V1 is a test resource.Entry + EntryN3I3V1 = &resource.Entry{ + Metadata: resource.Metadata{ + Name: resource.NewName("n3", "i3"), + Version: "v1", + }, + Item: parseStruct(`{ + "n3_i3": "v1" +}`), + } +) + +func parseStruct(s string) *types.Struct { + m := jsonpb.Unmarshaler{} + + str := &types.Struct{} + err := m.Unmarshal(bytes.NewReader([]byte(s)), str) + if err != nil { + panic(err) + } + + return str +} diff --git a/galley/pkg/config/testing/data/yaml.go b/galley/pkg/config/testing/data/yaml.go new file mode 100644 index 000000000000..63cc6e23d198 --- /dev/null +++ b/galley/pkg/config/testing/data/yaml.go @@ -0,0 +1,109 @@ +// 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 data + +var ( + // YamlN1I1V1 is a testing resource in Yaml form + YamlN1I1V1 = ` +apiVersion: testdata.istio.io/v1alpha1 +kind: Kind1 +metadata: + namespace: n1 + name: i1 +spec: + n1_i1: v1 +` + + // YamlN1I1V2 is a testing resource in Yaml form + YamlN1I1V2 = ` +apiVersion: testdata.istio.io/v1alpha1 +kind: Kind1 +metadata: + namespace: n1 + name: i1 +spec: + n1_i1: v2 +` + + // YamlN2I2V1 is a testing resource in Yaml form + YamlN2I2V1 = ` +apiVersion: testdata.istio.io/v1alpha1 +kind: Kind1 +metadata: + namespace: n2 + name: i2 +spec: + n2_i2: v1 +` + // YamlN2I2V2 is a testing resource in Yaml form + YamlN2I2V2 = ` +apiVersion: testdata.istio.io/v1alpha1 +kind: Kind1 +metadata: + namespace: n2 + name: i2 +spec: + n2_i2: v2 +` + + // YamlN3I3V1 is a testing resource in Yaml form + YamlN3I3V1 = ` +apiVersion: testdata.istio.io/v1alpha1 +kind: Kind1 +metadata: + namespace: n3 + name: i3 +spec: + n3_i3: v1 +` + + // YamlUnrecognized is a testing resource in Yaml form + YamlUnrecognized = ` +apiVersion: testdata.istio.io/v1alpha1 +kind: KindUnknown +metadata: + namespace: n1 + name: i1 +spec: + n1_i1: v1 +` + + // YamlUnparseableResource is a testing resource in Yaml form + YamlUnparseableResource = ` +apiVersion: testdata.istio.io/v1alpha1/foo/bar +kind: Kind1 +metadata: + namespace: n1 + name: i1 +spec: + foo: bar +` + + // YamlNonStringKey is a testing resource in Yaml form + YamlNonStringKey = ` +23: true +` + + // YamlN1I1V1Kind2 is a testing resource in Yaml form + YamlN1I1V1Kind2 = ` +apiVersion: testdata.istio.io/v1alpha1 +kind: Kind2 +metadata: + namespace: n1 + name: i1 +spec: + n1_i1: v1 +` +) diff --git a/galley/pkg/config/testing/fixtures/accumulator.go b/galley/pkg/config/testing/fixtures/accumulator.go new file mode 100644 index 000000000000..a00c2ba9c151 --- /dev/null +++ b/galley/pkg/config/testing/fixtures/accumulator.go @@ -0,0 +1,68 @@ +// 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 fixtures + +import ( + "fmt" + "sync" + + "istio.io/istio/galley/pkg/config/event" +) + +// Accumulator accumulates events that is dispatched to it. +type Accumulator struct { + mu sync.Mutex + + events []event.Event +} + +// Handle implements event.Handler +func (a *Accumulator) Handle(e event.Event) { + a.mu.Lock() + defer a.mu.Unlock() + + a.events = append(a.events, e) +} + +// Events return current set of accumulated events. +func (a *Accumulator) Events() []event.Event { + a.mu.Lock() + defer a.mu.Unlock() + + r := make([]event.Event, len(a.events)) + copy(r, a.events) + + return r +} + +// Clear all currently accummulated events. +func (a *Accumulator) Clear() { + a.mu.Lock() + defer a.mu.Unlock() + + a.events = nil +} + +func (a *Accumulator) String() string { + a.mu.Lock() + defer a.mu.Unlock() + + var result string + for _, e := range a.events { + result += fmt.Sprintf("%v\n", e) + } + + return result +} diff --git a/galley/pkg/config/testing/fixtures/accumulator_test.go b/galley/pkg/config/testing/fixtures/accumulator_test.go new file mode 100644 index 000000000000..a8b74562ab98 --- /dev/null +++ b/galley/pkg/config/testing/fixtures/accumulator_test.go @@ -0,0 +1,62 @@ +// 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 fixtures + +import ( + "testing" + + "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/testing/data" +) + +func TestAccumulator(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + a := &Accumulator{} + + a.Handle(data.Event1Col1AddItem1) + + expected := []event.Event{data.Event1Col1AddItem1} + g.Expect(a.Events()).To(gomega.Equal(expected)) + + a.Handle(data.Event2Col1AddItem2) + + expected = []event.Event{data.Event1Col1AddItem1, data.Event2Col1AddItem2} + g.Expect(a.Events()).To(gomega.Equal(expected)) +} + +func TestAccumulator_Clear(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + a := &Accumulator{} + + a.Handle(data.Event1Col1AddItem1) + a.Handle(data.Event2Col1AddItem2) + a.Clear() + + g.Expect(a.Events()).To(gomega.Equal([]event.Event{})) +} + +func TestAccumulator_String(t *testing.T) { + a := &Accumulator{} + + a.Handle(data.Event1Col1AddItem1) + a.Handle(data.Event2Col1AddItem2) + + // ensure that it does not crash + _ = a.String() +} diff --git a/galley/pkg/config/testing/fixtures/expect.go b/galley/pkg/config/testing/fixtures/expect.go new file mode 100644 index 000000000000..0bd8d11cce95 --- /dev/null +++ b/galley/pkg/config/testing/fixtures/expect.go @@ -0,0 +1,61 @@ +// 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 fixtures + +import ( + "testing" + "time" + + "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" +) + +// Expect calls gomega.Eventually to wait until the accumulator accumulated specified events. +func Expect(t *testing.T, acc *Accumulator, expected ...event.Event) { + ExpectFilter(t, acc, nil, expected...) +} + +// ExpectFullSync expects the given full sync event. +func ExpectFullSync(t *testing.T, acc *Accumulator, c collection.Name) { + e := event.FullSyncFor(c) + Expect(t, acc, e) +} + +// ExpectNone expects no events to arrive. +func ExpectNone(t *testing.T, acc *Accumulator) { + time.Sleep(time.Second) // Sleep for a long time to avoid missing any events that might be accumulated + Expect(t, acc) +} + +// ExpectFilter works similar to Expect, except it filters out events based on the given function. +func ExpectFilter(t *testing.T, acc *Accumulator, fn FilterFn, expected ...event.Event) { + t.Helper() + g := gomega.NewGomegaWithT(t) + + wrapFn := func() []event.Event { + e := acc.events + if fn != nil { + e = fn(e) + } + + if len(e) == 0 { + e = nil + } + return e + } + g.Eventually(wrapFn).Should(gomega.Equal(expected)) +} diff --git a/galley/pkg/config/testing/fixtures/expect_test.go b/galley/pkg/config/testing/fixtures/expect_test.go new file mode 100644 index 000000000000..d228ba418e99 --- /dev/null +++ b/galley/pkg/config/testing/fixtures/expect_test.go @@ -0,0 +1,53 @@ +// 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 fixtures_test + +import ( + "testing" + + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestExpect(t *testing.T) { + acc := &fixtures.Accumulator{} + acc.Handle(data.Event1Col1AddItem1) + acc.Handle(data.Event2Col1AddItem2) + + fixtures.Expect(t, acc, data.Event1Col1AddItem1, data.Event2Col1AddItem2) +} + +func TestExpect_FullSync(t *testing.T) { + acc := &fixtures.Accumulator{} + acc.Handle(data.Event1Col1Synced) + + fixtures.ExpectFullSync(t, acc, data.Collection1) +} + +func TestExpect_None(t *testing.T) { + acc := &fixtures.Accumulator{} + + fixtures.ExpectNone(t, acc) +} + +func TestExpect_ExpectFilter(t *testing.T) { + acc := &fixtures.Accumulator{} + acc.Handle(data.Event1Col1AddItem1) + acc.Handle(data.Event1Col1Synced) + acc.Handle(data.Event2Col1AddItem2) + + fixtures.ExpectFilter( + t, acc, fixtures.NoFullSync, data.Event1Col1AddItem1, data.Event2Col1AddItem2) +} diff --git a/galley/pkg/config/testing/fixtures/filters.go b/galley/pkg/config/testing/fixtures/filters.go new file mode 100644 index 000000000000..8e8bcb22bedc --- /dev/null +++ b/galley/pkg/config/testing/fixtures/filters.go @@ -0,0 +1,92 @@ +// 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 fixtures + +import ( + "fmt" + "sort" + "strings" + + "istio.io/istio/galley/pkg/config/event" +) + +// FilterFn is a function for filtering events +type FilterFn func(event []event.Event) []event.Event + +// NoVersions strips the versions from the given events +func NoVersions(events []event.Event) []event.Event { + result := make([]event.Event, len(events)) + copy(result, events) + + for i := range result { + result[i].Entry = result[i].Entry.Clone() + result[i].Entry.Metadata.Version = "" + } + + return result +} + +// NoFullSync filters FullSync events and returns. +func NoFullSync(events []event.Event) []event.Event { + result := make([]event.Event, 0, len(events)) + + for _, e := range events { + if e.Kind == event.FullSync { + continue + } + result = append(result, e) + } + + return result +} + +// Sort events in a stable order. +func Sort(events []event.Event) []event.Event { + result := make([]event.Event, len(events)) + copy(result, events) + + sort.SliceStable(result, func(i, j int) bool { + c := strings.Compare(result[i].String(), result[j].String()) + if c != 0 { + return c < 0 + } + + if result[i].Entry == nil && result[j].Entry == nil { + return false + } + + if result[i].Entry == nil { + return false + } + + if result[j].Entry == nil { + return true + } + + return strings.Compare(fmt.Sprintf("%+v", result[i].Entry), fmt.Sprintf("%+v", result[j].Entry)) < 0 + }) + + return result +} + +// Chain filters back to back +func Chain(fns ...FilterFn) FilterFn { + return func(e []event.Event) []event.Event { + for _, fn := range fns { + e = fn(e) + } + return e + } +} diff --git a/galley/pkg/config/testing/fixtures/filters_test.go b/galley/pkg/config/testing/fixtures/filters_test.go new file mode 100644 index 000000000000..942f2260b5d8 --- /dev/null +++ b/galley/pkg/config/testing/fixtures/filters_test.go @@ -0,0 +1,72 @@ +// 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 fixtures_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/resource" + "istio.io/istio/galley/pkg/config/testing/data" + "istio.io/istio/galley/pkg/config/testing/fixtures" +) + +func TestNoVersions(t *testing.T) { + g := NewGomegaWithT(t) + + events := []event.Event{data.Event1Col1AddItem1} + g.Expect(events[0].Entry.Metadata.Version).NotTo(Equal(resource.Version(""))) + events = fixtures.NoVersions(events) + g.Expect(events[0].Entry.Metadata.Version).To(Equal(resource.Version(""))) +} + +func TestNoFullSync(t *testing.T) { + g := NewGomegaWithT(t) + + events := []event.Event{data.Event1Col1AddItem1, data.Event1Col1Synced, data.Event2Col1AddItem2} + events = fixtures.NoFullSync(events) + + expected := []event.Event{data.Event1Col1AddItem1, data.Event2Col1AddItem2} + g.Expect(events).To(Equal(expected)) +} + +func TestSort(t *testing.T) { + g := NewGomegaWithT(t) + + events := []event.Event{data.Event2Col1AddItem2, data.Event1Col1Synced, data.Event1Col1AddItem1} + events = fixtures.Sort(events) + + expected := []event.Event{data.Event1Col1AddItem1, data.Event2Col1AddItem2, data.Event1Col1Synced} + g.Expect(events).To(Equal(expected)) +} + +func TestChain(t *testing.T) { + g := NewGomegaWithT(t) + + events := []event.Event{data.Event2Col1AddItem2, data.Event1Col1Synced, data.Event1Col1AddItem1} + fn := fixtures.Chain(fixtures.Sort, fixtures.NoFullSync, fixtures.NoVersions) + events = fn(events) + + expected := []event.Event{ + data.Event1Col1AddItem1.Clone(), + data.Event2Col1AddItem2.Clone(), + } + expected[0].Entry.Metadata.Version = resource.Version("") + expected[1].Entry.Metadata.Version = resource.Version("") + + g.Expect(events).To(Equal(expected)) +} diff --git a/galley/pkg/config/testing/fixtures/listener.go b/galley/pkg/config/testing/fixtures/listener.go new file mode 100644 index 000000000000..f6701d0f1d0c --- /dev/null +++ b/galley/pkg/config/testing/fixtures/listener.go @@ -0,0 +1,31 @@ +// 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 fixtures + +import ( + "istio.io/istio/galley/pkg/config/event" +) + +// Listener is a simple event.Dispatcher implementation for testing. +type Listener struct { + Handlers event.Handlers +} + +var _ event.Dispatcher = &Listener{} + +// Dispatch implements event.Listener +func (c *Listener) Dispatch(handler event.Handler) { + c.Handlers.Add(handler) +} diff --git a/galley/pkg/config/testing/fixtures/listener_test.go b/galley/pkg/config/testing/fixtures/listener_test.go new file mode 100644 index 000000000000..a34eb50120c6 --- /dev/null +++ b/galley/pkg/config/testing/fixtures/listener_test.go @@ -0,0 +1,39 @@ +// 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 fixtures + +import ( + "testing" + + "github.com/onsi/gomega" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/testing/data" +) + +func TestDispatcher(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + d := &Listener{} + + h1 := &Accumulator{} + d.Dispatch(h1) + + d.Handlers.Handle(data.Event1Col1AddItem1) + + expected := []event.Event{data.Event1Col1AddItem1} + g.Expect(h1.Events()).To(gomega.Equal(expected)) + +} diff --git a/galley/pkg/config/testing/fixtures/source.go b/galley/pkg/config/testing/fixtures/source.go new file mode 100644 index 000000000000..0a33b8e2e57e --- /dev/null +++ b/galley/pkg/config/testing/fixtures/source.go @@ -0,0 +1,45 @@ +// 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 fixtures + +import "istio.io/istio/galley/pkg/config/event" + +// Source is a test implementation of event.Source +type Source struct { + Handlers event.Handler + running bool +} + +var _ event.Source = &Source{} + +// Dispatch implements event.Dispatcher +func (s *Source) Dispatch(h event.Handler) { + s.Handlers = event.CombineHandlers(s.Handlers, h) +} + +// Start implements event.Source +func (s *Source) Start() { + s.running = true +} + +// Stop implements event.Source +func (s *Source) Stop() { + s.running = false +} + +// Running indicates whether the Source is currently running or not. +func (s *Source) Running() bool { + return s.running +} diff --git a/galley/pkg/config/testing/fixtures/source_test.go b/galley/pkg/config/testing/fixtures/source_test.go new file mode 100644 index 000000000000..e0fb1e83bb14 --- /dev/null +++ b/galley/pkg/config/testing/fixtures/source_test.go @@ -0,0 +1,45 @@ +// 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 fixtures + +import ( + "testing" + + "github.com/onsi/gomega" +) + +func TestSource(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + s := &Source{} + + s.Start() + g.Expect(s.running).To(gomega.BeTrue()) + + s.Stop() + g.Expect(s.running).To(gomega.BeFalse()) +} + +func TestSource_Dispatch(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + a := &Accumulator{} + + s := &Source{} + s.Dispatch(a) + s.Start() + + g.Expect(s.Handlers).To(gomega.Equal(a)) +} diff --git a/galley/pkg/config/testing/fixtures/transformer.go b/galley/pkg/config/testing/fixtures/transformer.go new file mode 100644 index 000000000000..31a5c4ccf56b --- /dev/null +++ b/galley/pkg/config/testing/fixtures/transformer.go @@ -0,0 +1,82 @@ +// 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 fixtures + +import ( + "istio.io/istio/galley/pkg/config/collection" + "istio.io/istio/galley/pkg/config/event" +) + +// Transformer implements event.Transformer for testing purposes. +type Transformer struct { + Handlers map[collection.Name]event.Handler + Started bool + InputCollections collection.Names + OutputCollections collection.Names + + fn func(tr *Transformer, e event.Event) +} + +var _ event.Transformer = &Transformer{} + +// NewTransformer returns a new fixture.Transformer. +func NewTransformer(inputs, outputs collection.Names, handlerFn func(tr *Transformer, e event.Event)) *Transformer { + return &Transformer{ + InputCollections: inputs, + OutputCollections: outputs, + Handlers: make(map[collection.Name]event.Handler), + fn: handlerFn, + } +} + +// Start implements event.Transformer +func (t *Transformer) Start() { + t.Started = true +} + +// Stop implements event.Transformer +func (t *Transformer) Stop() { + t.Started = false +} + +// Handle implements event.Transformer +func (t *Transformer) Handle(e event.Event) { + t.fn(t, e) +} + +// DispatchFor implements event.Transformer +func (t *Transformer) DispatchFor(c collection.Name, h event.Handler) { + handlers := t.Handlers[c] + handlers = event.CombineHandlers(handlers, h) + t.Handlers[c] = handlers +} + +// Inputs implements event.Transformer +func (t *Transformer) Inputs() collection.Names { + return t.InputCollections +} + +// Outputs implements event.Transformer +func (t *Transformer) Outputs() collection.Names { + return t.OutputCollections +} + +// Publish a message to the given collection +func (t *Transformer) Publish(c collection.Name, e event.Event) { + h := t.Handlers[c] + if h != nil { + h.Handle(e) + } +} diff --git a/galley/pkg/config/testing/k8smeta/collections.gen.go b/galley/pkg/config/testing/k8smeta/collections.gen.go new file mode 100755 index 000000000000..19b313bcf089 --- /dev/null +++ b/galley/pkg/config/testing/k8smeta/collections.gen.go @@ -0,0 +1,41 @@ +// GENERATED FILE -- DO NOT EDIT +// + +package k8smeta + +import ( + "istio.io/istio/galley/pkg/config/collection" +) + +var ( + + // K8SCoreV1Endpoints is the name of collection k8s/core/v1/endpoints + K8SCoreV1Endpoints = collection.NewName("k8s/core/v1/endpoints") + + // K8SCoreV1Namespaces is the name of collection k8s/core/v1/namespaces + K8SCoreV1Namespaces = collection.NewName("k8s/core/v1/namespaces") + + // K8SCoreV1Nodes is the name of collection k8s/core/v1/nodes + K8SCoreV1Nodes = collection.NewName("k8s/core/v1/nodes") + + // K8SCoreV1Pods is the name of collection k8s/core/v1/pods + K8SCoreV1Pods = collection.NewName("k8s/core/v1/pods") + + // K8SCoreV1Services is the name of collection k8s/core/v1/services + K8SCoreV1Services = collection.NewName("k8s/core/v1/services") + + // K8SExtensionsV1Beta1Ingresses is the name of collection k8s/extensions/v1beta1/ingresses + K8SExtensionsV1Beta1Ingresses = collection.NewName("k8s/extensions/v1beta1/ingresses") +) + +// CollectionNames returns the collection names declared in this package. +func CollectionNames() []collection.Name { + return []collection.Name{ + K8SCoreV1Endpoints, + K8SCoreV1Namespaces, + K8SCoreV1Nodes, + K8SCoreV1Pods, + K8SCoreV1Services, + K8SExtensionsV1Beta1Ingresses, + } +} diff --git a/galley/pkg/config/testing/k8smeta/gen.go b/galley/pkg/config/testing/k8smeta/gen.go new file mode 100644 index 000000000000..d7779de00f93 --- /dev/null +++ b/galley/pkg/config/testing/k8smeta/gen.go @@ -0,0 +1,27 @@ +// 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 k8smeta + +// Embed the core metadata file containing the collections as a resource +//go:generate go-bindata --nocompress --nometadata --pkg k8smeta -o k8smeta.gen.go k8smeta.yaml + +// Create static initializers file +//go:generate go run $GOPATH/src/istio.io/istio/galley/pkg/config/schema/codegen/tools/staticinit.main.go k8smeta k8smeta.yaml staticinit.gen.go + +// Create collection constants +//go:generate go run $GOPATH/src/istio.io/istio/galley/pkg/config/schema/codegen/tools/collections.main.go k8smeta k8smeta.yaml collections.gen.go + +//go:generate goimports -w -local istio.io "$GOPATH/src/istio.io/istio/galley/pkg/config/testing/k8smeta/collections.gen.go" +//go:generate goimports -w -local istio.io "$GOPATH/src/istio.io/istio/galley/pkg/config/testing/k8smeta/staticinit.gen.go" diff --git a/galley/pkg/config/testing/k8smeta/get.go b/galley/pkg/config/testing/k8smeta/get.go new file mode 100644 index 000000000000..2298e34397c0 --- /dev/null +++ b/galley/pkg/config/testing/k8smeta/get.go @@ -0,0 +1,45 @@ +// 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 k8smeta + +import ( + "fmt" + + "istio.io/istio/galley/pkg/config/schema" +) + +// Get returns the contained k8smeta.yaml file, in parsed form. +func Get() (*schema.Metadata, error) { + b, err := Asset("k8smeta.yaml") + if err != nil { + return nil, err + } + + m, err := schema.ParseAndBuild(string(b)) + if err != nil { + return nil, err + } + + return m, nil +} + +// MustGet calls GetBasicMeta and panics if it returns and error. +func MustGet() *schema.Metadata { + s, err := Get() + if err != nil { + panic(fmt.Sprintf("k8smeta.MustGet: %v", err)) + } + return s +} diff --git a/galley/pkg/config/testing/k8smeta/k8smeta.gen.go b/galley/pkg/config/testing/k8smeta/k8smeta.gen.go new file mode 100644 index 000000000000..1e3c1e45f632 --- /dev/null +++ b/galley/pkg/config/testing/k8smeta/k8smeta.gen.go @@ -0,0 +1,290 @@ +// Code generated by go-bindata. +// sources: +// k8smeta.yaml +// DO NOT EDIT! + +package k8smeta + +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 _k8smetaYaml = []byte(`# Copyright 2019 Istio Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in conformance 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. + + +collections: + # Built-in K8s collections + - name: "k8s/core/v1/endpoints" + proto: "k8s.io.api.core.v1.Endpoints" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/namespaces" + proto: "k8s.io.api.core.v1.NamespaceSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/nodes" + proto: "k8s.io.api.core.v1.NodeSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/pods" + proto: "k8s.io.api.core.v1.Pod" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/services" + proto: "k8s.io.api.core.v1.ServiceSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/extensions/v1beta1/ingresses" + proto: "k8s.io.api.extensions.v1beta1.IngressSpec" + protoPackage: "k8s.io/api/extensions/v1beta1" + +# Configuration for input sources +sources: + # Kubernetes specific configuration. + - type: kubernetes + resources: + - collection: "k8s/extensions/v1beta1/ingresses" + kind: "Ingress" + plural: "ingresses" + group: "extensions" + version: "v1beta1" + + - collection: "k8s/core/v1/services" + kind: "Service" + plural: "services" + version: "v1" + + - collection: "k8s/core/v1/namespaces" + kind: "Namespace" + plural: "namespaces" + version: "v1" + + - collection: "k8s/core/v1/nodes" + kind: "Node" + plural: "nodes" + version: "v1" + + - collection: "k8s/core/v1/pods" + kind: "Pod" + plural: "pods" + version: "v1" + + - collection: "k8s/core/v1/endpoints" + kind: "Endpoints" + plural: "endpoints" + version: "v1" + + +# Transform specific configurations +transforms: + - type: direct + mapping: +`) + +func k8smetaYamlBytes() ([]byte, error) { + return _k8smetaYaml, nil +} + +func k8smetaYaml() (*asset, error) { + bytes, err := k8smetaYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "k8smeta.yaml", 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){ + "k8smeta.yaml": k8smetaYaml, +} + +// 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{ + "k8smeta.yaml": &bintree{k8smetaYaml, 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/galley/pkg/config/testing/k8smeta/k8smeta.yaml b/galley/pkg/config/testing/k8smeta/k8smeta.yaml new file mode 100644 index 000000000000..c7e61af6399f --- /dev/null +++ b/galley/pkg/config/testing/k8smeta/k8smeta.yaml @@ -0,0 +1,82 @@ +# Copyright 2019 Istio Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in conformance 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. + + +collections: + # Built-in K8s collections + - name: "k8s/core/v1/endpoints" + proto: "k8s.io.api.core.v1.Endpoints" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/namespaces" + proto: "k8s.io.api.core.v1.NamespaceSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/nodes" + proto: "k8s.io.api.core.v1.NodeSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/pods" + proto: "k8s.io.api.core.v1.Pod" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/core/v1/services" + proto: "k8s.io.api.core.v1.ServiceSpec" + protoPackage: "k8s.io/api/core/v1" + + - name: "k8s/extensions/v1beta1/ingresses" + proto: "k8s.io.api.extensions.v1beta1.IngressSpec" + protoPackage: "k8s.io/api/extensions/v1beta1" + +# Configuration for input sources +sources: + # Kubernetes specific configuration. + - type: kubernetes + resources: + - collection: "k8s/extensions/v1beta1/ingresses" + kind: "Ingress" + plural: "ingresses" + group: "extensions" + version: "v1beta1" + + - collection: "k8s/core/v1/services" + kind: "Service" + plural: "services" + version: "v1" + + - collection: "k8s/core/v1/namespaces" + kind: "Namespace" + plural: "namespaces" + version: "v1" + + - collection: "k8s/core/v1/nodes" + kind: "Node" + plural: "nodes" + version: "v1" + + - collection: "k8s/core/v1/pods" + kind: "Pod" + plural: "pods" + version: "v1" + + - collection: "k8s/core/v1/endpoints" + kind: "Endpoints" + plural: "endpoints" + version: "v1" + + +# Transform specific configurations +transforms: + - type: direct + mapping: diff --git a/galley/pkg/config/testing/k8smeta/staticinit.gen.go b/galley/pkg/config/testing/k8smeta/staticinit.gen.go new file mode 100755 index 000000000000..441d13e10550 --- /dev/null +++ b/galley/pkg/config/testing/k8smeta/staticinit.gen.go @@ -0,0 +1,14 @@ +// GENERATED FILE -- DO NOT EDIT +// + +package k8smeta + +import ( + // Pull in all the known proto types to ensure we get their types registered. + + // Register protos in "k8s.io/api/core/v1" + _ "k8s.io/api/core/v1" + + // Register protos in "k8s.io/api/extensions/v1beta1" + _ "k8s.io/api/extensions/v1beta1" +) diff --git a/galley/pkg/config/util/kubeyaml/kubeyaml.go b/galley/pkg/config/util/kubeyaml/kubeyaml.go new file mode 100644 index 000000000000..e3e754fd6ec7 --- /dev/null +++ b/galley/pkg/config/util/kubeyaml/kubeyaml.go @@ -0,0 +1,93 @@ +// 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 kubeyaml + +import ( + "bytes" + "strings" +) + +const ( + yamlSeparator = "---\n" +) + +// Split the given yaml doc if it's multipart document. +func Split(yamlText []byte) [][]byte { + parts := bytes.Split(yamlText, []byte(yamlSeparator)) + var result [][]byte + for _, p := range parts { + if len(p) != 0 { + result = append(result, p) + } + } + return result +} + +// SplitString splits the given yaml doc if it's multipart document. +func SplitString(yamlText string) []string { + parts := strings.Split(yamlText, yamlSeparator) + var result []string + for _, p := range parts { + if len(p) != 0 { + result = append(result, p) + } + } + return result +} + +// Join the given yaml parts into a single multipart document. +func Join(parts ...[]byte) []byte { + var b bytes.Buffer + + var lastIsNewLine bool + for _, p := range parts { + if len(p) == 0 { + continue + } + if b.Len() != 0 { + if !lastIsNewLine { + _, _ = b.WriteString("\n") + } + b.WriteString(yamlSeparator) + } + _, _ = b.Write(p) + s := string(p) + lastIsNewLine = s[len(s)-1] == '\n' + } + + return b.Bytes() +} + +// JoinString joins the given yaml parts into a single multipart document. +func JoinString(parts ...string) string { + var st strings.Builder + + var lastIsNewLine bool + for _, p := range parts { + if len(p) == 0 { + continue + } + if st.Len() != 0 { + if !lastIsNewLine { + _, _ = st.WriteString("\n") + } + st.WriteString(yamlSeparator) + } + _, _ = st.WriteString(p) + lastIsNewLine = p[len(p)-1] == '\n' + } + + return st.String() +} diff --git a/galley/pkg/config/util/kubeyaml/kubeyaml_test.go b/galley/pkg/config/util/kubeyaml/kubeyaml_test.go new file mode 100644 index 000000000000..140edca17098 --- /dev/null +++ b/galley/pkg/config/util/kubeyaml/kubeyaml_test.go @@ -0,0 +1,179 @@ +// 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 kubeyaml + +import ( + "fmt" + "testing" + + . "github.com/onsi/gomega" +) + +var splitCases = []struct { + merged string + split []string +}{ + { + merged: "", + split: nil, + }, + { + merged: `yaml: foo`, + split: []string{ + `yaml: foo`, + }, + }, + { + merged: ` +yaml: foo +--- +bar: boo +`, + split: []string{ + ` +yaml: foo +`, + `bar: boo +`, + }, + }, + { + merged: ` +yaml: foo +--- +--- +bar: boo +`, + split: []string{ + ` +yaml: foo +`, + `bar: boo +`, + }, + }, +} + +func TestSplit(t *testing.T) { + for i, c := range splitCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + g := NewGomegaWithT(t) + + actual := Split([]byte(c.merged)) + + var exp [][]byte + for _, e := range c.split { + exp = append(exp, []byte(e)) + } + g.Expect(actual).To(Equal(exp)) + }) + } +} + +func TestSplitString(t *testing.T) { + for i, c := range splitCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + g := NewGomegaWithT(t) + + actual := SplitString(c.merged) + + g.Expect(actual).To(Equal(c.split)) + }) + } +} + +var joinCases = []struct { + merged string + split []string +}{ + { + merged: "", + split: nil, + }, + { + merged: `yaml: foo`, + split: []string{ + `yaml: foo`, + }, + }, + { + merged: ` +yaml: foo +--- +bar: boo +`, + split: []string{ + ` +yaml: foo +`, + `bar: boo +`, + }, + }, + { + merged: ` +yaml: foo +--- +bar: boo +`, + split: []string{ + ` +yaml: foo +`, + ``, + `bar: boo +`, + }, + }, + { + merged: ` +yaml: foo +--- +bar: boo`, + split: []string{ + ` +yaml: foo`, + `bar: boo`, + }, + }, +} + +func TestJoinBytes(t *testing.T) { + for i, c := range joinCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + g := NewGomegaWithT(t) + + var by [][]byte + for _, s := range c.split { + by = append(by, []byte(s)) + } + actual := Join(by...) + + g.Expect(actual).To(Equal([]byte(c.merged))) + }) + } +} + +func TestJoinString(t *testing.T) { + for i, c := range joinCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + g := NewGomegaWithT(t) + + actual := JoinString(c.split...) + + g.Expect(actual).To(Equal(c.merged)) + }) + } +} diff --git a/galley/pkg/config/util/pb/proto.go b/galley/pkg/config/util/pb/proto.go new file mode 100644 index 000000000000..c42d3382d996 --- /dev/null +++ b/galley/pkg/config/util/pb/proto.go @@ -0,0 +1,44 @@ +// 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 pb + +import ( + "github.com/ghodss/yaml" + "github.com/gogo/protobuf/jsonpb" + "github.com/gogo/protobuf/proto" + yaml2 "gopkg.in/yaml.v2" +) + +// UnmarshalData data into the proto. +func UnmarshalData(pb proto.Message, data interface{}) error { + js, err := toJSON(data) + if err == nil { + err = jsonpb.UnmarshalString(js, pb) + } + return err +} + +func toJSON(data interface{}) (string, error) { + + var result string + b, err := yaml2.Marshal(data) + if err == nil { + if b, err = yaml.YAMLToJSON(b); err == nil { + result = string(b) + } + } + + return result, err +} diff --git a/galley/pkg/config/util/pb/proto_test.go b/galley/pkg/config/util/pb/proto_test.go new file mode 100644 index 000000000000..599d845d1510 --- /dev/null +++ b/galley/pkg/config/util/pb/proto_test.go @@ -0,0 +1,59 @@ +// 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 pb + +import ( + "testing" + + gogoTypes "github.com/gogo/protobuf/types" + . "github.com/onsi/gomega" +) + +func TestToProto_Success(t *testing.T) { + g := NewGomegaWithT(t) + + data := map[string]interface{}{ + "foo": "bar", + "boo": "baz", + } + + p := &gogoTypes.Struct{} + err := UnmarshalData(p, data) + g.Expect(err).To(BeNil()) + expected := &gogoTypes.Struct{ + Fields: map[string]*gogoTypes.Value{ + "foo": { + Kind: &gogoTypes.Value_StringValue{StringValue: "bar"}, + }, + "boo": { + Kind: &gogoTypes.Value_StringValue{StringValue: "baz"}, + }, + }, + } + + g.Expect(p).To(Equal(expected)) +} + +func TestToProto_Error(t *testing.T) { + g := NewGomegaWithT(t) + + data := map[string]interface{}{ + "value": 23, + } + + p := &gogoTypes.Any{} + err := UnmarshalData(p, data) + g.Expect(err).NotTo(BeNil()) +} diff --git a/galley/pkg/runtime/distributor.go b/galley/pkg/runtime/distributor.go index 9e99dd530202..5e3b60d3ff77 100644 --- a/galley/pkg/runtime/distributor.go +++ b/galley/pkg/runtime/distributor.go @@ -29,7 +29,7 @@ type Distributor interface { // InMemoryDistributor is an in-memory distributor implementation. type InMemoryDistributor struct { - snapshotsLock sync.Mutex + snapshotsLock sync.RWMutex snapshots map[string]sn.Snapshot listenersLock sync.Mutex listeners []*listenerEntry @@ -73,8 +73,8 @@ func (d *InMemoryDistributor) ClearSnapshot(name string) { // GetSnapshot get the snapshot of the specified name func (d *InMemoryDistributor) GetSnapshot(name string) sn.Snapshot { - d.snapshotsLock.Lock() - defer d.snapshotsLock.Unlock() + d.snapshotsLock.RLock() + defer d.snapshotsLock.RUnlock() if s, ok := d.snapshots[name]; ok { return s } @@ -83,6 +83,8 @@ func (d *InMemoryDistributor) GetSnapshot(name string) sn.Snapshot { // NumSnapshots returns the current number of snapshots. func (d *InMemoryDistributor) NumSnapshots() int { + d.snapshotsLock.RLock() + defer d.snapshotsLock.RUnlock() return len(d.snapshots) } diff --git a/galley/pkg/server/components/patchtable.go b/galley/pkg/server/components/patchtable.go index 653e5364c58b..76785323a4f9 100644 --- a/galley/pkg/server/components/patchtable.go +++ b/galley/pkg/server/components/patchtable.go @@ -20,6 +20,11 @@ import ( "istio.io/pkg/filewatcher" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/meshcfg" + "istio.io/istio/galley/pkg/config/processor" + check2 "istio.io/istio/galley/pkg/config/source/kube/check" + fs2 "istio.io/istio/galley/pkg/config/source/kube/fs" "istio.io/istio/galley/pkg/meshconfig" "istio.io/istio/galley/pkg/source/fs" kubeSource "istio.io/istio/galley/pkg/source/kube" @@ -40,6 +45,11 @@ var ( newMeshConfigCache = func(path string) (meshconfig.Cache, error) { return meshconfig.NewCacheFromFile(path) } newFileWatcher = filewatcher.NewWatcher readFile = ioutil.ReadFile + + meshcfgNewFS = func(path string) (event.Source, error) { return meshcfg.NewFS(path) } + processorInitialize = processor.Initialize + checkResourceTypesPresence = check2.ResourceTypesPresence + fsNew2 = fs2.New ) func resetPatchTable() { @@ -53,4 +63,9 @@ func resetPatchTable() { newMeshConfigCache = func(path string) (meshconfig.Cache, error) { return meshconfig.NewCacheFromFile(path) } newFileWatcher = filewatcher.NewWatcher readFile = ioutil.ReadFile + + meshcfgNewFS = func(path string) (event.Source, error) { return meshcfg.NewFS(path) } + processorInitialize = processor.Initialize + checkResourceTypesPresence = check2.ResourceTypesPresence + fsNew2 = fs2.New } diff --git a/galley/pkg/server/components/processing2.go b/galley/pkg/server/components/processing2.go new file mode 100644 index 000000000000..2ed9c7acd9f8 --- /dev/null +++ b/galley/pkg/server/components/processing2.go @@ -0,0 +1,357 @@ +// 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 components + +import ( + "fmt" + "net" + "strings" + "sync" + "time" + + "golang.org/x/time/rate" + "google.golang.org/grpc" + "google.golang.org/grpc/keepalive" + grpcMetadata "google.golang.org/grpc/metadata" + + mcp "istio.io/api/mcp/v1alpha1" + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/processing" + "istio.io/istio/galley/pkg/config/processor/metadata" + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/source/kube" + "istio.io/istio/galley/pkg/config/source/kube/apiserver" + "istio.io/istio/galley/pkg/config/source/kube/rt" + "istio.io/istio/galley/pkg/runtime/groups" + "istio.io/istio/galley/pkg/server/process" + "istio.io/istio/galley/pkg/server/settings" + 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" + "istio.io/istio/pkg/mcp/server" + "istio.io/istio/pkg/mcp/snapshot" + "istio.io/istio/pkg/mcp/source" + "istio.io/pkg/ctrlz/fw" + "istio.io/pkg/log" + "istio.io/pkg/version" +) + +// Processing2 component is the main config processing component that will listen to a config source and publish +// resources through an MCP server, or a dialout connection. +type Processing2 struct { + args *settings.Args + + distributor *snapshot.Cache + configzTopic fw.Topic + + serveWG sync.WaitGroup + grpcServer *grpc.Server + runtime *processing.Runtime + mcpSource *source.Server + reporter monitoring.Reporter + callOut *callout + listenerMutex sync.Mutex + listener net.Listener + stopCh chan struct{} +} + +var _ process.Component = &Processing2{} + +// NewProcessing2 returns a new processing component. +func NewProcessing2(a *settings.Args) *Processing2 { + d := snapshot.New(groups.IndexFunction) + return &Processing2{ + args: a, + distributor: d, + configzTopic: configz.CreateTopic(d), + } +} + +// Start implements process.Component +func (p *Processing2) Start() (err error) { + var mesh event.Source + var src event.Source + + if mesh, err = meshcfgNewFS(p.args.MeshConfigFile); err != nil { + return + } + + m := metadata.MustGet() + + kubeResources := p.disableExcludedKubeResources(m) + + if src, err = p.createSource(kubeResources); err != nil { + return + } + + if p.runtime, err = processorInitialize(m, p.args.DomainSuffix, event.CombineSources(mesh, src), p.distributor); err != nil { + return + } + + grpcOptions := p.getServerGrpcOptions() + + p.stopCh = make(chan struct{}) + var checker source.AuthChecker = server.NewAllowAllChecker() + if !p.args.Insecure { + if checker, err = watchAccessList(p.stopCh, p.args.AccessListFile); err != nil { + return + } + + var watcher creds.CertificateWatcher + if watcher, err = creds.PollFiles(p.stopCh, p.args.CredentialOptions); err != nil { + return + } + credentials := creds.CreateForServer(watcher) + + grpcOptions = append(grpcOptions, grpc.Creds(credentials)) + } + grpc.EnableTracing = p.args.EnableGRPCTracing + p.grpcServer = grpc.NewServer(grpcOptions...) + + p.reporter = mcpMetricReporter("galley/mcp/source") + + options := &source.Options{ + Watcher: p.distributor, + Reporter: p.reporter, + CollectionsOptions: source.CollectionOptionsFromSlice(m.AllCollectionsInSnapshots()), + ConnRateLimiter: mcprate.NewRateLimiter(time.Second, 100), // TODO(Nino-K): https://github.com/istio/istio/issues/12074 + } + + md := grpcMetadata.MD{ + versionMetadataKey: []string{version.Info.Version}, + } + if err := parseSinkMeta(p.args.SinkMeta, md); err != nil { + return err + } + + if p.args.SinkAddress != "" { + p.callOut, err = newCallout(p.args.SinkAddress, p.args.SinkAuthMode, md, options) + if err != nil { + p.callOut = nil + err = fmt.Errorf("callout could not be initialized: %v", err) + return + } + } + + serverOptions := &source.ServerOptions{ + AuthChecker: checker, + RateLimiter: rate.NewLimiter(rate.Every(time.Second), 100), // TODO(Nino-K): https://github.com/istio/istio/issues/12074 + Metadata: md, + } + + p.mcpSource = source.NewServer(options, serverOptions) + + // get the network stuff setup + network := "tcp" + var address string + idx := strings.Index(p.args.APIAddress, "://") + if idx < 0 { + address = p.args.APIAddress + } else { + network = p.args.APIAddress[:idx] + address = p.args.APIAddress[idx+3:] + } + + if p.listener, err = netListen(network, address); err != nil { + err = fmt.Errorf("unable to listen: %v", err) + return + } + + mcp.RegisterResourceSourceServer(p.grpcServer, p.mcpSource) + + var startWG sync.WaitGroup + startWG.Add(1) + + p.serveWG.Add(1) + go func() { + defer p.serveWG.Done() + p.runtime.Start() + + l := p.getListener() + if l != nil { + // start serving + gs := p.grpcServer + startWG.Done() + err = gs.Serve(l) + if err != nil { + scope.Errorf("Galley Server unexpectedly terminated: %v", err) + } + } + }() + + if p.callOut != nil { + p.serveWG.Add(1) + go func() { + defer p.serveWG.Done() + p.callOut.run() + }() + } + + startWG.Wait() + + return nil +} + +func (p *Processing2) disableExcludedKubeResources(m *schema.Metadata) schema.KubeResources { + + // Behave in the same way as existing logic: + // - Builtin types are excluded by default. + // - If ServiceDiscovery is enabled, any built-in type should be readded. + + var result schema.KubeResources + for _, r := range m.KubeSource().Resources() { + + if p.isKindExcluded(r.Kind) { + // Found a matching exclude directive for this KubeResource. Disable the resource. + r.Disabled = true + + // Check and see if this is needed for Service Discovery. If needed, we will need to re-enable. + if p.args.EnableServiceDiscovery { + // IsBuiltIn is a proxy for types needed for service discovery + a := rt.DefaultProvider().GetAdapter(r) + if a.IsBuiltIn() { + // This is needed for service discovery. Re-enable. + r.Disabled = false + } + } + } + + result = append(result, r) + } + + return result +} + +// ConfigZTopic returns the ConfigZTopic for the processor. +func (p *Processing2) ConfigZTopic() fw.Topic { + return p.configzTopic +} + +func (p *Processing2) getServerGrpcOptions() []grpc.ServerOption { + var grpcOptions []grpc.ServerOption + grpcOptions = append(grpcOptions, + grpc.MaxConcurrentStreams(uint32(p.args.MaxConcurrentStreams)), + grpc.MaxRecvMsgSize(int(p.args.MaxReceivedMessageSize)), + grpc.InitialWindowSize(int32(p.args.InitialWindowSize)), + grpc.InitialConnWindowSize(int32(p.args.InitialConnectionWindowSize)), + grpc.KeepaliveParams(keepalive.ServerParameters{ + Timeout: p.args.KeepAlive.Timeout, + Time: p.args.KeepAlive.Time, + MaxConnectionAge: p.args.KeepAlive.MaxServerConnectionAge, + MaxConnectionAgeGrace: p.args.KeepAlive.MaxServerConnectionAgeGrace, + }), + // Relax keepalive enforcement policy requirements to avoid dropping connections due to too many pings. + grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ + MinTime: 30 * time.Second, + PermitWithoutStream: true, + }), + ) + + return grpcOptions +} + +func (p *Processing2) createSource(resources schema.KubeResources) (src event.Source, err error) { + if p.args.ConfigPath != "" { + if src, err = fsNew2(p.args.ConfigPath, resources); err != nil { + return + } + } else { + var k kube.Interfaces + if k, err = newKubeFromConfigFile(p.args.KubeConfig); err != nil { + return + } + if !p.args.DisableResourceReadyCheck { + if err = checkResourceTypesPresence(k, resources); err != nil { + return + } + } + + o := apiserver.Options{ + Client: k, + ResyncPeriod: p.args.ResyncPeriod, + Resources: resources, + } + src = apiserver.New(o) + } + return +} + +func (p *Processing2) isKindExcluded(kind string) bool { + for _, excludedKind := range p.args.ExcludedResourceKinds { + if kind == excludedKind { + return true + } + } + + return false +} + +// Stop implements process.Component +func (p *Processing2) Stop() { + if p.stopCh != nil { + close(p.stopCh) + p.stopCh = nil + } + + if p.grpcServer != nil { + p.grpcServer.GracefulStop() + p.grpcServer = nil + } + + if p.runtime != nil { + p.runtime.Stop() + p.runtime = nil + } + + p.listenerMutex.Lock() + if p.listener != nil { + _ = p.listener.Close() + p.listener = nil + } + p.listenerMutex.Unlock() + + if p.reporter != nil { + _ = p.reporter.Close() + p.reporter = nil + } + + if p.callOut != nil { + p.callOut.stop() + p.callOut = nil + } + + if p.grpcServer != nil || p.callOut != nil { + p.serveWG.Wait() + } + + // final attempt to purge buffered logs + _ = log.Sync() +} + +func (p *Processing2) getListener() net.Listener { + p.listenerMutex.Lock() + defer p.listenerMutex.Unlock() + return p.listener +} + +// Address returns the Address of the MCP service. +func (p *Processing2) Address() net.Addr { + l := p.getListener() + if l == nil { + return nil + } + return l.Addr() +} diff --git a/galley/pkg/server/components/processing2_test.go b/galley/pkg/server/components/processing2_test.go new file mode 100644 index 000000000000..91cafda669b8 --- /dev/null +++ b/galley/pkg/server/components/processing2_test.go @@ -0,0 +1,138 @@ +// 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 components + +import ( + "fmt" + "io/ioutil" + "net" + "os" + "path" + "testing" + + . "github.com/onsi/gomega" + k8sRuntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic/fake" + + "istio.io/istio/galley/pkg/config/event" + "istio.io/istio/galley/pkg/config/meshcfg" + "istio.io/istio/galley/pkg/config/processing" + "istio.io/istio/galley/pkg/config/processing/snapshotter" + "istio.io/istio/galley/pkg/config/schema" + "istio.io/istio/galley/pkg/config/source/kube" + "istio.io/istio/galley/pkg/server/settings" + "istio.io/istio/galley/pkg/source/kube/client" + "istio.io/istio/galley/pkg/testing/mock" + "istio.io/istio/pkg/mcp/monitoring" + mcptestmon "istio.io/istio/pkg/mcp/testing/monitoring" +) + +func TestProcessing2_StartErrors(t *testing.T) { + g := NewGomegaWithT(t) + defer resetPatchTable() +loop: + for i := 0; ; i++ { + resetPatchTable() + mk := mock.NewKube() + newKubeFromConfigFile = func(string) (client.Interfaces, error) { return mk, nil } + checkResourceTypesPresence = func(_ kube.Interfaces, _ schema.KubeResources) error { return nil } + + e := fmt.Errorf("err%d", i) + + tmpDir, err := ioutil.TempDir(os.TempDir(), t.Name()) + g.Expect(err).To(BeNil()) + + meshCfgDir := path.Join(tmpDir, "meshcfg") + err = os.Mkdir(meshCfgDir, os.ModePerm) + g.Expect(err).To(BeNil()) + + meshCfgFile := path.Join(tmpDir, "meshcfg.yaml") + _, err = os.Create(meshCfgFile) + g.Expect(err).To(BeNil()) + + args := settings.DefaultArgs() + args.APIAddress = "tcp://0.0.0.0:0" + args.Insecure = true + args.MeshConfigFile = meshCfgFile + + switch i { + case 0: + newKubeFromConfigFile = func(string) (client.Interfaces, error) { return nil, e } + case 1: + meshcfgNewFS = func(path string) (event.Source, error) { return nil, e } + case 2: + processorInitialize = func(_ *schema.Metadata, _ string, _ event.Source, _ snapshotter.Distributor) (*processing.Runtime, error) { + return nil, e + } + case 3: + args.Insecure = false + args.AccessListFile = os.TempDir() + case 4: + args.Insecure = false + args.AccessListFile = "invalid file" + case 5: + args.SinkAddress = "localhost:8080" + args.SinkAuthMode = "foo" + case 6: + netListen = func(network, address string) (net.Listener, error) { return nil, e } + case 7: + args.DisableResourceReadyCheck = false + checkResourceTypesPresence = func(_ kube.Interfaces, _ schema.KubeResources) error { return e } + case 8: + args.ConfigPath = "aaa" + fsNew2 = func(_ string, _ schema.KubeResources) (event.Source, error) { return nil, e } + default: + break loop + + } + + p := NewProcessing2(args) + err = p.Start() + g.Expect(err).NotTo(BeNil()) + t.Logf("%d) err: %v", i, err) + p.Stop() + } +} + +func TestProcessing2_Basic(t *testing.T) { + g := NewGomegaWithT(t) + resetPatchTable() + defer resetPatchTable() + + mk := mock.NewKube() + cl := fake.NewSimpleDynamicClient(k8sRuntime.NewScheme()) + + mk.AddResponse(cl, nil) + newKubeFromConfigFile = func(string) (client.Interfaces, error) { return mk, nil } + mcpMetricReporter = func(s string) monitoring.Reporter { + return mcptestmon.NewInMemoryStatsContext() + } + checkResourceTypesPresence = func(_ kube.Interfaces, _ schema.KubeResources) error { return nil } + meshcfgNewFS = func(path string) (event.Source, error) { return meshcfg.NewInmemory(), nil } + + args := settings.DefaultArgs() + args.APIAddress = "tcp://0.0.0.0:0" + args.Insecure = true + + p := NewProcessing2(args) + err := p.Start() + g.Expect(err).To(BeNil()) + + g.Expect(p.Address()).NotTo(BeNil()) + + p.Stop() + + g.Expect(p.Address()).To(BeNil()) +} diff --git a/galley/pkg/server/components/processing_test.go b/galley/pkg/server/components/processing_test.go index a09683605b96..06c2bcefd8ac 100644 --- a/galley/pkg/server/components/processing_test.go +++ b/galley/pkg/server/components/processing_test.go @@ -81,6 +81,9 @@ loop: fsNew = func(string, *schema.Instance, *converter.Config) (runtime.Source, error) { return nil, e } case 5: args.DisableResourceReadyCheck = true + findSupportedResources = func(k client.Interfaces, specs []sourceSchema.ResourceSpec) ([]sourceSchema.ResourceSpec, error) { + return nil, e + } case 6: args.Insecure = false args.AccessListFile = os.TempDir() diff --git a/galley/pkg/server/server.go b/galley/pkg/server/server.go index 41e7553a34ad..d336e8eaf6be 100644 --- a/galley/pkg/server/server.go +++ b/galley/pkg/server/server.go @@ -28,7 +28,8 @@ import ( type Server struct { host process.Host - p *components.Processing + p *components.Processing + p2 *components.Processing2 } // New returns a new instance of a Server. @@ -47,10 +48,17 @@ func New(a *settings.Args) *Server { s.host.Add(validation) if a.EnableServer { - s.p = components.NewProcessing(a) - s.host.Add(s.p) - t := s.p.ConfigZTopic() - topics = append(topics, t) + if a.UseOldProcessor { + s.p = components.NewProcessing(a) + s.host.Add(s.p) + t := s.p.ConfigZTopic() + topics = append(topics, t) + } else { + s.p2 = components.NewProcessing2(a) + s.host.Add(s.p2) + t := s.p2.ConfigZTopic() + topics = append(topics, t) + } } mon := components.NewMonitoring(a.MonitoringPort) @@ -69,7 +77,11 @@ func New(a *settings.Args) *Server { // Address returns the address of the config processing server. func (s *Server) Address() net.Addr { - return s.p.Address() + if s.p != nil { + return s.p.Address() + } + return s.p2.Address() + } // Start the process. diff --git a/galley/pkg/server/settings/args.go b/galley/pkg/server/settings/args.go index 8946572b1974..1a47742dedc2 100644 --- a/galley/pkg/server/settings/args.go +++ b/galley/pkg/server/settings/args.go @@ -1,4 +1,4 @@ -// Copyright 2019 Istio Authors +// 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. @@ -123,9 +123,11 @@ type Args struct { // nolint:maligned MonitoringPort uint EnableProfiling bool PprofPort uint + + UseOldProcessor bool } -// DefaultArgs allocates an Args struct initialized with Mixer's default configuration. +// DefaultArgs allocates an Args struct initialized with Galley's default configuration. func DefaultArgs() *Args { return &Args{ ResyncPeriod: 0, @@ -151,6 +153,7 @@ func DefaultArgs() *Args { MonitoringPort: 15014, EnableProfiling: false, PprofPort: 9094, + UseOldProcessor: true, Liveness: probe.Options{ Path: defaultLivenessProbeFilePath, UpdateInterval: defaultProbeCheckInterval, diff --git a/galley/pkg/source/kube/tombstone/recover.go b/galley/pkg/source/kube/tombstone/tombstone.go similarity index 100% rename from galley/pkg/source/kube/tombstone/recover.go rename to galley/pkg/source/kube/tombstone/tombstone.go diff --git a/galley/pkg/source/kube/tombstone/recover_test.go b/galley/pkg/source/kube/tombstone/tombstone_test.go similarity index 100% rename from galley/pkg/source/kube/tombstone/recover_test.go rename to galley/pkg/source/kube/tombstone/tombstone_test.go diff --git a/galley/pkg/testing/mock/extensionsv1.go b/galley/pkg/testing/mock/extensionsv1.go new file mode 100644 index 000000000000..d75f159be792 --- /dev/null +++ b/galley/pkg/testing/mock/extensionsv1.go @@ -0,0 +1,49 @@ +// 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 mock + +import ( + extensionsv1 "k8s.io/client-go/kubernetes/typed/extensions/v1beta1" + "k8s.io/client-go/rest" +) + +type extensionsv1Impl struct { + ingresses extensionsv1.IngressInterface +} + +var _ extensionsv1.ExtensionsV1beta1Interface = &extensionsv1Impl{} + +func (e *extensionsv1Impl) Ingresses(namespace string) extensionsv1.IngressInterface { + return e.ingresses +} + +func (e *extensionsv1Impl) RESTClient() rest.Interface { + panic("not implemented") +} + +func (e *extensionsv1Impl) DaemonSets(namespace string) extensionsv1.DaemonSetInterface { + panic("not implemented") +} + +func (e *extensionsv1Impl) Deployments(namespace string) extensionsv1.DeploymentInterface { + panic("not implemented") +} +func (e *extensionsv1Impl) PodSecurityPolicies() extensionsv1.PodSecurityPolicyInterface { + panic("not implemented") +} + +func (e *extensionsv1Impl) ReplicaSets(namespace string) extensionsv1.ReplicaSetInterface { + panic("not implemented") +} diff --git a/galley/pkg/testing/mock/ingresses.go b/galley/pkg/testing/mock/ingresses.go new file mode 100644 index 000000000000..ba23c4d04081 --- /dev/null +++ b/galley/pkg/testing/mock/ingresses.go @@ -0,0 +1,135 @@ +// 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 mock + +import ( + "fmt" + "sync" + + "k8s.io/api/extensions/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + extensionsv1 "k8s.io/client-go/kubernetes/typed/extensions/v1beta1" +) + +var _ extensionsv1.IngressInterface = &ingressImpl{} + +type ingressImpl struct { + mux sync.Mutex + ingresses map[string]*v1beta1.Ingress + watches Watches +} + +func newIngressInterface() extensionsv1.IngressInterface { + return &ingressImpl{ + ingresses: make(map[string]*v1beta1.Ingress), + } +} + +func (i *ingressImpl) Create(obj *v1beta1.Ingress) (*v1beta1.Ingress, error) { + i.mux.Lock() + defer i.mux.Unlock() + + i.ingresses[obj.Name] = obj + + i.watches.Send(watch.Event{ + Type: watch.Added, + Object: obj, + }) + return obj, nil +} + +func (i *ingressImpl) Update(obj *v1beta1.Ingress) (*v1beta1.Ingress, error) { + i.mux.Lock() + defer i.mux.Unlock() + + i.ingresses[obj.Name] = obj + + i.watches.Send(watch.Event{ + Type: watch.Modified, + Object: obj, + }) + return obj, nil +} + +func (i *ingressImpl) Delete(name string, options *v1.DeleteOptions) error { + i.mux.Lock() + defer i.mux.Unlock() + + obj := i.ingresses[name] + if obj == nil { + return fmt.Errorf("unable to delete ingress %s", name) + } + + delete(i.ingresses, name) + + i.watches.Send(watch.Event{ + Type: watch.Deleted, + Object: obj, + }) + return nil +} + +func (i *ingressImpl) List(opts v1.ListOptions) (*v1beta1.IngressList, error) { + i.mux.Lock() + defer i.mux.Unlock() + + out := &v1beta1.IngressList{} + + for _, v := range i.ingresses { + out.Items = append(out.Items, *v) + } + + return out, nil +} + +func (i *ingressImpl) Watch(opts v1.ListOptions) (watch.Interface, error) { + i.mux.Lock() + defer i.mux.Unlock() + + w := NewWatch() + i.watches = append(i.watches, w) + + // Send add events for all current resources. + for _, i := range i.ingresses { + w.Send(watch.Event{ + Type: watch.Added, + Object: i, + }) + } + + return w, nil +} + +func (i *ingressImpl) UpdateStatus(*v1beta1.Ingress) (*v1beta1.Ingress, error) { + panic("not implemented") + +} + +func (i *ingressImpl) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + panic("not implemented") + +} + +func (i *ingressImpl) Get(name string, options v1.GetOptions) (*v1beta1.Ingress, error) { + panic("not implemented") + +} + +func (i *ingressImpl) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.Ingress, err error) { + panic("not implemented") + +} diff --git a/galley/pkg/testing/mock/kube.go b/galley/pkg/testing/mock/kube.go index 6687397c3a01..7cb9aa0d597b 100644 --- a/galley/pkg/testing/mock/kube.go +++ b/galley/pkg/testing/mock/kube.go @@ -15,11 +15,10 @@ package mock import ( - "errors" - "istio.io/istio/galley/pkg/source/kube/client" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" ) @@ -31,13 +30,15 @@ type Kube struct { response1 []interface{} response2 []error - client kubernetes.Interface + client kubernetes.Interface + APIExtClientSet *fake.Clientset } // NewKube returns a new instance of mock Kube. func NewKube() *Kube { return &Kube{ - client: newKubeInterface(), + client: newKubeInterface(), + APIExtClientSet: fake.NewSimpleClientset(), } } @@ -65,11 +66,12 @@ func (k *Kube) AddResponse(r1 interface{}, r2 error) { k.response2 = append(k.response2, r2) } -// APIExtensionsClientset returns a new apiextensions clientset +// APIExtensionsClientset implements client.Interfaces func (k *Kube) APIExtensionsClientset() (clientset.Interface, error) { - return nil, errors.New("not supported") + return k.APIExtClientSet, nil } +// KubeClient implements client.Interfaces func (k *Kube) KubeClient() (kubernetes.Interface, error) { return k.client, nil } diff --git a/galley/pkg/testing/mock/kube_interface.go b/galley/pkg/testing/mock/kube_interface.go index be3933da2912..979b542667ec 100644 --- a/galley/pkg/testing/mock/kube_interface.go +++ b/galley/pkg/testing/mock/kube_interface.go @@ -54,7 +54,8 @@ import ( var _ kubernetes.Interface = &kubeInterface{} type kubeInterface struct { - core corev1.CoreV1Interface + core corev1.CoreV1Interface + extensions extensionsv1beta1.ExtensionsV1beta1Interface } // newKubeInterface returns a lightweight fake that implements kubernetes.Interface. Only implements a portion of the @@ -70,6 +71,10 @@ func newKubeInterface() kubernetes.Interface { endpoints: newEndpointsInterface(), namespaces: newNamespaceInterface(), }, + + extensions: &extensionsv1Impl{ + ingresses: newIngressInterface(), + }, } } @@ -202,7 +207,7 @@ func (c *kubeInterface) Events() eventsv1beta1.EventsV1beta1Interface { } func (c *kubeInterface) ExtensionsV1beta1() extensionsv1beta1.ExtensionsV1beta1Interface { - panic("not implemented") + return c.extensions } func (c *kubeInterface) Extensions() extensionsv1beta1.ExtensionsV1beta1Interface { diff --git a/install/kubernetes/helm/istio/charts/galley/templates/clusterrole.yaml b/install/kubernetes/helm/istio/charts/galley/templates/clusterrole.yaml index 6385c888298b..77d362862c6f 100644 --- a/install/kubernetes/helm/istio/charts/galley/templates/clusterrole.yaml +++ b/install/kubernetes/helm/istio/charts/galley/templates/clusterrole.yaml @@ -37,3 +37,6 @@ rules: resources: ["deployments/finalizers"] resourceNames: ["istio-galley"] verbs: ["update"] +- apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch"]