package resource import ( "context" "errors" "sort" "strconv" "strings" "github.com/derailed/k9s/internal/k8s" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" ) const lbIPWidth = 16 // Service tracks a kubernetes resource. type Service struct { *Base instance *v1.Service } // NewServiceList returns a new resource list. func NewServiceList(c Connection, ns string) List { return NewList( ns, "svc", NewService(c), AllVerbsAccess|DescribeAccess, ) } // NewService instantiates a new Service. func NewService(c Connection) *Service { s := &Service{&Base{Connection: c, Resource: k8s.NewService(c)}, nil} s.Factory = s return s } // New builds a new Service instance from a k8s resource. func (r *Service) New(i interface{}) Columnar { c := NewService(r.Connection) switch instance := i.(type) { case *v1.Service: c.instance = instance case v1.Service: c.instance = &instance default: log.Fatal().Msgf("unknown Service type %#v", i) } c.path = c.namespacedName(c.instance.ObjectMeta) return c } // Marshal resource to yaml. // BOZO!! Why you need to fill type info?? func (r *Service) Marshal(path string) (string, error) { ns, n := namespaced(path) i, err := r.Resource.Get(ns, n) if err != nil { return "", err } svc := i.(*v1.Service) svc.TypeMeta.APIVersion = "v1" svc.TypeMeta.Kind = "Service" return r.marshalObject(svc) } // Logs tail logs for all pods represented by this service. func (r *Service) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { instance, err := r.Resource.Get(opts.Namespace, opts.Name) if err != nil { return err } svc := instance.(*v1.Service) log.Debug().Msgf("Service %s--%s", svc.Name, svc.Spec.Selector) if len(svc.Spec.Selector) == 0 { return errors.New("No logs for headless service") } return r.podLogs(ctx, c, svc.Spec.Selector, opts) } // Header returns resource header. func (*Service) Header(ns string) Row { hh := Row{} if ns == AllNamespaces { hh = append(hh, "NAMESPACE") } return append(hh, "NAME", "TYPE", "CLUSTER-IP", "EXTERNAL-IP", "SELECTOR", "PORTS", "AGE", ) } // Fields retrieves displayable fields. func (r *Service) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) i := r.instance if ns == AllNamespaces { ff = append(ff, i.Namespace) } return append(ff, i.ObjectMeta.Name, string(i.Spec.Type), i.Spec.ClusterIP, r.toIPs(i.Spec.Type, r.getSvcExtIPS(i)), mapToStr(i.Spec.Selector), r.toPorts(i.Spec.Ports), toAge(i.ObjectMeta.CreationTimestamp), ) } // ---------------------------------------------------------------------------- // Helpers... func (r *Service) getSvcExtIPS(svc *v1.Service) []string { results := []string{} switch svc.Spec.Type { case v1.ServiceTypeClusterIP: fallthrough case v1.ServiceTypeNodePort: return svc.Spec.ExternalIPs case v1.ServiceTypeLoadBalancer: lbIps := r.lbIngressIP(svc.Status.LoadBalancer) if len(svc.Spec.ExternalIPs) > 0 { if len(lbIps) > 0 { results = append(results, lbIps) } return append(results, svc.Spec.ExternalIPs...) } if len(lbIps) > 0 { results = append(results, lbIps) } case v1.ServiceTypeExternalName: results = append(results, svc.Spec.ExternalName) } return results } func (*Service) lbIngressIP(s v1.LoadBalancerStatus) string { ingress := s.Ingress result := []string{} for i := range ingress { if len(ingress[i].IP) > 0 { result = append(result, ingress[i].IP) } else if len(ingress[i].Hostname) > 0 { result = append(result, ingress[i].Hostname) } } return strings.Join(result, ",") } func (*Service) toIPs(svcType v1.ServiceType, ips []string) string { if len(ips) == 0 { if svcType == v1.ServiceTypeLoadBalancer { return "" } return MissingValue } sort.Strings(ips) return strings.Join(ips, ",") } func (*Service) toPorts(pp []v1.ServicePort) string { ports := make([]string, len(pp)) for i, p := range pp { if len(p.Name) > 0 { ports[i] = p.Name + ":" } ports[i] += strconv.Itoa(int(p.Port)) + "►" + strconv.Itoa(int(p.NodePort)) if p.Protocol != "TCP" { ports[i] += "╱" + string(p.Protocol) } } return strings.Join(ports, " ") }