github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/portforward/resource_forwarder.go (about) 1 /* 2 Copyright 2019 The Skaffold Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package portforward 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "sync" 24 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 27 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants" 28 kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client" 29 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log" 30 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 31 schemautil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/util" 32 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 33 ) 34 35 // ResourceForwarder is responsible for forwarding user defined port forwarding resources and automatically forwarding 36 // services deployed by skaffold. 37 type ResourceForwarder struct { 38 output io.Writer 39 entryManager *EntryManager 40 label string 41 kubeContext string 42 userDefinedResources []*latest.PortForwardResource 43 services bool 44 } 45 46 var ( 47 // For testing 48 retrieveAvailablePort = util.GetAvailablePort 49 retrieveServices = retrieveServiceResources 50 ) 51 52 // NewServicesForwarder returns a struct that tracks and port-forwards services as they are created and modified 53 func NewServicesForwarder(entryManager *EntryManager, kubeContext string, label string) *ResourceForwarder { 54 return &ResourceForwarder{ 55 entryManager: entryManager, 56 label: label, 57 services: true, 58 kubeContext: kubeContext, 59 } 60 } 61 62 // NewUserDefinedForwarder returns a struct that tracks and port-forwards services as they are created and modified 63 func NewUserDefinedForwarder(entryManager *EntryManager, kubeContext string, userDefinedResources []*latest.PortForwardResource) *ResourceForwarder { 64 return &ResourceForwarder{ 65 entryManager: entryManager, 66 kubeContext: kubeContext, 67 userDefinedResources: userDefinedResources, 68 } 69 } 70 71 // Start gets a list of services deployed by skaffold as []latest.PortForwardResource and 72 // forwards them. 73 func (p *ResourceForwarder) Start(ctx context.Context, out io.Writer, namespaces []string) error { 74 p.output = out 75 if len(namespaces) == 1 { 76 for _, pf := range p.userDefinedResources { 77 if err := applyWithTemplate(pf); err != nil { 78 return err 79 } 80 if pf.Namespace == "" { 81 pf.Namespace = namespaces[0] 82 } 83 } 84 } else { 85 var validResources []*latest.PortForwardResource 86 for _, pf := range p.userDefinedResources { 87 if pf.Namespace != "" { 88 if err := applyWithTemplate(pf); err != nil { 89 return err 90 } 91 validResources = append(validResources, pf) 92 } else { 93 log.Entry(ctx).Warnf("Skipping the port forwarding resource %s/%s because namespace is not specified", pf.Type, pf.Name) 94 } 95 } 96 p.userDefinedResources = validResources 97 } 98 99 var serviceResources []*latest.PortForwardResource 100 if p.services { 101 found, err := retrieveServices(ctx, p.label, namespaces, p.kubeContext) 102 if err != nil { 103 return fmt.Errorf("retrieving services for automatic port forwarding: %w", err) 104 } 105 serviceResources = found 106 } 107 p.portForwardResources(ctx, append(p.userDefinedResources, serviceResources...)) 108 return nil 109 } 110 111 func applyWithTemplate(resource *latest.PortForwardResource) error { 112 if resource.Namespace != "" { 113 namespace, err := util.ExpandEnvTemplateOrFail(resource.Namespace, nil) 114 if err != nil { 115 return fmt.Errorf("cannot parse the namespace template on user defined port forwarder: %w", err) 116 } 117 resource.Namespace = namespace 118 } 119 name, err := util.ExpandEnvTemplateOrFail(resource.Name, nil) 120 if err != nil { 121 return fmt.Errorf("cannot parse the name template on user defined port forwarder: %w", err) 122 } 123 resource.Name = name 124 return nil 125 } 126 127 func (p *ResourceForwarder) Stop() { 128 p.entryManager.Stop() 129 } 130 131 // Port forward each resource individually in a goroutine 132 func (p *ResourceForwarder) portForwardResources(ctx context.Context, resources []*latest.PortForwardResource) { 133 var wg sync.WaitGroup 134 for _, r := range resources { 135 wg.Add(1) 136 go func(r latest.PortForwardResource) { 137 defer wg.Done() 138 p.portForwardResource(ctx, r) 139 }(*r) 140 } 141 wg.Wait() 142 } 143 144 func (p *ResourceForwarder) portForwardResource(ctx context.Context, resource latest.PortForwardResource) { 145 // Get port forward entry for this resource 146 entry := p.getCurrentEntry(resource) 147 // Forward the entry 148 p.entryManager.forwardPortForwardEntry(ctx, p.output, entry) 149 } 150 151 func (p *ResourceForwarder) getCurrentEntry(resource latest.PortForwardResource) *portForwardEntry { 152 // determine if we have seen this before 153 entry := newPortForwardEntry(0, resource, "", "", "", "", 0, false) 154 155 // If we have, return the current entry 156 oe, ok := p.entryManager.forwardedResources.Load(entry.key()) 157 if ok { 158 oldEntry := oe.(*portForwardEntry) 159 entry.localPort = oldEntry.localPort 160 return entry 161 } 162 163 // Try to request matching local port *providing* that it is not a system port. 164 // https://github.com/GoogleContainerTools/skaffold/pull/5554#issuecomment-803270340 165 requestPort := resource.LocalPort 166 if requestPort == 0 && resource.Port.IntVal >= 1024 { 167 requestPort = resource.Port.IntVal 168 } 169 entry.localPort = retrieveAvailablePort(resource.Address, requestPort, &p.entryManager.forwardedPorts) 170 return entry 171 } 172 173 // retrieveServiceResources retrieves all services in the cluster matching the given label 174 // as a list of PortForwardResources 175 func retrieveServiceResources(ctx context.Context, label string, namespaces []string, kubeContext string) ([]*latest.PortForwardResource, error) { 176 client, err := kubernetesclient.Client(kubeContext) 177 if err != nil { 178 return nil, fmt.Errorf("getting Kubernetes client: %w", err) 179 } 180 181 var resources []*latest.PortForwardResource 182 for _, ns := range namespaces { 183 services, err := client.CoreV1().Services(ns).List(ctx, metav1.ListOptions{ 184 LabelSelector: label, 185 }) 186 if err != nil { 187 return nil, fmt.Errorf("selecting services by label %q: %w", label, err) 188 } 189 for _, s := range services.Items { 190 for _, p := range s.Spec.Ports { 191 resources = append(resources, &latest.PortForwardResource{ 192 Type: constants.Service, 193 Name: s.Name, 194 Namespace: s.Namespace, 195 Port: schemautil.FromInt(int(p.Port)), 196 Address: constants.DefaultPortForwardAddress, 197 }) 198 } 199 } 200 } 201 return resources, nil 202 }