github.com/splunk/dan1-qbec@v0.7.3/internal/remote/k8smeta/meta.go (about) 1 /* 2 Copyright 2019 Splunk Inc. 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 k8smeta implements metadata discovery and normalization of K8s resources. 18 package k8smeta 19 20 import ( 21 "fmt" 22 "os" 23 "sort" 24 "strings" 25 "sync" 26 27 "github.com/pkg/errors" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/runtime/schema" 30 ) 31 32 var defaultVerbs = []string{"create", "delete", "get", "list"} 33 34 // gvkInfo is all the information we need for k8s types as represented by group-version-kind. 35 type gvkInfo struct { 36 canonical schema.GroupVersionKind // the preferred gvk that includes aliasing (e.g. extensions/v1beta1 => apps/v1) 37 resource metav1.APIResource // the API resource for the gvk 38 } 39 40 // ResourceDiscovery is the minimal interface required to gather information on 41 // server resources. 42 type ResourceDiscovery interface { 43 ServerGroups() (*metav1.APIGroupList, error) 44 ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) 45 } 46 47 // Resources provides resource information for a K8s cluster. 48 type Resources struct { 49 disco ResourceDiscovery 50 registry map[schema.GroupVersionKind]*gvkInfo 51 } 52 53 // ResourceOpts is optional information for loading resources. 54 type ResourceOpts struct { 55 RequiredVerbs []string // verbs that a resource must support in order to be loaded. Defaults to create/delete/get/list 56 WarnFn func(...interface{}) // a function that can print warnings in the resource discovery. 57 } 58 59 func (o *ResourceOpts) setDefaults() { 60 if o.WarnFn == nil { 61 o.WarnFn = func(args ...interface{}) { 62 fmt.Fprintln(os.Stderr, args...) 63 } 64 } 65 if len(o.RequiredVerbs) == 0 { 66 o.RequiredVerbs = defaultVerbs 67 } 68 } 69 70 // NewResources loads server resources using the supplied discovery interface. 71 func NewResources(disco ResourceDiscovery, opts ResourceOpts) (*Resources, error) { 72 sm := &Resources{ 73 disco: disco, 74 registry: map[schema.GroupVersionKind]*gvkInfo{}, 75 } 76 opts.setDefaults() 77 if err := sm.init(opts); err != nil { 78 return nil, err 79 } 80 return sm, nil 81 } 82 83 // APIResource returns the API resource for the supplied group version kind or nil 84 // if no resource could be found. 85 func (r *Resources) APIResource(gvk schema.GroupVersionKind) *metav1.APIResource { 86 r0, ok := r.registry[gvk] 87 if !ok { 88 return nil 89 } 90 res := r0.resource 91 return &res 92 } 93 94 // CanonicalResources returns a map of API resources keyed by group-kind. 95 func (r *Resources) CanonicalResources() map[schema.GroupKind]metav1.APIResource { 96 canonical := map[schema.GroupVersionKind]bool{} 97 for _, v := range r.registry { 98 canonical[v.canonical] = true 99 } 100 101 ret := map[schema.GroupKind]metav1.APIResource{} 102 for k := range canonical { 103 r0 := r.registry[k] 104 res := r0.resource 105 res.Group = k.Group 106 res.Version = k.Version 107 res.Kind = k.Kind 108 ret[k.GroupKind()] = res 109 } 110 return ret 111 } 112 113 // CanonicalGroupVersionKind provides the preferred/ canonical group version kind for the supplied input. 114 // It takes aliases into account (e.g. extensions/Deployment same as apps/Deployment) for doing so. 115 func (r *Resources) CanonicalGroupVersionKind(gvk schema.GroupVersionKind) (schema.GroupVersionKind, error) { 116 res, ok := r.registry[gvk] 117 if !ok { 118 return gvk, fmt.Errorf("server does not recognize gvk %s", gvk) 119 } 120 return res.canonical, nil 121 } 122 123 // Dump dumps resource mappings using the supplied println function. 124 func (r *Resources) Dump(println func(...interface{})) { 125 var display []string 126 for k, v := range r.registry { 127 l := fmt.Sprintf("%s/%s:%s", k.Group, k.Version, k.Kind) 128 r := fmt.Sprintf("%s/%s:%s", v.canonical.Group, v.canonical.Version, v.canonical.Kind) 129 ns := "cluster scoped" 130 if v.resource.Namespaced { 131 ns = "namespaced" 132 } 133 display = append(display, fmt.Sprintf("\t%-70s => %s (%s)", l, r, ns)) 134 } 135 sort.Strings(display) 136 println() 137 println("group version kind map:") 138 for _, line := range display { 139 println(line) 140 } 141 println() 142 } 143 144 type equivalence struct { 145 gk1 schema.GroupKind 146 gk2 schema.GroupKind 147 } 148 149 // equivalences from https://github.com/kubernetes/kubernetes/blob/master/pkg/kubeapiserver/default_storage_factory_builder.go 150 var equivalences = []equivalence{ 151 { 152 gk1: schema.GroupKind{Group: "networking.k8s.io", Kind: "NetworkPolicy"}, 153 gk2: schema.GroupKind{Group: "extensions", Kind: "NetworkPolicy"}, 154 }, 155 { 156 gk1: schema.GroupKind{Group: "networking.k8s.io", Kind: "Ingress"}, 157 gk2: schema.GroupKind{Group: "extensions", Kind: "Ingress"}, 158 }, 159 { 160 gk1: schema.GroupKind{Group: "apps", Kind: "Deployment"}, 161 gk2: schema.GroupKind{Group: "extensions", Kind: "Deployment"}, 162 }, 163 { 164 gk1: schema.GroupKind{Group: "apps", Kind: "DaemonSet"}, 165 gk2: schema.GroupKind{Group: "extensions", Kind: "DaemonSet"}, 166 }, 167 { 168 gk1: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}, 169 gk2: schema.GroupKind{Group: "extensions", Kind: "ReplicaSet"}, 170 }, 171 { 172 gk1: schema.GroupKind{Group: "", Kind: "Event"}, 173 gk2: schema.GroupKind{Group: "events.k8s.io", Kind: "Event"}, 174 }, 175 { 176 gk1: schema.GroupKind{Group: "policy", Kind: "PodSecurityPolicy"}, 177 gk2: schema.GroupKind{Group: "extensions", Kind: "PodSecurityPolicy"}, 178 }, 179 } 180 181 func eligibleResource(r metav1.APIResource, requiredVerbs []string) bool { 182 for _, n := range requiredVerbs { 183 found := false 184 for _, v := range r.Verbs { 185 if n == v { 186 found = true 187 break 188 } 189 } 190 if !found { 191 return false 192 } 193 } 194 return true 195 } 196 197 type resolver struct { 198 warnFn func(...interface{}) 199 requiredVerbs []string 200 group string 201 version string 202 groupVersion string 203 preferredVersion string 204 registry map[schema.GroupVersionKind]*gvkInfo 205 tracker map[schema.GroupKind][]schema.GroupVersionKind 206 err error 207 } 208 209 func (r *resolver) resolve(disco ResourceDiscovery) { 210 if r.warnFn == nil { 211 r.warnFn = func(args ...interface{}) { fmt.Fprintln(os.Stderr, args...) } 212 } 213 reg := map[schema.GroupVersionKind]*gvkInfo{} 214 tracker := map[schema.GroupKind][]schema.GroupVersionKind{} 215 list, err := disco.ServerResourcesForGroupVersion(r.groupVersion) 216 if err != nil { 217 r.warnFn("error getting resources for type", r.groupVersion, ":", err) 218 } 219 if list != nil { 220 for _, res := range list.APIResources { 221 if strings.Contains(res.Name, "/") { // ignore sub-resources 222 continue 223 } 224 if !eligibleResource(res, r.requiredVerbs) { // remove stuff we cannot manipulate. 225 continue 226 } 227 228 // backfill the gv into res 229 res.Group = r.group 230 res.Version = r.version 231 gvk := schema.GroupVersionKind{Group: res.Group, Version: res.Version, Kind: res.Kind} 232 // the canonical version of the type may not be correct at this stage if the preferred group version 233 // does not have the specific kind. We will fix these anomalies later when all objects have been loaded 234 // and are known. 235 reg[gvk] = &gvkInfo{ 236 canonical: schema.GroupVersionKind{Group: r.group, Version: r.preferredVersion, Kind: res.Kind}, 237 resource: res, 238 } 239 gk := schema.GroupKind{Group: r.group, Kind: res.Kind} 240 tracker[gk] = append(tracker[gk], gvk) 241 } 242 } 243 r.registry = reg 244 r.tracker = tracker 245 } 246 247 func (r *Resources) init(opts ResourceOpts) error { 248 groups, err := r.disco.ServerGroups() 249 if err != nil { 250 return errors.Wrap(err, "get server groups") 251 } 252 253 order := 0 254 groupOrderMap := map[string]int{} 255 256 var resolvers []*resolver 257 for _, group := range groups.Groups { 258 groupName := group.Name 259 order++ 260 groupOrderMap[groupName] = order 261 preferredVersionName := group.PreferredVersion.Version 262 for _, gv := range group.Versions { 263 versionName := gv.Version 264 resolvers = append(resolvers, &resolver{ 265 warnFn: opts.WarnFn, 266 requiredVerbs: opts.RequiredVerbs, 267 group: groupName, 268 version: versionName, 269 preferredVersion: preferredVersionName, 270 groupVersion: gv.GroupVersion, 271 }) 272 } 273 } 274 275 var wg sync.WaitGroup 276 wg.Add(len(resolvers)) 277 for _, r0 := range resolvers { 278 go func(resolver *resolver) { 279 defer wg.Done() 280 resolver.resolve(r.disco) 281 }(r0) 282 } 283 wg.Wait() 284 285 reg := map[schema.GroupVersionKind]*gvkInfo{} 286 // tracker tracks all known versions for a given group kind for the purposes of updating 287 // the canonical versions for equivalences. 288 tracker := map[schema.GroupKind][]schema.GroupVersionKind{} 289 for _, r := range resolvers { 290 if r.err != nil { 291 return r.err 292 } 293 for k, v := range r.registry { 294 reg[k] = v 295 } 296 for k, v := range r.tracker { 297 tracker[k] = append(tracker[k], v...) 298 } 299 } 300 301 // now deal with incorrect preferred versions when specific types do not exist for those 302 var fixTypes []schema.GroupVersionKind // collect list of types to be fixed 303 for k, v := range reg { 304 canon := v.canonical 305 if reg[canon] == nil { 306 fixTypes = append(fixTypes, k) 307 } 308 } 309 for _, k := range fixTypes { 310 v := reg[k] 311 reg[k] = &gvkInfo{ 312 canonical: k, 313 resource: v.resource, 314 } 315 } 316 317 // then process aliases 318 for _, eq := range equivalences { 319 gk1 := eq.gk1 320 gk2 := eq.gk2 321 _, gk1Present := tracker[gk1] 322 _, gk2Present := tracker[gk2] 323 if !(gk1Present && gk2Present) { 324 continue 325 } 326 g1Order := groupOrderMap[gk1.Group] 327 g2Order := groupOrderMap[gk2.Group] 328 var canonicalGK, aliasGK schema.GroupKind 329 if g1Order < g2Order { 330 canonicalGK, aliasGK = eq.gk1, eq.gk2 331 } else { 332 canonicalGK, aliasGK = eq.gk2, eq.gk1 333 } 334 anyGKV := tracker[canonicalGK][0] 335 canonicalGKV := reg[anyGKV].canonical 336 for _, gkv := range tracker[aliasGK] { 337 reg[gkv] = &gvkInfo{ 338 canonical: canonicalGKV, 339 resource: reg[gkv].resource, 340 } 341 } 342 } 343 344 r.registry = reg 345 return nil 346 }