1// Copyright 2017 Istio Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package model
16
17import (
18	"fmt"
19	"sort"
20	"strings"
21	"time"
22
23	"istio.io/pkg/ledger"
24
25	udpa "github.com/cncf/udpa/go/udpa/type/v1"
26	"github.com/gogo/protobuf/proto"
27
28	mccpb "istio.io/api/mixer/v1/config/client"
29	networking "istio.io/api/networking/v1alpha3"
30
31	"istio.io/istio/pkg/config/constants"
32	"istio.io/istio/pkg/config/host"
33	"istio.io/istio/pkg/config/labels"
34	"istio.io/istio/pkg/config/schema/collection"
35	"istio.io/istio/pkg/config/schema/collections"
36	"istio.io/istio/pkg/config/schema/resource"
37)
38
39var (
40	// Statically link protobuf descriptors from UDPA
41	_ = udpa.TypedStruct{}
42)
43
44// ConfigKey describe a specific config item.
45// In most cases, the name is the config's name. However, for ServiceEntry it is service's FQDN.
46type ConfigKey struct {
47	Kind      resource.GroupVersionKind
48	Name      string
49	Namespace string
50}
51
52// ConfigsOfKind extracts configs of the specified kind.
53func ConfigsOfKind(configs map[ConfigKey]struct{}, kind resource.GroupVersionKind) map[ConfigKey]struct{} {
54	ret := make(map[ConfigKey]struct{})
55
56	for conf := range configs {
57		if conf.Kind == kind {
58			ret[conf] = struct{}{}
59		}
60	}
61
62	return ret
63}
64
65// ConfigNamesOfKind extracts config names of the specified kind.
66func ConfigNamesOfKind(configs map[ConfigKey]struct{}, kind resource.GroupVersionKind) map[string]struct{} {
67	ret := make(map[string]struct{})
68
69	for conf := range configs {
70		if conf.Kind == kind {
71			ret[conf.Name] = struct{}{}
72		}
73	}
74
75	return ret
76}
77
78// ConfigMeta is metadata attached to each configuration unit.
79// The revision is optional, and if provided, identifies the
80// last update operation on the object.
81type ConfigMeta struct {
82	// Type is a short configuration name that matches the content message type
83	// (e.g. "route-rule")
84	Type string `json:"type,omitempty"`
85
86	// Group is the API group of the config.
87	Group string `json:"group,omitempty"`
88
89	// Version is the API version of the Config.
90	Version string `json:"version,omitempty"`
91
92	// Name is a unique immutable identifier in a namespace
93	Name string `json:"name,omitempty"`
94
95	// Namespace defines the space for names (optional for some types),
96	// applications may choose to use namespaces for a variety of purposes
97	// (security domains, fault domains, organizational domains)
98	Namespace string `json:"namespace,omitempty"`
99
100	// Domain defines the suffix of the fully qualified name past the namespace.
101	// Domain is not a part of the unique key unlike name and namespace.
102	Domain string `json:"domain,omitempty"`
103
104	// Map of string keys and values that can be used to organize and categorize
105	// (scope and select) objects.
106	Labels map[string]string `json:"labels,omitempty"`
107
108	// Annotations is an unstructured key value map stored with a resource that may be
109	// set by external tools to store and retrieve arbitrary metadata. They are not
110	// queryable and should be preserved when modifying objects.
111	Annotations map[string]string `json:"annotations,omitempty"`
112
113	// ResourceVersion is an opaque identifier for tracking updates to the config registry.
114	// The implementation may use a change index or a commit log for the revision.
115	// The config client should not make any assumptions about revisions and rely only on
116	// exact equality to implement optimistic concurrency of read-write operations.
117	//
118	// The lifetime of an object of a particular revision depends on the underlying data store.
119	// The data store may compactify old revisions in the interest of storage optimization.
120	//
121	// An empty revision carries a special meaning that the associated object has
122	// not been stored and assigned a revision.
123	ResourceVersion string `json:"resourceVersion,omitempty"`
124
125	// CreationTimestamp records the creation time
126	CreationTimestamp time.Time `json:"creationTimestamp,omitempty"`
127}
128
129func (c *Config) GroupVersionKind() resource.GroupVersionKind {
130	return resource.GroupVersionKind{
131		Group:   c.Group,
132		Version: c.Version,
133		Kind:    c.Type,
134	}
135}
136
137// Config is a configuration unit consisting of the type of configuration, the
138// key identifier that is unique per type, and the content represented as a
139// protobuf message.
140type Config struct {
141	ConfigMeta
142
143	// Spec holds the configuration object as a gogo protobuf message
144	Spec proto.Message
145}
146
147// ConfigStore describes a set of platform agnostic APIs that must be supported
148// by the underlying platform to store and retrieve Istio configuration.
149//
150// Configuration key is defined to be a combination of the type, name, and
151// namespace of the configuration object. The configuration key is guaranteed
152// to be unique in the store.
153//
154// The storage interface presented here assumes that the underlying storage
155// layer supports _Get_ (list), _Update_ (update), _Create_ (create) and
156// _Delete_ semantics but does not guarantee any transactional semantics.
157//
158// _Update_, _Create_, and _Delete_ are mutator operations. These operations
159// are asynchronous, and you might not see the effect immediately (e.g. _Get_
160// might not return the object by key immediately after you mutate the store.)
161// Intermittent errors might occur even though the operation succeeds, so you
162// should always check if the object store has been modified even if the
163// mutating operation returns an error.  Objects should be created with
164// _Create_ operation and updated with _Update_ operation.
165//
166// Resource versions record the last mutation operation on each object. If a
167// mutation is applied to a different revision of an object than what the
168// underlying storage expects as defined by pure equality, the operation is
169// blocked.  The client of this interface should not make assumptions about the
170// structure or ordering of the revision identifier.
171//
172// Object references supplied and returned from this interface should be
173// treated as read-only. Modifying them violates thread-safety.
174type ConfigStore interface {
175	// Schemas exposes the configuration type schema known by the config store.
176	// The type schema defines the bidrectional mapping between configuration
177	// types and the protobuf encoding schema.
178	Schemas() collection.Schemas
179
180	// Get retrieves a configuration element by a type and a key
181	Get(typ resource.GroupVersionKind, name, namespace string) *Config
182
183	// List returns objects by type and namespace.
184	// Use "" for the namespace to list across namespaces.
185	List(typ resource.GroupVersionKind, namespace string) ([]Config, error)
186
187	// Create adds a new configuration object to the store. If an object with the
188	// same name and namespace for the type already exists, the operation fails
189	// with no side effects.
190	Create(config Config) (revision string, err error)
191
192	// Update modifies an existing configuration object in the store.  Update
193	// requires that the object has been created.  Resource version prevents
194	// overriding a value that has been changed between prior _Get_ and _Put_
195	// operation to achieve optimistic concurrency. This method returns a new
196	// revision if the operation succeeds.
197	Update(config Config) (newRevision string, err error)
198
199	// Delete removes an object from the store by key
200	Delete(typ resource.GroupVersionKind, name, namespace string) error
201
202	Version() string
203
204	GetResourceAtVersion(version string, key string) (resourceVersion string, err error)
205
206	GetLedger() ledger.Ledger
207
208	SetLedger(ledger.Ledger) error
209}
210
211// Key function for the configuration objects
212func Key(typ, name, namespace string) string {
213	return fmt.Sprintf("%s/%s/%s", typ, namespace, name)
214}
215
216func ParseKey(key string) (typ, name, namespace string, err error) {
217	out := strings.Split(key, "/")
218	if len(out) != 3 {
219		err = fmt.Errorf("key '%s' could not be parsed into a key", key)
220	} else {
221		typ = out[0]
222		name = out[1]
223		namespace = out[2]
224	}
225	return
226}
227
228// Key is the unique identifier for a configuration object
229func (meta *ConfigMeta) Key() string {
230	return Key(meta.Type, meta.Name, meta.Namespace)
231}
232
233// ConfigStoreCache is a local fully-replicated cache of the config store.  The
234// cache actively synchronizes its local state with the remote store and
235// provides a notification mechanism to receive update events. As such, the
236// notification handlers must be registered prior to calling _Run_, and the
237// cache requires initial synchronization grace period after calling  _Run_.
238//
239// Update notifications require the following consistency guarantee: the view
240// in the cache must be AT LEAST as fresh as the moment notification arrives, but
241// MAY BE more fresh (e.g. if _Delete_ cancels an _Add_ event).
242//
243// Handlers execute on the single worker queue in the order they are appended.
244// Handlers receive the notification event and the associated object.  Note
245// that all handlers must be registered before starting the cache controller.
246//go:generate counterfeiter -o ../config/aggregate/fakes/config_store_cache.gen.go --fake-name ConfigStoreCache . ConfigStoreCache
247type ConfigStoreCache interface {
248	ConfigStore
249
250	// RegisterEventHandler adds a handler to receive config update events for a
251	// configuration type
252	RegisterEventHandler(kind resource.GroupVersionKind, handler func(Config, Config, Event))
253
254	// Run until a signal is received
255	Run(stop <-chan struct{})
256
257	// HasSynced returns true after initial cache synchronization is complete
258	HasSynced() bool
259}
260
261// IstioConfigStore is a specialized interface to access config store using
262// Istio configuration types
263// nolint
264//go:generate counterfeiter -o ../networking/core/v1alpha3/fakes/fake_istio_config_store.gen.go --fake-name IstioConfigStore . IstioConfigStore
265type IstioConfigStore interface {
266	ConfigStore
267
268	// ServiceEntries lists all service entries
269	ServiceEntries() []Config
270
271	// Gateways lists all gateways bound to the specified workload labels
272	Gateways(workloadLabels labels.Collection) []Config
273
274	// QuotaSpecByDestination selects Mixerclient quota specifications
275	// associated with destination service instances.
276	QuotaSpecByDestination(hostname host.Name) []Config
277
278	// ServiceRoles selects ServiceRoles in the specified namespace.
279	ServiceRoles(namespace string) []Config
280
281	// ServiceRoleBindings selects ServiceRoleBindings in the specified namespace.
282	ServiceRoleBindings(namespace string) []Config
283
284	// RbacConfig selects the RbacConfig of name DefaultRbacConfigName.
285	RbacConfig() *Config
286
287	// ClusterRbacConfig selects the ClusterRbacConfig of name DefaultRbacConfigName.
288	ClusterRbacConfig() *Config
289
290	// AuthorizationPolicies selects AuthorizationPolicies in the specified namespace.
291	AuthorizationPolicies(namespace string) []Config
292}
293
294const (
295	// NamespaceAll is a designated symbol for listing across all namespaces
296	NamespaceAll = ""
297)
298
299/*
300  This conversion of CRD (== yaml files with k8s metadata) is extremely inefficient.
301  The yaml is parsed (kubeyaml), converted to YAML again (FromJSONMap),
302  converted to JSON (YAMLToJSON) and finally UnmarshallString in proto is called.
303
304  The result is not cached in the model.
305
306  In 0.7, this was the biggest factor in scalability. Moving forward we will likely
307  deprecate model, and do the conversion (hopefully more efficient) only once, when
308  an object is first read.
309*/
310
311// ResolveHostname produces a FQDN based on either the service or
312// a concat of the namespace + domain
313// Deprecated. Do not use
314func ResolveHostname(meta ConfigMeta, svc *mccpb.IstioService) host.Name {
315	out := svc.Name
316	// if FQDN is specified, do not append domain or namespace to hostname
317	// Service field has precedence over Name
318	if svc.Service != "" {
319		out = svc.Service
320	} else {
321		if svc.Namespace != "" {
322			out = out + "." + svc.Namespace
323		} else if meta.Namespace != "" {
324			out = out + "." + meta.Namespace
325		}
326
327		if svc.Domain != "" {
328			out = out + "." + svc.Domain
329		} else if meta.Domain != "" {
330			out = out + ".svc." + meta.Domain
331		}
332	}
333
334	return host.Name(out)
335}
336
337// ResolveShortnameToFQDN uses metadata information to resolve a reference
338// to shortname of the service to FQDN
339func ResolveShortnameToFQDN(hostname string, meta ConfigMeta) host.Name {
340	out := hostname
341	// Treat the wildcard hostname as fully qualified. Any other variant of a wildcard hostname will contain a `.` too,
342	// and skip the next if, so we only need to check for the literal wildcard itself.
343	if hostname == "*" {
344		return host.Name(out)
345	}
346	// if FQDN is specified, do not append domain or namespace to hostname
347	if !strings.Contains(hostname, ".") {
348		if meta.Namespace != "" {
349			out = out + "." + meta.Namespace
350		}
351
352		// FIXME this is a gross hack to hardcode a service's domain name in kubernetes
353		// BUG this will break non kubernetes environments if they use shortnames in the
354		// rules.
355		if meta.Domain != "" {
356			out = out + ".svc." + meta.Domain
357		}
358	}
359
360	return host.Name(out)
361}
362
363// resolveGatewayName uses metadata information to resolve a reference
364// to shortname of the gateway to FQDN
365func resolveGatewayName(gwname string, meta ConfigMeta) string {
366	out := gwname
367
368	// New way of binding to a gateway in remote namespace
369	// is ns/name. Old way is either FQDN or short name
370	if !strings.Contains(gwname, "/") {
371		if !strings.Contains(gwname, ".") {
372			// we have a short name. Resolve to a gateway in same namespace
373			out = meta.Namespace + "/" + gwname
374		} else {
375			// parse namespace from FQDN. This is very hacky, but meant for backward compatibility only
376			i := strings.Index(gwname, ".")
377			out = gwname[i+1:] + "/" + gwname[:i]
378		}
379	} else {
380		// remove the . from ./gateway and substitute it with the namespace name
381		i := strings.Index(gwname, "/")
382		if gwname[:i] == "." {
383			out = meta.Namespace + "/" + gwname[i+1:]
384		}
385	}
386	return out
387}
388
389// MostSpecificHostMatch compares the elements of the stack to the needle, and returns the longest stack element
390// matching the needle, or false if no element in the stack matches the needle.
391func MostSpecificHostMatch(needle host.Name, stack []host.Name) (host.Name, bool) {
392	matches := []host.Name{}
393	for _, h := range stack {
394		if needle == h {
395			// exact match, return immediately
396			return needle, true
397		}
398		if needle.SubsetOf(h) {
399			matches = append(matches, h)
400		}
401	}
402	if len(matches) > 0 {
403		// TODO: return closest match out of all non-exact matching hosts
404		return matches[0], true
405	}
406	return "", false
407}
408
409// istioConfigStore provides a simple adapter for Istio configuration types
410// from the generic config registry
411type istioConfigStore struct {
412	ConfigStore
413}
414
415// MakeIstioStore creates a wrapper around a store.
416// In pilot it is initialized with a ConfigStoreCache, tests only use
417// a regular ConfigStore.
418func MakeIstioStore(store ConfigStore) IstioConfigStore {
419	return &istioConfigStore{store}
420}
421
422func (store *istioConfigStore) ServiceEntries() []Config {
423	serviceEntries, err := store.List(collections.IstioNetworkingV1Alpha3Serviceentries.Resource().GroupVersionKind(), NamespaceAll)
424	if err != nil {
425		return nil
426	}
427	return serviceEntries
428}
429
430// sortConfigByCreationTime sorts the list of config objects in ascending order by their creation time (if available).
431func sortConfigByCreationTime(configs []Config) {
432	sort.SliceStable(configs, func(i, j int) bool {
433		// If creation time is the same, then behavior is nondeterministic. In this case, we can
434		// pick an arbitrary but consistent ordering based on name and namespace, which is unique.
435		// CreationTimestamp is stored in seconds, so this is not uncommon.
436		if configs[i].CreationTimestamp == configs[j].CreationTimestamp {
437			in := configs[i].Name + "." + configs[i].Namespace
438			jn := configs[j].Name + "." + configs[j].Namespace
439			return in < jn
440		}
441		return configs[i].CreationTimestamp.Before(configs[j].CreationTimestamp)
442	})
443}
444
445func (store *istioConfigStore) Gateways(workloadLabels labels.Collection) []Config {
446	configs, err := store.List(collections.IstioNetworkingV1Alpha3Gateways.Resource().GroupVersionKind(), NamespaceAll)
447	if err != nil {
448		return nil
449	}
450
451	sortConfigByCreationTime(configs)
452	out := make([]Config, 0)
453	for _, cfg := range configs {
454		gateway := cfg.Spec.(*networking.Gateway)
455		if gateway.GetSelector() == nil {
456			// no selector. Applies to all workloads asking for the gateway
457			out = append(out, cfg)
458		} else {
459			gatewaySelector := labels.Instance(gateway.GetSelector())
460			if workloadLabels.IsSupersetOf(gatewaySelector) {
461				out = append(out, cfg)
462			}
463		}
464	}
465	return out
466}
467
468// matchWildcardService matches destinationHost to a wildcarded svc.
469// checked values for svc
470//     '*'  matches everything
471//     '*.ns.*'  matches anything in the same namespace
472//		strings of any other form are not matched.
473func matchWildcardService(destinationHost, svc string) bool {
474	if len(svc) == 0 || !strings.Contains(svc, "*") {
475		return false
476	}
477
478	if svc == "*" {
479		return true
480	}
481
482	// check for namespace match with svc like '*.ns.*'
483	// extract match substring by dropping '*'
484	if strings.HasPrefix(svc, "*") && strings.HasSuffix(svc, "*") {
485		return strings.Contains(destinationHost, svc[1:len(svc)-1])
486	}
487
488	log.Warnf("Wildcard pattern '%s' is not allowed. Only '*' or '*.<ns>.*' is allowed.", svc)
489
490	return false
491}
492
493// MatchesDestHost returns true if the service instance matches the given IstioService
494// ex: binding host(details.istio-system.svc.cluster.local) ?= instance(reviews.default.svc.cluster.local)
495func MatchesDestHost(destinationHost string, meta ConfigMeta, svc *mccpb.IstioService) bool {
496	if matchWildcardService(destinationHost, svc.Service) {
497		return true
498	}
499
500	// try exact matches
501	hostname := string(ResolveHostname(meta, svc))
502	if destinationHost == hostname {
503		return true
504	}
505	shortName := hostname[0:strings.Index(hostname, ".")]
506	if strings.HasPrefix(destinationHost, shortName) {
507		log.Warnf("Quota excluded. service: %s matches binding shortname: %s, but does not match fqdn: %s",
508			destinationHost, shortName, hostname)
509	}
510
511	return false
512}
513
514func recordSpecRef(refs map[string]bool, bindingNamespace string, quotas []*mccpb.QuotaSpecBinding_QuotaSpecReference) {
515	for _, spec := range quotas {
516		namespace := spec.Namespace
517		if namespace == "" {
518			namespace = bindingNamespace
519		}
520		refs[key(spec.Name, namespace)] = true
521	}
522}
523
524// key creates a key from a reference's name and namespace.
525func key(name, namespace string) string {
526	return name + "/" + namespace
527}
528
529// findQuotaSpecRefs returns a set of quotaSpec reference names
530func findQuotaSpecRefs(hostname host.Name, bindings []Config) map[string]bool {
531	// Build the set of quota spec references bound to the service instance.
532	refs := make(map[string]bool)
533	for _, binding := range bindings {
534		b := binding.Spec.(*mccpb.QuotaSpecBinding)
535		for _, service := range b.Services {
536			if MatchesDestHost(string(hostname), binding.ConfigMeta, service) {
537				recordSpecRef(refs, binding.Namespace, b.QuotaSpecs)
538				// found a binding that matches the instance.
539				break
540			}
541		}
542	}
543
544	return refs
545}
546
547// filterQuotaSpecsByDestination provides QuotaSpecByDestination filtering logic as a
548// function that can be called on cached binding + spec sets
549func filterQuotaSpecsByDestination(hostname host.Name, bindings []Config, specs []Config) []Config {
550	// Build the set of quota spec references bound to the service instance.
551	refs := findQuotaSpecRefs(hostname, bindings)
552	log.Debugf("QuotaSpecByDestination refs:%v", refs)
553
554	// Append any spec that is in the set of references.
555	// Remove matching specs from refs so refs only contains dangling references.
556	var out []Config
557	for _, spec := range specs {
558		refkey := key(spec.ConfigMeta.Name, spec.ConfigMeta.Namespace)
559		if refs[refkey] {
560			out = append(out, spec)
561			delete(refs, refkey)
562		}
563	}
564
565	if len(refs) > 0 {
566		log.Warnf("Some matched QuotaSpecs were not found: %v", refs)
567	}
568	return out
569}
570
571// QuotaSpecByDestination selects Mixerclient quota specifications
572// associated with destination service instances.
573func (store *istioConfigStore) QuotaSpecByDestination(hostname host.Name) []Config {
574	log.Debugf("QuotaSpecByDestination(%v)", hostname)
575	bindings, err := store.List(collections.IstioMixerV1ConfigClientQuotaspecbindings.Resource().GroupVersionKind(), NamespaceAll)
576	if err != nil {
577		log.Warnf("Unable to fetch QuotaSpecBindings: %v", err)
578		return nil
579	}
580
581	log.Debugf("QuotaSpecByDestination bindings[%d] %v", len(bindings), bindings)
582	specs, err := store.List(collections.IstioMixerV1ConfigClientQuotaspecs.Resource().GroupVersionKind(), NamespaceAll)
583	if err != nil {
584		log.Warnf("Unable to fetch QuotaSpecs: %v", err)
585		return nil
586	}
587
588	log.Debugf("QuotaSpecByDestination specs[%d] %v", len(specs), specs)
589
590	return filterQuotaSpecsByDestination(hostname, bindings, specs)
591}
592
593func (store *istioConfigStore) ServiceRoles(namespace string) []Config {
594	roles, err := store.List(collections.IstioRbacV1Alpha1Serviceroles.Resource().GroupVersionKind(), namespace)
595	if err != nil {
596		log.Errorf("failed to get ServiceRoles in namespace %s: %v", namespace, err)
597		return nil
598	}
599
600	return roles
601}
602
603func (store *istioConfigStore) ServiceRoleBindings(namespace string) []Config {
604	bindings, err := store.List(collections.IstioRbacV1Alpha1Servicerolebindings.Resource().GroupVersionKind(), namespace)
605	if err != nil {
606		log.Errorf("failed to get ServiceRoleBinding in namespace %s: %v", namespace, err)
607		return nil
608	}
609
610	return bindings
611}
612
613func (store *istioConfigStore) ClusterRbacConfig() *Config {
614	clusterRbacConfig, err := store.List(collections.IstioRbacV1Alpha1Clusterrbacconfigs.Resource().GroupVersionKind(), "")
615	if err != nil {
616		log.Errorf("failed to get ClusterRbacConfig: %v", err)
617	}
618	for _, rc := range clusterRbacConfig {
619		if rc.Name == constants.DefaultRbacConfigName {
620			return &rc
621		}
622	}
623	return nil
624}
625
626func (store *istioConfigStore) RbacConfig() *Config {
627	rbacConfigs, err := store.List(collections.IstioRbacV1Alpha1Rbacconfigs.Resource().GroupVersionKind(), "")
628	if err != nil {
629		return nil
630	}
631
632	if len(rbacConfigs) > 1 {
633		log.Errorf("found %d RbacConfigs, expecting only 1.", len(rbacConfigs))
634	}
635	for _, rc := range rbacConfigs {
636		if rc.Name == constants.DefaultRbacConfigName {
637			log.Warnf("RbacConfig is deprecated, Use ClusterRbacConfig instead.")
638			return &rc
639		}
640	}
641	return nil
642}
643
644func (store *istioConfigStore) AuthorizationPolicies(namespace string) []Config {
645	authorizationPolicies, err := store.List(collections.IstioSecurityV1Beta1Authorizationpolicies.Resource().GroupVersionKind(), namespace)
646	if err != nil {
647		log.Errorf("failed to get AuthorizationPolicy in namespace %s: %v", namespace, err)
648		return nil
649	}
650
651	return authorizationPolicies
652}
653
654// SortQuotaSpec sorts a slice in a stable manner.
655func SortQuotaSpec(specs []Config) {
656	sort.Slice(specs, func(i, j int) bool {
657		// protect against incompatible types
658		irule, _ := specs[i].Spec.(*mccpb.QuotaSpec)
659		jrule, _ := specs[j].Spec.(*mccpb.QuotaSpec)
660		return irule == nil || jrule == nil || (specs[i].Key() < specs[j].Key())
661	})
662}
663
664func (c Config) DeepCopy() Config {
665	var clone Config
666	clone.ConfigMeta = c.ConfigMeta
667	clone.Spec = proto.Clone(c.Spec)
668	return clone
669}
670