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