github.com/google/cloudprober@v0.11.3/rds/kubernetes/ingresses.go (about) 1 // Copyright 2020 The Cloudprober 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 15 package kubernetes 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "math/rand" 21 "strings" 22 "sync" 23 "time" 24 25 "github.com/golang/protobuf/proto" 26 "github.com/google/cloudprober/logger" 27 configpb "github.com/google/cloudprober/rds/kubernetes/proto" 28 pb "github.com/google/cloudprober/rds/proto" 29 "github.com/google/cloudprober/rds/server/filter" 30 ) 31 32 type ingressesLister struct { 33 c *configpb.Ingresses 34 namespace string 35 kClient *client 36 37 mu sync.RWMutex // Mutex for names and cache 38 keys []resourceKey 39 cache map[resourceKey]*ingressInfo 40 l *logger.Logger 41 } 42 43 func ingressesURL(ns string) string { 44 // TODO(manugarg): Update version to v1 once it's more widely available. 45 if ns == "" { 46 return "apis/networking.k8s.io/v1beta1/ingresses" 47 } 48 return fmt.Sprintf("apis/networking.k8s.io/v1beta1/namespaces/%s/ingresses", ns) 49 } 50 51 func (lister *ingressesLister) listResources(req *pb.ListResourcesRequest) ([]*pb.Resource, error) { 52 var resources []*pb.Resource 53 54 var resName string 55 tok := strings.SplitN(req.GetResourcePath(), "/", 2) 56 if len(tok) == 2 { 57 resName = tok[1] 58 } 59 60 allFilters, err := filter.ParseFilters(req.GetFilter(), SupportedFilters.RegexFilterKeys, "") 61 if err != nil { 62 return nil, err 63 } 64 65 nameFilter, nsFilter, labelsFilter := allFilters.RegexFilters["name"], allFilters.RegexFilters["namespace"], allFilters.LabelsFilter 66 67 lister.mu.RLock() 68 defer lister.mu.RUnlock() 69 70 for _, key := range lister.keys { 71 if resName != "" && key.name != resName { 72 continue 73 } 74 75 ingress := lister.cache[key] 76 if nsFilter != nil && !nsFilter.Match(ingress.Metadata.Namespace, lister.l) { 77 continue 78 } 79 80 for _, res := range ingress.resources() { 81 if nameFilter != nil && !nameFilter.Match(res.GetName(), lister.l) { 82 continue 83 } 84 if labelsFilter != nil && !labelsFilter.Match(res.GetLabels(), lister.l) { 85 continue 86 } 87 resources = append(resources, res) 88 } 89 } 90 91 lister.l.Infof("kubernetes.listResources: returning %d ingresses", len(resources)) 92 return resources, nil 93 } 94 95 type ingressRule struct { 96 Host string 97 HTTP struct { 98 Paths []struct { 99 Path string 100 } 101 } 102 } 103 104 type ingressInfo struct { 105 Metadata kMetadata 106 Spec struct { 107 Rules []ingressRule 108 } 109 Status struct { 110 LoadBalancer loadBalancerStatus 111 } 112 } 113 114 // resources returns RDS resources corresponding to an ingress resource. 115 func (i *ingressInfo) resources() (resources []*pb.Resource) { 116 resName := i.Metadata.Name 117 baseLabels := i.Metadata.Labels 118 119 // Note that for ingress we don't check the type of the IP in the request. 120 // That is mainly because ingresses typically have only one ingress 121 // controller and hence one IP address. Also, the difference of private vs 122 // public IP doesn't really exist. 123 var ip string 124 if len(i.Status.LoadBalancer.Ingress) > 0 { 125 ii := i.Status.LoadBalancer.Ingress[0] 126 ip = ii.IP 127 if ip == "" && ii.Hostname != "" { 128 ip = ii.Hostname 129 } 130 } 131 132 if len(i.Spec.Rules) == 0 { 133 return []*pb.Resource{ 134 { 135 Name: proto.String(resName), 136 Labels: baseLabels, 137 Ip: proto.String(ip), 138 }, 139 } 140 } 141 142 for _, rule := range i.Spec.Rules { 143 nameWithHost := fmt.Sprintf("%s_%s", resName, rule.Host) 144 145 for _, p := range rule.HTTP.Paths { 146 nameWithPath := nameWithHost 147 if p.Path != "/" { 148 nameWithPath = fmt.Sprintf("%s_%s", nameWithHost, strings.Replace(p.Path, "/", "_", -1)) 149 } 150 151 // Add fqdn and url labels to the resources. 152 labels := make(map[string]string, len(baseLabels)+2) 153 for k, v := range baseLabels { 154 labels[k] = v 155 } 156 if _, ok := labels["fqdn"]; !ok { 157 labels["fqdn"] = rule.Host 158 } 159 if _, ok := labels["relative_url"]; !ok { 160 labels["relative_url"] = p.Path 161 } 162 163 resources = append(resources, &pb.Resource{ 164 Name: proto.String(nameWithPath), 165 Labels: labels, 166 Ip: proto.String(ip), 167 }) 168 } 169 } 170 171 return 172 } 173 174 func parseIngressesJSON(resp []byte) (keys []resourceKey, ingresses map[resourceKey]*ingressInfo, err error) { 175 var itemList struct { 176 Items []*ingressInfo 177 } 178 179 if err = json.Unmarshal(resp, &itemList); err != nil { 180 return 181 } 182 183 keys = make([]resourceKey, len(itemList.Items)) 184 ingresses = make(map[resourceKey]*ingressInfo) 185 for i, item := range itemList.Items { 186 keys[i] = resourceKey{item.Metadata.Namespace, item.Metadata.Name} 187 ingresses[keys[i]] = item 188 } 189 190 return 191 } 192 193 func (lister *ingressesLister) expand() { 194 resp, err := lister.kClient.getURL(ingressesURL(lister.namespace)) 195 if err != nil { 196 lister.l.Warningf("ingressesLister.expand(): error while getting ingresses list from API: %v", err) 197 } 198 199 keys, ingresses, err := parseIngressesJSON(resp) 200 if err != nil { 201 lister.l.Warningf("ingressesLister.expand(): error while parsing ingresses API response (%s): %v", string(resp), err) 202 } 203 204 lister.l.Infof("ingressesLister.expand(): got %d ingresses", len(keys)) 205 206 lister.mu.Lock() 207 defer lister.mu.Unlock() 208 lister.keys = keys 209 lister.cache = ingresses 210 } 211 212 func newIngressesLister(c *configpb.Ingresses, namespace string, reEvalInterval time.Duration, kc *client, l *logger.Logger) (*ingressesLister, error) { 213 lister := &ingressesLister{ 214 c: c, 215 kClient: kc, 216 namespace: namespace, 217 l: l, 218 } 219 220 go func() { 221 lister.expand() 222 // Introduce a random delay between 0-reEvalInterval before 223 // starting the refresh loop. If there are multiple cloudprober 224 // gceInstances, this will make sure that each instance calls GCE 225 // API at a different point of time. 226 rand.Seed(time.Now().UnixNano()) 227 randomDelaySec := rand.Intn(int(reEvalInterval.Seconds())) 228 time.Sleep(time.Duration(randomDelaySec) * time.Second) 229 for range time.Tick(reEvalInterval) { 230 lister.expand() 231 } 232 }() 233 234 return lister, nil 235 }