mirror of
https://github.com/coredns/coredns.git
synced 2026-08-20 23:08:28 -04:00
* plugin/kubernetes: copy Labels in Pod.DeepCopyObject Pod.DeepCopyObject built the copy field by field and left Labels out, so every copy came back unlabelled. Every other object in this package copies all of its fields; Pod was the only one missing any. Labels feeds the kubernetes/client-label/<key> metadata, so anything that reached a pod through a deep copy would see no labels at all rather than an error. Nothing does today - DefaultProcessor stores the converted object straight into the indexer without copying it, which is why this has not surfaced - so this is a latent bug rather than a live one. Clone the map instead of assigning it, so the copy does not alias the original. maps.Clone returns nil for a nil map, so an unlabelled pod stays unlabelled and a round trip does not turn a nil map into an empty one. The test covers every runtime.Object in the package rather than just Pod, and refuses to pass if a fixture leaves a field at its zero value. Adding a field to any of these types therefore fails the test until the fixture sets it, which is what makes the round-trip assertion cover the new field too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Manuel Rüger <manuel@rueg.eu> * plugin/kubernetes/object: deep copy the Service and ServiceImport ports The deepcopy tests claimed a package-wide guarantee that a copy shares nothing with its original, but only checked it for Pod and a hand-written Endpoints value. Both api.ServicePort and mcs.ServicePort hold an AppProtocol *string, so the elementwise copy(s1.Ports, s.Ports) in Service.DeepCopyObject and ServiceImport.DeepCopyObject left the copy pointing at the original's string. Mutating it through either side was visible from the other, and the tests still passed. Copy the ports with the generated DeepCopyInto so the copy owns everything it can reach, and make the guarantee real: TestDeepCopyObjectIsDeep now runs over every case in deepCopyCases, mutates every value reachable through a pointer, slice, or map, and requires the copy to still equal a pristine fixture. A type that gains a reference-bearing field is covered as soon as assertAllFieldsSet forces the fixture to populate it, rather than needing a new hand-written case. Failure messages render as JSON, because %+v prints an aliased pointer field as an address and hides the value that actually differs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Manuel Rüger <manuel@rueg.eu> --------- Signed-off-by: Manuel Rüger <manuel@rueg.eu> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
126 lines
3.5 KiB
Go
126 lines
3.5 KiB
Go
package object
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
api "k8s.io/api/core/v1"
|
|
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
)
|
|
|
|
// Service is a stripped down api.Service with only the items we need for CoreDNS.
|
|
type Service struct {
|
|
// Don't add new fields to this struct without talking to the CoreDNS maintainers.
|
|
Version string
|
|
Name string
|
|
Namespace string
|
|
Index string
|
|
ClusterIPs []string
|
|
Type api.ServiceType
|
|
ExternalName string
|
|
Ports []api.ServicePort
|
|
|
|
// ExternalIPs we may want to export.
|
|
ExternalIPs []string
|
|
|
|
*Empty
|
|
}
|
|
|
|
// ServiceKey returns a string using for the index.
|
|
func ServiceKey(name, namespace string) string { return name + "." + namespace }
|
|
|
|
// ToService converts an api.Service to a *Service.
|
|
func ToService(obj meta.Object) (meta.Object, error) {
|
|
svc, ok := obj.(*api.Service)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected object %v", obj)
|
|
}
|
|
s := &Service{
|
|
Version: svc.GetResourceVersion(),
|
|
Name: svc.GetName(),
|
|
Namespace: svc.GetNamespace(),
|
|
Index: ServiceKey(svc.GetName(), svc.GetNamespace()),
|
|
Type: svc.Spec.Type,
|
|
ExternalName: svc.Spec.ExternalName,
|
|
|
|
ExternalIPs: make([]string, len(svc.Status.LoadBalancer.Ingress)+len(svc.Spec.ExternalIPs)),
|
|
}
|
|
|
|
if len(svc.Spec.ClusterIPs) > 0 {
|
|
s.ClusterIPs = make([]string, len(svc.Spec.ClusterIPs))
|
|
copy(s.ClusterIPs, svc.Spec.ClusterIPs)
|
|
} else {
|
|
s.ClusterIPs = []string{svc.Spec.ClusterIP}
|
|
}
|
|
|
|
if len(svc.Spec.Ports) == 0 {
|
|
// Add sentinel if there are no ports.
|
|
s.Ports = []api.ServicePort{{Port: -1}}
|
|
} else {
|
|
s.Ports = make([]api.ServicePort, len(svc.Spec.Ports))
|
|
copy(s.Ports, svc.Spec.Ports)
|
|
}
|
|
|
|
li := copy(s.ExternalIPs, svc.Spec.ExternalIPs)
|
|
for i, lb := range svc.Status.LoadBalancer.Ingress {
|
|
if lb.IP != "" {
|
|
s.ExternalIPs[li+i] = lb.IP
|
|
continue
|
|
}
|
|
s.ExternalIPs[li+i] = lb.Hostname
|
|
}
|
|
|
|
*svc = api.Service{}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// Headless returns true if the service is headless
|
|
func (s *Service) Headless() bool {
|
|
return s.ClusterIPs[0] == api.ClusterIPNone
|
|
}
|
|
|
|
var _ runtime.Object = &Service{}
|
|
|
|
// DeepCopyObject implements the ObjectKind interface.
|
|
func (s *Service) DeepCopyObject() runtime.Object {
|
|
s1 := &Service{
|
|
Version: s.Version,
|
|
Name: s.Name,
|
|
Namespace: s.Namespace,
|
|
Index: s.Index,
|
|
Type: s.Type,
|
|
ExternalName: s.ExternalName,
|
|
ClusterIPs: make([]string, len(s.ClusterIPs)),
|
|
Ports: make([]api.ServicePort, len(s.Ports)),
|
|
ExternalIPs: make([]string, len(s.ExternalIPs)),
|
|
}
|
|
copy(s1.ClusterIPs, s.ClusterIPs)
|
|
copy(s1.ExternalIPs, s.ExternalIPs)
|
|
// api.ServicePort holds an AppProtocol *string, so copying the slice elementwise
|
|
// would leave both services pointing at the same string. Use the generated
|
|
// deep copy so the copy owns everything it can reach.
|
|
for i := range s.Ports {
|
|
s.Ports[i].DeepCopyInto(&s1.Ports[i])
|
|
}
|
|
return s1
|
|
}
|
|
|
|
// GetNamespace implements the metav1.Object interface.
|
|
func (s *Service) GetNamespace() string { return s.Namespace }
|
|
|
|
// SetNamespace implements the metav1.Object interface.
|
|
func (s *Service) SetNamespace(_namespace string) {}
|
|
|
|
// GetName implements the metav1.Object interface.
|
|
func (s *Service) GetName() string { return s.Name }
|
|
|
|
// SetName implements the metav1.Object interface.
|
|
func (s *Service) SetName(_name string) {}
|
|
|
|
// GetResourceVersion implements the metav1.Object interface.
|
|
func (s *Service) GetResourceVersion() string { return s.Version }
|
|
|
|
// SetResourceVersion implements the metav1.Object interface.
|
|
func (s *Service) SetResourceVersion(_version string) {}
|