working commit

This commit is contained in:
2026-03-13 19:02:42 +02:00
parent bebbf79c7a
commit 5c1da77f4c
1329 changed files with 314708 additions and 39 deletions
@@ -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
}
@@ -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
}
}
@@ -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
}
@@ -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
}
@@ -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
View File
@@ -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)
}
@@ -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
}
@@ -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
View File
@@ -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()
}
@@ -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]]
}
@@ -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"
}
@@ -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)
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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)
}
@@ -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)
}