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  }