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)
|
||||
}
|
||||
Reference in New Issue
Block a user