working commit
This commit is contained in:
+47
@@ -0,0 +1,47 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package aggregator
|
||||
|
||||
import (
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
)
|
||||
|
||||
// AggregateStatus computes the aggregate status for all the resources.
|
||||
// The rules are the following:
|
||||
// - If any of the resources has the FailedStatus, the aggregate status is also
|
||||
// FailedStatus
|
||||
// - If none of the resources have the FailedStatus and at least one is
|
||||
// UnknownStatus, the aggregate status is UnknownStatus
|
||||
// - If all the resources have the desired status, the aggregate status is the
|
||||
// desired status.
|
||||
// - If none of the first three rules apply, the aggregate status is
|
||||
// InProgressStatus
|
||||
func AggregateStatus(rss []*event.ResourceStatus, desired status.Status) status.Status {
|
||||
if len(rss) == 0 {
|
||||
return desired
|
||||
}
|
||||
|
||||
allDesired := true
|
||||
anyUnknown := false
|
||||
for _, rs := range rss {
|
||||
s := rs.Status
|
||||
if s == status.FailedStatus {
|
||||
return status.FailedStatus
|
||||
}
|
||||
if s == status.UnknownStatus {
|
||||
anyUnknown = true
|
||||
}
|
||||
if s != desired {
|
||||
allDesired = false
|
||||
}
|
||||
}
|
||||
if anyUnknown {
|
||||
return status.UnknownStatus
|
||||
}
|
||||
if allDesired {
|
||||
return desired
|
||||
}
|
||||
return status.InProgressStatus
|
||||
}
|
||||
Generated
Vendored
+338
@@ -0,0 +1,338 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package clusterreader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/tools/pager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// This map is hard-coded knowledge that a Deployment contains and
|
||||
// ReplicaSet, and that a ReplicaSet in turn contains Pods, etc., and the
|
||||
// approach to finding status being used here requires hardcoding that
|
||||
// knowledge in the status client library.
|
||||
// TODO: These should probably be defined in the statusreaders rather than here.
|
||||
var genGroupKinds = map[schema.GroupKind][]schema.GroupKind{
|
||||
schema.GroupKind{Group: "apps", Kind: "Deployment"}: { //nolint:gofmt
|
||||
{
|
||||
Group: "apps",
|
||||
Kind: "ReplicaSet",
|
||||
},
|
||||
},
|
||||
schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}: { //nolint:gofmt
|
||||
{
|
||||
Group: "",
|
||||
Kind: "Pod",
|
||||
},
|
||||
},
|
||||
schema.GroupKind{Group: "apps", Kind: "StatefulSet"}: { //nolint:gofmt
|
||||
{
|
||||
Group: "",
|
||||
Kind: "Pod",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewCachingClusterReader returns a new instance of the ClusterReader. The
|
||||
// ClusterReader needs will use the clusterreader to fetch resources from the cluster,
|
||||
// while the mapper is used to resolve the version for GroupKinds. The set of
|
||||
// identifiers is needed so the ClusterReader can figure out which GroupKind
|
||||
// and namespace combinations it needs to cache when the Sync function is called.
|
||||
// We only want to fetch the resources that are actually needed.
|
||||
func NewCachingClusterReader(reader client.Reader, mapper meta.RESTMapper, identifiers object.ObjMetadataSet) (engine.ClusterReader, error) {
|
||||
gvkNamespaceSet := newGnSet()
|
||||
for _, id := range identifiers {
|
||||
// For every identifier, add the GroupVersionKind and namespace combination to the gvkNamespaceSet and
|
||||
// check the genGroupKinds map for any generated resources that also should be included.
|
||||
err := buildGvkNamespaceSet([]schema.GroupKind{id.GroupKind}, id.Namespace, gvkNamespaceSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &CachingClusterReader{
|
||||
reader: reader,
|
||||
mapper: mapper,
|
||||
gns: gvkNamespaceSet.gvkNamespaces,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildGvkNamespaceSet(gks []schema.GroupKind, namespace string, gvkNamespaceSet *gvkNamespaceSet) error {
|
||||
for _, gk := range gks {
|
||||
gvkNamespaceSet.add(gkNamespace{
|
||||
GroupKind: gk,
|
||||
Namespace: namespace,
|
||||
})
|
||||
genGKs, found := genGroupKinds[gk]
|
||||
if found {
|
||||
err := buildGvkNamespaceSet(genGKs, namespace, gvkNamespaceSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type gvkNamespaceSet struct {
|
||||
gvkNamespaces []gkNamespace
|
||||
seen map[gkNamespace]struct{}
|
||||
}
|
||||
|
||||
func newGnSet() *gvkNamespaceSet {
|
||||
return &gvkNamespaceSet{
|
||||
seen: make(map[gkNamespace]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *gvkNamespaceSet) add(gn gkNamespace) {
|
||||
if _, found := g.seen[gn]; !found {
|
||||
g.gvkNamespaces = append(g.gvkNamespaces, gn)
|
||||
g.seen[gn] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// CachingClusterReader is an implementation of the ObserverReader interface that will
|
||||
// pre-fetch all resources needed before every sync loop. The resources needed are decided by
|
||||
// finding all combinations of GroupVersionKind and namespace referenced by the provided
|
||||
// identifiers. This list is then expanded to include any known generated resource types.
|
||||
type CachingClusterReader struct {
|
||||
mx sync.RWMutex
|
||||
|
||||
// clusterreader provides functions to read and list resources from the
|
||||
// cluster.
|
||||
reader client.Reader
|
||||
|
||||
// mapper is the client-side representation of the server-side scheme. It is used
|
||||
// to resolve GroupVersionKind from GroupKind.
|
||||
mapper meta.RESTMapper
|
||||
|
||||
// gns contains the slice of all the GVK and namespace combinations that
|
||||
// should be included in the cache. This is computed based the resource identifiers
|
||||
// passed in when the CachingClusterReader is created and augmented with other
|
||||
// resource types needed to compute status (see genGroupKinds).
|
||||
gns []gkNamespace
|
||||
|
||||
// cache contains the resources found in the cluster for the given combination
|
||||
// of GVK and namespace. Before each polling cycle, the framework will call the
|
||||
// Sync function, which is responsible for repopulating the cache.
|
||||
cache map[gkNamespace]cacheEntry
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
resources unstructured.UnstructuredList
|
||||
err error
|
||||
}
|
||||
|
||||
// gkNamespace contains information about a GroupVersionKind and a namespace.
|
||||
type gkNamespace struct {
|
||||
GroupKind schema.GroupKind
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// Get looks up the resource identified by the key and the object GVK in the cache. If the needed combination
|
||||
// of GVK and namespace is not part of the cache, that is considered an error.
|
||||
func (c *CachingClusterReader) Get(_ context.Context, key client.ObjectKey, obj *unstructured.Unstructured) error {
|
||||
c.mx.RLock()
|
||||
defer c.mx.RUnlock()
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
mapping, err := c.mapper.RESTMapping(gvk.GroupKind())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gn := gkNamespace{
|
||||
GroupKind: gvk.GroupKind(),
|
||||
Namespace: key.Namespace,
|
||||
}
|
||||
cacheEntry, found := c.cache[gn]
|
||||
if !found {
|
||||
return fmt.Errorf("GVK %s and Namespace %s not found in cache", gvk.String(), gn.Namespace)
|
||||
}
|
||||
|
||||
if cacheEntry.err != nil {
|
||||
return cacheEntry.err
|
||||
}
|
||||
for _, u := range cacheEntry.resources.Items {
|
||||
if u.GetName() == key.Name {
|
||||
obj.Object = u.Object
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return apierrors.NewNotFound(mapping.Resource.GroupResource(), key.Name)
|
||||
}
|
||||
|
||||
// ListNamespaceScoped lists all resource identifier by the GVK of the list, the namespace and the selector
|
||||
// from the cache. If the needed combination of GVK and namespace is not part of the cache, that is considered an error.
|
||||
func (c *CachingClusterReader) ListNamespaceScoped(_ context.Context, list *unstructured.UnstructuredList, namespace string, selector labels.Selector) error {
|
||||
c.mx.RLock()
|
||||
defer c.mx.RUnlock()
|
||||
gvk := list.GroupVersionKind()
|
||||
gn := gkNamespace{
|
||||
GroupKind: gvk.GroupKind(),
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
cacheEntry, found := c.cache[gn]
|
||||
if !found {
|
||||
return fmt.Errorf("GVK %s and Namespace %s not found in cache", gvk.String(), gn.Namespace)
|
||||
}
|
||||
|
||||
if cacheEntry.err != nil {
|
||||
return cacheEntry.err
|
||||
}
|
||||
|
||||
var items []unstructured.Unstructured
|
||||
for _, u := range cacheEntry.resources.Items {
|
||||
if selector.Matches(labels.Set(u.GetLabels())) {
|
||||
items = append(items, u)
|
||||
}
|
||||
}
|
||||
list.Items = items
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListClusterScoped lists all resource identifier by the GVK of the list and selector
|
||||
// from the cache. If the needed combination of GVK and namespace (which for clusterscoped resources
|
||||
// will always be the empty string) is not part of the cache, that is considered an error.
|
||||
func (c *CachingClusterReader) ListClusterScoped(ctx context.Context, list *unstructured.UnstructuredList, selector labels.Selector) error {
|
||||
return c.ListNamespaceScoped(ctx, list, "", selector)
|
||||
}
|
||||
|
||||
// Sync loops over the list of gkNamespace we know of, and uses list calls to fetch the resources.
|
||||
// This information populates the cache.
|
||||
func (c *CachingClusterReader) Sync(ctx context.Context) error {
|
||||
c.mx.Lock()
|
||||
defer c.mx.Unlock()
|
||||
cache := make(map[gkNamespace]cacheEntry)
|
||||
for _, gn := range c.gns {
|
||||
mapping, err := c.mapper.RESTMapping(gn.GroupKind)
|
||||
if err != nil {
|
||||
if meta.IsNoMatchError(err) {
|
||||
// If we get a NoMatchError, it means we are checking for
|
||||
// a type that doesn't exist. Presumably the CRD is being
|
||||
// applied, so it will be added. Reset the RESTMapper to
|
||||
// make sure we pick up any new resource types on the
|
||||
// APIServer.
|
||||
cache[gn] = cacheEntry{
|
||||
err: err,
|
||||
}
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
ns := ""
|
||||
if mapping.Scope == meta.RESTScopeNamespace {
|
||||
ns = gn.Namespace
|
||||
}
|
||||
list, err := c.listUnstructured(ctx, mapping.GroupVersionKind, ns)
|
||||
if err != nil {
|
||||
// If the context was cancelled, we just stop the work and return
|
||||
// the error.
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return err
|
||||
}
|
||||
// For other errors, we just keep it the error. Whenever any pollers
|
||||
// request a resource covered by this gns, we just return the
|
||||
// error.
|
||||
cache[gn] = cacheEntry{
|
||||
err: err,
|
||||
}
|
||||
continue
|
||||
}
|
||||
cache[gn] = cacheEntry{
|
||||
resources: *list,
|
||||
}
|
||||
}
|
||||
c.cache = cache
|
||||
return nil
|
||||
}
|
||||
|
||||
// listUnstructured performs one or more LIST calls, paginating the requests
|
||||
// and aggregating the results. If aggregated, only the ResourceVersion,
|
||||
// SelfLink, and Items will be populated. The default page size is 500.
|
||||
func (c *CachingClusterReader) listUnstructured(
|
||||
ctx context.Context,
|
||||
gvk schema.GroupVersionKind,
|
||||
namespace string,
|
||||
) (*unstructured.UnstructuredList, error) {
|
||||
mOpts := metav1.ListOptions{}
|
||||
mOpts.SetGroupVersionKind(gvk)
|
||||
obj, _, err := pager.New(c.listPageFunc(namespace)).List(ctx, mOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch t := obj.(type) {
|
||||
case *unstructured.UnstructuredList:
|
||||
// all in one
|
||||
return t, nil
|
||||
case *metainternalversion.List:
|
||||
// aggregated result
|
||||
u := &unstructured.UnstructuredList{}
|
||||
u.SetGroupVersionKind(gvk)
|
||||
// Only ResourceVersion & SelfLink are copied into the aggregated result
|
||||
// by ListPager.
|
||||
if t.ResourceVersion != "" {
|
||||
u.SetResourceVersion(t.ResourceVersion)
|
||||
}
|
||||
if t.SelfLink != "" { // nolint:staticcheck
|
||||
u.SetSelfLink(t.SelfLink) // nolint:staticcheck
|
||||
}
|
||||
u.Items = make([]unstructured.Unstructured, len(t.Items))
|
||||
for i, item := range t.Items {
|
||||
ui, ok := item.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected list item type: %t", item)
|
||||
}
|
||||
u.Items[i] = *ui
|
||||
}
|
||||
return u, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected list type: %t", t)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CachingClusterReader) listPageFunc(namespace string) pager.ListPageFunc {
|
||||
return func(ctx context.Context, mOpts metav1.ListOptions) (runtime.Object, error) {
|
||||
mOptsCopy := mOpts
|
||||
labelSelector, err := labels.Parse(mOpts.LabelSelector)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse label selector: %w", err)
|
||||
}
|
||||
fieldSelector, err := fields.ParseSelector(mOpts.FieldSelector)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse field selector: %w", err)
|
||||
}
|
||||
cOpts := &client.ListOptions{
|
||||
LabelSelector: labelSelector,
|
||||
FieldSelector: fieldSelector,
|
||||
Namespace: namespace,
|
||||
Limit: mOpts.Limit,
|
||||
Continue: mOpts.Continue,
|
||||
Raw: &mOptsCopy,
|
||||
}
|
||||
var list unstructured.UnstructuredList
|
||||
list.SetGroupVersionKind(mOpts.GroupVersionKind())
|
||||
// Note: client.ListOptions only supports Exact ResourceVersion matching.
|
||||
// So leave ResourceVersion blank to get Any ResourceVersion.
|
||||
err = c.reader.List(ctx, &list, cOpts)
|
||||
return &list, err
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package clusterreader
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// NewDirectClusterReader creates a new implementation of the engine.ClusterReader interface that makes
|
||||
// calls directly to the cluster without any caching.
|
||||
func NewDirectClusterReader(reader client.Reader, _ meta.RESTMapper, _ object.ObjMetadataSet) (engine.ClusterReader, error) {
|
||||
return &DirectClusterReader{
|
||||
Reader: reader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DirectClusterReader is an implementation of the ClusterReader that just delegates all calls directly to
|
||||
// the underlying clusterreader. No caching.
|
||||
type DirectClusterReader struct {
|
||||
Reader client.Reader
|
||||
}
|
||||
|
||||
func (n *DirectClusterReader) Get(ctx context.Context, key client.ObjectKey, obj *unstructured.Unstructured) error {
|
||||
return n.Reader.Get(ctx, key, obj)
|
||||
}
|
||||
|
||||
func (n *DirectClusterReader) ListNamespaceScoped(ctx context.Context, list *unstructured.UnstructuredList, namespace string, selector labels.Selector) error {
|
||||
return n.Reader.List(ctx, list, client.InNamespace(namespace), client.MatchingLabelsSelector{Selector: selector})
|
||||
}
|
||||
|
||||
func (n *DirectClusterReader) ListClusterScoped(ctx context.Context, list *unstructured.UnstructuredList, selector labels.Selector) error {
|
||||
return n.Reader.List(ctx, list, client.MatchingLabelsSelector{Selector: selector})
|
||||
}
|
||||
|
||||
func (n *DirectClusterReader) Sync(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
Generated
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package clusterreader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// DynamicClusterReader is an implementation of the ClusterReader that delegates
|
||||
// all calls directly to the underlying DynamicClient. No caching.
|
||||
type DynamicClusterReader struct {
|
||||
DynamicClient dynamic.Interface
|
||||
Mapper meta.RESTMapper
|
||||
}
|
||||
|
||||
func (n *DynamicClusterReader) Get(ctx context.Context, key client.ObjectKey, obj *unstructured.Unstructured) error {
|
||||
mapping, err := n.Mapper.RESTMapping(obj.GroupVersionKind().GroupKind())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to map object: %w", err)
|
||||
}
|
||||
|
||||
serverObj, err := n.DynamicClient.Resource(mapping.Resource).
|
||||
Namespace(key.Namespace).
|
||||
Get(ctx, key.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serverObj.DeepCopyInto(obj)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *DynamicClusterReader) ListNamespaceScoped(ctx context.Context, list *unstructured.UnstructuredList, namespace string, selector labels.Selector) error {
|
||||
mapping, err := n.Mapper.RESTMapping(list.GroupVersionKind().GroupKind())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to map object: %w", err)
|
||||
}
|
||||
|
||||
serverObj, err := n.DynamicClient.Resource(mapping.Resource).
|
||||
Namespace(namespace).
|
||||
List(ctx, metav1.ListOptions{
|
||||
LabelSelector: selector.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serverObj.DeepCopyInto(list)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *DynamicClusterReader) ListClusterScoped(ctx context.Context, list *unstructured.UnstructuredList, selector labels.Selector) error {
|
||||
mapping, err := n.Mapper.RESTMapping(list.GroupVersionKind().GroupKind())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to map object: %w", err)
|
||||
}
|
||||
|
||||
serverObj, err := n.DynamicClient.Resource(mapping.Resource).
|
||||
List(ctx, metav1.ListOptions{
|
||||
LabelSelector: selector.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serverObj.DeepCopyInto(list)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *DynamicClusterReader) Sync(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
)
|
||||
|
||||
func NewResourceStatusCollector(identifiers object.ObjMetadataSet) *ResourceStatusCollector {
|
||||
resourceStatuses := make(map[object.ObjMetadata]*event.ResourceStatus)
|
||||
for _, id := range identifiers {
|
||||
resourceStatuses[id] = &event.ResourceStatus{
|
||||
Identifier: id,
|
||||
Status: status.UnknownStatus,
|
||||
}
|
||||
}
|
||||
return &ResourceStatusCollector{
|
||||
ResourceStatuses: resourceStatuses,
|
||||
}
|
||||
}
|
||||
|
||||
// Observer is an interface that can be implemented to have the
|
||||
// ResourceStatusCollector invoke the function on every event that
|
||||
// comes through the eventChannel.
|
||||
// The callback happens in the processing goroutine and while the
|
||||
// goroutine holds the lock, so any processing in the callback
|
||||
// must be done quickly.
|
||||
type Observer interface {
|
||||
Notify(*ResourceStatusCollector, event.Event)
|
||||
}
|
||||
|
||||
// ObserverFunc is a function implementation of the Observer
|
||||
// interface.
|
||||
type ObserverFunc func(*ResourceStatusCollector, event.Event)
|
||||
|
||||
func (o ObserverFunc) Notify(rsc *ResourceStatusCollector, e event.Event) {
|
||||
o(rsc, e)
|
||||
}
|
||||
|
||||
// ResourceStatusCollector is for use by clients of the polling library and provides
|
||||
// a way to keep track of the latest status/state for all the polled resources. The collector
|
||||
// is set up to listen to the eventChannel and keep the latest event for each resource. It also
|
||||
// provides a way to fetch the latest state for all resources and the aggregated status at any point.
|
||||
// The functions already handles synchronization so it can be used by multiple goroutines.
|
||||
type ResourceStatusCollector struct {
|
||||
mux sync.RWMutex
|
||||
|
||||
LastEventType event.Type
|
||||
|
||||
ResourceStatuses map[object.ObjMetadata]*event.ResourceStatus
|
||||
|
||||
Error error
|
||||
}
|
||||
|
||||
// ListenerResult is the type of the object passed back to the caller to
|
||||
// Listen and ListenWithObserver if a fatal error has been encountered.
|
||||
type ListenerResult struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// Listen kicks off the goroutine that will listen for the events on the
|
||||
// eventChannel. It returns a channel that will be closed the collector stops
|
||||
// listening to the eventChannel.
|
||||
func (o *ResourceStatusCollector) Listen(eventChannel <-chan event.Event) <-chan ListenerResult {
|
||||
return o.ListenWithObserver(eventChannel, nil)
|
||||
}
|
||||
|
||||
// ListenWithObserver kicks off the goroutine that will listen for the events on the
|
||||
// eventChannel. It returns a channel that will be closed the collector stops
|
||||
// listening to the eventChannel.
|
||||
// The provided observer will be invoked on every event, after the event
|
||||
// has been processed.
|
||||
func (o *ResourceStatusCollector) ListenWithObserver(eventChannel <-chan event.Event,
|
||||
observer Observer) <-chan ListenerResult {
|
||||
completed := make(chan ListenerResult)
|
||||
go func() {
|
||||
defer close(completed)
|
||||
for e := range eventChannel {
|
||||
err := o.processEvent(e)
|
||||
if err != nil {
|
||||
completed <- ListenerResult{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if observer != nil {
|
||||
observer.Notify(o, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return completed
|
||||
}
|
||||
|
||||
func (o *ResourceStatusCollector) processEvent(e event.Event) error {
|
||||
o.mux.Lock()
|
||||
defer o.mux.Unlock()
|
||||
o.LastEventType = e.Type
|
||||
if e.Type == event.ErrorEvent {
|
||||
o.Error = e.Error
|
||||
return e.Error
|
||||
}
|
||||
if e.Type == event.ResourceUpdateEvent {
|
||||
resourceStatus := e.Resource
|
||||
o.ResourceStatuses[resourceStatus.Identifier] = resourceStatus
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Observation contains the latest state known by the collector as returned
|
||||
// by a call to the LatestObservation function.
|
||||
type Observation struct {
|
||||
LastEventType event.Type
|
||||
|
||||
ResourceStatuses []*event.ResourceStatus
|
||||
|
||||
Error error
|
||||
}
|
||||
|
||||
// LatestObservation returns an Observation instance, which contains the
|
||||
// latest information about the resources known by the collector.
|
||||
func (o *ResourceStatusCollector) LatestObservation() *Observation {
|
||||
o.mux.RLock()
|
||||
defer o.mux.RUnlock()
|
||||
|
||||
var resourceStatuses event.ResourceStatuses
|
||||
for _, resourceStatus := range o.ResourceStatuses {
|
||||
resourceStatuses = append(resourceStatuses, resourceStatus)
|
||||
}
|
||||
sort.Sort(resourceStatuses)
|
||||
|
||||
return &Observation{
|
||||
LastEventType: o.LastEventType,
|
||||
ResourceStatuses: resourceStatuses,
|
||||
Error: o.Error,
|
||||
}
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// ClusterReaderFactory provides an interface that can be implemented to provide custom
|
||||
// ClusterReader implementations in the StatusPoller.
|
||||
type ClusterReaderFactory interface {
|
||||
New(reader client.Reader, mapper meta.RESTMapper, identifiers object.ObjMetadataSet) (ClusterReader, error)
|
||||
}
|
||||
|
||||
type ClusterReaderFactoryFunc func(client.Reader, meta.RESTMapper, object.ObjMetadataSet) (ClusterReader, error)
|
||||
|
||||
func (c ClusterReaderFactoryFunc) New(r client.Reader, m meta.RESTMapper, ids object.ObjMetadataSet) (ClusterReader, error) {
|
||||
return c(r, m, ids)
|
||||
}
|
||||
|
||||
// PollerEngine provides functionality for polling a cluster for status of a set of resources.
|
||||
type PollerEngine struct {
|
||||
Reader client.Reader
|
||||
Mapper meta.RESTMapper
|
||||
StatusReaders []StatusReader
|
||||
DefaultStatusReader StatusReader
|
||||
ClusterReaderFactory ClusterReaderFactory
|
||||
}
|
||||
|
||||
// Poll will create a new statusPollerRunner that will poll all the resources provided and report their status
|
||||
// back on the event channel returned. The statusPollerRunner can be cancelled at any time by cancelling the
|
||||
// context passed in.
|
||||
// The context can be used to stop the polling process by using timeout, deadline or
|
||||
// cancellation.
|
||||
func (s *PollerEngine) Poll(ctx context.Context, identifiers object.ObjMetadataSet, options Options) <-chan event.Event {
|
||||
eventChannel := make(chan event.Event)
|
||||
|
||||
go func() {
|
||||
defer close(eventChannel)
|
||||
|
||||
err := s.validateIdentifiers(identifiers)
|
||||
if err != nil {
|
||||
handleError(eventChannel, err)
|
||||
return
|
||||
}
|
||||
|
||||
clusterReader, err := s.ClusterReaderFactory.New(s.Reader, s.Mapper, identifiers)
|
||||
if err != nil {
|
||||
handleError(eventChannel, fmt.Errorf("error creating new ClusterReader: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
runner := &statusPollerRunner{
|
||||
clusterReader: clusterReader,
|
||||
statusReaders: s.StatusReaders,
|
||||
defaultStatusReader: s.DefaultStatusReader,
|
||||
identifiers: identifiers,
|
||||
previousResourceStatuses: make(map[object.ObjMetadata]*event.ResourceStatus),
|
||||
eventChannel: eventChannel,
|
||||
pollingInterval: options.PollInterval,
|
||||
}
|
||||
runner.Run(ctx)
|
||||
}()
|
||||
|
||||
return eventChannel
|
||||
}
|
||||
|
||||
func handleError(eventChannel chan event.Event, err error) {
|
||||
eventChannel <- event.Event{
|
||||
Type: event.ErrorEvent,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// validateIdentifiers makes sure that all namespaced resources
|
||||
// passed in
|
||||
func (s *PollerEngine) validateIdentifiers(identifiers object.ObjMetadataSet) error {
|
||||
for _, id := range identifiers {
|
||||
mapping, err := s.Mapper.RESTMapping(id.GroupKind)
|
||||
if err != nil {
|
||||
// If we can't find a match, just keep going. This can happen
|
||||
// if CRDs and CRs are applied at the same time.
|
||||
if meta.IsNoMatchError(err) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
if mapping.Scope.Name() == meta.RESTScopeNameNamespace && id.Namespace == "" {
|
||||
return fmt.Errorf("resource %s %s is namespace scoped, but namespace is not set",
|
||||
id.GroupKind.String(), id.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Options contains the different parameters that can be used to adjust the
|
||||
// behavior of the PollerEngine.
|
||||
// Timeout is not one of the options here as this should be accomplished by
|
||||
// setting a timeout on the context: https://golang.org/pkg/context/
|
||||
type Options struct {
|
||||
|
||||
// PollInterval defines how often the PollerEngine should poll the cluster for the latest
|
||||
// state of the resources.
|
||||
PollInterval time.Duration
|
||||
}
|
||||
|
||||
// statusPollerRunner is responsible for polling of a set of resources. Each call to Poll will create
|
||||
// a new statusPollerRunner, which means we can keep state in the runner and all data will only be accessed
|
||||
// by a single goroutine, meaning we don't need synchronization.
|
||||
// The statusPollerRunner uses an implementation of the ClusterReader interface to talk to the
|
||||
// kubernetes cluster. Currently this can be either the cached ClusterReader that syncs all needed resources
|
||||
// with LIST calls before each polling loop, or the normal ClusterReader that just forwards each call
|
||||
// to the client.Reader from controller-runtime.
|
||||
type statusPollerRunner struct {
|
||||
// clusterReader is the interface for fetching and listing resources from the cluster. It can be implemented
|
||||
// to make call directly to the cluster or use caching to reduce the number of calls to the cluster.
|
||||
clusterReader ClusterReader
|
||||
|
||||
// statusReaders contains the resource specific statusReaders. These will contain logic for how to
|
||||
// compute status for specific GroupKinds. These will use an ClusterReader to fetch
|
||||
// status of a resource and any generated resources.
|
||||
statusReaders []StatusReader
|
||||
|
||||
// defaultStatusReader is the generic engine that is used for all GroupKinds that
|
||||
// doesn't have a specific engine in the statusReaders map.
|
||||
defaultStatusReader StatusReader
|
||||
|
||||
// identifiers contains the set of identifiers for the resources that should be polled.
|
||||
// Each resource is identified by GroupKind, namespace and name.
|
||||
identifiers object.ObjMetadataSet
|
||||
|
||||
// previousResourceStatuses keeps track of the last event for each
|
||||
// of the polled resources. This is used to make sure we only
|
||||
// send events on the event channel when something has actually changed.
|
||||
previousResourceStatuses map[object.ObjMetadata]*event.ResourceStatus
|
||||
|
||||
// eventChannel is a channel where any updates to the status of resources
|
||||
// will be sent. The caller of Poll will listen for updates.
|
||||
eventChannel chan event.Event
|
||||
|
||||
// pollingInterval determines how often we should poll the cluster for
|
||||
// the latest state of resources.
|
||||
pollingInterval time.Duration
|
||||
}
|
||||
|
||||
// Run starts the polling loop of the statusReaders.
|
||||
func (r *statusPollerRunner) Run(ctx context.Context) {
|
||||
// Sets up ticker that will trigger the regular polling loop at a regular interval.
|
||||
ticker := time.NewTicker(r.pollingInterval)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
}()
|
||||
|
||||
err := r.syncAndPoll(ctx)
|
||||
if err != nil {
|
||||
r.handleSyncAndPollErr(err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// First sync and then compute status for all resources.
|
||||
err := r.syncAndPoll(ctx)
|
||||
if err != nil {
|
||||
r.handleSyncAndPollErr(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleSyncAndPollErr decides what to do if we encounter an error while
|
||||
// fetching resources to compute status. Errors are usually returned
|
||||
// as an ErrorEvent, but we handle context cancellation or deadline exceeded
|
||||
// differently since they aren't really errors, but a signal that the
|
||||
// process should shut down.
|
||||
func (r *statusPollerRunner) handleSyncAndPollErr(err error) {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return
|
||||
}
|
||||
r.eventChannel <- event.Event{
|
||||
Type: event.ErrorEvent,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *statusPollerRunner) syncAndPoll(ctx context.Context) error {
|
||||
// First trigger a sync of the ClusterReader. This may or may not actually
|
||||
// result in calls to the cluster, depending on the implementation.
|
||||
// If this call fails, there is no clean way to recover, so we just return an ErrorEvent
|
||||
// and shut down.
|
||||
err := r.clusterReader.Sync(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Poll all resources and compute status. If the polling of resources has completed (based
|
||||
// on information from the StatusAggregator and the value of pollUntilCancelled), we send
|
||||
// a CompletedEvent and return.
|
||||
return r.pollStatusForAllResources(ctx)
|
||||
}
|
||||
|
||||
// pollStatusForAllResources iterates over all the resources in the set and delegates
|
||||
// to the appropriate engine to compute the status.
|
||||
func (r *statusPollerRunner) pollStatusForAllResources(ctx context.Context) error {
|
||||
for _, id := range r.identifiers {
|
||||
// Check if the context has been cancelled on every iteration.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
gk := id.GroupKind
|
||||
statusReader := r.statusReaderForGroupKind(gk)
|
||||
resourceStatus, err := statusReader.ReadStatus(ctx, r.clusterReader, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.isUpdatedResourceStatus(resourceStatus) {
|
||||
r.previousResourceStatuses[id] = resourceStatus
|
||||
r.eventChannel <- event.Event{
|
||||
Type: event.ResourceUpdateEvent,
|
||||
Resource: resourceStatus,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *statusPollerRunner) statusReaderForGroupKind(gk schema.GroupKind) StatusReader {
|
||||
for _, sr := range r.statusReaders {
|
||||
if sr.Supports(gk) {
|
||||
return sr
|
||||
}
|
||||
}
|
||||
return r.defaultStatusReader
|
||||
}
|
||||
|
||||
func (r *statusPollerRunner) isUpdatedResourceStatus(resourceStatus *event.ResourceStatus) bool {
|
||||
oldResourceStatus, found := r.previousResourceStatuses[resourceStatus.Identifier]
|
||||
if !found {
|
||||
return true
|
||||
}
|
||||
return !event.ResourceStatusEqual(resourceStatus, oldResourceStatus)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// ClusterReader is the interface provided to the statusReaders to talk to the cluster. Implementations
|
||||
// of this interface allows different caching strategies, for example by pre-fetching resources using
|
||||
// LIST calls rather than letting each engine run multiple GET calls against the cluster. This can
|
||||
// significantly reduce the number of requests.
|
||||
type ClusterReader interface {
|
||||
// Get looks up the resource identifier by the key and the GVK in the provided obj reference. If something
|
||||
// goes wrong or the resource doesn't exist, an error is returned.
|
||||
Get(ctx context.Context, key client.ObjectKey, obj *unstructured.Unstructured) error
|
||||
// ListNamespaceScoped looks up the resources of the GVK given in the list and matches the namespace and
|
||||
// selector provided.
|
||||
ListNamespaceScoped(ctx context.Context, list *unstructured.UnstructuredList,
|
||||
namespace string, selector labels.Selector) error
|
||||
// ListClusterScoped looks up the resources of the GVK given in the list and that matches the selector
|
||||
// provided.
|
||||
ListClusterScoped(ctx context.Context, list *unstructured.UnstructuredList, selector labels.Selector) error
|
||||
// Sync is called by the engine before every polling loop, which provides an opportunity for the Reader
|
||||
// to sync caches.
|
||||
Sync(ctx context.Context) error
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// StatusReader is the main interface for computing status for resources. In this context,
|
||||
// a status reader is an object that can fetch a resource of a specific
|
||||
// GroupKind from the cluster and compute its status. For resources that
|
||||
// can own generated resources, the engine might also have knowledge about
|
||||
// how to identify these generated resources and how to compute status for
|
||||
// these generated resources.
|
||||
type StatusReader interface {
|
||||
// Supports tells the caller whether the StatusReader can compute status for
|
||||
// the provided GroupKind.
|
||||
Supports(schema.GroupKind) bool
|
||||
|
||||
// ReadStatus will fetch the resource identified by the given identifier
|
||||
// from the cluster and return an ResourceStatus that will contain
|
||||
// information about the latest state of the resource, its computed status
|
||||
// and information about any generated resources. Errors would usually be
|
||||
// added to the event.ResourceStatus, but in the case of fatal errors
|
||||
// that aren't connected to the particular resource, an error can also
|
||||
// be returned. Currently, only context cancellation and deadline exceeded
|
||||
// will cause an error to be returned.
|
||||
ReadStatus(ctx context.Context, reader ClusterReader, resource object.ObjMetadata) (*event.ResourceStatus, error)
|
||||
|
||||
// ReadStatusForObject is similar to ReadStatus, but instead of looking up the
|
||||
// resource based on an identifier, it will use the passed-in resource.
|
||||
// Errors would usually be added to the event.ResourceStatus, but in the case
|
||||
// of fatal errors that aren't connected to the particular resource, an error
|
||||
// can also be returned. Currently, only context cancellation and deadline exceeded
|
||||
// will cause an error to be returned.
|
||||
ReadStatusForObject(ctx context.Context, reader ClusterReader, object *unstructured.Unstructured) (*event.ResourceStatus, error)
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// Type is the type that describes the type of an Event that is passed back to the caller
|
||||
// as resources in the cluster are being polled.
|
||||
//
|
||||
//go:generate stringer -type=Type -linecomment
|
||||
type Type int
|
||||
|
||||
const (
|
||||
// ResourceUpdateEvent describes events related to a change in the status of one of the polled resources.
|
||||
ResourceUpdateEvent Type = iota // Update
|
||||
// ErrorEvent signals that the engine has encountered an error that it can not recover from. The engine
|
||||
// is shutting down and the event channel will be closed after this event.
|
||||
ErrorEvent // Error
|
||||
// SyncEvent signals that the engine has completed its initial
|
||||
// synchronization, and the cache is primed. After this point, it's safe to
|
||||
// assume that you won't miss events caused by your own subsequent actions.
|
||||
SyncEvent // Sync
|
||||
)
|
||||
|
||||
// Event defines that type that is passed back through the event channel to notify the caller of changes
|
||||
// as resources are being polled.
|
||||
type Event struct {
|
||||
// Type defines the type of event.
|
||||
Type Type
|
||||
|
||||
// Resource is only available for ResourceUpdateEvents. It includes information about the resource,
|
||||
// including the resource status, any errors and the resource itself (as an unstructured).
|
||||
Resource *ResourceStatus
|
||||
|
||||
// Error is only available for ErrorEvents. It contains the error that caused the engine to
|
||||
// give up.
|
||||
Error error
|
||||
}
|
||||
|
||||
// String returns a string suitable for logging
|
||||
func (e Event) String() string {
|
||||
if e.Error != nil {
|
||||
return fmt.Sprintf("Event{ Type: %q, Resource: %v, Error: %q }",
|
||||
e.Type, e.Resource, e.Error)
|
||||
}
|
||||
return fmt.Sprintf("Event{ Type: %q, Resource: %v }",
|
||||
e.Type, e.Resource)
|
||||
}
|
||||
|
||||
// ResourceStatus contains information about a resource after we have
|
||||
// fetched it from the cluster and computed status.
|
||||
type ResourceStatus struct {
|
||||
// Identifier contains the information necessary to locate the
|
||||
// resource within a cluster.
|
||||
Identifier object.ObjMetadata
|
||||
|
||||
// Status is the computed status for this resource.
|
||||
Status status.Status
|
||||
|
||||
// Resource contains the actual manifest for the resource that
|
||||
// was fetched from the cluster and used to compute status.
|
||||
Resource *unstructured.Unstructured
|
||||
|
||||
// Errors contains the error if something went wrong during the
|
||||
// process of fetching the resource and computing the status.
|
||||
Error error
|
||||
|
||||
// Message is text describing the status of the resource.
|
||||
Message string
|
||||
|
||||
// GeneratedResources is a slice of ResourceStatus that
|
||||
// contains information and status for any generated resources
|
||||
// of the current resource.
|
||||
GeneratedResources ResourceStatuses
|
||||
}
|
||||
|
||||
// String returns a string suitable for logging
|
||||
func (rs ResourceStatus) String() string {
|
||||
if rs.Error != nil {
|
||||
return fmt.Sprintf("ResourceStatus{ Identifier: %q, Status: %q, Message: %q, Resource: %v, GeneratedResources: %v, Error: %q }",
|
||||
rs.Identifier, rs.Status, rs.Message, rs.Resource, rs.GeneratedResources, rs.Error)
|
||||
}
|
||||
return fmt.Sprintf("ResourceStatus{ Identifier: %q, Status: %q, Message: %q, Resource: %v, GeneratedResources: %v }",
|
||||
rs.Identifier, rs.Status, rs.Message, rs.Resource, rs.GeneratedResources)
|
||||
}
|
||||
|
||||
type ResourceStatuses []*ResourceStatus
|
||||
|
||||
func (g ResourceStatuses) Len() int {
|
||||
return len(g)
|
||||
}
|
||||
|
||||
func (g ResourceStatuses) Less(i, j int) bool {
|
||||
idI := g[i].Identifier
|
||||
idJ := g[j].Identifier
|
||||
|
||||
if idI.Namespace != idJ.Namespace {
|
||||
return idI.Namespace < idJ.Namespace
|
||||
}
|
||||
if idI.GroupKind.Group != idJ.GroupKind.Group {
|
||||
return idI.GroupKind.Group < idJ.GroupKind.Group
|
||||
}
|
||||
if idI.GroupKind.Kind != idJ.GroupKind.Kind {
|
||||
return idI.GroupKind.Kind < idJ.GroupKind.Kind
|
||||
}
|
||||
return idI.Name < idJ.Name
|
||||
}
|
||||
|
||||
func (g ResourceStatuses) Swap(i, j int) {
|
||||
g[i], g[j] = g[j], g[i]
|
||||
}
|
||||
|
||||
// ResourceStatusEqual checks if two instances of ResourceStatus are the same.
|
||||
// This is used to determine whether status has changed for a particular resource.
|
||||
// Important to note that this does not check all fields, but only the ones
|
||||
// that are considered part of the status for a resource. So if the status
|
||||
// or the message of an ResourceStatus (or any of its generated ResourceStatuses)
|
||||
// have changed, this will return true. Changes to the state of the resource
|
||||
// itself that doesn't impact status are not considered.
|
||||
func ResourceStatusEqual(or1, or2 *ResourceStatus) bool {
|
||||
if or1.Identifier != or2.Identifier ||
|
||||
or1.Status != or2.Status ||
|
||||
or1.Message != or2.Message {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if generation has changed to make sure that even if
|
||||
// an update to a resource doesn't affect the status, a status event
|
||||
// will still be sent.
|
||||
if getGeneration(or1) != getGeneration(or2) {
|
||||
return false
|
||||
}
|
||||
|
||||
if or1.Error != nil && or2.Error != nil && or1.Error.Error() != or2.Error.Error() {
|
||||
return false
|
||||
}
|
||||
if (or1.Error == nil && or2.Error != nil) || (or1.Error != nil && or2.Error == nil) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(or1.GeneratedResources) != len(or2.GeneratedResources) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range or1.GeneratedResources {
|
||||
if !ResourceStatusEqual(or1.GeneratedResources[i], or2.GeneratedResources[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getGeneration(r *ResourceStatus) int64 {
|
||||
if r.Resource == nil {
|
||||
return 0
|
||||
}
|
||||
return r.Resource.GetGeneration()
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Code generated by "stringer -type=Type -linecomment"; DO NOT EDIT.
|
||||
|
||||
package event
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[ResourceUpdateEvent-0]
|
||||
_ = x[ErrorEvent-1]
|
||||
_ = x[SyncEvent-2]
|
||||
}
|
||||
|
||||
const _Type_name = "UpdateErrorSync"
|
||||
|
||||
var _Type_index = [...]uint8{0, 6, 11, 15}
|
||||
|
||||
func (i Type) String() string {
|
||||
if i < 0 || i >= Type(len(_Type_index)-1) {
|
||||
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _Type_name[_Type_index[i]:_Type_index[i+1]]
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package statusreaders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
// baseStatusReader is the implementation of the StatusReader interface defined
|
||||
// in the engine package. It contains the basic logic needed for every resource.
|
||||
// In order to handle resource specific logic, it must include an implementation
|
||||
// of the resourceTypeStatusReader interface.
|
||||
// In practice we will create many instances of baseStatusReader, each with a different
|
||||
// implementation of the resourceTypeStatusReader interface and therefore each
|
||||
// of the instances will be able to handle different resource types.
|
||||
type baseStatusReader struct {
|
||||
// mapper provides a way to look up the resource types that are available
|
||||
// in the cluster.
|
||||
mapper meta.RESTMapper
|
||||
|
||||
// resourceStatusReader is an resource-type specific implementation
|
||||
// of the resourceTypeStatusReader interface. While the baseStatusReader
|
||||
// contains the logic shared between all resource types, this implementation
|
||||
// will contain the resource specific info.
|
||||
resourceStatusReader resourceTypeStatusReader
|
||||
}
|
||||
|
||||
// resourceTypeStatusReader is an interface that can be implemented differently
|
||||
// for each resource type.
|
||||
type resourceTypeStatusReader interface {
|
||||
Supports(gk schema.GroupKind) bool
|
||||
ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, object *unstructured.Unstructured) (*event.ResourceStatus, error)
|
||||
}
|
||||
|
||||
func (b *baseStatusReader) Supports(gk schema.GroupKind) bool {
|
||||
return b.resourceStatusReader.Supports(gk)
|
||||
}
|
||||
|
||||
// ReadStatus reads the object identified by the passed-in identifier and computes it's status. It reads
|
||||
// the resource here, but computing status is delegated to the ReadStatusForObject function.
|
||||
func (b *baseStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, identifier object.ObjMetadata) (*event.ResourceStatus, error) {
|
||||
object, err := b.lookupResource(ctx, reader, identifier)
|
||||
if err != nil {
|
||||
return errIdentifierToResourceStatus(err, identifier)
|
||||
}
|
||||
return b.resourceStatusReader.ReadStatusForObject(ctx, reader, object)
|
||||
}
|
||||
|
||||
// ReadStatusForObject computes the status for the passed-in object. Since this is specific for each
|
||||
// resource type, the actual work is delegated to the implementation of the resourceTypeStatusReader interface.
|
||||
func (b *baseStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, object *unstructured.Unstructured) (*event.ResourceStatus, error) {
|
||||
return b.resourceStatusReader.ReadStatusForObject(ctx, reader, object)
|
||||
}
|
||||
|
||||
// lookupResource looks up a resource with the given identifier. It will use the rest mapper to resolve
|
||||
// the version of the GroupKind given in the identifier.
|
||||
// If the resource is found, it is returned. If it is not found or something
|
||||
// went wrong, the function will return an error.
|
||||
func (b *baseStatusReader) lookupResource(ctx context.Context, reader engine.ClusterReader, identifier object.ObjMetadata) (*unstructured.Unstructured, error) {
|
||||
GVK, err := gvk(identifier.GroupKind, b.mapper)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var u unstructured.Unstructured
|
||||
u.SetGroupVersionKind(GVK)
|
||||
key := types.NamespacedName{
|
||||
Name: identifier.Name,
|
||||
Namespace: identifier.Namespace,
|
||||
}
|
||||
err = reader.Get(ctx, key, &u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
// statusForGenResourcesFunc defines the function type used by the statusForGeneratedResource function.
|
||||
// TODO: Find a better solution for this. Maybe put the logic for looking up generated resources
|
||||
// into a separate type.
|
||||
type statusForGenResourcesFunc func(ctx context.Context, mapper meta.RESTMapper, reader engine.ClusterReader, statusReader resourceTypeStatusReader,
|
||||
object *unstructured.Unstructured, gk schema.GroupKind, selectorPath ...string) (event.ResourceStatuses, error)
|
||||
|
||||
// statusForGeneratedResources provides a way to fetch the statuses for all resources of a given GroupKind
|
||||
// that match the selector in the provided resource. Typically, this is used to fetch the status of generated
|
||||
// resources.
|
||||
func statusForGeneratedResources(ctx context.Context, mapper meta.RESTMapper, reader engine.ClusterReader, statusReader resourceTypeStatusReader,
|
||||
object *unstructured.Unstructured, gk schema.GroupKind, selectorPath ...string) (event.ResourceStatuses, error) {
|
||||
selector, err := toSelector(object, selectorPath...)
|
||||
if err != nil {
|
||||
return event.ResourceStatuses{}, err
|
||||
}
|
||||
|
||||
var objectList unstructured.UnstructuredList
|
||||
gvk, err := gvk(gk, mapper)
|
||||
if err != nil {
|
||||
return event.ResourceStatuses{}, err
|
||||
}
|
||||
objectList.SetGroupVersionKind(gvk)
|
||||
err = reader.ListNamespaceScoped(ctx, &objectList, object.GetNamespace(), selector)
|
||||
if err != nil {
|
||||
return event.ResourceStatuses{}, err
|
||||
}
|
||||
|
||||
var resourceStatuses event.ResourceStatuses
|
||||
for i := range objectList.Items {
|
||||
generatedObject := objectList.Items[i]
|
||||
resourceStatus, err := statusReader.ReadStatusForObject(ctx, reader, &generatedObject)
|
||||
if err != nil {
|
||||
return event.ResourceStatuses{}, err
|
||||
}
|
||||
resourceStatuses = append(resourceStatuses, resourceStatus)
|
||||
}
|
||||
sort.Sort(resourceStatuses)
|
||||
return resourceStatuses, nil
|
||||
}
|
||||
|
||||
// gvk looks up the GVK from a GroupKind using the rest mapper.
|
||||
func gvk(gk schema.GroupKind, mapper meta.RESTMapper) (schema.GroupVersionKind, error) {
|
||||
mapping, err := mapper.RESTMapping(gk)
|
||||
if err != nil {
|
||||
return schema.GroupVersionKind{}, err
|
||||
}
|
||||
return mapping.GroupVersionKind, nil
|
||||
}
|
||||
|
||||
func toSelector(resource *unstructured.Unstructured, path ...string) (labels.Selector, error) {
|
||||
selector, found, err := unstructured.NestedMap(resource.Object, path...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("no selector found")
|
||||
}
|
||||
bytes, err := json.Marshal(selector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var s metav1.LabelSelector
|
||||
err = json.Unmarshal(bytes, &s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return metav1.LabelSelectorAsSelector(&s)
|
||||
}
|
||||
|
||||
// errResourceToResourceStatus construct the appropriate ResourceStatus
|
||||
// object based on an error and the resource itself.
|
||||
func errResourceToResourceStatus(err error, resource *unstructured.Unstructured, genResources ...*event.ResourceStatus) (*event.ResourceStatus, error) {
|
||||
// If the error is from the context, we don't attach that to the ResourceStatus,
|
||||
// but just return it directly so the caller can decide how to handle this
|
||||
// situation.
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || isRateLimiterContextDeadlineExceeded(err) {
|
||||
return nil, err
|
||||
}
|
||||
identifier := object.UnstructuredToObjMetadata(resource)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return &event.ResourceStatus{
|
||||
Identifier: identifier,
|
||||
Status: status.NotFoundStatus,
|
||||
Message: "Resource not found",
|
||||
}, nil
|
||||
}
|
||||
return &event.ResourceStatus{
|
||||
Identifier: identifier,
|
||||
Status: status.UnknownStatus,
|
||||
Resource: resource,
|
||||
Error: err,
|
||||
GeneratedResources: genResources,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// errIdentifierToResourceStatus construct the appropriate ResourceStatus
|
||||
// object based on an error and the identifier for a resource.
|
||||
func errIdentifierToResourceStatus(err error, identifier object.ObjMetadata) (*event.ResourceStatus, error) {
|
||||
// If the error is from the context, we don't attach that to the ResourceStatus,
|
||||
// but just return it directly so the caller can decide how to handle this
|
||||
// situation.
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || isRateLimiterContextDeadlineExceeded(err) {
|
||||
return nil, err
|
||||
}
|
||||
if apierrors.IsNotFound(err) {
|
||||
return &event.ResourceStatus{
|
||||
Identifier: identifier,
|
||||
Status: status.NotFoundStatus,
|
||||
Message: "Resource not found",
|
||||
}, nil
|
||||
}
|
||||
return &event.ResourceStatus{
|
||||
Identifier: identifier,
|
||||
Status: status.UnknownStatus,
|
||||
Error: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isRateLimiterContextDeadlineExceeded checks if the error is a rate limiter "would exceed context deadline" error
|
||||
// this allows us to treat it the same way as the context.Canceled and context.DeadlineExceeded errors
|
||||
// instead of attaching the error to the ResourceStatus, caller can decide how to handle this
|
||||
func isRateLimiterContextDeadlineExceeded(err error) bool {
|
||||
for {
|
||||
next := errors.Unwrap(err)
|
||||
if next == nil {
|
||||
break
|
||||
}
|
||||
err = next
|
||||
}
|
||||
|
||||
// there's no dedicated error type for this, hence we check the error message
|
||||
// https://cs.opensource.google/go/x/time/+/refs/tags/v0.10.0:rate/rate.go;l=276
|
||||
return err != nil && err.Error() == "rate: Wait(n=1) would exceed context deadline"
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package statusreaders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// NewDefaultStatusReader returns a DelegatingStatusReader that wraps a list of
|
||||
// statusreaders to cover all built-in Kubernetes resources and other CRDs that
|
||||
// follow known status conventions.
|
||||
func NewDefaultStatusReader(mapper meta.RESTMapper) engine.StatusReader {
|
||||
return NewStatusReader(mapper)
|
||||
}
|
||||
|
||||
// NewStatusReader returns a DelegatingStatusReader that includes the statusreaders
|
||||
// for the build-in Kubernetes resources and also any provided custom status readers.
|
||||
func NewStatusReader(mapper meta.RESTMapper, statusReaders ...engine.StatusReader) engine.StatusReader {
|
||||
defaultStatusReader := NewGenericStatusReader(mapper, status.Compute)
|
||||
|
||||
replicaSetStatusReader := NewReplicaSetStatusReader(mapper, defaultStatusReader)
|
||||
deploymentStatusReader := NewDeploymentResourceReader(mapper, replicaSetStatusReader)
|
||||
statefulSetStatusReader := NewStatefulSetResourceReader(mapper, defaultStatusReader)
|
||||
|
||||
statusReaders = append(statusReaders,
|
||||
deploymentStatusReader,
|
||||
statefulSetStatusReader,
|
||||
replicaSetStatusReader,
|
||||
defaultStatusReader,
|
||||
)
|
||||
|
||||
return &DelegatingStatusReader{
|
||||
StatusReaders: statusReaders,
|
||||
}
|
||||
}
|
||||
|
||||
type DelegatingStatusReader struct {
|
||||
StatusReaders []engine.StatusReader
|
||||
}
|
||||
|
||||
func (dsr *DelegatingStatusReader) Supports(gk schema.GroupKind) bool {
|
||||
for _, sr := range dsr.StatusReaders {
|
||||
if sr.Supports(gk) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (dsr *DelegatingStatusReader) ReadStatus(
|
||||
ctx context.Context,
|
||||
reader engine.ClusterReader,
|
||||
id object.ObjMetadata,
|
||||
) (*event.ResourceStatus, error) {
|
||||
gk := id.GroupKind
|
||||
for _, sr := range dsr.StatusReaders {
|
||||
if sr.Supports(gk) {
|
||||
return sr.ReadStatus(ctx, reader, id)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no status reader supports this resource: %v", gk)
|
||||
}
|
||||
|
||||
func (dsr *DelegatingStatusReader) ReadStatusForObject(
|
||||
ctx context.Context,
|
||||
reader engine.ClusterReader,
|
||||
obj *unstructured.Unstructured,
|
||||
) (*event.ResourceStatus, error) {
|
||||
gk := obj.GroupVersionKind().GroupKind()
|
||||
for _, sr := range dsr.StatusReaders {
|
||||
if sr.Supports(gk) {
|
||||
return sr.ReadStatusForObject(ctx, reader, obj)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no status reader supports this resource: %v", gk)
|
||||
}
|
||||
Generated
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package statusreaders
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func NewDeploymentResourceReader(mapper meta.RESTMapper, rsStatusReader resourceTypeStatusReader) engine.StatusReader {
|
||||
return &baseStatusReader{
|
||||
mapper: mapper,
|
||||
resourceStatusReader: &deploymentResourceReader{
|
||||
mapper: mapper,
|
||||
rsStatusReader: rsStatusReader,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// deploymentResourceReader is a resourceTypeStatusReader that can fetch Deployment
|
||||
// resources from the cluster, knows how to find any ReplicaSets belonging to the
|
||||
// Deployment, and compute status for the deployment.
|
||||
type deploymentResourceReader struct {
|
||||
mapper meta.RESTMapper
|
||||
|
||||
// rsStatusReader is the implementation of the resourceTypeStatusReader
|
||||
// the knows how to compute the status for ReplicaSets.
|
||||
rsStatusReader resourceTypeStatusReader
|
||||
}
|
||||
|
||||
var _ resourceTypeStatusReader = &deploymentResourceReader{}
|
||||
|
||||
func (d *deploymentResourceReader) Supports(gk schema.GroupKind) bool {
|
||||
return gk == appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind()
|
||||
}
|
||||
|
||||
func (d *deploymentResourceReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader,
|
||||
deployment *unstructured.Unstructured) (*event.ResourceStatus, error) {
|
||||
identifier := object.UnstructuredToObjMetadata(deployment)
|
||||
|
||||
replicaSetStatuses, err := statusForGeneratedResources(ctx, d.mapper, reader, d.rsStatusReader, deployment,
|
||||
appsv1.SchemeGroupVersion.WithKind("ReplicaSet").GroupKind(), "spec", "selector")
|
||||
if err != nil {
|
||||
return errResourceToResourceStatus(err, deployment)
|
||||
}
|
||||
|
||||
// Currently this engine just uses the status library for computing
|
||||
// status for the deployment. But we do have the status and state for all
|
||||
// ReplicaSets and Pods in the ObservedReplicaSets data structure, so the
|
||||
// rules can be improved to take advantage of this information.
|
||||
res, err := status.Compute(deployment)
|
||||
if err != nil {
|
||||
return errResourceToResourceStatus(err, deployment, replicaSetStatuses...)
|
||||
}
|
||||
|
||||
return &event.ResourceStatus{
|
||||
Identifier: identifier,
|
||||
Status: res.Status,
|
||||
Resource: deployment,
|
||||
Message: res.Message,
|
||||
GeneratedResources: replicaSetStatuses,
|
||||
}, nil
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package statusreaders
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// StatusFunc returns the status of the given object. This func is passed into
|
||||
// NewGenericStatusReader so that the returned StatusReader can be used for custom types.
|
||||
// An example of a StatusFunc is status.Compute.
|
||||
type StatusFunc func(u *unstructured.Unstructured) (*status.Result, error)
|
||||
|
||||
func NewGenericStatusReader(mapper meta.RESTMapper, statusFunc StatusFunc) engine.StatusReader {
|
||||
return &baseStatusReader{
|
||||
mapper: mapper,
|
||||
resourceStatusReader: &genericStatusReader{
|
||||
mapper: mapper,
|
||||
statusFunc: statusFunc,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// genericStatusReader is a resourceTypeStatusReader that will be used for
|
||||
// any resource that doesn't have a specific engine. It will just delegate
|
||||
// computation of status to the status library.
|
||||
// This should work pretty well for resources that doesn't have any
|
||||
// generated resources and where status can be computed only based on the
|
||||
// resource itself.
|
||||
type genericStatusReader struct {
|
||||
mapper meta.RESTMapper
|
||||
|
||||
statusFunc StatusFunc
|
||||
}
|
||||
|
||||
var _ resourceTypeStatusReader = &genericStatusReader{}
|
||||
|
||||
func (g *genericStatusReader) Supports(schema.GroupKind) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *genericStatusReader) ReadStatusForObject(_ context.Context, _ engine.ClusterReader, resource *unstructured.Unstructured) (*event.ResourceStatus, error) {
|
||||
identifier := object.UnstructuredToObjMetadata(resource)
|
||||
|
||||
res, err := g.statusFunc(resource)
|
||||
if err != nil {
|
||||
return errResourceToResourceStatus(err, resource)
|
||||
}
|
||||
|
||||
return &event.ResourceStatus{
|
||||
Identifier: identifier,
|
||||
Status: res.Status,
|
||||
Resource: resource,
|
||||
Message: res.Message,
|
||||
}, nil
|
||||
}
|
||||
Generated
Vendored
+140
@@ -0,0 +1,140 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package statusreaders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func newPodControllerStatusReader(mapper meta.RESTMapper, podStatusReader resourceTypeStatusReader) *podControllerStatusReader {
|
||||
return &podControllerStatusReader{
|
||||
mapper: mapper,
|
||||
podStatusReader: podStatusReader,
|
||||
groupKind: schema.GroupKind{
|
||||
Group: "",
|
||||
Kind: "Pod",
|
||||
},
|
||||
statusFunc: status.Compute,
|
||||
statusForGenResourcesFunc: statusForGeneratedResources,
|
||||
}
|
||||
}
|
||||
|
||||
// podControllerStatusReader encapsulates the logic needed to compute the status
|
||||
// for resource types that act as controllers for pods. This is quite common, so
|
||||
// the logic is here instead of duplicated in each resource specific StatusReader.
|
||||
type podControllerStatusReader struct {
|
||||
mapper meta.RESTMapper
|
||||
podStatusReader resourceTypeStatusReader
|
||||
groupKind schema.GroupKind
|
||||
|
||||
statusFunc func(u *unstructured.Unstructured) (*status.Result, error)
|
||||
// TODO(mortent): See if we can avoid this. For now it is useful for testing.
|
||||
statusForGenResourcesFunc statusForGenResourcesFunc
|
||||
}
|
||||
|
||||
func (p *podControllerStatusReader) readStatus(ctx context.Context, reader engine.ClusterReader, obj *unstructured.Unstructured) (*event.ResourceStatus, error) {
|
||||
identifier := object.UnstructuredToObjMetadata(obj)
|
||||
|
||||
podResourceStatuses, err := p.statusForGenResourcesFunc(ctx, p.mapper, reader, p.podStatusReader, obj,
|
||||
p.groupKind, "spec", "selector")
|
||||
if err != nil {
|
||||
return errResourceToResourceStatus(err, obj)
|
||||
}
|
||||
|
||||
res, err := p.statusFunc(obj)
|
||||
if err != nil {
|
||||
return errResourceToResourceStatus(err, obj, podResourceStatuses...)
|
||||
}
|
||||
|
||||
// If the status comes back as pending, we take a look at the pods to make sure
|
||||
// none of them have terminally failed. Pods that are pending scheduling are
|
||||
// excluded, as this is a transient state that cluster autoscalers can resolve.
|
||||
// Pods that are being deleted (e.g. during a rolling update) are also excluded.
|
||||
if res.Status == status.InProgressStatus {
|
||||
var failedPods []*event.ResourceStatus
|
||||
for _, podResourceStatus := range podResourceStatuses {
|
||||
if podResourceStatus.Status == status.FailedStatus {
|
||||
if isTransientPodFailure(podResourceStatus) {
|
||||
continue
|
||||
}
|
||||
failedPods = append(failedPods, podResourceStatus)
|
||||
}
|
||||
}
|
||||
if len(failedPods) > 0 {
|
||||
return &event.ResourceStatus{
|
||||
Identifier: identifier,
|
||||
Status: status.FailedStatus,
|
||||
Resource: obj,
|
||||
Message: fmt.Sprintf("%d pods have failed", len(failedPods)),
|
||||
GeneratedResources: podResourceStatuses,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return &event.ResourceStatus{
|
||||
Identifier: identifier,
|
||||
Status: res.Status,
|
||||
Resource: obj,
|
||||
Message: res.Message,
|
||||
GeneratedResources: podResourceStatuses,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isTransientPodFailure returns true if the pod's failure is likely transient
|
||||
// and should not cause the parent controller to be marked as failed. This
|
||||
// includes pods that are pending scheduling (which an autoscaler may resolve)
|
||||
// and pods that are being deleted (during a rolling update).
|
||||
func isTransientPodFailure(podStatus *event.ResourceStatus) bool {
|
||||
pod := podStatus.Resource
|
||||
if pod == nil {
|
||||
// If the resource is not available, we cannot determine whether the
|
||||
// failure is transient. Treat it as transient to avoid prematurely
|
||||
// marking the parent controller as failed.
|
||||
return true
|
||||
}
|
||||
|
||||
// Pods being deleted are expected during rolling updates.
|
||||
if pod.GetDeletionTimestamp() != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Pods that are pending scheduling due to insufficient resources are
|
||||
// transient failures that a cluster autoscaler can resolve.
|
||||
if isPodUnschedulable(pod) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isPodUnschedulable returns true if the object is a pod with a PodScheduled
|
||||
// condition indicating it is Unschedulable.
|
||||
func isPodUnschedulable(obj *unstructured.Unstructured) bool {
|
||||
gk := obj.GroupVersionKind().GroupKind()
|
||||
if gk != (schema.GroupKind{Kind: "Pod"}) {
|
||||
return false
|
||||
}
|
||||
objWithConditions, err := status.GetObjectWithConditions(obj.Object)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, cond := range objWithConditions.Status.Conditions {
|
||||
if cond.Type == string(corev1.PodScheduled) &&
|
||||
cond.Status == corev1.ConditionFalse &&
|
||||
cond.Reason == corev1.PodReasonUnschedulable {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Generated
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package statusreaders
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func NewReplicaSetStatusReader(mapper meta.RESTMapper, podStatusReader resourceTypeStatusReader) engine.StatusReader {
|
||||
return &baseStatusReader{
|
||||
mapper: mapper,
|
||||
resourceStatusReader: &replicaSetStatusReader{
|
||||
mapper: mapper,
|
||||
podStatusReader: podStatusReader,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// replicaSetStatusReader is an engine that can fetch ReplicaSet resources
|
||||
// from the cluster, knows how to find any Pods belonging to the ReplicaSet,
|
||||
// and compute status for the ReplicaSet.
|
||||
type replicaSetStatusReader struct {
|
||||
mapper meta.RESTMapper
|
||||
|
||||
podStatusReader resourceTypeStatusReader
|
||||
}
|
||||
|
||||
var _ resourceTypeStatusReader = &replicaSetStatusReader{}
|
||||
|
||||
func (r *replicaSetStatusReader) Supports(gk schema.GroupKind) bool {
|
||||
return gk == appsv1.SchemeGroupVersion.WithKind("ReplicaSet").GroupKind()
|
||||
}
|
||||
|
||||
func (r *replicaSetStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, rs *unstructured.Unstructured) (*event.ResourceStatus, error) {
|
||||
return newPodControllerStatusReader(r.mapper, r.podStatusReader).readStatus(ctx, reader, rs)
|
||||
}
|
||||
Generated
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package statusreaders
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func NewStatefulSetResourceReader(mapper meta.RESTMapper, podResourceReader resourceTypeStatusReader) engine.StatusReader {
|
||||
return &baseStatusReader{
|
||||
mapper: mapper,
|
||||
resourceStatusReader: &statefulSetResourceReader{
|
||||
mapper: mapper,
|
||||
podResourceReader: podResourceReader,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// statefulSetResourceReader is an implementation of the ResourceReader interface
|
||||
// that can fetch StatefulSet resources from the cluster, knows how to find any
|
||||
// Pods belonging to the StatefulSet, and compute status for the StatefulSet.
|
||||
type statefulSetResourceReader struct {
|
||||
mapper meta.RESTMapper
|
||||
|
||||
podResourceReader resourceTypeStatusReader
|
||||
}
|
||||
|
||||
var _ resourceTypeStatusReader = &statefulSetResourceReader{}
|
||||
|
||||
func (s *statefulSetResourceReader) Supports(gk schema.GroupKind) bool {
|
||||
return gk == appsv1.SchemeGroupVersion.WithKind("StatefulSet").GroupKind()
|
||||
}
|
||||
|
||||
func (s *statefulSetResourceReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader,
|
||||
statefulSet *unstructured.Unstructured) (*event.ResourceStatus, error) {
|
||||
return newPodControllerStatusReader(s.mapper, s.podResourceReader).readStatus(ctx, reader, statefulSet)
|
||||
}
|
||||
+627
@@ -0,0 +1,627 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// GetConditionsFn defines the signature for functions to compute the
|
||||
// status of a built-in resource.
|
||||
type GetConditionsFn func(*unstructured.Unstructured) (*Result, error)
|
||||
|
||||
// legacyTypes defines the mapping from GroupKind to a function that can
|
||||
// compute the status for the given resource.
|
||||
var legacyTypes = map[string]GetConditionsFn{
|
||||
"Service": serviceConditions,
|
||||
"Pod": podConditions,
|
||||
"Secret": alwaysReady,
|
||||
"PersistentVolumeClaim": pvcConditions,
|
||||
"apps/StatefulSet": stsConditions,
|
||||
"apps/DaemonSet": daemonsetConditions,
|
||||
"extensions/DaemonSet": daemonsetConditions,
|
||||
"apps/Deployment": deploymentConditions,
|
||||
"extensions/Deployment": deploymentConditions,
|
||||
"apps/ReplicaSet": replicasetConditions,
|
||||
"extensions/ReplicaSet": replicasetConditions,
|
||||
"policy/PodDisruptionBudget": pdbConditions,
|
||||
"batch/CronJob": alwaysReady,
|
||||
"ConfigMap": alwaysReady,
|
||||
"batch/Job": jobConditions,
|
||||
"apiextensions.k8s.io/CustomResourceDefinition": crdConditions,
|
||||
}
|
||||
|
||||
const (
|
||||
tooFewReady = "LessReady"
|
||||
tooFewAvailable = "LessAvailable"
|
||||
tooFewUpdated = "LessUpdated"
|
||||
tooFewReplicas = "LessReplicas"
|
||||
extraPods = "ExtraPods"
|
||||
|
||||
onDeleteUpdateStrategy = "OnDelete"
|
||||
|
||||
// How long a pod can be unscheduled before it is reported as
|
||||
// unschedulable.
|
||||
ScheduleWindow = 15 * time.Second
|
||||
)
|
||||
|
||||
// GetLegacyConditionsFn returns a function that can compute the status for the
|
||||
// given resource, or nil if the resource type is not known.
|
||||
func GetLegacyConditionsFn(u *unstructured.Unstructured) GetConditionsFn {
|
||||
gvk := u.GroupVersionKind()
|
||||
g := gvk.Group
|
||||
k := gvk.Kind
|
||||
key := g + "/" + k
|
||||
if g == "" {
|
||||
key = k
|
||||
}
|
||||
return legacyTypes[key]
|
||||
}
|
||||
|
||||
// alwaysReady Used for resources that are always ready
|
||||
func alwaysReady(u *unstructured.Unstructured) (*Result, error) {
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: "Resource is always ready",
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// stsConditions return standardized Conditions for Statefulset
|
||||
//
|
||||
// StatefulSet does define the .status.conditions property, but the controller never
|
||||
// actually sets any Conditions. Thus, status must be computed only based on the other
|
||||
// properties under .status. We don't have any way to find out if a reconcile for a
|
||||
// StatefulSet has failed.
|
||||
func stsConditions(u *unstructured.Unstructured) (*Result, error) {
|
||||
obj := u.UnstructuredContent()
|
||||
|
||||
// updateStrategy==ondelete is a user managed statefulset.
|
||||
updateStrategy := GetStringField(obj, ".spec.updateStrategy.type", "")
|
||||
if updateStrategy == onDeleteUpdateStrategy {
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: "StatefulSet is using the ondelete update strategy",
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Replicas
|
||||
specReplicas := GetIntField(obj, ".spec.replicas", 1)
|
||||
readyReplicas := GetIntField(obj, ".status.readyReplicas", 0)
|
||||
currentReplicas := GetIntField(obj, ".status.currentReplicas", 0)
|
||||
updatedReplicas := GetIntField(obj, ".status.updatedReplicas", 0)
|
||||
statusReplicas := GetIntField(obj, ".status.replicas", 0)
|
||||
partition := GetIntField(obj, ".spec.updateStrategy.rollingUpdate.partition", -1)
|
||||
|
||||
if specReplicas > statusReplicas {
|
||||
message := fmt.Sprintf("Replicas: %d/%d", statusReplicas, specReplicas)
|
||||
return newInProgressStatus(tooFewReplicas, message), nil
|
||||
}
|
||||
|
||||
if specReplicas > readyReplicas {
|
||||
message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas)
|
||||
return newInProgressStatus(tooFewReady, message), nil
|
||||
}
|
||||
|
||||
if statusReplicas > specReplicas {
|
||||
message := fmt.Sprintf("Pending termination: %d", statusReplicas-specReplicas)
|
||||
return newInProgressStatus(extraPods, message), nil
|
||||
}
|
||||
|
||||
// https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#partitions
|
||||
if partition != -1 {
|
||||
if updatedReplicas < (specReplicas - partition) {
|
||||
message := fmt.Sprintf("updated: %d/%d", updatedReplicas, specReplicas-partition)
|
||||
return newInProgressStatus("PartitionRollout", message), nil
|
||||
}
|
||||
// Partition case All ok
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: fmt.Sprintf("Partition rollout complete. updated: %d", updatedReplicas),
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if specReplicas > currentReplicas {
|
||||
message := fmt.Sprintf("current: %d/%d", currentReplicas, specReplicas)
|
||||
return newInProgressStatus("LessCurrent", message), nil
|
||||
}
|
||||
|
||||
// Revision
|
||||
currentRevision := GetStringField(obj, ".status.currentRevision", "")
|
||||
updatedRevision := GetStringField(obj, ".status.updateRevision", "")
|
||||
if currentRevision != updatedRevision {
|
||||
message := "Waiting for updated revision to match current"
|
||||
return newInProgressStatus("RevisionMismatch", message), nil
|
||||
}
|
||||
|
||||
// All ok
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: fmt.Sprintf("All replicas scheduled as expected. Replicas: %d", statusReplicas),
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// deploymentConditions return standardized Conditions for Deployment.
|
||||
//
|
||||
// For Deployments, we look at .status.conditions as well as the other properties
|
||||
// under .status. Status will be Failed if the progress deadline has been exceeded.
|
||||
func deploymentConditions(u *unstructured.Unstructured) (*Result, error) {
|
||||
obj := u.UnstructuredContent()
|
||||
|
||||
progressing := false
|
||||
|
||||
// Check if progressDeadlineSeconds is set. If not, the controller will not set
|
||||
// the `Progressing` condition, so it will always consider a deployment to be
|
||||
// progressing. The use of math.MaxInt32 is due to special handling in the
|
||||
// controller:
|
||||
// https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/deployment/util/deployment_util.go#L886
|
||||
progressDeadline := GetIntField(obj, ".spec.progressDeadlineSeconds", math.MaxInt32)
|
||||
if progressDeadline == math.MaxInt32 {
|
||||
progressing = true
|
||||
}
|
||||
|
||||
available := false
|
||||
|
||||
objc, err := GetObjectWithConditions(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, c := range objc.Status.Conditions {
|
||||
switch c.Type {
|
||||
case "Progressing": // appsv1.DeploymentProgressing:
|
||||
// https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/deployment/progress.go#L52
|
||||
if c.Reason == "ProgressDeadlineExceeded" {
|
||||
return &Result{
|
||||
Status: FailedStatus,
|
||||
Message: "Progress deadline exceeded",
|
||||
Conditions: []Condition{{ConditionStalled, corev1.ConditionTrue, c.Reason, c.Message}},
|
||||
}, nil
|
||||
}
|
||||
if c.Status == corev1.ConditionTrue && c.Reason == "NewReplicaSetAvailable" {
|
||||
progressing = true
|
||||
}
|
||||
case "Available": // appsv1.DeploymentAvailable:
|
||||
if c.Status == corev1.ConditionTrue {
|
||||
available = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replicas
|
||||
specReplicas := GetIntField(obj, ".spec.replicas", 1) // Controller uses 1 as default if not specified.
|
||||
statusReplicas := GetIntField(obj, ".status.replicas", 0)
|
||||
updatedReplicas := GetIntField(obj, ".status.updatedReplicas", 0)
|
||||
readyReplicas := GetIntField(obj, ".status.readyReplicas", 0)
|
||||
availableReplicas := GetIntField(obj, ".status.availableReplicas", 0)
|
||||
|
||||
// TODO spec.replicas zero case ??
|
||||
|
||||
if specReplicas > statusReplicas {
|
||||
message := fmt.Sprintf("Replicas: %d/%d", statusReplicas, specReplicas)
|
||||
return newInProgressStatus(tooFewReplicas, message), nil
|
||||
}
|
||||
|
||||
if specReplicas > updatedReplicas {
|
||||
message := fmt.Sprintf("Updated: %d/%d", updatedReplicas, specReplicas)
|
||||
return newInProgressStatus(tooFewUpdated, message), nil
|
||||
}
|
||||
|
||||
if statusReplicas > specReplicas {
|
||||
message := fmt.Sprintf("Pending termination: %d", statusReplicas-specReplicas)
|
||||
return newInProgressStatus(extraPods, message), nil
|
||||
}
|
||||
|
||||
if updatedReplicas > availableReplicas {
|
||||
message := fmt.Sprintf("Available: %d/%d", availableReplicas, updatedReplicas)
|
||||
return newInProgressStatus(tooFewAvailable, message), nil
|
||||
}
|
||||
|
||||
if specReplicas > readyReplicas {
|
||||
message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas)
|
||||
return newInProgressStatus(tooFewReady, message), nil
|
||||
}
|
||||
|
||||
// check conditions
|
||||
if !progressing {
|
||||
message := "ReplicaSet not Available"
|
||||
return newInProgressStatus("ReplicaSetNotAvailable", message), nil
|
||||
}
|
||||
if !available {
|
||||
message := "Deployment not Available"
|
||||
return newInProgressStatus("DeploymentNotAvailable", message), nil
|
||||
}
|
||||
// All ok
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: fmt.Sprintf("Deployment is available. Replicas: %d", statusReplicas),
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// replicasetConditions return standardized Conditions for Replicaset
|
||||
func replicasetConditions(u *unstructured.Unstructured) (*Result, error) {
|
||||
obj := u.UnstructuredContent()
|
||||
|
||||
// Conditions
|
||||
objc, err := GetObjectWithConditions(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, c := range objc.Status.Conditions {
|
||||
// https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/replicaset/replica_set_utils.go
|
||||
if c.Type == "ReplicaFailure" && c.Status == corev1.ConditionTrue {
|
||||
message := "Replica Failure condition. Check Pods"
|
||||
return newInProgressStatus("ReplicaFailure", message), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Replicas
|
||||
specReplicas := GetIntField(obj, ".spec.replicas", 1) // Controller uses 1 as default if not specified.
|
||||
statusReplicas := GetIntField(obj, ".status.replicas", 0)
|
||||
readyReplicas := GetIntField(obj, ".status.readyReplicas", 0)
|
||||
availableReplicas := GetIntField(obj, ".status.availableReplicas", 0)
|
||||
fullyLabelledReplicas := GetIntField(obj, ".status.fullyLabeledReplicas", 0)
|
||||
|
||||
if specReplicas > fullyLabelledReplicas {
|
||||
message := fmt.Sprintf("Labelled: %d/%d", fullyLabelledReplicas, specReplicas)
|
||||
return newInProgressStatus("LessLabelled", message), nil
|
||||
}
|
||||
|
||||
if specReplicas > availableReplicas {
|
||||
message := fmt.Sprintf("Available: %d/%d", availableReplicas, specReplicas)
|
||||
return newInProgressStatus(tooFewAvailable, message), nil
|
||||
}
|
||||
|
||||
if specReplicas > readyReplicas {
|
||||
message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas)
|
||||
return newInProgressStatus(tooFewReady, message), nil
|
||||
}
|
||||
|
||||
if statusReplicas > specReplicas {
|
||||
message := fmt.Sprintf("Pending termination: %d", statusReplicas-specReplicas)
|
||||
return newInProgressStatus(extraPods, message), nil
|
||||
}
|
||||
// All ok
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: fmt.Sprintf("ReplicaSet is available. Replicas: %d", statusReplicas),
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// daemonsetConditions return standardized Conditions for DaemonSet
|
||||
func daemonsetConditions(u *unstructured.Unstructured) (*Result, error) {
|
||||
// We check that the latest generation is equal to observed generation as
|
||||
// part of checking generic properties but in that case, we are lenient and
|
||||
// skip the check if those fields are unset. For daemonset, we know that if
|
||||
// the daemonset controller has acted on a resource, these fields would not
|
||||
// be unset. So, we ensure that here.
|
||||
res, err := checkGenerationSet(u)
|
||||
if err != nil || res != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
obj := u.UnstructuredContent()
|
||||
|
||||
// replicas
|
||||
desiredNumberScheduled := GetIntField(obj, ".status.desiredNumberScheduled", -1)
|
||||
currentNumberScheduled := GetIntField(obj, ".status.currentNumberScheduled", 0)
|
||||
updatedNumberScheduled := GetIntField(obj, ".status.updatedNumberScheduled", 0)
|
||||
numberAvailable := GetIntField(obj, ".status.numberAvailable", 0)
|
||||
numberReady := GetIntField(obj, ".status.numberReady", 0)
|
||||
|
||||
if desiredNumberScheduled == -1 {
|
||||
message := "Missing .status.desiredNumberScheduled"
|
||||
return newInProgressStatus("NoDesiredNumber", message), nil
|
||||
}
|
||||
|
||||
if desiredNumberScheduled > currentNumberScheduled {
|
||||
message := fmt.Sprintf("Current: %d/%d", currentNumberScheduled, desiredNumberScheduled)
|
||||
return newInProgressStatus("LessCurrent", message), nil
|
||||
}
|
||||
|
||||
if desiredNumberScheduled > updatedNumberScheduled {
|
||||
message := fmt.Sprintf("Updated: %d/%d", updatedNumberScheduled, desiredNumberScheduled)
|
||||
return newInProgressStatus(tooFewUpdated, message), nil
|
||||
}
|
||||
|
||||
if desiredNumberScheduled > numberAvailable {
|
||||
message := fmt.Sprintf("Available: %d/%d", numberAvailable, desiredNumberScheduled)
|
||||
return newInProgressStatus(tooFewAvailable, message), nil
|
||||
}
|
||||
|
||||
if desiredNumberScheduled > numberReady {
|
||||
message := fmt.Sprintf("Ready: %d/%d", numberReady, desiredNumberScheduled)
|
||||
return newInProgressStatus(tooFewReady, message), nil
|
||||
}
|
||||
|
||||
// All ok
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: fmt.Sprintf("All replicas scheduled as expected. Replicas: %d", desiredNumberScheduled),
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// checkGenerationSet checks that the metadata.generation and
|
||||
// status.observedGeneration fields are set.
|
||||
func checkGenerationSet(u *unstructured.Unstructured) (*Result, error) {
|
||||
_, found, err := unstructured.NestedInt64(u.Object, "metadata", "generation")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("looking up metadata.generation from resource: %w", err)
|
||||
}
|
||||
if !found {
|
||||
message := fmt.Sprintf("%s metadata.generation not found", u.GetKind())
|
||||
return &Result{
|
||||
Status: InProgressStatus,
|
||||
Message: message,
|
||||
Conditions: []Condition{newReconcilingCondition("NoGeneration", message)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
_, found, err = unstructured.NestedInt64(u.Object, "status", "observedGeneration")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("looking up status.observedGeneration from resource: %w", err)
|
||||
}
|
||||
if !found {
|
||||
message := fmt.Sprintf("%s status.observedGeneration not found", u.GetKind())
|
||||
return &Result{
|
||||
Status: InProgressStatus,
|
||||
Message: message,
|
||||
Conditions: []Condition{newReconcilingCondition("NoObservedGeneration", message)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// pvcConditions return standardized Conditions for PVC
|
||||
func pvcConditions(u *unstructured.Unstructured) (*Result, error) {
|
||||
obj := u.UnstructuredContent()
|
||||
|
||||
phase := GetStringField(obj, ".status.phase", "unknown")
|
||||
if phase != "Bound" { // corev1.ClaimBound
|
||||
message := fmt.Sprintf("PVC is not Bound. phase: %s", phase)
|
||||
return newInProgressStatus("NotBound", message), nil
|
||||
}
|
||||
// All ok
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: "PVC is Bound",
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// podConditions return standardized Conditions for Pod
|
||||
func podConditions(u *unstructured.Unstructured) (*Result, error) {
|
||||
obj := u.UnstructuredContent()
|
||||
objc, err := GetObjectWithConditions(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
phase := GetStringField(obj, ".status.phase", "")
|
||||
|
||||
switch phase {
|
||||
case "Succeeded":
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: "Pod has completed successfully",
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
case "Failed":
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: "Pod has completed, but not successfully",
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
case "Running":
|
||||
if hasConditionWithStatus(objc.Status.Conditions, "Ready", corev1.ConditionTrue) {
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: "Pod is Ready",
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
containerNames, isCrashLooping, err := getCrashLoopingContainers(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isCrashLooping {
|
||||
return newFailedStatus("ContainerCrashLooping",
|
||||
fmt.Sprintf("Containers in CrashLoop state: %s", strings.Join(containerNames, ","))), nil
|
||||
}
|
||||
|
||||
return newInProgressStatus("PodRunningNotReady", "Pod is running but is not Ready"), nil
|
||||
case "Pending":
|
||||
c, found := getConditionWithStatus(objc.Status.Conditions, "PodScheduled", corev1.ConditionFalse)
|
||||
if found && c.Reason == "Unschedulable" {
|
||||
if time.Now().Add(-ScheduleWindow).Before(u.GetCreationTimestamp().Time) {
|
||||
// We give the pod 15 seconds to be scheduled before we report it
|
||||
// as unschedulable.
|
||||
return newInProgressStatus("PodNotScheduled", "Pod has not been scheduled"), nil
|
||||
}
|
||||
return newFailedStatus("PodUnschedulable", "Pod could not be scheduled"), nil
|
||||
}
|
||||
return newInProgressStatus("PodPending", "Pod is in the Pending phase"), nil
|
||||
default:
|
||||
// If the controller hasn't observed the pod yet, there is no phase. We consider this as it
|
||||
// still being in progress.
|
||||
if phase == "" {
|
||||
return newInProgressStatus("PodNotObserved", "Pod phase not available"), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown phase %s", phase)
|
||||
}
|
||||
}
|
||||
|
||||
func getCrashLoopingContainers(obj map[string]interface{}) ([]string, bool, error) {
|
||||
var containerNames []string
|
||||
css, found, err := unstructured.NestedSlice(obj, "status", "containerStatuses")
|
||||
if !found || err != nil {
|
||||
return containerNames, found, err
|
||||
}
|
||||
for _, item := range css {
|
||||
cs := item.(map[string]interface{})
|
||||
n, found := cs["name"]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
name := n.(string)
|
||||
s, found := cs["state"]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
state := s.(map[string]interface{})
|
||||
|
||||
ws, found := state["waiting"]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
waitingState := ws.(map[string]interface{})
|
||||
|
||||
r, found := waitingState["reason"]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
reason := r.(string)
|
||||
if reason == "CrashLoopBackOff" {
|
||||
containerNames = append(containerNames, name)
|
||||
}
|
||||
}
|
||||
if len(containerNames) > 0 {
|
||||
return containerNames, true, nil
|
||||
}
|
||||
return containerNames, false, nil
|
||||
}
|
||||
|
||||
// pdbConditions computes the status for PodDisruptionBudgets. A PDB
|
||||
// is currently considered Current if the disruption controller has
|
||||
// observed the latest version of the PDB resource and has computed
|
||||
// the AllowedDisruptions. PDBs do have ObservedGeneration in the
|
||||
// Status object, so if this function gets called we know that
|
||||
// the controller has observed the latest changes.
|
||||
// The disruption controller does not set any conditions if
|
||||
// computing the AllowedDisruptions fails (and there are many ways
|
||||
// it can fail), but there is PR against OSS Kubernetes to address
|
||||
// this: https://github.com/kubernetes/kubernetes/pull/86929
|
||||
func pdbConditions(_ *unstructured.Unstructured) (*Result, error) {
|
||||
// All ok
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: "AllowedDisruptions has been computed.",
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// jobConditions return standardized Conditions for Job
|
||||
//
|
||||
// A job will have the InProgress status until it starts running. Then it will have the Current
|
||||
// status while the job is running and after it has been completed successfully. It
|
||||
// will have the Failed status if it the job has failed.
|
||||
func jobConditions(u *unstructured.Unstructured) (*Result, error) {
|
||||
obj := u.UnstructuredContent()
|
||||
|
||||
parallelism := GetIntField(obj, ".spec.parallelism", 1)
|
||||
completions := GetIntField(obj, ".spec.completions", parallelism)
|
||||
succeeded := GetIntField(obj, ".status.succeeded", 0)
|
||||
active := GetIntField(obj, ".status.active", 0)
|
||||
failed := GetIntField(obj, ".status.failed", 0)
|
||||
starttime := GetStringField(obj, ".status.startTime", "")
|
||||
|
||||
// Conditions
|
||||
// https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/utils.go#L24
|
||||
objc, err := GetObjectWithConditions(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range objc.Status.Conditions {
|
||||
switch c.Type {
|
||||
case "Complete":
|
||||
if c.Status == corev1.ConditionTrue {
|
||||
message := fmt.Sprintf("Job Completed. succeeded: %d/%d", succeeded, completions)
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: message,
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
case "Failed":
|
||||
if c.Status == corev1.ConditionTrue {
|
||||
return newFailedStatus("JobFailed",
|
||||
fmt.Sprintf("Job Failed. failed: %d/%d", failed, completions)), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replicas
|
||||
if starttime == "" {
|
||||
message := "Job not started"
|
||||
return newInProgressStatus("JobNotStarted", message), nil
|
||||
}
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: fmt.Sprintf("Job in progress. success:%d, active: %d, failed: %d", succeeded, active, failed),
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// serviceConditions return standardized Conditions for Service
|
||||
func serviceConditions(u *unstructured.Unstructured) (*Result, error) {
|
||||
obj := u.UnstructuredContent()
|
||||
|
||||
specType := GetStringField(obj, ".spec.type", "ClusterIP")
|
||||
specClusterIP := GetStringField(obj, ".spec.clusterIP", "")
|
||||
|
||||
if specType == "LoadBalancer" {
|
||||
if specClusterIP == "" {
|
||||
message := "ClusterIP not set. Service type: LoadBalancer"
|
||||
return newInProgressStatus("NoIPAssigned", message), nil
|
||||
}
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: "Service is ready",
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func crdConditions(u *unstructured.Unstructured) (*Result, error) {
|
||||
obj := u.UnstructuredContent()
|
||||
|
||||
objc, err := GetObjectWithConditions(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, c := range objc.Status.Conditions {
|
||||
if c.Type == "NamesAccepted" && c.Status == corev1.ConditionFalse {
|
||||
return newFailedStatus(c.Reason, c.Message), nil
|
||||
}
|
||||
if c.Type == "Established" {
|
||||
if c.Status == corev1.ConditionFalse && c.Reason != "Installing" {
|
||||
return newFailedStatus(c.Reason, c.Message), nil
|
||||
}
|
||||
if c.Status == corev1.ConditionTrue {
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: "CRD is established",
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return newInProgressStatus("Installing", "Install in progress"), nil
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package kstatus contains functionality for computing the status
|
||||
// of Kubernetes resources.
|
||||
//
|
||||
// The statuses defined in this package are:
|
||||
// - InProgress
|
||||
// - Current
|
||||
// - Failed
|
||||
// - Terminating
|
||||
// - NotFound
|
||||
// - Unknown
|
||||
//
|
||||
// Computing the status of a resources can be done by calling the
|
||||
// Compute function in the status package.
|
||||
//
|
||||
// import (
|
||||
// "github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
// )
|
||||
//
|
||||
// res, err := status.Compute(resource)
|
||||
//
|
||||
// The package also defines a set of new conditions:
|
||||
// - InProgress
|
||||
// - Failed
|
||||
//
|
||||
// These conditions have been chosen to follow the
|
||||
// "abnormal-true" pattern where conditions should be set to true
|
||||
// for error/abnormal conditions and the absence of a condition means
|
||||
// things are normal.
|
||||
//
|
||||
// The Augment function augments any unstructured resource with
|
||||
// the standard conditions described above. The values of
|
||||
// these conditions are decided based on other status information
|
||||
// available in the resources.
|
||||
//
|
||||
// import (
|
||||
// "github.com/fluxcd/cli-utils/pkg/kstatus/status
|
||||
// )
|
||||
//
|
||||
// err := status.Augment(resource)
|
||||
package status
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// checkGenericProperties looks at the properties that are available on
|
||||
// all or most of the Kubernetes resources. If a decision can be made based
|
||||
// on this information, there is no need to look at the resource-specidic
|
||||
// rules.
|
||||
// This also checks for the presence of the conditions defined in this package.
|
||||
// If any of these are set on the resource, a decision is made solely based
|
||||
// on this and none of the resource specific rules will be used. The goal here
|
||||
// is that if controllers, built-in or custom, use these conditions, we can easily
|
||||
// find status of resources.
|
||||
func checkGenericProperties(u *unstructured.Unstructured) (*Result, error) {
|
||||
obj := u.UnstructuredContent()
|
||||
|
||||
// Check if the resource is scheduled for deletion
|
||||
deletionTimestamp, found, err := unstructured.NestedString(obj, "metadata", "deletionTimestamp")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("looking up metadata.deletionTimestamp from resource: %w", err)
|
||||
}
|
||||
if found && deletionTimestamp != "" {
|
||||
return &Result{
|
||||
Status: TerminatingStatus,
|
||||
Message: "Resource scheduled for deletion",
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
res, err := checkGeneration(u)
|
||||
if res != nil || err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Check if the resource has any of the standard conditions. If so, we just use them
|
||||
// and no need to look at anything else.
|
||||
objWithConditions, err := GetObjectWithConditions(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, cond := range objWithConditions.Status.Conditions {
|
||||
if cond.Type == string(ConditionReconciling) && cond.Status == corev1.ConditionTrue {
|
||||
return newInProgressStatus(cond.Reason, cond.Message), nil
|
||||
}
|
||||
if cond.Type == string(ConditionStalled) && cond.Status == corev1.ConditionTrue {
|
||||
return &Result{
|
||||
Status: FailedStatus,
|
||||
Message: cond.Message,
|
||||
Conditions: []Condition{
|
||||
{
|
||||
Type: ConditionStalled,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: cond.Reason,
|
||||
Message: cond.Message,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func checkGeneration(u *unstructured.Unstructured) (*Result, error) {
|
||||
// ensure that the meta generation is observed
|
||||
generation, found, err := unstructured.NestedInt64(u.Object, "metadata", "generation")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("looking up metadata.generation from resource: %w", err)
|
||||
}
|
||||
if !found {
|
||||
return nil, nil
|
||||
}
|
||||
observedGeneration, found, err := unstructured.NestedInt64(u.Object, "status", "observedGeneration")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("looking up status.observedGeneration from resource: %w", err)
|
||||
}
|
||||
if found {
|
||||
// Resource does not have this field, so we can't do this check.
|
||||
// TODO(mortent): Verify behavior of not set vs does not exist.
|
||||
if observedGeneration != generation {
|
||||
message := fmt.Sprintf("%s generation is %d, but latest observed generation is %d", u.GetKind(), generation, observedGeneration)
|
||||
return &Result{
|
||||
Status: InProgressStatus,
|
||||
Message: message,
|
||||
Conditions: []Condition{newReconcilingCondition("LatestGenerationNotObserved", message)},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
const (
|
||||
// The set of standard conditions defined in this package. These follow the "abnormality-true"
|
||||
// convention where conditions should have a true value for abnormal/error situations and the absence
|
||||
// of a condition should be interpreted as a false value, i.e. everything is normal.
|
||||
ConditionStalled ConditionType = "Stalled"
|
||||
ConditionReconciling ConditionType = "Reconciling"
|
||||
|
||||
// The set of status conditions which can be assigned to resources.
|
||||
InProgressStatus Status = "InProgress"
|
||||
FailedStatus Status = "Failed"
|
||||
CurrentStatus Status = "Current"
|
||||
TerminatingStatus Status = "Terminating"
|
||||
NotFoundStatus Status = "NotFound"
|
||||
UnknownStatus Status = "Unknown"
|
||||
)
|
||||
|
||||
var (
|
||||
Statuses = []Status{InProgressStatus, FailedStatus, CurrentStatus, TerminatingStatus, UnknownStatus}
|
||||
)
|
||||
|
||||
// ConditionType defines the set of condition types allowed inside a Condition struct.
|
||||
type ConditionType string
|
||||
|
||||
// String returns the ConditionType as a string.
|
||||
func (c ConditionType) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
// Status defines the set of statuses a resource can have.
|
||||
type Status string
|
||||
|
||||
// String returns the status as a string.
|
||||
func (s Status) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// StatusFromString turns a string into a Status. Will panic if the provided string is
|
||||
// not a valid status.
|
||||
func FromStringOrDie(text string) Status {
|
||||
s := Status(text)
|
||||
for _, r := range Statuses {
|
||||
if s == r {
|
||||
return s
|
||||
}
|
||||
}
|
||||
panic(fmt.Errorf("string has invalid status: %s", s))
|
||||
}
|
||||
|
||||
// Result contains the results of a call to compute the status of
|
||||
// a resource.
|
||||
type Result struct {
|
||||
// Status
|
||||
Status Status
|
||||
// Message
|
||||
Message string
|
||||
// Conditions list of extracted conditions from Resource
|
||||
Conditions []Condition
|
||||
}
|
||||
|
||||
// Condition defines the general format for conditions on Kubernetes resources.
|
||||
// In practice, each kubernetes resource defines their own format for conditions, but
|
||||
// most (maybe all) follows this structure.
|
||||
type Condition struct {
|
||||
// Type condition type
|
||||
Type ConditionType `json:"type,omitempty"`
|
||||
// Status String that describes the condition status
|
||||
Status corev1.ConditionStatus `json:"status,omitempty"`
|
||||
// Reason one work CamelCase reason
|
||||
Reason string `json:"reason,omitempty"`
|
||||
// Message Human readable reason string
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// Compute finds the status of a given unstructured resource. It does not
|
||||
// fetch the state of the resource from a cluster, so the provided unstructured
|
||||
// must have the complete state, including status.
|
||||
//
|
||||
// The returned result contains the status of the resource, which will be
|
||||
// one of
|
||||
// - InProgress
|
||||
// - Current
|
||||
// - Failed
|
||||
// - Terminating
|
||||
//
|
||||
// It also contains a message that provides more information on why
|
||||
// the resource has the given status. Finally, the result also contains
|
||||
// a list of standard resources that would belong on the given resource.
|
||||
func Compute(u *unstructured.Unstructured) (*Result, error) {
|
||||
res, err := checkGenericProperties(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If res is not nil, it means the generic checks was able to determine
|
||||
// the status of the resource. We don't need to check the type-specific
|
||||
// rules.
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
fn := GetLegacyConditionsFn(u)
|
||||
if fn != nil {
|
||||
return fn(u)
|
||||
}
|
||||
|
||||
// If neither the generic properties of the resource-specific rules
|
||||
// can determine status, we do one last check to see if the resource
|
||||
// does expose a Ready condition. Ready conditions do not adhere
|
||||
// to the Kubernetes design recommendations, but they are pretty widely
|
||||
// used.
|
||||
res, err = checkReadyCondition(u)
|
||||
if res != nil || err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// The resource is not one of the built-in types with specific
|
||||
// rules and we were unable to make a decision based on the
|
||||
// generic rules. In this case we assume that the absence of any known
|
||||
// conditions means the resource is current.
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: "Resource is current",
|
||||
Conditions: []Condition{},
|
||||
}, err
|
||||
}
|
||||
|
||||
// checkReadyCondition checks if a resource has a Ready condition, and
|
||||
// if so, it will use the value of this condition to determine the
|
||||
// status.
|
||||
// There are a few challenges with this:
|
||||
// - If a resource doesn't set the Ready condition until it is True,
|
||||
// the library have no way of telling whether the resource is using the
|
||||
// Ready condition, so it will fall back to the strategy for unknown
|
||||
// resources, which is to assume they are always reconciled.
|
||||
// - If the library sees the resource before the controller has had
|
||||
// a chance to update the conditions, it also will not realize the
|
||||
// resource use the Ready condition.
|
||||
// - There is no way to determine if a resource with the Ready condition
|
||||
// set to False is making progress or is doomed.
|
||||
func checkReadyCondition(u *unstructured.Unstructured) (*Result, error) {
|
||||
objWithConditions, err := GetObjectWithConditions(u.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, cond := range objWithConditions.Status.Conditions {
|
||||
if cond.Type != "Ready" {
|
||||
continue
|
||||
}
|
||||
switch cond.Status {
|
||||
case corev1.ConditionTrue:
|
||||
return &Result{
|
||||
Status: CurrentStatus,
|
||||
Message: "Resource is Ready",
|
||||
Conditions: []Condition{},
|
||||
}, nil
|
||||
case corev1.ConditionFalse:
|
||||
return newInProgressStatus(cond.Reason, cond.Message), nil
|
||||
case corev1.ConditionUnknown:
|
||||
// For now we just treat an unknown condition value as
|
||||
// InProgress. We should consider if there are better ways
|
||||
// to handle it.
|
||||
return newInProgressStatus(cond.Reason, cond.Message), nil
|
||||
default:
|
||||
// Do nothing in this case.
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Augment takes a resource and augments the resource with the
|
||||
// standard status conditions.
|
||||
func Augment(u *unstructured.Unstructured) error {
|
||||
res, err := Compute(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conditions, found, err := unstructured.NestedSlice(u.Object, "status", "conditions")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !found {
|
||||
conditions = make([]interface{}, 0)
|
||||
}
|
||||
|
||||
currentTime := time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
for _, resCondition := range res.Conditions {
|
||||
present := false
|
||||
for _, c := range conditions {
|
||||
condition, ok := c.(map[string]interface{})
|
||||
if !ok {
|
||||
return errors.New("condition does not have the expected structure")
|
||||
}
|
||||
conditionType, ok := condition["type"].(string)
|
||||
if !ok {
|
||||
return errors.New("condition type does not have the expected type")
|
||||
}
|
||||
if conditionType == string(resCondition.Type) {
|
||||
conditionStatus, ok := condition["status"].(string)
|
||||
if !ok {
|
||||
return errors.New("condition status does not have the expected type")
|
||||
}
|
||||
if conditionStatus != string(resCondition.Status) {
|
||||
condition["lastTransitionTime"] = currentTime
|
||||
}
|
||||
condition["status"] = string(resCondition.Status)
|
||||
condition["lastUpdateTime"] = currentTime
|
||||
condition["reason"] = resCondition.Reason
|
||||
condition["message"] = resCondition.Message
|
||||
present = true
|
||||
}
|
||||
}
|
||||
if !present {
|
||||
conditions = append(conditions, map[string]interface{}{
|
||||
"lastTransitionTime": currentTime,
|
||||
"lastUpdateTime": currentTime,
|
||||
"message": resCondition.Message,
|
||||
"reason": resCondition.Reason,
|
||||
"status": string(resCondition.Status),
|
||||
"type": string(resCondition.Type),
|
||||
})
|
||||
}
|
||||
}
|
||||
return unstructured.SetNestedSlice(u.Object, conditions, "status", "conditions")
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package status
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiunstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// newReconcilingCondition creates an reconciling condition with the given
|
||||
// reason and message.
|
||||
func newReconcilingCondition(reason, message string) Condition {
|
||||
return Condition{
|
||||
Type: ConditionReconciling,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func newStalledCondition(reason, message string) Condition {
|
||||
return Condition{
|
||||
Type: ConditionStalled,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// newInProgressStatus creates a status Result with the InProgress status
|
||||
// and an InProgress condition.
|
||||
func newInProgressStatus(reason, message string) *Result {
|
||||
return &Result{
|
||||
Status: InProgressStatus,
|
||||
Message: message,
|
||||
Conditions: []Condition{newReconcilingCondition(reason, message)},
|
||||
}
|
||||
}
|
||||
|
||||
func newFailedStatus(reason, message string) *Result {
|
||||
return &Result{
|
||||
Status: FailedStatus,
|
||||
Message: message,
|
||||
Conditions: []Condition{newStalledCondition(reason, message)},
|
||||
}
|
||||
}
|
||||
|
||||
// ObjWithConditions Represent meta object with status.condition array
|
||||
type ObjWithConditions struct {
|
||||
// Status as expected to be present in most compliant kubernetes resources
|
||||
Status ConditionStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// ConditionStatus represent status with condition array
|
||||
type ConditionStatus struct {
|
||||
// Array of Conditions as expected to be present in kubernetes resources
|
||||
Conditions []BasicCondition `json:"conditions" yaml:"conditions"`
|
||||
}
|
||||
|
||||
// BasicCondition fields that are expected in a condition
|
||||
type BasicCondition struct {
|
||||
// Type Condition type
|
||||
Type string `json:"type" yaml:"type"`
|
||||
// Status is one of True,False,Unknown
|
||||
Status corev1.ConditionStatus `json:"status" yaml:"status"`
|
||||
// Reason simple single word reason in CamleCase
|
||||
// +optional
|
||||
Reason string `json:"reason,omitempty" yaml:"reason"`
|
||||
// Message human readable reason
|
||||
// +optional
|
||||
Message string `json:"message,omitempty" yaml:"message"`
|
||||
}
|
||||
|
||||
// GetObjectWithConditions return typed object
|
||||
func GetObjectWithConditions(in map[string]interface{}) (*ObjWithConditions, error) {
|
||||
var out = new(ObjWithConditions)
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func hasConditionWithStatus(conditions []BasicCondition, conditionType string, status corev1.ConditionStatus) bool {
|
||||
_, found := getConditionWithStatus(conditions, conditionType, status)
|
||||
return found
|
||||
}
|
||||
|
||||
func getConditionWithStatus(conditions []BasicCondition, conditionType string, status corev1.ConditionStatus) (BasicCondition, bool) {
|
||||
for _, c := range conditions {
|
||||
if c.Type == conditionType && c.Status == status {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return BasicCondition{}, false
|
||||
}
|
||||
|
||||
// GetStringField return field as string defaulting to value if not found
|
||||
func GetStringField(obj map[string]interface{}, fieldPath string, defaultValue string) string {
|
||||
var rv = defaultValue
|
||||
|
||||
fields := strings.Split(fieldPath, ".")
|
||||
if fields[0] == "" {
|
||||
fields = fields[1:]
|
||||
}
|
||||
|
||||
val, found, err := apiunstructured.NestedFieldNoCopy(obj, fields...)
|
||||
if !found || err != nil {
|
||||
return rv
|
||||
}
|
||||
|
||||
if v, ok := val.(string); ok {
|
||||
return v
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
// GetIntField return field as string defaulting to value if not found
|
||||
func GetIntField(obj map[string]interface{}, fieldPath string, defaultValue int) int {
|
||||
fields := strings.Split(fieldPath, ".")
|
||||
if fields[0] == "" {
|
||||
fields = fields[1:]
|
||||
}
|
||||
|
||||
val, found, err := apiunstructured.NestedFieldNoCopy(obj, fields...)
|
||||
if !found || err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
return v
|
||||
case int32:
|
||||
return int(v)
|
||||
case int64:
|
||||
return int(v)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
)
|
||||
|
||||
// BlindStatusWatcher sees nothing.
|
||||
// BlindStatusWatcher sends no update or error events.
|
||||
// BlindStatusWatcher waits patiently to be cancelled.
|
||||
// BlindStatusWatcher implements the StatusWatcher interface.
|
||||
type BlindStatusWatcher struct{}
|
||||
|
||||
var _ StatusWatcher = BlindStatusWatcher{}
|
||||
|
||||
// Watch nothing. See no changes.
|
||||
func (w BlindStatusWatcher) Watch(ctx context.Context, _ object.ObjMetadataSet, _ Options) <-chan event.Event {
|
||||
doneCh := ctx.Done()
|
||||
eventCh := make(chan event.Event)
|
||||
go func() {
|
||||
// Send SyncEvent immediately.
|
||||
eventCh <- event.Event{Type: event.SyncEvent}
|
||||
// Block until the context is cancelled.
|
||||
<-doneCh
|
||||
// Signal to the caller there will be no more events.
|
||||
close(eventCh)
|
||||
}()
|
||||
return eventCh
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/clusterreader"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// DefaultStatusWatcher reports on status updates to a set of objects.
|
||||
//
|
||||
// Use NewDefaultStatusWatcher to build a DefaultStatusWatcher with default settings.
|
||||
type DefaultStatusWatcher struct {
|
||||
// DynamicClient is used to watch of resource objects.
|
||||
DynamicClient dynamic.Interface
|
||||
|
||||
// Mapper is used to map from GroupKind to GroupVersionKind.
|
||||
Mapper meta.RESTMapper
|
||||
|
||||
// ResyncPeriod is how often the objects are retrieved to re-synchronize,
|
||||
// in case any events were missed.
|
||||
ResyncPeriod time.Duration
|
||||
|
||||
// StatusReader specifies a custom implementation of the
|
||||
// engine.StatusReader interface that will be used to compute reconcile
|
||||
// status for resource objects.
|
||||
StatusReader engine.StatusReader
|
||||
|
||||
// ClusterReader is used to look up generated objects on-demand.
|
||||
// Generated objects (ex: Deployment > ReplicaSet > Pod) are sometimes
|
||||
// required for computing parent object status, to compensate for
|
||||
// controllers that aren't following status conventions.
|
||||
ClusterReader engine.ClusterReader
|
||||
}
|
||||
|
||||
var _ StatusWatcher = &DefaultStatusWatcher{}
|
||||
|
||||
// NewDefaultStatusWatcher constructs a DynamicStatusWatcher with defaults
|
||||
// chosen for general use. If you need different settings, consider building a
|
||||
// DynamicStatusWatcher directly.
|
||||
func NewDefaultStatusWatcher(dynamicClient dynamic.Interface, mapper meta.RESTMapper) *DefaultStatusWatcher {
|
||||
return &DefaultStatusWatcher{
|
||||
DynamicClient: dynamicClient,
|
||||
Mapper: mapper,
|
||||
ResyncPeriod: 1 * time.Hour,
|
||||
StatusReader: statusreaders.NewDefaultStatusReader(mapper),
|
||||
ClusterReader: &clusterreader.DynamicClusterReader{
|
||||
DynamicClient: dynamicClient,
|
||||
Mapper: mapper,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Watch the cluster for changes made to the specified objects.
|
||||
// Returns an event channel on which these updates (and errors) will be reported.
|
||||
// Each update event includes the computed status of the object.
|
||||
func (w *DefaultStatusWatcher) Watch(ctx context.Context, ids object.ObjMetadataSet, opts Options) <-chan event.Event {
|
||||
strategy := opts.RESTScopeStrategy
|
||||
if strategy == RESTScopeAutomatic {
|
||||
strategy = autoSelectRESTScopeStrategy(ids)
|
||||
}
|
||||
|
||||
var scope meta.RESTScope
|
||||
var targets []GroupKindNamespace
|
||||
switch strategy {
|
||||
case RESTScopeRoot:
|
||||
scope = meta.RESTScopeRoot
|
||||
targets = rootScopeGKNs(ids)
|
||||
klog.V(3).Infof("DynamicStatusWatcher starting in root-scoped mode (targets: %d)", len(targets))
|
||||
case RESTScopeNamespace:
|
||||
scope = meta.RESTScopeNamespace
|
||||
targets = namespaceScopeGKNs(ids)
|
||||
klog.V(3).Infof("DynamicStatusWatcher starting in namespace-scoped mode (targets: %d)", len(targets))
|
||||
default:
|
||||
return handleFatalError(fmt.Errorf("invalid RESTScopeStrategy: %v", strategy))
|
||||
}
|
||||
|
||||
informer := &ObjectStatusReporter{
|
||||
InformerFactory: NewDynamicInformerFactory(w.DynamicClient, w.ResyncPeriod),
|
||||
Mapper: w.Mapper,
|
||||
StatusReader: w.StatusReader,
|
||||
ClusterReader: w.ClusterReader,
|
||||
Targets: targets,
|
||||
ObjectFilter: &AllowListObjectFilter{AllowList: ids},
|
||||
RESTScope: scope,
|
||||
}
|
||||
return informer.Start(ctx)
|
||||
}
|
||||
|
||||
func handleFatalError(err error) <-chan event.Event {
|
||||
eventCh := make(chan event.Event)
|
||||
go func() {
|
||||
defer close(eventCh)
|
||||
eventCh <- event.Event{
|
||||
Type: event.ErrorEvent,
|
||||
Error: err,
|
||||
}
|
||||
}()
|
||||
return eventCh
|
||||
}
|
||||
|
||||
func autoSelectRESTScopeStrategy(ids object.ObjMetadataSet) RESTScopeStrategy {
|
||||
if len(uniqueNamespaces(ids)) > 1 {
|
||||
return RESTScopeRoot
|
||||
}
|
||||
return RESTScopeNamespace
|
||||
}
|
||||
|
||||
func rootScopeGKNs(ids object.ObjMetadataSet) []GroupKindNamespace {
|
||||
gks := uniqueGKs(ids)
|
||||
targets := make([]GroupKindNamespace, len(gks))
|
||||
for i, gk := range gks {
|
||||
targets[i] = GroupKindNamespace{
|
||||
Group: gk.Group,
|
||||
Kind: gk.Kind,
|
||||
Namespace: "",
|
||||
}
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
func namespaceScopeGKNs(ids object.ObjMetadataSet) []GroupKindNamespace {
|
||||
return uniqueGKNs(ids)
|
||||
}
|
||||
|
||||
// uniqueGKNs returns a set of unique GroupKindNamespaces from a set of object identifiers.
|
||||
func uniqueGKNs(ids object.ObjMetadataSet) []GroupKindNamespace {
|
||||
gknMap := make(map[GroupKindNamespace]struct{})
|
||||
for _, id := range ids {
|
||||
gkn := GroupKindNamespace{Group: id.GroupKind.Group, Kind: id.GroupKind.Kind, Namespace: id.Namespace}
|
||||
gknMap[gkn] = struct{}{}
|
||||
}
|
||||
gknList := make([]GroupKindNamespace, 0, len(gknMap))
|
||||
for gk := range gknMap {
|
||||
gknList = append(gknList, gk)
|
||||
}
|
||||
return gknList
|
||||
}
|
||||
|
||||
// uniqueGKs returns a set of unique GroupKinds from a set of object identifiers.
|
||||
func uniqueGKs(ids object.ObjMetadataSet) []schema.GroupKind {
|
||||
gkMap := make(map[schema.GroupKind]struct{})
|
||||
for _, id := range ids {
|
||||
gkn := schema.GroupKind{Group: id.GroupKind.Group, Kind: id.GroupKind.Kind}
|
||||
gkMap[gkn] = struct{}{}
|
||||
}
|
||||
gkList := make([]schema.GroupKind, 0, len(gkMap))
|
||||
for gk := range gkMap {
|
||||
gkList = append(gkList, gk)
|
||||
}
|
||||
return gkList
|
||||
}
|
||||
|
||||
func uniqueNamespaces(ids object.ObjMetadataSet) []string {
|
||||
nsMap := make(map[string]struct{})
|
||||
for _, id := range ids {
|
||||
nsMap[id.Namespace] = struct{}{}
|
||||
}
|
||||
nsList := make([]string, 0, len(nsMap))
|
||||
for ns := range nsMap {
|
||||
nsList = append(nsList, ns)
|
||||
}
|
||||
return nsList
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package watcher is a library for computing the status of kubernetes resource
|
||||
// objects based on watching object state from a cluster. It keeps watching
|
||||
// until it is cancelled through the provided context. Updates on the status of
|
||||
// objects are streamed back to the caller through a channel.
|
||||
//
|
||||
// # Watching Resources
|
||||
//
|
||||
// In order to watch a set of resources objects, create a StatusWatcher
|
||||
// and pass in the list of object identifiers to the Watch function.
|
||||
//
|
||||
// import (
|
||||
// "github.com/fluxcd/cli-utils/pkg/kstatus/watcher"
|
||||
// )
|
||||
//
|
||||
// ids := []prune.ObjMetadata{
|
||||
// {
|
||||
// GroupKind: schema.GroupKind{
|
||||
// Group: "apps",
|
||||
// Kind: "Deployment",
|
||||
// },
|
||||
// Name: "dep",
|
||||
// Namespace: "default",
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// statusWatcher := watcher.NewDefaultStatusWatcher(dynamicClient, mapper)
|
||||
// ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
// eventCh := statusWatcher.Watch(ctx, ids, watcher.Options{})
|
||||
// for e := range eventCh {
|
||||
// // Handle event
|
||||
// if e.Type == event.ErrorEvent {
|
||||
// cancelFunc()
|
||||
// return e.Err
|
||||
// }
|
||||
// }
|
||||
package watcher
|
||||
Generated
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
type DynamicInformerFactory struct {
|
||||
Client dynamic.Interface
|
||||
ResyncPeriod time.Duration
|
||||
Indexers cache.Indexers
|
||||
}
|
||||
|
||||
func NewDynamicInformerFactory(client dynamic.Interface, resyncPeriod time.Duration) *DynamicInformerFactory {
|
||||
return &DynamicInformerFactory{
|
||||
Client: client,
|
||||
ResyncPeriod: resyncPeriod,
|
||||
Indexers: cache.Indexers{
|
||||
cache.NamespaceIndex: cache.MetaNamespaceIndexFunc,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *DynamicInformerFactory) NewInformer(ctx context.Context, mapping *meta.RESTMapping, namespace string) cache.SharedIndexInformer {
|
||||
// Unstructured example output need `"apiVersion"` and `"kind"` set.
|
||||
example := &unstructured.Unstructured{}
|
||||
example.SetGroupVersionKind(mapping.GroupVersionKind)
|
||||
|
||||
lw := &cache.ListWatch{
|
||||
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
|
||||
return f.Client.Resource(mapping.Resource).
|
||||
Namespace(namespace).
|
||||
List(ctx, options)
|
||||
},
|
||||
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
return f.Client.Resource(mapping.Resource).
|
||||
Namespace(namespace).
|
||||
Watch(ctx, options)
|
||||
},
|
||||
}
|
||||
|
||||
// Wrap the ListWatch with the client to allow the reflector to detect
|
||||
// if the client supports WatchList semantics. This is important for
|
||||
// fake clients used in tests, which do not support WatchList.
|
||||
wrappedLW := cache.ToListWatcherWithWatchListSemantics(lw, f.Client)
|
||||
|
||||
return cache.NewSharedIndexInformer(
|
||||
wrappedLW,
|
||||
example,
|
||||
f.ResyncPeriod,
|
||||
f.Indexers,
|
||||
)
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// eventFunnel wraps a list of event channels and multiplexes them down to a
|
||||
// single event channel. New input channels can be added at runtime, and the
|
||||
// output channel will remain open until all input channels are closed.
|
||||
type eventFunnel struct {
|
||||
// ctx closure triggers shutdown
|
||||
ctx context.Context
|
||||
// outCh is the funnel that consumes all events from input channels
|
||||
outCh chan event.Event
|
||||
// doneCh is closed after outCh is closed.
|
||||
// This allows blocking until done without consuming events.
|
||||
doneCh chan struct{}
|
||||
// counterCh is used to track the number of open input channels.
|
||||
counterCh chan int
|
||||
}
|
||||
|
||||
func newEventFunnel(ctx context.Context) *eventFunnel {
|
||||
funnel := &eventFunnel{
|
||||
ctx: ctx,
|
||||
outCh: make(chan event.Event),
|
||||
doneCh: make(chan struct{}),
|
||||
counterCh: make(chan int),
|
||||
}
|
||||
// Wait until the context is done and all input channels are closed.
|
||||
// Then close out and done channels to signal completion.
|
||||
go func() {
|
||||
defer func() {
|
||||
// Don't close counterCh, otherwise AddInputChannel may panic.
|
||||
klog.V(5).Info("Closing funnel")
|
||||
close(funnel.outCh)
|
||||
close(funnel.doneCh)
|
||||
}()
|
||||
ctxDoneCh := ctx.Done()
|
||||
|
||||
// Count input channels that have been added and not closed.
|
||||
inputs := 0
|
||||
for {
|
||||
select {
|
||||
case delta := <-funnel.counterCh:
|
||||
inputs += delta
|
||||
klog.V(5).Infof("Funnel input channels (%+d): %d", delta, inputs)
|
||||
case <-ctxDoneCh:
|
||||
// Stop waiting for context closure.
|
||||
// Nil channel avoids busy waiting.
|
||||
ctxDoneCh = nil
|
||||
}
|
||||
if ctxDoneCh == nil && inputs <= 0 {
|
||||
// Context is closed and all input channels are closed.
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
return funnel
|
||||
}
|
||||
|
||||
// Add a new input channel to the multiplexer.
|
||||
func (m *eventFunnel) AddInputChannel(inCh <-chan event.Event) error {
|
||||
select {
|
||||
case <-m.ctx.Done(): // skip, if context is closed
|
||||
return &EventFunnelClosedError{ContextError: m.ctx.Err()}
|
||||
case m.counterCh <- 1: // increment counter
|
||||
}
|
||||
|
||||
// Create a multiplexer for each new event channel.
|
||||
go m.drain(inCh, m.outCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
// OutputChannel channel receives all events sent to input channels.
|
||||
// This channel is closed after all input channels are closed.
|
||||
func (m *eventFunnel) OutputChannel() <-chan event.Event {
|
||||
return m.outCh
|
||||
}
|
||||
|
||||
// Done channel is closed after the Output channel is closed.
|
||||
// This allows blocking until done without consuming events.
|
||||
// If no input channels have been added yet, the done channel will be nil.
|
||||
func (m *eventFunnel) Done() <-chan struct{} {
|
||||
return m.doneCh
|
||||
}
|
||||
|
||||
// drain a single input channel to a single output channel.
|
||||
func (m *eventFunnel) drain(inCh <-chan event.Event, outCh chan<- event.Event) {
|
||||
defer func() {
|
||||
m.counterCh <- -1 // decrement counter
|
||||
}()
|
||||
for event := range inCh {
|
||||
outCh <- event
|
||||
}
|
||||
}
|
||||
|
||||
type EventFunnelClosedError struct {
|
||||
ContextError error
|
||||
}
|
||||
|
||||
func (e *EventFunnelClosedError) Error() string {
|
||||
return fmt.Sprintf("event funnel closed: %v", e.ContextError)
|
||||
}
|
||||
|
||||
func (e *EventFunnelClosedError) Is(err error) bool {
|
||||
fcErr, ok := err.(*EventFunnelClosedError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return e.ContextError == fcErr.ContextError
|
||||
}
|
||||
|
||||
func (e *EventFunnelClosedError) Unwrap() error {
|
||||
return e.ContextError
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// ObjectFilter allows for filtering objects.
|
||||
type ObjectFilter interface {
|
||||
// Filter returns true if the object should be skipped.
|
||||
Filter(obj *unstructured.Unstructured) bool
|
||||
}
|
||||
|
||||
// AllowListObjectFilter filters objects not in the allow list.
|
||||
// AllowListObjectFilter implements ObjectFilter.
|
||||
type AllowListObjectFilter struct {
|
||||
AllowList object.ObjMetadataSet
|
||||
}
|
||||
|
||||
var _ ObjectFilter = &AllowListObjectFilter{}
|
||||
|
||||
// Filter returns true if the object should be skipped, because it is NOT in the
|
||||
// AllowList.
|
||||
func (f *AllowListObjectFilter) Filter(obj *unstructured.Unstructured) bool {
|
||||
id := object.UnstructuredToObjMetadata(obj)
|
||||
return !f.AllowList.Contains(id)
|
||||
}
|
||||
+895
@@ -0,0 +1,895 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/clock"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
)
|
||||
|
||||
// GroupKindNamespace identifies an informer target.
|
||||
// When used as an informer target, the namespace is optional.
|
||||
// When the namespace is empty for namespaced resources, all namespaces are watched.
|
||||
type GroupKindNamespace struct {
|
||||
Group string
|
||||
Kind string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// String returns a serialized form suitable for logging.
|
||||
func (gkn GroupKindNamespace) String() string {
|
||||
return fmt.Sprintf("%s/%s/namespaces/%s",
|
||||
gkn.Group, gkn.Kind, gkn.Namespace)
|
||||
}
|
||||
|
||||
func (gkn GroupKindNamespace) GroupKind() schema.GroupKind {
|
||||
return schema.GroupKind{Group: gkn.Group, Kind: gkn.Kind}
|
||||
}
|
||||
|
||||
// ObjectStatusReporter reports on updates to objects (instances) using a
|
||||
// network of informers to watch one or more resources (types).
|
||||
//
|
||||
// Unlike SharedIndexInformer, ObjectStatusReporter...
|
||||
// - Reports object status.
|
||||
// - Can watch multiple resource types simultaneously.
|
||||
// - Specific objects can be ignored for efficiency by specifying an ObjectFilter.
|
||||
// - Resolves GroupKinds into Resources at runtime, to pick up newly added
|
||||
// resources.
|
||||
// - Starts and Stops individual watches automaically to reduce errors when a
|
||||
// CRD or Namespace is deleted.
|
||||
// - Resources can be watched in root-scope mode or namespace-scope mode,
|
||||
// allowing the caller to optimize for efficiency or least-privilege.
|
||||
// - Gives unschedulable Pods (and objects that generate them) a 15s grace
|
||||
// period before reporting them as Failed.
|
||||
// - Resets the RESTMapper cache automatically when CRDs are modified.
|
||||
//
|
||||
// ObjectStatusReporter is NOT repeatable. It will panic if started more than
|
||||
// once. If you need a repeatable factory, use DefaultStatusWatcher.
|
||||
//
|
||||
// TODO: support detection of added/removed api extensions at runtime
|
||||
// TODO: Watch CRDs & Namespaces, even if not in the set of IDs.
|
||||
// TODO: Retry with backoff if in namespace-scoped mode, to allow CRDs & namespaces to be created asynchronously
|
||||
type ObjectStatusReporter struct {
|
||||
// InformerFactory is used to build informers
|
||||
InformerFactory *DynamicInformerFactory
|
||||
|
||||
// Mapper is used to map from GroupKind to GroupVersionKind.
|
||||
Mapper meta.RESTMapper
|
||||
|
||||
// StatusReader specifies a custom implementation of the
|
||||
// engine.StatusReader interface that will be used to compute reconcile
|
||||
// status for resource objects.
|
||||
StatusReader engine.StatusReader
|
||||
|
||||
// ClusterReader is used to look up generated objects on-demand.
|
||||
// Generated objects (ex: Deployment > ReplicaSet > Pod) are sometimes
|
||||
// required for computing parent object status, to compensate for
|
||||
// controllers that aren't following status conventions.
|
||||
ClusterReader engine.ClusterReader
|
||||
|
||||
// GroupKinds is the list of GroupKinds to watch.
|
||||
Targets []GroupKindNamespace
|
||||
|
||||
// ObjectFilter is used to decide which objects to ingore.
|
||||
ObjectFilter ObjectFilter
|
||||
|
||||
// RESTScope specifies whether to ListAndWatch resources at the namespace
|
||||
// or cluster (root) level. Using root scope is more efficient, but
|
||||
// namespace scope may require fewer permissions.
|
||||
RESTScope meta.RESTScope
|
||||
|
||||
// lock guards modification of the subsequent stateful fields
|
||||
lock sync.Mutex
|
||||
|
||||
// gk2gkn maps GKs to GKNs to make it easy/cheap to look up.
|
||||
gk2gkn map[schema.GroupKind]map[GroupKindNamespace]struct{}
|
||||
|
||||
// ns2gkn maps Namespaces to GKNs to make it easy/cheap to look up.
|
||||
ns2gkn map[string]map[GroupKindNamespace]struct{}
|
||||
|
||||
// informerRefs tracks which informers have been started and stopped
|
||||
informerRefs map[GroupKindNamespace]*informerReference
|
||||
|
||||
// context will be cancelled when the reporter should stop.
|
||||
context context.Context
|
||||
|
||||
// cancel function that stops the context.
|
||||
// This should only be called after the terminal error event has been sent.
|
||||
cancel context.CancelFunc
|
||||
|
||||
// funnel multiplexes multiple input channels into one output channel,
|
||||
// allowing input channels to be added and removed at runtime.
|
||||
funnel *eventFunnel
|
||||
|
||||
// taskManager makes it possible to cancel scheduled tasks.
|
||||
taskManager *taskManager
|
||||
|
||||
started bool
|
||||
stopped bool
|
||||
}
|
||||
|
||||
func (w *ObjectStatusReporter) Start(ctx context.Context) <-chan event.Event {
|
||||
w.lock.Lock()
|
||||
defer w.lock.Unlock()
|
||||
|
||||
if w.started {
|
||||
panic("ObjectStatusInformer cannot be restarted")
|
||||
}
|
||||
|
||||
w.taskManager = &taskManager{}
|
||||
|
||||
// Map GroupKinds to sets of GroupKindNamespaces for fast lookups.
|
||||
// This is the only time we modify the map.
|
||||
// So it should be safe to read from multiple threads after this.
|
||||
w.gk2gkn = make(map[schema.GroupKind]map[GroupKindNamespace]struct{})
|
||||
for _, gkn := range w.Targets {
|
||||
gk := gkn.GroupKind()
|
||||
m, found := w.gk2gkn[gk]
|
||||
if !found {
|
||||
m = make(map[GroupKindNamespace]struct{})
|
||||
w.gk2gkn[gk] = m
|
||||
}
|
||||
m[gkn] = struct{}{}
|
||||
}
|
||||
|
||||
// Map namespaces to sets of GroupKindNamespaces for fast lookups.
|
||||
// This is the only time we modify the map.
|
||||
// So it should be safe to read from multiple threads after this.
|
||||
w.ns2gkn = make(map[string]map[GroupKindNamespace]struct{})
|
||||
for _, gkn := range w.Targets {
|
||||
ns := gkn.Namespace
|
||||
m, found := w.ns2gkn[ns]
|
||||
if !found {
|
||||
m = make(map[GroupKindNamespace]struct{})
|
||||
w.ns2gkn[ns] = m
|
||||
}
|
||||
m[gkn] = struct{}{}
|
||||
}
|
||||
|
||||
// Initialize the informer map with references to track their start/stop.
|
||||
// This is the only time we modify the map.
|
||||
// So it should be safe to read from multiple threads after this.
|
||||
w.informerRefs = make(map[GroupKindNamespace]*informerReference, len(w.Targets))
|
||||
for _, gkn := range w.Targets {
|
||||
w.informerRefs[gkn] = &informerReference{}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
w.context = ctx
|
||||
w.cancel = cancel
|
||||
|
||||
// Use an event funnel to multiplex events through multiple input channels
|
||||
// into out output channel. We can't use the normal fan-in pattern, because
|
||||
// we need to be able to add and remove new input channels at runtime, as
|
||||
// new informers are created and destroyed.
|
||||
w.funnel = newEventFunnel(ctx)
|
||||
|
||||
// Send start requests.
|
||||
for _, gkn := range w.Targets {
|
||||
w.startInformer(gkn)
|
||||
}
|
||||
|
||||
w.started = true
|
||||
|
||||
// Block until the event funnel is closed.
|
||||
// The event funnel will close after all the informer channels are closed.
|
||||
// The informer channels will close after the informers have stopped.
|
||||
// The informers will stop after their context is cancelled.
|
||||
go func() {
|
||||
<-w.funnel.Done()
|
||||
|
||||
w.lock.Lock()
|
||||
defer w.lock.Unlock()
|
||||
w.stopped = true
|
||||
}()
|
||||
|
||||
// Wait until all informers are synced or stopped, then send a SyncEvent.
|
||||
syncEventCh := make(chan event.Event)
|
||||
err := w.funnel.AddInputChannel(syncEventCh)
|
||||
if err != nil {
|
||||
// Reporter already stopped.
|
||||
return handleFatalError(fmt.Errorf("reporter failed to start: %v", err))
|
||||
}
|
||||
go func() {
|
||||
defer close(syncEventCh)
|
||||
// TODO: should we use something less aggressive, like wait.BackoffUntil?
|
||||
if cache.WaitForCacheSync(ctx.Done(), w.HasSynced) {
|
||||
syncEventCh <- event.Event{
|
||||
Type: event.SyncEvent,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return w.funnel.OutputChannel()
|
||||
}
|
||||
|
||||
// Stop triggers the cancellation of the reporter context, and closure of the
|
||||
// event channel without sending an error event.
|
||||
func (w *ObjectStatusReporter) Stop() {
|
||||
klog.V(4).Info("Stopping reporter")
|
||||
w.cancel()
|
||||
}
|
||||
|
||||
// HasSynced returns true if all the started informers have been synced.
|
||||
//
|
||||
// Use the following to block waiting for synchronization:
|
||||
// synced := cache.WaitForCacheSync(stopCh, informer.HasSynced)
|
||||
func (w *ObjectStatusReporter) HasSynced() bool {
|
||||
w.lock.Lock()
|
||||
defer w.lock.Unlock()
|
||||
|
||||
if w.stopped || !w.started {
|
||||
return false
|
||||
}
|
||||
|
||||
pending := make([]GroupKindNamespace, 0, len(w.informerRefs))
|
||||
for gke, informer := range w.informerRefs {
|
||||
if informer.HasStarted() && !informer.HasSynced() {
|
||||
pending = append(pending, gke)
|
||||
}
|
||||
}
|
||||
if len(pending) > 0 {
|
||||
klog.V(5).Infof("Informers pending synchronization: %v", pending)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// startInformer adds the specified GroupKindNamespace to the start channel to
|
||||
// be started asynchronously.
|
||||
func (w *ObjectStatusReporter) startInformer(gkn GroupKindNamespace) {
|
||||
ctx, ok := w.informerRefs[gkn].Start(w.context)
|
||||
if !ok {
|
||||
klog.V(5).Infof("Watch start skipped (already started): %v", gkn)
|
||||
// already started
|
||||
return
|
||||
}
|
||||
go w.startInformerWithRetry(ctx, gkn)
|
||||
}
|
||||
|
||||
// stopInformer stops the informer watching the specified GroupKindNamespace.
|
||||
func (w *ObjectStatusReporter) stopInformer(gkn GroupKindNamespace) {
|
||||
w.informerRefs[gkn].Stop()
|
||||
}
|
||||
|
||||
func (w *ObjectStatusReporter) startInformerWithRetry(ctx context.Context, gkn GroupKindNamespace) {
|
||||
realClock := &clock.RealClock{}
|
||||
// TODO nolint can be removed once https://github.com/kubernetes/kubernetes/issues/118638 is resolved
|
||||
backoffManager := wait.NewExponentialBackoffManager(800*time.Millisecond, 30*time.Second, 2*time.Minute, 2.0, 1.0, realClock) //nolint:staticcheck
|
||||
retryCtx, retryCancel := context.WithCancel(ctx)
|
||||
|
||||
wait.BackoffUntil(func() {
|
||||
err := w.startInformerNow(
|
||||
ctx,
|
||||
gkn,
|
||||
)
|
||||
if err != nil {
|
||||
if meta.IsNoMatchError(err) {
|
||||
// CRD (or api extension) not installed
|
||||
// TODO: retry if CRDs are not being watched
|
||||
klog.V(3).Infof("Watch start error (blocking until CRD is added): %v: %v", gkn, err)
|
||||
// Cancel the parent context, which will stop the retries too.
|
||||
w.stopInformer(gkn)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a temporary input channel to send the error event.
|
||||
eventCh := make(chan event.Event)
|
||||
defer close(eventCh)
|
||||
err := w.funnel.AddInputChannel(eventCh)
|
||||
if err != nil {
|
||||
// Reporter already stopped.
|
||||
// This is fine. 🔥
|
||||
klog.V(5).Infof("Informer failed to start: %v", err)
|
||||
return
|
||||
}
|
||||
// Send error event and stop the reporter!
|
||||
w.handleFatalError(eventCh, err)
|
||||
return
|
||||
}
|
||||
// Success! - Stop retrying
|
||||
retryCancel()
|
||||
}, backoffManager, true, retryCtx.Done())
|
||||
}
|
||||
|
||||
// startInformerNow starts an informer to watch for changes to a
|
||||
// GroupKindNamespace. Changes are filtered and passed by event channel into the
|
||||
// funnel. Each update event includes the computed status of the object.
|
||||
// An error is returned if the informer could not be created.
|
||||
func (w *ObjectStatusReporter) startInformerNow(
|
||||
ctx context.Context,
|
||||
gkn GroupKindNamespace,
|
||||
) error {
|
||||
// Look up the mapping for this GroupKind.
|
||||
// If it doesn't exist, either delay watching or emit an error.
|
||||
gk := gkn.GroupKind()
|
||||
mapping, err := w.Mapper.RESTMapping(gk)
|
||||
if err != nil {
|
||||
// Might be a NoResourceMatchError/NoKindMatchError
|
||||
return err
|
||||
}
|
||||
|
||||
informer := w.InformerFactory.NewInformer(ctx, mapping, gkn.Namespace)
|
||||
|
||||
w.informerRefs[gkn].SetInformer(informer)
|
||||
|
||||
eventCh := make(chan event.Event)
|
||||
|
||||
// Add this event channel to the output multiplexer
|
||||
err = w.funnel.AddInputChannel(eventCh)
|
||||
if err != nil {
|
||||
// Reporter already stopped.
|
||||
return fmt.Errorf("informer failed to build event handler: %w", err)
|
||||
}
|
||||
|
||||
// Handler called when ListAndWatch errors.
|
||||
// Custom handler stops the informer if the resource is NotFound (CRD deleted).
|
||||
err = informer.SetWatchErrorHandler(func(r *cache.Reflector, err error) {
|
||||
w.watchErrorHandler(gkn, eventCh, err)
|
||||
})
|
||||
if err != nil {
|
||||
// Should never happen.
|
||||
// Informer can't have started yet. We just created it.
|
||||
return fmt.Errorf("failed to set error handler on new informer for %v: %v", mapping.Resource, err)
|
||||
}
|
||||
|
||||
_, err = informer.AddEventHandler(w.eventHandler(ctx, eventCh))
|
||||
if err != nil {
|
||||
// Should never happen.
|
||||
return fmt.Errorf("failed add event handler on new informer for %v: %v", mapping.Resource, err)
|
||||
}
|
||||
|
||||
// Start the informer in the background.
|
||||
// Informer will be stopped when the context is cancelled.
|
||||
go func() {
|
||||
klog.V(3).Infof("Watch starting: %v", gkn)
|
||||
informer.Run(ctx.Done())
|
||||
klog.V(3).Infof("Watch stopped: %v", gkn)
|
||||
// Signal to the caller there will be no more events for this GroupKind.
|
||||
close(eventCh)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *ObjectStatusReporter) forEachTargetWithGroupKind(gk schema.GroupKind, fn func(GroupKindNamespace)) {
|
||||
for gkn := range w.gk2gkn[gk] {
|
||||
fn(gkn)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ObjectStatusReporter) forEachTargetWithNamespace(ns string, fn func(GroupKindNamespace)) {
|
||||
for gkn := range w.ns2gkn[ns] {
|
||||
fn(gkn)
|
||||
}
|
||||
}
|
||||
|
||||
// readStatusFromObject is a convenience function to read object status with a
|
||||
// StatusReader using a ClusterReader to retrieve generated objects.
|
||||
func (w *ObjectStatusReporter) readStatusFromObject(
|
||||
ctx context.Context,
|
||||
obj *unstructured.Unstructured,
|
||||
) (*event.ResourceStatus, error) {
|
||||
return w.StatusReader.ReadStatusForObject(ctx, w.ClusterReader, obj)
|
||||
}
|
||||
|
||||
// readStatusFromCluster is a convenience function to read object status with a
|
||||
// StatusReader using a ClusterReader to retrieve the object and its generated
|
||||
// objects.
|
||||
func (w *ObjectStatusReporter) readStatusFromCluster(
|
||||
ctx context.Context,
|
||||
id object.ObjMetadata,
|
||||
) (*event.ResourceStatus, error) {
|
||||
return w.StatusReader.ReadStatus(ctx, w.ClusterReader, id)
|
||||
}
|
||||
|
||||
// deletedStatus builds a ResourceStatus for a deleted object.
|
||||
//
|
||||
// StatusReader.ReadStatusForObject doesn't handle nil objects as input. So
|
||||
// this builds the status manually.
|
||||
// TODO: find a way to delegate this back to the status package.
|
||||
func deletedStatus(id object.ObjMetadata) *event.ResourceStatus {
|
||||
// Status is always NotFound after deltion.
|
||||
// Passed obj represents the last known state, not the current state.
|
||||
result := &event.ResourceStatus{
|
||||
Identifier: id,
|
||||
Status: status.NotFoundStatus,
|
||||
Message: "Resource not found",
|
||||
}
|
||||
|
||||
return &event.ResourceStatus{
|
||||
Identifier: id,
|
||||
Resource: nil, // deleted object has no
|
||||
Status: result.Status,
|
||||
Message: result.Message,
|
||||
// If deleted with foreground deletion, a finalizer will have blocked
|
||||
// deletion until all the generated resources are deleted.
|
||||
// TODO: Handle lookup of generated resources when not using foreground deletion.
|
||||
GeneratedResources: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// eventHandler builds an event handler to compute object status.
|
||||
// Returns an event channel on which these stats updates will be reported.
|
||||
func (w *ObjectStatusReporter) eventHandler(
|
||||
ctx context.Context,
|
||||
eventCh chan<- event.Event,
|
||||
) cache.ResourceEventHandler {
|
||||
var handler cache.ResourceEventHandlerFuncs
|
||||
|
||||
handler.AddFunc = func(iobj interface{}) {
|
||||
// Bail early if the context is cancelled, to avoid unnecessary work.
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
obj, ok := iobj.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("AddFunc received unexpected object type %T", iobj))
|
||||
}
|
||||
id := object.UnstructuredToObjMetadata(obj)
|
||||
if w.ObjectFilter.Filter(obj) {
|
||||
klog.V(7).Infof("Watch Event Skipped: AddFunc: %s", id)
|
||||
return
|
||||
}
|
||||
klog.V(5).Infof("AddFunc: Computing status for object: %s", id)
|
||||
|
||||
// cancel any scheduled status update for this object
|
||||
w.taskManager.Cancel(id)
|
||||
|
||||
rs, err := w.readStatusFromObject(ctx, obj)
|
||||
if err != nil {
|
||||
// Send error event and stop the reporter!
|
||||
w.handleFatalError(eventCh, fmt.Errorf("failed to compute object status: %s: %w", id, err))
|
||||
return
|
||||
}
|
||||
|
||||
if object.IsNamespace(obj) {
|
||||
klog.V(5).Infof("AddFunc: Namespace added: %v", id)
|
||||
w.onNamespaceAdd(obj)
|
||||
} else if object.IsCRD(obj) {
|
||||
klog.V(5).Infof("AddFunc: CRD added: %v", id)
|
||||
w.onCRDAdd(obj)
|
||||
}
|
||||
|
||||
if isObjectUnschedulable(rs) {
|
||||
klog.V(5).Infof("AddFunc: object unschedulable: %v", id)
|
||||
// schedule delayed status update
|
||||
w.taskManager.Schedule(ctx, id, status.ScheduleWindow,
|
||||
w.newStatusCheckTaskFunc(ctx, eventCh, id))
|
||||
}
|
||||
|
||||
klog.V(7).Infof("AddFunc: sending update event: %v", rs)
|
||||
eventCh <- event.Event{
|
||||
Type: event.ResourceUpdateEvent,
|
||||
Resource: rs,
|
||||
}
|
||||
}
|
||||
|
||||
handler.UpdateFunc = func(_, iobj interface{}) {
|
||||
// Bail early if the context is cancelled, to avoid unnecessary work.
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
obj, ok := iobj.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("UpdateFunc received unexpected object type %T", iobj))
|
||||
}
|
||||
id := object.UnstructuredToObjMetadata(obj)
|
||||
if w.ObjectFilter.Filter(obj) {
|
||||
klog.V(7).Infof("UpdateFunc: Watch Event Skipped: %s", id)
|
||||
return
|
||||
}
|
||||
klog.V(5).Infof("UpdateFunc: Computing status for object: %s", id)
|
||||
|
||||
// cancel any scheduled status update for this object
|
||||
w.taskManager.Cancel(id)
|
||||
|
||||
rs, err := w.readStatusFromObject(ctx, obj)
|
||||
if err != nil {
|
||||
// Send error event and stop the reporter!
|
||||
w.handleFatalError(eventCh, fmt.Errorf("failed to compute object status: %s: %w", id, err))
|
||||
return
|
||||
}
|
||||
|
||||
if object.IsNamespace(obj) {
|
||||
klog.V(5).Infof("UpdateFunc: Namespace updated: %v", id)
|
||||
w.onNamespaceUpdate(obj)
|
||||
} else if object.IsCRD(obj) {
|
||||
klog.V(5).Infof("UpdateFunc: CRD updated: %v", id)
|
||||
w.onCRDUpdate(obj)
|
||||
}
|
||||
|
||||
if isObjectUnschedulable(rs) {
|
||||
klog.V(5).Infof("UpdateFunc: object unschedulable: %v", id)
|
||||
// schedule delayed status update
|
||||
w.taskManager.Schedule(ctx, id, status.ScheduleWindow,
|
||||
w.newStatusCheckTaskFunc(ctx, eventCh, id))
|
||||
}
|
||||
|
||||
klog.V(7).Infof("UpdateFunc: sending update event: %v", rs)
|
||||
eventCh <- event.Event{
|
||||
Type: event.ResourceUpdateEvent,
|
||||
Resource: rs,
|
||||
}
|
||||
}
|
||||
|
||||
handler.DeleteFunc = func(iobj interface{}) {
|
||||
// Bail early if the context is cancelled, to avoid unnecessary work.
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if tombstone, ok := iobj.(cache.DeletedFinalStateUnknown); ok {
|
||||
// Last state unknown. Possibly stale.
|
||||
// TODO: Should we propegate this uncertainty to the caller?
|
||||
iobj = tombstone.Obj
|
||||
}
|
||||
obj, ok := iobj.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("DeleteFunc received unexpected object type %T", iobj))
|
||||
}
|
||||
id := object.UnstructuredToObjMetadata(obj)
|
||||
if w.ObjectFilter.Filter(obj) {
|
||||
klog.V(7).Infof("DeleteFunc: Watch Event Skipped: %s", id)
|
||||
return
|
||||
}
|
||||
klog.V(5).Infof("DeleteFunc: Computing status for object: %s", id)
|
||||
|
||||
// cancel any scheduled status update for this object
|
||||
w.taskManager.Cancel(id)
|
||||
|
||||
if object.IsNamespace(obj) {
|
||||
klog.V(5).Infof("DeleteFunc: Namespace deleted: %v", id)
|
||||
w.onNamespaceDelete(obj)
|
||||
} else if object.IsCRD(obj) {
|
||||
klog.V(5).Infof("DeleteFunc: CRD deleted: %v", id)
|
||||
w.onCRDDelete(obj)
|
||||
}
|
||||
|
||||
rs := deletedStatus(id)
|
||||
klog.V(7).Infof("DeleteFunc: sending update event: %v", rs)
|
||||
eventCh <- event.Event{
|
||||
Type: event.ResourceUpdateEvent,
|
||||
Resource: rs,
|
||||
}
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// onCRDAdd handles creating a new informer to watch the new resource type.
|
||||
func (w *ObjectStatusReporter) onCRDAdd(obj *unstructured.Unstructured) {
|
||||
gk, found := object.GetCRDGroupKind(obj)
|
||||
if !found {
|
||||
id := object.UnstructuredToObjMetadata(obj)
|
||||
klog.Warningf("Invalid CRD added: missing group and/or kind: %v", id)
|
||||
// Don't return an error, because this should not inturrupt the task queue.
|
||||
// TODO: Allow non-fatal errors to be reported using a specific error type.
|
||||
return
|
||||
}
|
||||
klog.V(3).Infof("CRD added for %s", gk)
|
||||
|
||||
klog.V(3).Info("Resetting RESTMapper")
|
||||
// Reset mapper to invalidate cache.
|
||||
meta.MaybeResetRESTMapper(w.Mapper)
|
||||
|
||||
w.forEachTargetWithGroupKind(gk, func(gkn GroupKindNamespace) {
|
||||
w.startInformer(gkn)
|
||||
})
|
||||
}
|
||||
|
||||
// onCRDUpdate handles creating a new informer to watch the updated resource type.
|
||||
func (w *ObjectStatusReporter) onCRDUpdate(newObj *unstructured.Unstructured) {
|
||||
gk, found := object.GetCRDGroupKind(newObj)
|
||||
if !found {
|
||||
id := object.UnstructuredToObjMetadata(newObj)
|
||||
klog.Warningf("Invalid CRD updated: missing group and/or kind: %v", id)
|
||||
// Don't return an error, because this should not inturrupt the task queue.
|
||||
// TODO: Allow non-fatal errors to be reported using a specific error type.
|
||||
return
|
||||
}
|
||||
klog.V(3).Infof("CRD updated for %s", gk)
|
||||
|
||||
klog.V(3).Info("Resetting RESTMapper")
|
||||
// Reset mapper to invalidate cache.
|
||||
meta.MaybeResetRESTMapper(w.Mapper)
|
||||
|
||||
w.forEachTargetWithGroupKind(gk, func(gkn GroupKindNamespace) {
|
||||
w.startInformer(gkn)
|
||||
})
|
||||
}
|
||||
|
||||
// onCRDDelete handles stopping the informer watching the deleted resource type.
|
||||
func (w *ObjectStatusReporter) onCRDDelete(oldObj *unstructured.Unstructured) {
|
||||
gk, found := object.GetCRDGroupKind(oldObj)
|
||||
if !found {
|
||||
id := object.UnstructuredToObjMetadata(oldObj)
|
||||
klog.Warningf("Invalid CRD deleted: missing group and/or kind: %v", id)
|
||||
// Don't return an error, because this should not inturrupt the task queue.
|
||||
// TODO: Allow non-fatal errors to be reported using a specific error type.
|
||||
return
|
||||
}
|
||||
klog.V(3).Infof("CRD deleted for %s", gk)
|
||||
|
||||
w.forEachTargetWithGroupKind(gk, func(gkn GroupKindNamespace) {
|
||||
w.stopInformer(gkn)
|
||||
})
|
||||
|
||||
klog.V(3).Info("Resetting RESTMapper")
|
||||
// Reset mapper to invalidate cache.
|
||||
meta.MaybeResetRESTMapper(w.Mapper)
|
||||
}
|
||||
|
||||
// onNamespaceAdd handles creating new informers to watch this namespace.
|
||||
func (w *ObjectStatusReporter) onNamespaceAdd(obj *unstructured.Unstructured) {
|
||||
if w.RESTScope == meta.RESTScopeRoot {
|
||||
// When watching resources across all namespaces,
|
||||
// we don't need to start or stop any
|
||||
// namespace-specific informers.
|
||||
return
|
||||
}
|
||||
namespace := obj.GetName()
|
||||
w.forEachTargetWithNamespace(namespace, func(gkn GroupKindNamespace) {
|
||||
w.startInformer(gkn)
|
||||
})
|
||||
}
|
||||
|
||||
// onNamespaceUpdate handles creating new informers to watch this namespace.
|
||||
func (w *ObjectStatusReporter) onNamespaceUpdate(obj *unstructured.Unstructured) {
|
||||
if w.RESTScope == meta.RESTScopeRoot {
|
||||
// When watching resources across all namespaces,
|
||||
// we don't need to start or stop any
|
||||
// namespace-specific informers.
|
||||
return
|
||||
}
|
||||
namespace := obj.GetName()
|
||||
w.forEachTargetWithNamespace(namespace, func(gkn GroupKindNamespace) {
|
||||
w.startInformer(gkn)
|
||||
})
|
||||
}
|
||||
|
||||
// onNamespaceDelete handles stopping informers watching this namespace.
|
||||
func (w *ObjectStatusReporter) onNamespaceDelete(obj *unstructured.Unstructured) {
|
||||
if w.RESTScope == meta.RESTScopeRoot {
|
||||
// When watching resources across all namespaces,
|
||||
// we don't need to start or stop any
|
||||
// namespace-specific informers.
|
||||
return
|
||||
}
|
||||
namespace := obj.GetName()
|
||||
w.forEachTargetWithNamespace(namespace, func(gkn GroupKindNamespace) {
|
||||
w.stopInformer(gkn)
|
||||
})
|
||||
}
|
||||
|
||||
// newStatusCheckTaskFunc returns a taskFund that reads the status of an object
|
||||
// from the cluster and sends it over the event channel.
|
||||
//
|
||||
// This method should only be used for generated resource objects, as it's much
|
||||
// slower at scale than watching the resource for updates.
|
||||
func (w *ObjectStatusReporter) newStatusCheckTaskFunc(
|
||||
ctx context.Context,
|
||||
eventCh chan<- event.Event,
|
||||
id object.ObjMetadata,
|
||||
) taskFunc {
|
||||
return func() {
|
||||
klog.V(5).Infof("Re-reading object status: %s", id)
|
||||
// check again
|
||||
rs, err := w.readStatusFromCluster(ctx, id)
|
||||
if err != nil {
|
||||
// Send error event and stop the reporter!
|
||||
// TODO: retry N times before terminating
|
||||
w.handleFatalError(eventCh, err)
|
||||
return
|
||||
}
|
||||
eventCh <- event.Event{
|
||||
Type: event.ResourceUpdateEvent,
|
||||
Resource: rs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ObjectStatusReporter) handleFatalError(eventCh chan<- event.Event, err error) {
|
||||
klog.V(5).Infof("Reporter error: %v", err)
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return
|
||||
}
|
||||
eventCh <- event.Event{
|
||||
Type: event.ErrorEvent,
|
||||
Error: err,
|
||||
}
|
||||
w.Stop()
|
||||
}
|
||||
|
||||
// watchErrorHandler logs errors and cancels the informer for this GroupKind
|
||||
// if the NotFound error is received, which usually means the CRD was deleted.
|
||||
// Based on DefaultWatchErrorHandler from k8s.io/client-go@v0.23.2/tools/cache/reflector.go
|
||||
func (w *ObjectStatusReporter) watchErrorHandler(gkn GroupKindNamespace, eventCh chan<- event.Event, err error) {
|
||||
switch {
|
||||
// Stop channel closed
|
||||
case err == io.EOF:
|
||||
klog.V(5).Infof("ListAndWatch error (termination expected): %v: %v", gkn, err)
|
||||
|
||||
// Watch connection closed
|
||||
case err == io.ErrUnexpectedEOF:
|
||||
klog.V(1).Infof("ListAndWatch error (retry expected): %v: %v", gkn, err)
|
||||
|
||||
// Context done
|
||||
case errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded):
|
||||
klog.V(5).Infof("ListAndWatch error (termination expected): %v: %v", gkn, err)
|
||||
|
||||
// resourceVersion too old
|
||||
case apierrors.IsResourceExpired(err):
|
||||
// Keep retrying
|
||||
klog.V(5).Infof("ListAndWatch error (retry expected): %v: %v", gkn, err)
|
||||
|
||||
// Resource unregistered (DEPRECATED, see NotFound)
|
||||
case apierrors.IsGone(err):
|
||||
klog.V(5).Infof("ListAndWatch error (retry expected): %v: %v", gkn, err)
|
||||
|
||||
// Resource not registered
|
||||
case apierrors.IsNotFound(err):
|
||||
klog.V(3).Infof("ListAndWatch error (termination expected): %v: stopping all informers for this GroupKind: %v", gkn, err)
|
||||
w.forEachTargetWithGroupKind(gkn.GroupKind(), func(gkn GroupKindNamespace) {
|
||||
w.stopInformer(gkn)
|
||||
})
|
||||
|
||||
// Insufficient permissions
|
||||
case apierrors.IsForbidden(err):
|
||||
klog.V(3).Infof("ListAndWatch error (termination expected): %v: stopping all informers: %v", gkn, err)
|
||||
w.handleFatalError(eventCh, err)
|
||||
|
||||
// Unexpected error
|
||||
default:
|
||||
klog.Warningf("ListAndWatch error (retry expected): %v: %v", gkn, err)
|
||||
}
|
||||
}
|
||||
|
||||
// informerReference tracks informer lifecycle.
|
||||
type informerReference struct {
|
||||
// lock guards the subsequent stateful fields
|
||||
lock sync.Mutex
|
||||
|
||||
informer cache.SharedIndexInformer
|
||||
context context.Context
|
||||
cancel context.CancelFunc
|
||||
started bool
|
||||
}
|
||||
|
||||
// Start returns a wrapped context that can be cancelled.
|
||||
// Returns nil & false if already started.
|
||||
func (ir *informerReference) Start(ctx context.Context) (context.Context, bool) {
|
||||
ir.lock.Lock()
|
||||
defer ir.lock.Unlock()
|
||||
|
||||
if ir.started {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
ir.context = ctx
|
||||
ir.cancel = cancel
|
||||
ir.started = true
|
||||
|
||||
return ctx, true
|
||||
}
|
||||
|
||||
func (ir *informerReference) SetInformer(informer cache.SharedIndexInformer) {
|
||||
ir.lock.Lock()
|
||||
defer ir.lock.Unlock()
|
||||
|
||||
ir.informer = informer
|
||||
}
|
||||
|
||||
func (ir *informerReference) HasSynced() bool {
|
||||
ir.lock.Lock()
|
||||
defer ir.lock.Unlock()
|
||||
|
||||
if !ir.started {
|
||||
return false
|
||||
}
|
||||
if ir.informer == nil {
|
||||
return false
|
||||
}
|
||||
return ir.informer.HasSynced()
|
||||
}
|
||||
|
||||
func (ir *informerReference) HasStarted() bool {
|
||||
ir.lock.Lock()
|
||||
defer ir.lock.Unlock()
|
||||
|
||||
return ir.started
|
||||
}
|
||||
|
||||
// Stop cancels the context, if it's been started.
|
||||
func (ir *informerReference) Stop() {
|
||||
ir.lock.Lock()
|
||||
defer ir.lock.Unlock()
|
||||
|
||||
if !ir.started {
|
||||
return
|
||||
}
|
||||
|
||||
ir.cancel()
|
||||
ir.started = false
|
||||
ir.context = nil
|
||||
}
|
||||
|
||||
type taskFunc func()
|
||||
|
||||
// taskManager manages a set of tasks with object identifiers.
|
||||
// This makes starting and stopping the tasks thread-safe.
|
||||
type taskManager struct {
|
||||
lock sync.Mutex
|
||||
cancelFuncs map[object.ObjMetadata]context.CancelFunc
|
||||
}
|
||||
|
||||
func (tm *taskManager) Schedule(parentCtx context.Context, id object.ObjMetadata, delay time.Duration, task taskFunc) {
|
||||
tm.lock.Lock()
|
||||
defer tm.lock.Unlock()
|
||||
|
||||
if tm.cancelFuncs == nil {
|
||||
tm.cancelFuncs = make(map[object.ObjMetadata]context.CancelFunc)
|
||||
}
|
||||
|
||||
cancel, found := tm.cancelFuncs[id]
|
||||
if found {
|
||||
// Cancel the existing scheduled task and replace it.
|
||||
cancel()
|
||||
}
|
||||
|
||||
taskCtx, cancel := context.WithTimeout(context.Background(), delay)
|
||||
tm.cancelFuncs[id] = cancel
|
||||
|
||||
go func() {
|
||||
klog.V(5).Infof("Task scheduled (%v) for object (%s)", delay, id)
|
||||
select {
|
||||
case <-parentCtx.Done():
|
||||
// stop waiting
|
||||
cancel()
|
||||
case <-taskCtx.Done():
|
||||
if taskCtx.Err() == context.DeadlineExceeded {
|
||||
klog.V(5).Infof("Task executing (after %v) for object (%v)", delay, id)
|
||||
task()
|
||||
}
|
||||
// else stop waiting
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (tm *taskManager) Cancel(id object.ObjMetadata) {
|
||||
tm.lock.Lock()
|
||||
defer tm.lock.Unlock()
|
||||
|
||||
cancelFunc, found := tm.cancelFuncs[id]
|
||||
if !found {
|
||||
// already cancelled or not added
|
||||
return
|
||||
}
|
||||
delete(tm.cancelFuncs, id)
|
||||
cancelFunc()
|
||||
if len(tm.cancelFuncs) == 0 {
|
||||
tm.cancelFuncs = nil
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
// Code generated by "stringer -type=RESTScopeStrategy -linecomment"; DO NOT EDIT.
|
||||
|
||||
package watcher
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[RESTScopeAutomatic-0]
|
||||
_ = x[RESTScopeRoot-1]
|
||||
_ = x[RESTScopeNamespace-2]
|
||||
}
|
||||
|
||||
const _RESTScopeStrategy_name = "automaticrootnamespace"
|
||||
|
||||
var _RESTScopeStrategy_index = [...]uint8{0, 9, 13, 22}
|
||||
|
||||
func (i RESTScopeStrategy) String() string {
|
||||
if i < 0 || i >= RESTScopeStrategy(len(_RESTScopeStrategy_index)-1) {
|
||||
return "RESTScopeStrategy(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _RESTScopeStrategy_name[_RESTScopeStrategy_index[i]:_RESTScopeStrategy_index[i+1]]
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// isObjectUnschedulable returns true if the object or any of its generated resources
|
||||
// is an unschedulable pod.
|
||||
//
|
||||
// This status is computed recursively, so it can handle objects that generate
|
||||
// objects that generate pods, as long as the input ResourceStatus has those
|
||||
// GeneratedResources computed.
|
||||
func isObjectUnschedulable(rs *event.ResourceStatus) bool {
|
||||
if rs.Error != nil {
|
||||
return false
|
||||
}
|
||||
if rs.Status != status.InProgressStatus {
|
||||
return false
|
||||
}
|
||||
if isPodUnschedulable(rs.Resource) {
|
||||
return true
|
||||
}
|
||||
// recurse through generated resources
|
||||
for _, subRS := range rs.GeneratedResources {
|
||||
if isObjectUnschedulable(subRS) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isPodUnschedulable returns true if the object is a pod and is unschedulable
|
||||
// according to a False PodScheduled condition.
|
||||
func isPodUnschedulable(obj *unstructured.Unstructured) bool {
|
||||
if obj == nil {
|
||||
return false
|
||||
}
|
||||
gk := obj.GroupVersionKind().GroupKind()
|
||||
if gk != (schema.GroupKind{Kind: "Pod"}) {
|
||||
return false
|
||||
}
|
||||
icnds, found, err := object.NestedField(obj.Object, "status", "conditions")
|
||||
if err != nil || !found {
|
||||
return false
|
||||
}
|
||||
cnds, ok := icnds.([]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, icnd := range cnds {
|
||||
cnd, ok := icnd.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if cnd["type"] == "PodScheduled" &&
|
||||
cnd["status"] == "False" &&
|
||||
cnd["reason"] == "Unschedulable" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
)
|
||||
|
||||
// StatusWatcher watches a set of objects for status updates.
|
||||
type StatusWatcher interface {
|
||||
|
||||
// Watch a set of objects for status updates.
|
||||
// Watching should stop if the context is cancelled.
|
||||
// Events should only be sent for the specified objects.
|
||||
// The event channel should be closed when the watching stops.
|
||||
Watch(context.Context, object.ObjMetadataSet, Options) <-chan event.Event
|
||||
}
|
||||
|
||||
// Options can be provided when creating a new StatusWatcher to customize the
|
||||
// behavior.
|
||||
type Options struct {
|
||||
// RESTScopeStrategy specifies which strategy to use when listing and
|
||||
// watching resources. By default, the strategy is selected automatically.
|
||||
RESTScopeStrategy RESTScopeStrategy
|
||||
}
|
||||
|
||||
//go:generate stringer -type=RESTScopeStrategy -linecomment
|
||||
type RESTScopeStrategy int
|
||||
|
||||
const (
|
||||
RESTScopeAutomatic RESTScopeStrategy = iota // automatic
|
||||
RESTScopeRoot // root
|
||||
RESTScopeNamespace // namespace
|
||||
)
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// InvalidAnnotationError represents an invalid annotation.
|
||||
// Fields are exposed to allow callers to perform introspection.
|
||||
type InvalidAnnotationError struct {
|
||||
Annotation string
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (iae InvalidAnnotationError) Error() string {
|
||||
return fmt.Sprintf("invalid %q annotation: %v",
|
||||
iae.Annotation, iae.Cause)
|
||||
}
|
||||
|
||||
func (iae InvalidAnnotationError) Unwrap() error {
|
||||
return iae.Cause
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// NestedField gets a value from a KRM map, if it exists, otherwise nil.
|
||||
// Fields can be string (map key) or int (array index).
|
||||
func NestedField(obj map[string]interface{}, fields ...interface{}) (interface{}, bool, error) {
|
||||
var val interface{} = obj
|
||||
|
||||
for i, field := range fields {
|
||||
if val == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
switch typedField := field.(type) {
|
||||
case string:
|
||||
if m, ok := val.(map[string]interface{}); ok {
|
||||
val, ok = m[typedField]
|
||||
if !ok {
|
||||
// not in map
|
||||
return nil, false, nil
|
||||
}
|
||||
} else {
|
||||
return nil, false, InvalidType(fields[:i+1], val, "map[string]interface{}")
|
||||
}
|
||||
case int:
|
||||
if s, ok := val.([]interface{}); ok {
|
||||
if typedField >= len(s) {
|
||||
// index out of range
|
||||
return nil, false, nil
|
||||
}
|
||||
val = s[typedField]
|
||||
} else {
|
||||
return nil, false, InvalidType(fields[:i+1], val, "[]interface{}")
|
||||
}
|
||||
default:
|
||||
return nil, false, InvalidType(fields[:i+1], val, "string or int")
|
||||
}
|
||||
}
|
||||
return val, true, nil
|
||||
}
|
||||
|
||||
// InvalidType returns a *Error indicating "invalid value type". This is used
|
||||
// to report malformed values (e.g. found int, expected string).
|
||||
func InvalidType(fieldPath []interface{}, value interface{}, validTypes string) *field.Error {
|
||||
return Invalid(fieldPath, value,
|
||||
fmt.Sprintf("found type %T, expected %s", value, validTypes))
|
||||
}
|
||||
|
||||
// Invalid returns a *Error indicating "invalid value". This is used
|
||||
// to report malformed values (e.g. failed regex match, too long, out of bounds).
|
||||
func Invalid(fieldPath []interface{}, value interface{}, detail string) *field.Error {
|
||||
return &field.Error{
|
||||
Type: field.ErrorTypeInvalid,
|
||||
Field: FieldPath(fieldPath),
|
||||
BadValue: value,
|
||||
Detail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound returns a *Error indicating "value not found". This is
|
||||
// used to report failure to find a requested value (e.g. looking up an ID).
|
||||
func NotFound(fieldPath []interface{}, value interface{}) *field.Error {
|
||||
return &field.Error{
|
||||
Type: field.ErrorTypeNotFound,
|
||||
Field: FieldPath(fieldPath),
|
||||
BadValue: value,
|
||||
Detail: "",
|
||||
}
|
||||
}
|
||||
|
||||
// FieldPath formats a list of KRM field keys as a JSONPath expression.
|
||||
// The only valid field keys in KRM are strings (map keys) and ints (list keys).
|
||||
// Simple strings (see isSimpleString) will be delimited with a period.
|
||||
// Complex strings will be wrapped with square brackets and double quotes.
|
||||
// Integers will be wrapped with square brackets.
|
||||
// All other types will be formatted best-effort within square brackets.
|
||||
func FieldPath(fieldPath []interface{}) string {
|
||||
var sb strings.Builder
|
||||
for _, field := range fieldPath {
|
||||
switch typedField := field.(type) {
|
||||
case string:
|
||||
if isSimpleString(typedField) {
|
||||
_, _ = fmt.Fprintf(&sb, ".%s", typedField)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(&sb, "[%q]", typedField)
|
||||
}
|
||||
case int:
|
||||
_, _ = fmt.Fprintf(&sb, "[%d]", typedField)
|
||||
default:
|
||||
// invalid type. try anyway...
|
||||
_, _ = fmt.Fprintf(&sb, "[%#v]", typedField)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
var simpleStringRegex = regexp.MustCompile(`^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$`)
|
||||
|
||||
// isSimpleString returns true if the input follows the following rules:
|
||||
// - contains only alphanumeric characters, '_' or '-'
|
||||
// - starts with an alphabetic character
|
||||
// - ends with an alphanumeric character
|
||||
func isSimpleString(s string) bool {
|
||||
return simpleStringRegex.FindString(s) != ""
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
)
|
||||
|
||||
// InfosToObjMetas returns object metadata (ObjMetadata) for the
|
||||
// passed objects (infos); returns an error if one occurs.
|
||||
func InfosToObjMetas(infos []*resource.Info) ([]ObjMetadata, error) {
|
||||
objMetas := make([]ObjMetadata, 0, len(infos))
|
||||
for _, info := range infos {
|
||||
objMeta, err := InfoToObjMeta(info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objMetas = append(objMetas, objMeta)
|
||||
}
|
||||
return objMetas, nil
|
||||
}
|
||||
|
||||
// InfoToObjMeta takes information from the provided info and
|
||||
// returns an ObjMetadata that identifies the resource.
|
||||
func InfoToObjMeta(info *resource.Info) (ObjMetadata, error) {
|
||||
if info == nil || info.Object == nil {
|
||||
return ObjMetadata{}, fmt.Errorf("attempting to transform info, but it is empty")
|
||||
}
|
||||
id := ObjMetadata{
|
||||
Namespace: info.Namespace,
|
||||
Name: info.Name,
|
||||
GroupKind: info.Object.GetObjectKind().GroupVersionKind().GroupKind(),
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// InfoToUnstructured transforms the passed info object into unstructured format.
|
||||
func InfoToUnstructured(info *resource.Info) *unstructured.Unstructured {
|
||||
return info.Object.(*unstructured.Unstructured)
|
||||
}
|
||||
|
||||
// UnstructuredToInfo transforms the passed Unstructured object into Info format,
|
||||
// or an error if one occurs.
|
||||
func UnstructuredToInfo(obj *unstructured.Unstructured) (*resource.Info, error) {
|
||||
// make a copy of the input object to avoid modifying the input
|
||||
obj = obj.DeepCopy()
|
||||
|
||||
annos := obj.GetAnnotations()
|
||||
|
||||
source := "unstructured"
|
||||
path, ok := annos[kioutil.PathAnnotation]
|
||||
if ok {
|
||||
source = path
|
||||
}
|
||||
StripKyamlAnnotations(obj)
|
||||
|
||||
return &resource.Info{
|
||||
Name: obj.GetName(),
|
||||
Namespace: obj.GetNamespace(),
|
||||
Source: source,
|
||||
Object: obj,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InfosToUnstructureds transforms the passed objects in Info format to Unstructured.
|
||||
func InfosToUnstructureds(infos []*resource.Info) []*unstructured.Unstructured {
|
||||
var objs []*unstructured.Unstructured
|
||||
for _, info := range infos {
|
||||
objs = append(objs, InfoToUnstructured(info))
|
||||
}
|
||||
return objs
|
||||
}
|
||||
|
||||
// UnstructuredsToInfos transforms the passed Unstructured objects into Info format
|
||||
// or an error if one occurs.
|
||||
func UnstructuredsToInfos(objs []*unstructured.Unstructured) ([]*resource.Info, error) {
|
||||
var infos []*resource.Info
|
||||
for _, obj := range objs {
|
||||
inf, err := UnstructuredToInfo(obj)
|
||||
if err != nil {
|
||||
return infos, err
|
||||
}
|
||||
infos = append(infos, inf)
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// ObjMetadata is the minimal set of information to
|
||||
// uniquely identify an object. The four fields are:
|
||||
//
|
||||
// Group/Kind (NOTE: NOT version)
|
||||
// Namespace
|
||||
// Name
|
||||
//
|
||||
// We specifically do not use the "version", because
|
||||
// the APIServer does not recognize a version as a
|
||||
// different resource. This metadata is used to identify
|
||||
// resources for pruning and teardown.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
// Separates inventory fields. This string is allowable as a
|
||||
// ConfigMap key, but it is not allowed as a character in
|
||||
// resource name.
|
||||
fieldSeparator = "_"
|
||||
// Transform colons in the RBAC resource names to double
|
||||
// underscore.
|
||||
colonTranscoded = "__"
|
||||
)
|
||||
|
||||
var (
|
||||
NilObjMetadata = ObjMetadata{}
|
||||
)
|
||||
|
||||
// RBACGroupKind is a map of the RBAC resources. Needed since name validation
|
||||
// is different than other k8s resources.
|
||||
var RBACGroupKind = map[schema.GroupKind]bool{
|
||||
{Group: rbacv1.GroupName, Kind: "Role"}: true,
|
||||
{Group: rbacv1.GroupName, Kind: "ClusterRole"}: true,
|
||||
{Group: rbacv1.GroupName, Kind: "RoleBinding"}: true,
|
||||
{Group: rbacv1.GroupName, Kind: "ClusterRoleBinding"}: true,
|
||||
}
|
||||
|
||||
// ObjMetadata organizes and stores the indentifying information
|
||||
// for an object. This struct (as a string) is stored in a
|
||||
// inventory object to keep track of sets of applied objects.
|
||||
type ObjMetadata struct {
|
||||
Namespace string
|
||||
Name string
|
||||
GroupKind schema.GroupKind
|
||||
}
|
||||
|
||||
// ParseObjMetadata takes a string, splits it into its four fields,
|
||||
// and returns an ObjMetadata struct storing the four fields.
|
||||
// Example inventory string:
|
||||
//
|
||||
// test-namespace_test-name_apps_ReplicaSet
|
||||
//
|
||||
// Returns an error if unable to parse and create the ObjMetadata struct.
|
||||
//
|
||||
// NOTE: name field can contain double underscore (__), which represents
|
||||
// a colon. RBAC resources can have this additional character (:) in their name.
|
||||
func ParseObjMetadata(s string) (ObjMetadata, error) {
|
||||
// Parse first field namespace
|
||||
index := strings.Index(s, fieldSeparator)
|
||||
if index == -1 {
|
||||
return NilObjMetadata, fmt.Errorf("unable to parse stored object metadata: %s", s)
|
||||
}
|
||||
namespace := s[:index]
|
||||
s = s[index+1:]
|
||||
// Next, parse last field kind
|
||||
index = strings.LastIndex(s, fieldSeparator)
|
||||
if index == -1 {
|
||||
return NilObjMetadata, fmt.Errorf("unable to parse stored object metadata: %s", s)
|
||||
}
|
||||
kind := s[index+1:]
|
||||
s = s[:index]
|
||||
// Next, parse next to last field group
|
||||
index = strings.LastIndex(s, fieldSeparator)
|
||||
if index == -1 {
|
||||
return NilObjMetadata, fmt.Errorf("unable to parse stored object metadata: %s", s)
|
||||
}
|
||||
group := s[index+1:]
|
||||
// Finally, second field name. Name may contain colon transcoded as double underscore.
|
||||
name := s[:index]
|
||||
name = strings.ReplaceAll(name, colonTranscoded, ":")
|
||||
// Check that there are no extra fields by search for fieldSeparator.
|
||||
if strings.Contains(name, fieldSeparator) {
|
||||
return NilObjMetadata, fmt.Errorf("too many fields within: %s", s)
|
||||
}
|
||||
// Create the ObjMetadata object from the four parsed fields.
|
||||
id := ObjMetadata{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
GroupKind: schema.GroupKind{
|
||||
Group: group,
|
||||
Kind: kind,
|
||||
},
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Equals compares two ObjMetadata and returns true if they are equal. This does
|
||||
// not contain any special treatment for the extensions API group.
|
||||
func (o *ObjMetadata) Equals(other *ObjMetadata) bool {
|
||||
if other == nil {
|
||||
return false
|
||||
}
|
||||
return *o == *other
|
||||
}
|
||||
|
||||
// String create a string version of the ObjMetadata struct. For RBAC resources,
|
||||
// the "name" field transcodes ":" into double underscore for valid storing
|
||||
// as the label of a ConfigMap.
|
||||
func (o ObjMetadata) String() string {
|
||||
name := o.Name
|
||||
if _, exists := RBACGroupKind[o.GroupKind]; exists {
|
||||
name = strings.ReplaceAll(name, ":", colonTranscoded)
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s%s%s%s%s",
|
||||
o.Namespace, fieldSeparator,
|
||||
name, fieldSeparator,
|
||||
o.GroupKind.Group, fieldSeparator,
|
||||
o.GroupKind.Kind)
|
||||
}
|
||||
|
||||
// RuntimeToObjMeta extracts the object metadata information from a
|
||||
// runtime.Object and returns it as ObjMetadata.
|
||||
func RuntimeToObjMeta(obj runtime.Object) (ObjMetadata, error) {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return NilObjMetadata, err
|
||||
}
|
||||
id := ObjMetadata{
|
||||
Namespace: accessor.GetNamespace(),
|
||||
Name: accessor.GetName(),
|
||||
GroupKind: obj.GetObjectKind().GroupVersionKind().GroupKind(),
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"hash/fnv"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ObjMetadataSet is an ordered list of ObjMetadata that acts like an unordered
|
||||
// set for comparison purposes.
|
||||
type ObjMetadataSet []ObjMetadata
|
||||
|
||||
// UnstructuredSetEquals returns true if the slice of objects in setA equals
|
||||
// the slice of objects in setB.
|
||||
func ObjMetadataSetEquals(setA []ObjMetadata, setB []ObjMetadata) bool {
|
||||
return ObjMetadataSet(setA).Equal(ObjMetadataSet(setB))
|
||||
}
|
||||
|
||||
// ObjMetadataSetFromMap constructs a set from a map
|
||||
func ObjMetadataSetFromMap(mapA map[ObjMetadata]struct{}) ObjMetadataSet {
|
||||
setA := make(ObjMetadataSet, 0, len(mapA))
|
||||
for f := range mapA {
|
||||
setA = append(setA, f)
|
||||
}
|
||||
return setA
|
||||
}
|
||||
|
||||
// Equal returns true if the two sets contain equivalent objects. Duplicates are
|
||||
// ignored.
|
||||
// This function satisfies the cmp.Equal interface from github.com/google/go-cmp
|
||||
func (setA ObjMetadataSet) Equal(setB ObjMetadataSet) bool {
|
||||
mapA := make(map[ObjMetadata]struct{}, len(setA))
|
||||
for _, a := range setA {
|
||||
mapA[a] = struct{}{}
|
||||
}
|
||||
mapB := make(map[ObjMetadata]struct{}, len(setB))
|
||||
for _, b := range setB {
|
||||
mapB[b] = struct{}{}
|
||||
}
|
||||
if len(mapA) != len(mapB) {
|
||||
return false
|
||||
}
|
||||
for b := range mapB {
|
||||
if _, exists := mapA[b]; !exists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains checks if the provided ObjMetadata exists in the set.
|
||||
func (setA ObjMetadataSet) Contains(id ObjMetadata) bool {
|
||||
for _, om := range setA {
|
||||
if om == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove the object from the set and return the updated set.
|
||||
func (setA ObjMetadataSet) Remove(obj ObjMetadata) ObjMetadataSet {
|
||||
for i, a := range setA {
|
||||
if a == obj {
|
||||
setA[len(setA)-1], setA[i] = setA[i], setA[len(setA)-1]
|
||||
return setA[:len(setA)-1]
|
||||
}
|
||||
}
|
||||
return setA
|
||||
}
|
||||
|
||||
// Intersection returns the set of unique objects in both set A and set B.
|
||||
func (setA ObjMetadataSet) Intersection(setB ObjMetadataSet) ObjMetadataSet {
|
||||
var maxlen int
|
||||
if len(setA) > len(setB) {
|
||||
maxlen = len(setA)
|
||||
} else {
|
||||
maxlen = len(setB)
|
||||
}
|
||||
mapI := make(map[ObjMetadata]struct{}, maxlen)
|
||||
mapB := setB.ToMap()
|
||||
for _, a := range setA {
|
||||
if _, ok := mapB[a]; ok {
|
||||
mapI[a] = struct{}{}
|
||||
}
|
||||
}
|
||||
intersection := make(ObjMetadataSet, 0, len(mapI))
|
||||
// Iterate over setA & setB to retain input order and have stable output
|
||||
for _, id := range setA {
|
||||
if _, ok := mapI[id]; ok {
|
||||
intersection = append(intersection, id)
|
||||
delete(mapI, id)
|
||||
}
|
||||
}
|
||||
for _, id := range setB {
|
||||
if _, ok := mapI[id]; ok {
|
||||
intersection = append(intersection, id)
|
||||
delete(mapI, id)
|
||||
}
|
||||
}
|
||||
return intersection
|
||||
}
|
||||
|
||||
// Union returns the set of unique objects from the merging of set A and set B.
|
||||
func (setA ObjMetadataSet) Union(setB ObjMetadataSet) ObjMetadataSet {
|
||||
m := make(map[ObjMetadata]struct{}, len(setA)+len(setB))
|
||||
for _, a := range setA {
|
||||
m[a] = struct{}{}
|
||||
}
|
||||
for _, b := range setB {
|
||||
m[b] = struct{}{}
|
||||
}
|
||||
union := make(ObjMetadataSet, 0, len(m))
|
||||
// Iterate over setA & setB to retain input order and have stable output
|
||||
for _, id := range setA {
|
||||
if _, ok := m[id]; ok {
|
||||
union = append(union, id)
|
||||
delete(m, id)
|
||||
}
|
||||
}
|
||||
for _, id := range setB {
|
||||
if _, ok := m[id]; ok {
|
||||
union = append(union, id)
|
||||
delete(m, id)
|
||||
}
|
||||
}
|
||||
return union
|
||||
}
|
||||
|
||||
// Diff returns the set of objects that exist in set A, but not in set B (A - B).
|
||||
func (setA ObjMetadataSet) Diff(setB ObjMetadataSet) ObjMetadataSet {
|
||||
// Create a map of the elements of A
|
||||
m := make(map[ObjMetadata]struct{}, len(setA))
|
||||
for _, a := range setA {
|
||||
m[a] = struct{}{}
|
||||
}
|
||||
// Remove from A each element of B
|
||||
for _, b := range setB {
|
||||
delete(m, b) // OK to delete even if b not in m
|
||||
}
|
||||
// Create/return slice from the map of remaining items
|
||||
diff := make(ObjMetadataSet, 0, len(m))
|
||||
// Iterate over setA to retain input order and have stable output
|
||||
for _, id := range setA {
|
||||
if _, ok := m[id]; ok {
|
||||
diff = append(diff, id)
|
||||
delete(m, id)
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
// Unique returns the set with duplicates removed.
|
||||
// Order may or may not remain consistent.
|
||||
func (setA ObjMetadataSet) Unique() ObjMetadataSet {
|
||||
return ObjMetadataSetFromMap(setA.ToMap())
|
||||
}
|
||||
|
||||
// Hash the objects in the set by serializing, sorting, concatonating, and
|
||||
// hashing the result with the 32-bit FNV-1a algorithm.
|
||||
func (setA ObjMetadataSet) Hash() string {
|
||||
objStrs := make([]string, 0, len(setA))
|
||||
for _, obj := range setA {
|
||||
objStrs = append(objStrs, obj.String())
|
||||
}
|
||||
sort.Strings(objStrs)
|
||||
h := fnv.New32a()
|
||||
for _, obj := range objStrs {
|
||||
// Hash32.Write never returns an error
|
||||
// https://pkg.go.dev/hash#pkg-types
|
||||
_, _ = h.Write([]byte(obj))
|
||||
}
|
||||
return strconv.FormatUint(uint64(h.Sum32()), 16)
|
||||
}
|
||||
|
||||
// ToMap returns the set as a map, with objMeta keys and empty struct values.
|
||||
func (setA ObjMetadataSet) ToMap() map[ObjMetadata]struct{} {
|
||||
m := make(map[ObjMetadata]struct{}, len(setA))
|
||||
for _, objMeta := range setA {
|
||||
m[objMeta] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ToStringMap returns the set as a serializable map, with objMeta keys and
|
||||
// empty string values.
|
||||
func (setA ObjMetadataSet) ToStringMap() map[string]string {
|
||||
stringMap := make(map[string]string, len(setA))
|
||||
for _, objMeta := range setA {
|
||||
stringMap[objMeta.String()] = ""
|
||||
}
|
||||
return stringMap
|
||||
}
|
||||
|
||||
// FromStringMap returns a set from a serializable map, with objMeta keys and
|
||||
// empty string values. Errors if parsing fails.
|
||||
func FromStringMap(in map[string]string) (ObjMetadataSet, error) {
|
||||
var set ObjMetadataSet
|
||||
for s := range in {
|
||||
objMeta, err := ParseObjMetadata(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
set = append(set, objMeta)
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// YamlStringer delays YAML marshalling for logging until String() is called.
|
||||
type YamlStringer struct {
|
||||
O *unstructured.Unstructured
|
||||
}
|
||||
|
||||
// String marshals the wrapped object to a YAML string. If serializing errors,
|
||||
// the error string will be returned instead. This is primarily for use with
|
||||
// verbose logging.
|
||||
func (ys YamlStringer) String() string {
|
||||
yamlBytes, err := yaml.Marshal(ys.O)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("<<failed to serialize as yaml: %s>>", err)
|
||||
}
|
||||
return string(yamlBytes)
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
)
|
||||
|
||||
var (
|
||||
namespaceGK = schema.GroupKind{Group: "", Kind: "Namespace"}
|
||||
crdGK = schema.GroupKind{Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition"}
|
||||
)
|
||||
|
||||
// UnstructuredSetToObjMetadataSet converts a UnstructuredSet to a ObjMetadataSet.
|
||||
func UnstructuredSetToObjMetadataSet(objs UnstructuredSet) ObjMetadataSet {
|
||||
objMetas := make([]ObjMetadata, len(objs))
|
||||
for i, obj := range objs {
|
||||
objMetas[i] = UnstructuredToObjMetadata(obj)
|
||||
}
|
||||
return objMetas
|
||||
}
|
||||
|
||||
// UnstructuredToObjMetadata extracts the identifying information from an
|
||||
// Unstructured object and returns it as ObjMetadata object.
|
||||
func UnstructuredToObjMetadata(obj *unstructured.Unstructured) ObjMetadata {
|
||||
return ObjMetadata{
|
||||
Namespace: obj.GetNamespace(),
|
||||
Name: obj.GetName(),
|
||||
GroupKind: obj.GroupVersionKind().GroupKind(),
|
||||
}
|
||||
}
|
||||
|
||||
// IsKindNamespace returns true if the passed Unstructured object is
|
||||
// GroupKind == Core/Namespace (no version checked); false otherwise.
|
||||
func IsKindNamespace(u *unstructured.Unstructured) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
gvk := u.GroupVersionKind()
|
||||
return namespaceGK == gvk.GroupKind()
|
||||
}
|
||||
|
||||
// IsNamespaced returns true if the passed Unstructured object
|
||||
// is namespace-scoped (not cluster-scoped); false otherwise.
|
||||
func IsNamespaced(u *unstructured.Unstructured) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
return u.GetNamespace() != ""
|
||||
}
|
||||
|
||||
// IsNamespace returns true if the passed Unstructured object
|
||||
// is Namespace in the core (empty string) group.
|
||||
func IsNamespace(u *unstructured.Unstructured) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
gvk := u.GroupVersionKind()
|
||||
// core group, any version
|
||||
return gvk.Group == "" && gvk.Kind == "Namespace"
|
||||
}
|
||||
|
||||
// IsCRD returns true if the passed Unstructured object has
|
||||
// GroupKind == Extensions/CustomResourceDefinition; false otherwise.
|
||||
func IsCRD(u *unstructured.Unstructured) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
gvk := u.GroupVersionKind()
|
||||
return crdGK == gvk.GroupKind()
|
||||
}
|
||||
|
||||
// GetCRDGroupKind returns the GroupKind stored in the passed
|
||||
// Unstructured CustomResourceDefinition and true if the passed object
|
||||
// is a CRD.
|
||||
func GetCRDGroupKind(u *unstructured.Unstructured) (schema.GroupKind, bool) {
|
||||
emptyGroupKind := schema.GroupKind{Group: "", Kind: ""}
|
||||
if u == nil {
|
||||
return emptyGroupKind, false
|
||||
}
|
||||
group, found, err := unstructured.NestedString(u.Object, "spec", "group")
|
||||
if found && err == nil {
|
||||
kind, found, err := unstructured.NestedString(u.Object, "spec", "names", "kind")
|
||||
if found && err == nil {
|
||||
return schema.GroupKind{Group: group, Kind: kind}, true
|
||||
}
|
||||
}
|
||||
return emptyGroupKind, false
|
||||
}
|
||||
|
||||
// UnknownTypeError captures information about a type for which no information
|
||||
// could be found in the cluster or among the known CRDs.
|
||||
type UnknownTypeError struct {
|
||||
GroupVersionKind schema.GroupVersionKind
|
||||
}
|
||||
|
||||
func (e *UnknownTypeError) Error() string {
|
||||
return fmt.Sprintf("unknown resource type: %q", e.GroupVersionKind.String())
|
||||
}
|
||||
|
||||
// LookupResourceScope tries to look up the scope of the type of the provided
|
||||
// resource, looking at both the types known to the cluster (through the
|
||||
// RESTMapper) and the provided CRDs. If no information about the type can
|
||||
// be found, an UnknownTypeError wil be returned.
|
||||
func LookupResourceScope(u *unstructured.Unstructured, crds []*unstructured.Unstructured, mapper meta.RESTMapper) (meta.RESTScope, error) {
|
||||
gvk := u.GroupVersionKind()
|
||||
// First see if we can find the type (and the scope) in the cluster through
|
||||
// the RESTMapper.
|
||||
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||
if err == nil {
|
||||
// If we find the type in the cluster, we just look up the scope there.
|
||||
return mapping.Scope, nil
|
||||
}
|
||||
// Not finding a match is not an error here, so only error out for other
|
||||
// error types.
|
||||
if !meta.IsNoMatchError(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If we couldn't find the type in the cluster, check if we find a
|
||||
// match in any of the provided CRDs.
|
||||
for _, crd := range crds {
|
||||
group, found, err := NestedField(crd.Object, "spec", "group")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found || group == "" {
|
||||
return nil, NotFound([]interface{}{"spec", "group"}, group)
|
||||
}
|
||||
kind, found, err := NestedField(crd.Object, "spec", "names", "kind")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found || kind == "" {
|
||||
return nil, NotFound([]interface{}{"spec", "kind"}, group)
|
||||
}
|
||||
if gvk.Kind != kind || gvk.Group != group {
|
||||
continue
|
||||
}
|
||||
versionDefined, err := crdDefinesVersion(crd, gvk.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !versionDefined {
|
||||
return nil, &UnknownTypeError{
|
||||
GroupVersionKind: gvk,
|
||||
}
|
||||
}
|
||||
scopeName, _, err := NestedField(crd.Object, "spec", "scope")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch scopeName {
|
||||
case "Namespaced":
|
||||
return meta.RESTScopeNamespace, nil
|
||||
case "Cluster":
|
||||
return meta.RESTScopeRoot, nil
|
||||
default:
|
||||
return nil, Invalid([]interface{}{"spec", "scope"}, scopeName,
|
||||
"expected Namespaced or Cluster")
|
||||
}
|
||||
}
|
||||
return nil, &UnknownTypeError{
|
||||
GroupVersionKind: gvk,
|
||||
}
|
||||
}
|
||||
|
||||
func crdDefinesVersion(crd *unstructured.Unstructured, version string) (bool, error) {
|
||||
versions, found, err := NestedField(crd.Object, "spec", "versions")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !found {
|
||||
return false, NotFound([]interface{}{"spec", "versions"}, versions)
|
||||
}
|
||||
versionsSlice, ok := versions.([]interface{})
|
||||
if !ok {
|
||||
return false, InvalidType([]interface{}{"spec", "versions"}, versions, "[]interface{}")
|
||||
}
|
||||
if len(versionsSlice) == 0 {
|
||||
return false, Invalid([]interface{}{"spec", "versions"}, versionsSlice, "must not be empty")
|
||||
}
|
||||
for i := range versionsSlice {
|
||||
name, found, err := NestedField(crd.Object, "spec", "versions", i, "name")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !found {
|
||||
return false, NotFound([]interface{}{"spec", "versions", i, "name"}, name)
|
||||
}
|
||||
if name == version {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// StripKyamlAnnotations removes any path and index annotations from the
|
||||
// unstructured resource.
|
||||
func StripKyamlAnnotations(u *unstructured.Unstructured) {
|
||||
annos := u.GetAnnotations()
|
||||
delete(annos, kioutil.PathAnnotation)
|
||||
delete(annos, kioutil.LegacyPathAnnotation) //nolint:staticcheck
|
||||
delete(annos, kioutil.IndexAnnotation)
|
||||
delete(annos, kioutil.LegacyIndexAnnotation) //nolint:staticcheck
|
||||
u.SetAnnotations(annos)
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// UnstructuredSet is an ordered list of Unstructured that acts like an
|
||||
// unordered set for comparison purposes.
|
||||
type UnstructuredSet []*unstructured.Unstructured
|
||||
|
||||
// UnstructuredSetEquals returns true if the slice of objects in setA equals
|
||||
// the slice of objects in setB.
|
||||
func UnstructuredSetEquals(setA []*unstructured.Unstructured, setB []*unstructured.Unstructured) bool {
|
||||
return UnstructuredSet(setA).Equal(UnstructuredSet(setB))
|
||||
}
|
||||
|
||||
func (setA UnstructuredSet) Equal(setB UnstructuredSet) bool {
|
||||
mapA := make(map[string]string, len(setA))
|
||||
for _, a := range setA {
|
||||
jsonBytes, err := a.MarshalJSON()
|
||||
if err != nil {
|
||||
mapA[string(jsonBytes)] = err.Error()
|
||||
} else {
|
||||
mapA[string(jsonBytes)] = ""
|
||||
}
|
||||
}
|
||||
mapB := make(map[string]string, len(setB))
|
||||
for _, b := range setB {
|
||||
jsonBytes, err := b.MarshalJSON()
|
||||
if err != nil {
|
||||
mapB[string(jsonBytes)] = err.Error()
|
||||
} else {
|
||||
mapB[string(jsonBytes)] = ""
|
||||
}
|
||||
}
|
||||
if len(mapA) != len(mapB) {
|
||||
return false
|
||||
}
|
||||
for b, errB := range mapB {
|
||||
if errA, exists := mapA[b]; !exists {
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
if errA != errB {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user