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
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+2
View File
@@ -0,0 +1,2 @@
Copyright {{.Year}} {{.Holder}}
SPDX-License-Identifier: Apache-2.0
+2
View File
@@ -0,0 +1,2 @@
// Copyright YEAR The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
@@ -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)
}
+627
View File
@@ -0,0 +1,627 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package status
import (
"fmt"
"math"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// GetConditionsFn defines the signature for functions to compute the
// status of a built-in resource.
type GetConditionsFn func(*unstructured.Unstructured) (*Result, error)
// legacyTypes defines the mapping from GroupKind to a function that can
// compute the status for the given resource.
var legacyTypes = map[string]GetConditionsFn{
"Service": serviceConditions,
"Pod": podConditions,
"Secret": alwaysReady,
"PersistentVolumeClaim": pvcConditions,
"apps/StatefulSet": stsConditions,
"apps/DaemonSet": daemonsetConditions,
"extensions/DaemonSet": daemonsetConditions,
"apps/Deployment": deploymentConditions,
"extensions/Deployment": deploymentConditions,
"apps/ReplicaSet": replicasetConditions,
"extensions/ReplicaSet": replicasetConditions,
"policy/PodDisruptionBudget": pdbConditions,
"batch/CronJob": alwaysReady,
"ConfigMap": alwaysReady,
"batch/Job": jobConditions,
"apiextensions.k8s.io/CustomResourceDefinition": crdConditions,
}
const (
tooFewReady = "LessReady"
tooFewAvailable = "LessAvailable"
tooFewUpdated = "LessUpdated"
tooFewReplicas = "LessReplicas"
extraPods = "ExtraPods"
onDeleteUpdateStrategy = "OnDelete"
// How long a pod can be unscheduled before it is reported as
// unschedulable.
ScheduleWindow = 15 * time.Second
)
// GetLegacyConditionsFn returns a function that can compute the status for the
// given resource, or nil if the resource type is not known.
func GetLegacyConditionsFn(u *unstructured.Unstructured) GetConditionsFn {
gvk := u.GroupVersionKind()
g := gvk.Group
k := gvk.Kind
key := g + "/" + k
if g == "" {
key = k
}
return legacyTypes[key]
}
// alwaysReady Used for resources that are always ready
func alwaysReady(u *unstructured.Unstructured) (*Result, error) {
return &Result{
Status: CurrentStatus,
Message: "Resource is always ready",
Conditions: []Condition{},
}, nil
}
// stsConditions return standardized Conditions for Statefulset
//
// StatefulSet does define the .status.conditions property, but the controller never
// actually sets any Conditions. Thus, status must be computed only based on the other
// properties under .status. We don't have any way to find out if a reconcile for a
// StatefulSet has failed.
func stsConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
// updateStrategy==ondelete is a user managed statefulset.
updateStrategy := GetStringField(obj, ".spec.updateStrategy.type", "")
if updateStrategy == onDeleteUpdateStrategy {
return &Result{
Status: CurrentStatus,
Message: "StatefulSet is using the ondelete update strategy",
Conditions: []Condition{},
}, nil
}
// Replicas
specReplicas := GetIntField(obj, ".spec.replicas", 1)
readyReplicas := GetIntField(obj, ".status.readyReplicas", 0)
currentReplicas := GetIntField(obj, ".status.currentReplicas", 0)
updatedReplicas := GetIntField(obj, ".status.updatedReplicas", 0)
statusReplicas := GetIntField(obj, ".status.replicas", 0)
partition := GetIntField(obj, ".spec.updateStrategy.rollingUpdate.partition", -1)
if specReplicas > statusReplicas {
message := fmt.Sprintf("Replicas: %d/%d", statusReplicas, specReplicas)
return newInProgressStatus(tooFewReplicas, message), nil
}
if specReplicas > readyReplicas {
message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas)
return newInProgressStatus(tooFewReady, message), nil
}
if statusReplicas > specReplicas {
message := fmt.Sprintf("Pending termination: %d", statusReplicas-specReplicas)
return newInProgressStatus(extraPods, message), nil
}
// https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#partitions
if partition != -1 {
if updatedReplicas < (specReplicas - partition) {
message := fmt.Sprintf("updated: %d/%d", updatedReplicas, specReplicas-partition)
return newInProgressStatus("PartitionRollout", message), nil
}
// Partition case All ok
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("Partition rollout complete. updated: %d", updatedReplicas),
Conditions: []Condition{},
}, nil
}
if specReplicas > currentReplicas {
message := fmt.Sprintf("current: %d/%d", currentReplicas, specReplicas)
return newInProgressStatus("LessCurrent", message), nil
}
// Revision
currentRevision := GetStringField(obj, ".status.currentRevision", "")
updatedRevision := GetStringField(obj, ".status.updateRevision", "")
if currentRevision != updatedRevision {
message := "Waiting for updated revision to match current"
return newInProgressStatus("RevisionMismatch", message), nil
}
// All ok
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("All replicas scheduled as expected. Replicas: %d", statusReplicas),
Conditions: []Condition{},
}, nil
}
// deploymentConditions return standardized Conditions for Deployment.
//
// For Deployments, we look at .status.conditions as well as the other properties
// under .status. Status will be Failed if the progress deadline has been exceeded.
func deploymentConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
progressing := false
// Check if progressDeadlineSeconds is set. If not, the controller will not set
// the `Progressing` condition, so it will always consider a deployment to be
// progressing. The use of math.MaxInt32 is due to special handling in the
// controller:
// https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/deployment/util/deployment_util.go#L886
progressDeadline := GetIntField(obj, ".spec.progressDeadlineSeconds", math.MaxInt32)
if progressDeadline == math.MaxInt32 {
progressing = true
}
available := false
objc, err := GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
for _, c := range objc.Status.Conditions {
switch c.Type {
case "Progressing": // appsv1.DeploymentProgressing:
// https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/deployment/progress.go#L52
if c.Reason == "ProgressDeadlineExceeded" {
return &Result{
Status: FailedStatus,
Message: "Progress deadline exceeded",
Conditions: []Condition{{ConditionStalled, corev1.ConditionTrue, c.Reason, c.Message}},
}, nil
}
if c.Status == corev1.ConditionTrue && c.Reason == "NewReplicaSetAvailable" {
progressing = true
}
case "Available": // appsv1.DeploymentAvailable:
if c.Status == corev1.ConditionTrue {
available = true
}
}
}
// replicas
specReplicas := GetIntField(obj, ".spec.replicas", 1) // Controller uses 1 as default if not specified.
statusReplicas := GetIntField(obj, ".status.replicas", 0)
updatedReplicas := GetIntField(obj, ".status.updatedReplicas", 0)
readyReplicas := GetIntField(obj, ".status.readyReplicas", 0)
availableReplicas := GetIntField(obj, ".status.availableReplicas", 0)
// TODO spec.replicas zero case ??
if specReplicas > statusReplicas {
message := fmt.Sprintf("Replicas: %d/%d", statusReplicas, specReplicas)
return newInProgressStatus(tooFewReplicas, message), nil
}
if specReplicas > updatedReplicas {
message := fmt.Sprintf("Updated: %d/%d", updatedReplicas, specReplicas)
return newInProgressStatus(tooFewUpdated, message), nil
}
if statusReplicas > specReplicas {
message := fmt.Sprintf("Pending termination: %d", statusReplicas-specReplicas)
return newInProgressStatus(extraPods, message), nil
}
if updatedReplicas > availableReplicas {
message := fmt.Sprintf("Available: %d/%d", availableReplicas, updatedReplicas)
return newInProgressStatus(tooFewAvailable, message), nil
}
if specReplicas > readyReplicas {
message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas)
return newInProgressStatus(tooFewReady, message), nil
}
// check conditions
if !progressing {
message := "ReplicaSet not Available"
return newInProgressStatus("ReplicaSetNotAvailable", message), nil
}
if !available {
message := "Deployment not Available"
return newInProgressStatus("DeploymentNotAvailable", message), nil
}
// All ok
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("Deployment is available. Replicas: %d", statusReplicas),
Conditions: []Condition{},
}, nil
}
// replicasetConditions return standardized Conditions for Replicaset
func replicasetConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
// Conditions
objc, err := GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
for _, c := range objc.Status.Conditions {
// https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/replicaset/replica_set_utils.go
if c.Type == "ReplicaFailure" && c.Status == corev1.ConditionTrue {
message := "Replica Failure condition. Check Pods"
return newInProgressStatus("ReplicaFailure", message), nil
}
}
// Replicas
specReplicas := GetIntField(obj, ".spec.replicas", 1) // Controller uses 1 as default if not specified.
statusReplicas := GetIntField(obj, ".status.replicas", 0)
readyReplicas := GetIntField(obj, ".status.readyReplicas", 0)
availableReplicas := GetIntField(obj, ".status.availableReplicas", 0)
fullyLabelledReplicas := GetIntField(obj, ".status.fullyLabeledReplicas", 0)
if specReplicas > fullyLabelledReplicas {
message := fmt.Sprintf("Labelled: %d/%d", fullyLabelledReplicas, specReplicas)
return newInProgressStatus("LessLabelled", message), nil
}
if specReplicas > availableReplicas {
message := fmt.Sprintf("Available: %d/%d", availableReplicas, specReplicas)
return newInProgressStatus(tooFewAvailable, message), nil
}
if specReplicas > readyReplicas {
message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas)
return newInProgressStatus(tooFewReady, message), nil
}
if statusReplicas > specReplicas {
message := fmt.Sprintf("Pending termination: %d", statusReplicas-specReplicas)
return newInProgressStatus(extraPods, message), nil
}
// All ok
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("ReplicaSet is available. Replicas: %d", statusReplicas),
Conditions: []Condition{},
}, nil
}
// daemonsetConditions return standardized Conditions for DaemonSet
func daemonsetConditions(u *unstructured.Unstructured) (*Result, error) {
// We check that the latest generation is equal to observed generation as
// part of checking generic properties but in that case, we are lenient and
// skip the check if those fields are unset. For daemonset, we know that if
// the daemonset controller has acted on a resource, these fields would not
// be unset. So, we ensure that here.
res, err := checkGenerationSet(u)
if err != nil || res != nil {
return res, err
}
obj := u.UnstructuredContent()
// replicas
desiredNumberScheduled := GetIntField(obj, ".status.desiredNumberScheduled", -1)
currentNumberScheduled := GetIntField(obj, ".status.currentNumberScheduled", 0)
updatedNumberScheduled := GetIntField(obj, ".status.updatedNumberScheduled", 0)
numberAvailable := GetIntField(obj, ".status.numberAvailable", 0)
numberReady := GetIntField(obj, ".status.numberReady", 0)
if desiredNumberScheduled == -1 {
message := "Missing .status.desiredNumberScheduled"
return newInProgressStatus("NoDesiredNumber", message), nil
}
if desiredNumberScheduled > currentNumberScheduled {
message := fmt.Sprintf("Current: %d/%d", currentNumberScheduled, desiredNumberScheduled)
return newInProgressStatus("LessCurrent", message), nil
}
if desiredNumberScheduled > updatedNumberScheduled {
message := fmt.Sprintf("Updated: %d/%d", updatedNumberScheduled, desiredNumberScheduled)
return newInProgressStatus(tooFewUpdated, message), nil
}
if desiredNumberScheduled > numberAvailable {
message := fmt.Sprintf("Available: %d/%d", numberAvailable, desiredNumberScheduled)
return newInProgressStatus(tooFewAvailable, message), nil
}
if desiredNumberScheduled > numberReady {
message := fmt.Sprintf("Ready: %d/%d", numberReady, desiredNumberScheduled)
return newInProgressStatus(tooFewReady, message), nil
}
// All ok
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("All replicas scheduled as expected. Replicas: %d", desiredNumberScheduled),
Conditions: []Condition{},
}, nil
}
// checkGenerationSet checks that the metadata.generation and
// status.observedGeneration fields are set.
func checkGenerationSet(u *unstructured.Unstructured) (*Result, error) {
_, found, err := unstructured.NestedInt64(u.Object, "metadata", "generation")
if err != nil {
return nil, fmt.Errorf("looking up metadata.generation from resource: %w", err)
}
if !found {
message := fmt.Sprintf("%s metadata.generation not found", u.GetKind())
return &Result{
Status: InProgressStatus,
Message: message,
Conditions: []Condition{newReconcilingCondition("NoGeneration", message)},
}, nil
}
_, found, err = unstructured.NestedInt64(u.Object, "status", "observedGeneration")
if err != nil {
return nil, fmt.Errorf("looking up status.observedGeneration from resource: %w", err)
}
if !found {
message := fmt.Sprintf("%s status.observedGeneration not found", u.GetKind())
return &Result{
Status: InProgressStatus,
Message: message,
Conditions: []Condition{newReconcilingCondition("NoObservedGeneration", message)},
}, nil
}
return nil, nil
}
// pvcConditions return standardized Conditions for PVC
func pvcConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
phase := GetStringField(obj, ".status.phase", "unknown")
if phase != "Bound" { // corev1.ClaimBound
message := fmt.Sprintf("PVC is not Bound. phase: %s", phase)
return newInProgressStatus("NotBound", message), nil
}
// All ok
return &Result{
Status: CurrentStatus,
Message: "PVC is Bound",
Conditions: []Condition{},
}, nil
}
// podConditions return standardized Conditions for Pod
func podConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
objc, err := GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
phase := GetStringField(obj, ".status.phase", "")
switch phase {
case "Succeeded":
return &Result{
Status: CurrentStatus,
Message: "Pod has completed successfully",
Conditions: []Condition{},
}, nil
case "Failed":
return &Result{
Status: CurrentStatus,
Message: "Pod has completed, but not successfully",
Conditions: []Condition{},
}, nil
case "Running":
if hasConditionWithStatus(objc.Status.Conditions, "Ready", corev1.ConditionTrue) {
return &Result{
Status: CurrentStatus,
Message: "Pod is Ready",
Conditions: []Condition{},
}, nil
}
containerNames, isCrashLooping, err := getCrashLoopingContainers(obj)
if err != nil {
return nil, err
}
if isCrashLooping {
return newFailedStatus("ContainerCrashLooping",
fmt.Sprintf("Containers in CrashLoop state: %s", strings.Join(containerNames, ","))), nil
}
return newInProgressStatus("PodRunningNotReady", "Pod is running but is not Ready"), nil
case "Pending":
c, found := getConditionWithStatus(objc.Status.Conditions, "PodScheduled", corev1.ConditionFalse)
if found && c.Reason == "Unschedulable" {
if time.Now().Add(-ScheduleWindow).Before(u.GetCreationTimestamp().Time) {
// We give the pod 15 seconds to be scheduled before we report it
// as unschedulable.
return newInProgressStatus("PodNotScheduled", "Pod has not been scheduled"), nil
}
return newFailedStatus("PodUnschedulable", "Pod could not be scheduled"), nil
}
return newInProgressStatus("PodPending", "Pod is in the Pending phase"), nil
default:
// If the controller hasn't observed the pod yet, there is no phase. We consider this as it
// still being in progress.
if phase == "" {
return newInProgressStatus("PodNotObserved", "Pod phase not available"), nil
}
return nil, fmt.Errorf("unknown phase %s", phase)
}
}
func getCrashLoopingContainers(obj map[string]interface{}) ([]string, bool, error) {
var containerNames []string
css, found, err := unstructured.NestedSlice(obj, "status", "containerStatuses")
if !found || err != nil {
return containerNames, found, err
}
for _, item := range css {
cs := item.(map[string]interface{})
n, found := cs["name"]
if !found {
continue
}
name := n.(string)
s, found := cs["state"]
if !found {
continue
}
state := s.(map[string]interface{})
ws, found := state["waiting"]
if !found {
continue
}
waitingState := ws.(map[string]interface{})
r, found := waitingState["reason"]
if !found {
continue
}
reason := r.(string)
if reason == "CrashLoopBackOff" {
containerNames = append(containerNames, name)
}
}
if len(containerNames) > 0 {
return containerNames, true, nil
}
return containerNames, false, nil
}
// pdbConditions computes the status for PodDisruptionBudgets. A PDB
// is currently considered Current if the disruption controller has
// observed the latest version of the PDB resource and has computed
// the AllowedDisruptions. PDBs do have ObservedGeneration in the
// Status object, so if this function gets called we know that
// the controller has observed the latest changes.
// The disruption controller does not set any conditions if
// computing the AllowedDisruptions fails (and there are many ways
// it can fail), but there is PR against OSS Kubernetes to address
// this: https://github.com/kubernetes/kubernetes/pull/86929
func pdbConditions(_ *unstructured.Unstructured) (*Result, error) {
// All ok
return &Result{
Status: CurrentStatus,
Message: "AllowedDisruptions has been computed.",
Conditions: []Condition{},
}, nil
}
// jobConditions return standardized Conditions for Job
//
// A job will have the InProgress status until it starts running. Then it will have the Current
// status while the job is running and after it has been completed successfully. It
// will have the Failed status if it the job has failed.
func jobConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
parallelism := GetIntField(obj, ".spec.parallelism", 1)
completions := GetIntField(obj, ".spec.completions", parallelism)
succeeded := GetIntField(obj, ".status.succeeded", 0)
active := GetIntField(obj, ".status.active", 0)
failed := GetIntField(obj, ".status.failed", 0)
starttime := GetStringField(obj, ".status.startTime", "")
// Conditions
// https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/utils.go#L24
objc, err := GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
for _, c := range objc.Status.Conditions {
switch c.Type {
case "Complete":
if c.Status == corev1.ConditionTrue {
message := fmt.Sprintf("Job Completed. succeeded: %d/%d", succeeded, completions)
return &Result{
Status: CurrentStatus,
Message: message,
Conditions: []Condition{},
}, nil
}
case "Failed":
if c.Status == corev1.ConditionTrue {
return newFailedStatus("JobFailed",
fmt.Sprintf("Job Failed. failed: %d/%d", failed, completions)), nil
}
}
}
// replicas
if starttime == "" {
message := "Job not started"
return newInProgressStatus("JobNotStarted", message), nil
}
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("Job in progress. success:%d, active: %d, failed: %d", succeeded, active, failed),
Conditions: []Condition{},
}, nil
}
// serviceConditions return standardized Conditions for Service
func serviceConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
specType := GetStringField(obj, ".spec.type", "ClusterIP")
specClusterIP := GetStringField(obj, ".spec.clusterIP", "")
if specType == "LoadBalancer" {
if specClusterIP == "" {
message := "ClusterIP not set. Service type: LoadBalancer"
return newInProgressStatus("NoIPAssigned", message), nil
}
}
return &Result{
Status: CurrentStatus,
Message: "Service is ready",
Conditions: []Condition{},
}, nil
}
func crdConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
objc, err := GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
for _, c := range objc.Status.Conditions {
if c.Type == "NamesAccepted" && c.Status == corev1.ConditionFalse {
return newFailedStatus(c.Reason, c.Message), nil
}
if c.Type == "Established" {
if c.Status == corev1.ConditionFalse && c.Reason != "Installing" {
return newFailedStatus(c.Reason, c.Message), nil
}
if c.Status == corev1.ConditionTrue {
return &Result{
Status: CurrentStatus,
Message: "CRD is established",
Conditions: []Condition{},
}, nil
}
}
}
return newInProgressStatus("Installing", "Install in progress"), nil
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package kstatus contains functionality for computing the status
// of Kubernetes resources.
//
// The statuses defined in this package are:
// - InProgress
// - Current
// - Failed
// - Terminating
// - NotFound
// - Unknown
//
// Computing the status of a resources can be done by calling the
// Compute function in the status package.
//
// import (
// "github.com/fluxcd/cli-utils/pkg/kstatus/status"
// )
//
// res, err := status.Compute(resource)
//
// The package also defines a set of new conditions:
// - InProgress
// - Failed
//
// These conditions have been chosen to follow the
// "abnormal-true" pattern where conditions should be set to true
// for error/abnormal conditions and the absence of a condition means
// things are normal.
//
// The Augment function augments any unstructured resource with
// the standard conditions described above. The values of
// these conditions are decided based on other status information
// available in the resources.
//
// import (
// "github.com/fluxcd/cli-utils/pkg/kstatus/status
// )
//
// err := status.Augment(resource)
package status
+99
View File
@@ -0,0 +1,99 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package status
import (
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// checkGenericProperties looks at the properties that are available on
// all or most of the Kubernetes resources. If a decision can be made based
// on this information, there is no need to look at the resource-specidic
// rules.
// This also checks for the presence of the conditions defined in this package.
// If any of these are set on the resource, a decision is made solely based
// on this and none of the resource specific rules will be used. The goal here
// is that if controllers, built-in or custom, use these conditions, we can easily
// find status of resources.
func checkGenericProperties(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
// Check if the resource is scheduled for deletion
deletionTimestamp, found, err := unstructured.NestedString(obj, "metadata", "deletionTimestamp")
if err != nil {
return nil, fmt.Errorf("looking up metadata.deletionTimestamp from resource: %w", err)
}
if found && deletionTimestamp != "" {
return &Result{
Status: TerminatingStatus,
Message: "Resource scheduled for deletion",
Conditions: []Condition{},
}, nil
}
res, err := checkGeneration(u)
if res != nil || err != nil {
return res, err
}
// Check if the resource has any of the standard conditions. If so, we just use them
// and no need to look at anything else.
objWithConditions, err := GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
for _, cond := range objWithConditions.Status.Conditions {
if cond.Type == string(ConditionReconciling) && cond.Status == corev1.ConditionTrue {
return newInProgressStatus(cond.Reason, cond.Message), nil
}
if cond.Type == string(ConditionStalled) && cond.Status == corev1.ConditionTrue {
return &Result{
Status: FailedStatus,
Message: cond.Message,
Conditions: []Condition{
{
Type: ConditionStalled,
Status: corev1.ConditionTrue,
Reason: cond.Reason,
Message: cond.Message,
},
},
}, nil
}
}
return nil, nil
}
func checkGeneration(u *unstructured.Unstructured) (*Result, error) {
// ensure that the meta generation is observed
generation, found, err := unstructured.NestedInt64(u.Object, "metadata", "generation")
if err != nil {
return nil, fmt.Errorf("looking up metadata.generation from resource: %w", err)
}
if !found {
return nil, nil
}
observedGeneration, found, err := unstructured.NestedInt64(u.Object, "status", "observedGeneration")
if err != nil {
return nil, fmt.Errorf("looking up status.observedGeneration from resource: %w", err)
}
if found {
// Resource does not have this field, so we can't do this check.
// TODO(mortent): Verify behavior of not set vs does not exist.
if observedGeneration != generation {
message := fmt.Sprintf("%s generation is %d, but latest observed generation is %d", u.GetKind(), generation, observedGeneration)
return &Result{
Status: InProgressStatus,
Message: message,
Conditions: []Condition{newReconcilingCondition("LatestGenerationNotObserved", message)},
}, nil
}
}
return nil, nil
}
+242
View File
@@ -0,0 +1,242 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package status
import (
"errors"
"fmt"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
const (
// The set of standard conditions defined in this package. These follow the "abnormality-true"
// convention where conditions should have a true value for abnormal/error situations and the absence
// of a condition should be interpreted as a false value, i.e. everything is normal.
ConditionStalled ConditionType = "Stalled"
ConditionReconciling ConditionType = "Reconciling"
// The set of status conditions which can be assigned to resources.
InProgressStatus Status = "InProgress"
FailedStatus Status = "Failed"
CurrentStatus Status = "Current"
TerminatingStatus Status = "Terminating"
NotFoundStatus Status = "NotFound"
UnknownStatus Status = "Unknown"
)
var (
Statuses = []Status{InProgressStatus, FailedStatus, CurrentStatus, TerminatingStatus, UnknownStatus}
)
// ConditionType defines the set of condition types allowed inside a Condition struct.
type ConditionType string
// String returns the ConditionType as a string.
func (c ConditionType) String() string {
return string(c)
}
// Status defines the set of statuses a resource can have.
type Status string
// String returns the status as a string.
func (s Status) String() string {
return string(s)
}
// StatusFromString turns a string into a Status. Will panic if the provided string is
// not a valid status.
func FromStringOrDie(text string) Status {
s := Status(text)
for _, r := range Statuses {
if s == r {
return s
}
}
panic(fmt.Errorf("string has invalid status: %s", s))
}
// Result contains the results of a call to compute the status of
// a resource.
type Result struct {
// Status
Status Status
// Message
Message string
// Conditions list of extracted conditions from Resource
Conditions []Condition
}
// Condition defines the general format for conditions on Kubernetes resources.
// In practice, each kubernetes resource defines their own format for conditions, but
// most (maybe all) follows this structure.
type Condition struct {
// Type condition type
Type ConditionType `json:"type,omitempty"`
// Status String that describes the condition status
Status corev1.ConditionStatus `json:"status,omitempty"`
// Reason one work CamelCase reason
Reason string `json:"reason,omitempty"`
// Message Human readable reason string
Message string `json:"message,omitempty"`
}
// Compute finds the status of a given unstructured resource. It does not
// fetch the state of the resource from a cluster, so the provided unstructured
// must have the complete state, including status.
//
// The returned result contains the status of the resource, which will be
// one of
// - InProgress
// - Current
// - Failed
// - Terminating
//
// It also contains a message that provides more information on why
// the resource has the given status. Finally, the result also contains
// a list of standard resources that would belong on the given resource.
func Compute(u *unstructured.Unstructured) (*Result, error) {
res, err := checkGenericProperties(u)
if err != nil {
return nil, err
}
// If res is not nil, it means the generic checks was able to determine
// the status of the resource. We don't need to check the type-specific
// rules.
if res != nil {
return res, nil
}
fn := GetLegacyConditionsFn(u)
if fn != nil {
return fn(u)
}
// If neither the generic properties of the resource-specific rules
// can determine status, we do one last check to see if the resource
// does expose a Ready condition. Ready conditions do not adhere
// to the Kubernetes design recommendations, but they are pretty widely
// used.
res, err = checkReadyCondition(u)
if res != nil || err != nil {
return res, err
}
// The resource is not one of the built-in types with specific
// rules and we were unable to make a decision based on the
// generic rules. In this case we assume that the absence of any known
// conditions means the resource is current.
return &Result{
Status: CurrentStatus,
Message: "Resource is current",
Conditions: []Condition{},
}, err
}
// checkReadyCondition checks if a resource has a Ready condition, and
// if so, it will use the value of this condition to determine the
// status.
// There are a few challenges with this:
// - If a resource doesn't set the Ready condition until it is True,
// the library have no way of telling whether the resource is using the
// Ready condition, so it will fall back to the strategy for unknown
// resources, which is to assume they are always reconciled.
// - If the library sees the resource before the controller has had
// a chance to update the conditions, it also will not realize the
// resource use the Ready condition.
// - There is no way to determine if a resource with the Ready condition
// set to False is making progress or is doomed.
func checkReadyCondition(u *unstructured.Unstructured) (*Result, error) {
objWithConditions, err := GetObjectWithConditions(u.Object)
if err != nil {
return nil, err
}
for _, cond := range objWithConditions.Status.Conditions {
if cond.Type != "Ready" {
continue
}
switch cond.Status {
case corev1.ConditionTrue:
return &Result{
Status: CurrentStatus,
Message: "Resource is Ready",
Conditions: []Condition{},
}, nil
case corev1.ConditionFalse:
return newInProgressStatus(cond.Reason, cond.Message), nil
case corev1.ConditionUnknown:
// For now we just treat an unknown condition value as
// InProgress. We should consider if there are better ways
// to handle it.
return newInProgressStatus(cond.Reason, cond.Message), nil
default:
// Do nothing in this case.
}
}
return nil, nil
}
// Augment takes a resource and augments the resource with the
// standard status conditions.
func Augment(u *unstructured.Unstructured) error {
res, err := Compute(u)
if err != nil {
return err
}
conditions, found, err := unstructured.NestedSlice(u.Object, "status", "conditions")
if err != nil {
return err
}
if !found {
conditions = make([]interface{}, 0)
}
currentTime := time.Now().UTC().Format(time.RFC3339)
for _, resCondition := range res.Conditions {
present := false
for _, c := range conditions {
condition, ok := c.(map[string]interface{})
if !ok {
return errors.New("condition does not have the expected structure")
}
conditionType, ok := condition["type"].(string)
if !ok {
return errors.New("condition type does not have the expected type")
}
if conditionType == string(resCondition.Type) {
conditionStatus, ok := condition["status"].(string)
if !ok {
return errors.New("condition status does not have the expected type")
}
if conditionStatus != string(resCondition.Status) {
condition["lastTransitionTime"] = currentTime
}
condition["status"] = string(resCondition.Status)
condition["lastUpdateTime"] = currentTime
condition["reason"] = resCondition.Reason
condition["message"] = resCondition.Message
present = true
}
}
if !present {
conditions = append(conditions, map[string]interface{}{
"lastTransitionTime": currentTime,
"lastUpdateTime": currentTime,
"message": resCondition.Message,
"reason": resCondition.Reason,
"status": string(resCondition.Status),
"type": string(resCondition.Type),
})
}
}
return unstructured.SetNestedSlice(u.Object, conditions, "status", "conditions")
}
+143
View File
@@ -0,0 +1,143 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package status
import (
"strings"
corev1 "k8s.io/api/core/v1"
apiunstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
// newReconcilingCondition creates an reconciling condition with the given
// reason and message.
func newReconcilingCondition(reason, message string) Condition {
return Condition{
Type: ConditionReconciling,
Status: corev1.ConditionTrue,
Reason: reason,
Message: message,
}
}
func newStalledCondition(reason, message string) Condition {
return Condition{
Type: ConditionStalled,
Status: corev1.ConditionTrue,
Reason: reason,
Message: message,
}
}
// newInProgressStatus creates a status Result with the InProgress status
// and an InProgress condition.
func newInProgressStatus(reason, message string) *Result {
return &Result{
Status: InProgressStatus,
Message: message,
Conditions: []Condition{newReconcilingCondition(reason, message)},
}
}
func newFailedStatus(reason, message string) *Result {
return &Result{
Status: FailedStatus,
Message: message,
Conditions: []Condition{newStalledCondition(reason, message)},
}
}
// ObjWithConditions Represent meta object with status.condition array
type ObjWithConditions struct {
// Status as expected to be present in most compliant kubernetes resources
Status ConditionStatus `json:"status" yaml:"status"`
}
// ConditionStatus represent status with condition array
type ConditionStatus struct {
// Array of Conditions as expected to be present in kubernetes resources
Conditions []BasicCondition `json:"conditions" yaml:"conditions"`
}
// BasicCondition fields that are expected in a condition
type BasicCondition struct {
// Type Condition type
Type string `json:"type" yaml:"type"`
// Status is one of True,False,Unknown
Status corev1.ConditionStatus `json:"status" yaml:"status"`
// Reason simple single word reason in CamleCase
// +optional
Reason string `json:"reason,omitempty" yaml:"reason"`
// Message human readable reason
// +optional
Message string `json:"message,omitempty" yaml:"message"`
}
// GetObjectWithConditions return typed object
func GetObjectWithConditions(in map[string]interface{}) (*ObjWithConditions, error) {
var out = new(ObjWithConditions)
err := runtime.DefaultUnstructuredConverter.FromUnstructured(in, out)
if err != nil {
return nil, err
}
return out, nil
}
func hasConditionWithStatus(conditions []BasicCondition, conditionType string, status corev1.ConditionStatus) bool {
_, found := getConditionWithStatus(conditions, conditionType, status)
return found
}
func getConditionWithStatus(conditions []BasicCondition, conditionType string, status corev1.ConditionStatus) (BasicCondition, bool) {
for _, c := range conditions {
if c.Type == conditionType && c.Status == status {
return c, true
}
}
return BasicCondition{}, false
}
// GetStringField return field as string defaulting to value if not found
func GetStringField(obj map[string]interface{}, fieldPath string, defaultValue string) string {
var rv = defaultValue
fields := strings.Split(fieldPath, ".")
if fields[0] == "" {
fields = fields[1:]
}
val, found, err := apiunstructured.NestedFieldNoCopy(obj, fields...)
if !found || err != nil {
return rv
}
if v, ok := val.(string); ok {
return v
}
return rv
}
// GetIntField return field as string defaulting to value if not found
func GetIntField(obj map[string]interface{}, fieldPath string, defaultValue int) int {
fields := strings.Split(fieldPath, ".")
if fields[0] == "" {
fields = fields[1:]
}
val, found, err := apiunstructured.NestedFieldNoCopy(obj, fields...)
if !found || err != nil {
return defaultValue
}
switch v := val.(type) {
case int:
return v
case int32:
return int(v)
case int64:
return int(v)
}
return defaultValue
}
@@ -0,0 +1,34 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package watcher
import (
"context"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
"github.com/fluxcd/cli-utils/pkg/object"
)
// BlindStatusWatcher sees nothing.
// BlindStatusWatcher sends no update or error events.
// BlindStatusWatcher waits patiently to be cancelled.
// BlindStatusWatcher implements the StatusWatcher interface.
type BlindStatusWatcher struct{}
var _ StatusWatcher = BlindStatusWatcher{}
// Watch nothing. See no changes.
func (w BlindStatusWatcher) Watch(ctx context.Context, _ object.ObjMetadataSet, _ Options) <-chan event.Event {
doneCh := ctx.Done()
eventCh := make(chan event.Event)
go func() {
// Send SyncEvent immediately.
eventCh <- event.Event{Type: event.SyncEvent}
// Block until the context is cancelled.
<-doneCh
// Signal to the caller there will be no more events.
close(eventCh)
}()
return eventCh
}
@@ -0,0 +1,176 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package watcher
import (
"context"
"fmt"
"time"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/clusterreader"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders"
"github.com/fluxcd/cli-utils/pkg/object"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/klog/v2"
)
// DefaultStatusWatcher reports on status updates to a set of objects.
//
// Use NewDefaultStatusWatcher to build a DefaultStatusWatcher with default settings.
type DefaultStatusWatcher struct {
// DynamicClient is used to watch of resource objects.
DynamicClient dynamic.Interface
// Mapper is used to map from GroupKind to GroupVersionKind.
Mapper meta.RESTMapper
// ResyncPeriod is how often the objects are retrieved to re-synchronize,
// in case any events were missed.
ResyncPeriod time.Duration
// StatusReader specifies a custom implementation of the
// engine.StatusReader interface that will be used to compute reconcile
// status for resource objects.
StatusReader engine.StatusReader
// ClusterReader is used to look up generated objects on-demand.
// Generated objects (ex: Deployment > ReplicaSet > Pod) are sometimes
// required for computing parent object status, to compensate for
// controllers that aren't following status conventions.
ClusterReader engine.ClusterReader
}
var _ StatusWatcher = &DefaultStatusWatcher{}
// NewDefaultStatusWatcher constructs a DynamicStatusWatcher with defaults
// chosen for general use. If you need different settings, consider building a
// DynamicStatusWatcher directly.
func NewDefaultStatusWatcher(dynamicClient dynamic.Interface, mapper meta.RESTMapper) *DefaultStatusWatcher {
return &DefaultStatusWatcher{
DynamicClient: dynamicClient,
Mapper: mapper,
ResyncPeriod: 1 * time.Hour,
StatusReader: statusreaders.NewDefaultStatusReader(mapper),
ClusterReader: &clusterreader.DynamicClusterReader{
DynamicClient: dynamicClient,
Mapper: mapper,
},
}
}
// Watch the cluster for changes made to the specified objects.
// Returns an event channel on which these updates (and errors) will be reported.
// Each update event includes the computed status of the object.
func (w *DefaultStatusWatcher) Watch(ctx context.Context, ids object.ObjMetadataSet, opts Options) <-chan event.Event {
strategy := opts.RESTScopeStrategy
if strategy == RESTScopeAutomatic {
strategy = autoSelectRESTScopeStrategy(ids)
}
var scope meta.RESTScope
var targets []GroupKindNamespace
switch strategy {
case RESTScopeRoot:
scope = meta.RESTScopeRoot
targets = rootScopeGKNs(ids)
klog.V(3).Infof("DynamicStatusWatcher starting in root-scoped mode (targets: %d)", len(targets))
case RESTScopeNamespace:
scope = meta.RESTScopeNamespace
targets = namespaceScopeGKNs(ids)
klog.V(3).Infof("DynamicStatusWatcher starting in namespace-scoped mode (targets: %d)", len(targets))
default:
return handleFatalError(fmt.Errorf("invalid RESTScopeStrategy: %v", strategy))
}
informer := &ObjectStatusReporter{
InformerFactory: NewDynamicInformerFactory(w.DynamicClient, w.ResyncPeriod),
Mapper: w.Mapper,
StatusReader: w.StatusReader,
ClusterReader: w.ClusterReader,
Targets: targets,
ObjectFilter: &AllowListObjectFilter{AllowList: ids},
RESTScope: scope,
}
return informer.Start(ctx)
}
func handleFatalError(err error) <-chan event.Event {
eventCh := make(chan event.Event)
go func() {
defer close(eventCh)
eventCh <- event.Event{
Type: event.ErrorEvent,
Error: err,
}
}()
return eventCh
}
func autoSelectRESTScopeStrategy(ids object.ObjMetadataSet) RESTScopeStrategy {
if len(uniqueNamespaces(ids)) > 1 {
return RESTScopeRoot
}
return RESTScopeNamespace
}
func rootScopeGKNs(ids object.ObjMetadataSet) []GroupKindNamespace {
gks := uniqueGKs(ids)
targets := make([]GroupKindNamespace, len(gks))
for i, gk := range gks {
targets[i] = GroupKindNamespace{
Group: gk.Group,
Kind: gk.Kind,
Namespace: "",
}
}
return targets
}
func namespaceScopeGKNs(ids object.ObjMetadataSet) []GroupKindNamespace {
return uniqueGKNs(ids)
}
// uniqueGKNs returns a set of unique GroupKindNamespaces from a set of object identifiers.
func uniqueGKNs(ids object.ObjMetadataSet) []GroupKindNamespace {
gknMap := make(map[GroupKindNamespace]struct{})
for _, id := range ids {
gkn := GroupKindNamespace{Group: id.GroupKind.Group, Kind: id.GroupKind.Kind, Namespace: id.Namespace}
gknMap[gkn] = struct{}{}
}
gknList := make([]GroupKindNamespace, 0, len(gknMap))
for gk := range gknMap {
gknList = append(gknList, gk)
}
return gknList
}
// uniqueGKs returns a set of unique GroupKinds from a set of object identifiers.
func uniqueGKs(ids object.ObjMetadataSet) []schema.GroupKind {
gkMap := make(map[schema.GroupKind]struct{})
for _, id := range ids {
gkn := schema.GroupKind{Group: id.GroupKind.Group, Kind: id.GroupKind.Kind}
gkMap[gkn] = struct{}{}
}
gkList := make([]schema.GroupKind, 0, len(gkMap))
for gk := range gkMap {
gkList = append(gkList, gk)
}
return gkList
}
func uniqueNamespaces(ids object.ObjMetadataSet) []string {
nsMap := make(map[string]struct{})
for _, id := range ids {
nsMap[id.Namespace] = struct{}{}
}
nsList := make([]string, 0, len(nsMap))
for ns := range nsMap {
nsList = append(nsList, ns)
}
return nsList
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package watcher is a library for computing the status of kubernetes resource
// objects based on watching object state from a cluster. It keeps watching
// until it is cancelled through the provided context. Updates on the status of
// objects are streamed back to the caller through a channel.
//
// # Watching Resources
//
// In order to watch a set of resources objects, create a StatusWatcher
// and pass in the list of object identifiers to the Watch function.
//
// import (
// "github.com/fluxcd/cli-utils/pkg/kstatus/watcher"
// )
//
// ids := []prune.ObjMetadata{
// {
// GroupKind: schema.GroupKind{
// Group: "apps",
// Kind: "Deployment",
// },
// Name: "dep",
// Namespace: "default",
// }
// }
//
// statusWatcher := watcher.NewDefaultStatusWatcher(dynamicClient, mapper)
// ctx, cancelFunc := context.WithCancel(context.Background())
// eventCh := statusWatcher.Watch(ctx, ids, watcher.Options{})
// for e := range eventCh {
// // Handle event
// if e.Type == event.ErrorEvent {
// cancelFunc()
// return e.Err
// }
// }
package watcher
@@ -0,0 +1,64 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package watcher
import (
"context"
"time"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/cache"
)
type DynamicInformerFactory struct {
Client dynamic.Interface
ResyncPeriod time.Duration
Indexers cache.Indexers
}
func NewDynamicInformerFactory(client dynamic.Interface, resyncPeriod time.Duration) *DynamicInformerFactory {
return &DynamicInformerFactory{
Client: client,
ResyncPeriod: resyncPeriod,
Indexers: cache.Indexers{
cache.NamespaceIndex: cache.MetaNamespaceIndexFunc,
},
}
}
func (f *DynamicInformerFactory) NewInformer(ctx context.Context, mapping *meta.RESTMapping, namespace string) cache.SharedIndexInformer {
// Unstructured example output need `"apiVersion"` and `"kind"` set.
example := &unstructured.Unstructured{}
example.SetGroupVersionKind(mapping.GroupVersionKind)
lw := &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return f.Client.Resource(mapping.Resource).
Namespace(namespace).
List(ctx, options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return f.Client.Resource(mapping.Resource).
Namespace(namespace).
Watch(ctx, options)
},
}
// Wrap the ListWatch with the client to allow the reflector to detect
// if the client supports WatchList semantics. This is important for
// fake clients used in tests, which do not support WatchList.
wrappedLW := cache.ToListWatcherWithWatchListSemantics(lw, f.Client)
return cache.NewSharedIndexInformer(
wrappedLW,
example,
f.ResyncPeriod,
f.Indexers,
)
}
+122
View File
@@ -0,0 +1,122 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package watcher
import (
"context"
"fmt"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
"k8s.io/klog/v2"
)
// eventFunnel wraps a list of event channels and multiplexes them down to a
// single event channel. New input channels can be added at runtime, and the
// output channel will remain open until all input channels are closed.
type eventFunnel struct {
// ctx closure triggers shutdown
ctx context.Context
// outCh is the funnel that consumes all events from input channels
outCh chan event.Event
// doneCh is closed after outCh is closed.
// This allows blocking until done without consuming events.
doneCh chan struct{}
// counterCh is used to track the number of open input channels.
counterCh chan int
}
func newEventFunnel(ctx context.Context) *eventFunnel {
funnel := &eventFunnel{
ctx: ctx,
outCh: make(chan event.Event),
doneCh: make(chan struct{}),
counterCh: make(chan int),
}
// Wait until the context is done and all input channels are closed.
// Then close out and done channels to signal completion.
go func() {
defer func() {
// Don't close counterCh, otherwise AddInputChannel may panic.
klog.V(5).Info("Closing funnel")
close(funnel.outCh)
close(funnel.doneCh)
}()
ctxDoneCh := ctx.Done()
// Count input channels that have been added and not closed.
inputs := 0
for {
select {
case delta := <-funnel.counterCh:
inputs += delta
klog.V(5).Infof("Funnel input channels (%+d): %d", delta, inputs)
case <-ctxDoneCh:
// Stop waiting for context closure.
// Nil channel avoids busy waiting.
ctxDoneCh = nil
}
if ctxDoneCh == nil && inputs <= 0 {
// Context is closed and all input channels are closed.
break
}
}
}()
return funnel
}
// Add a new input channel to the multiplexer.
func (m *eventFunnel) AddInputChannel(inCh <-chan event.Event) error {
select {
case <-m.ctx.Done(): // skip, if context is closed
return &EventFunnelClosedError{ContextError: m.ctx.Err()}
case m.counterCh <- 1: // increment counter
}
// Create a multiplexer for each new event channel.
go m.drain(inCh, m.outCh)
return nil
}
// OutputChannel channel receives all events sent to input channels.
// This channel is closed after all input channels are closed.
func (m *eventFunnel) OutputChannel() <-chan event.Event {
return m.outCh
}
// Done channel is closed after the Output channel is closed.
// This allows blocking until done without consuming events.
// If no input channels have been added yet, the done channel will be nil.
func (m *eventFunnel) Done() <-chan struct{} {
return m.doneCh
}
// drain a single input channel to a single output channel.
func (m *eventFunnel) drain(inCh <-chan event.Event, outCh chan<- event.Event) {
defer func() {
m.counterCh <- -1 // decrement counter
}()
for event := range inCh {
outCh <- event
}
}
type EventFunnelClosedError struct {
ContextError error
}
func (e *EventFunnelClosedError) Error() string {
return fmt.Sprintf("event funnel closed: %v", e.ContextError)
}
func (e *EventFunnelClosedError) Is(err error) bool {
fcErr, ok := err.(*EventFunnelClosedError)
if !ok {
return false
}
return e.ContextError == fcErr.ContextError
}
func (e *EventFunnelClosedError) Unwrap() error {
return e.ContextError
}
@@ -0,0 +1,30 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package watcher
import (
"github.com/fluxcd/cli-utils/pkg/object"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// ObjectFilter allows for filtering objects.
type ObjectFilter interface {
// Filter returns true if the object should be skipped.
Filter(obj *unstructured.Unstructured) bool
}
// AllowListObjectFilter filters objects not in the allow list.
// AllowListObjectFilter implements ObjectFilter.
type AllowListObjectFilter struct {
AllowList object.ObjMetadataSet
}
var _ ObjectFilter = &AllowListObjectFilter{}
// Filter returns true if the object should be skipped, because it is NOT in the
// AllowList.
func (f *AllowListObjectFilter) Filter(obj *unstructured.Unstructured) bool {
id := object.UnstructuredToObjMetadata(obj)
return !f.AllowList.Contains(id)
}
@@ -0,0 +1,895 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package watcher
import (
"context"
"errors"
"fmt"
"io"
"sync"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
"k8s.io/utils/clock"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
"github.com/fluxcd/cli-utils/pkg/object"
)
// GroupKindNamespace identifies an informer target.
// When used as an informer target, the namespace is optional.
// When the namespace is empty for namespaced resources, all namespaces are watched.
type GroupKindNamespace struct {
Group string
Kind string
Namespace string
}
// String returns a serialized form suitable for logging.
func (gkn GroupKindNamespace) String() string {
return fmt.Sprintf("%s/%s/namespaces/%s",
gkn.Group, gkn.Kind, gkn.Namespace)
}
func (gkn GroupKindNamespace) GroupKind() schema.GroupKind {
return schema.GroupKind{Group: gkn.Group, Kind: gkn.Kind}
}
// ObjectStatusReporter reports on updates to objects (instances) using a
// network of informers to watch one or more resources (types).
//
// Unlike SharedIndexInformer, ObjectStatusReporter...
// - Reports object status.
// - Can watch multiple resource types simultaneously.
// - Specific objects can be ignored for efficiency by specifying an ObjectFilter.
// - Resolves GroupKinds into Resources at runtime, to pick up newly added
// resources.
// - Starts and Stops individual watches automaically to reduce errors when a
// CRD or Namespace is deleted.
// - Resources can be watched in root-scope mode or namespace-scope mode,
// allowing the caller to optimize for efficiency or least-privilege.
// - Gives unschedulable Pods (and objects that generate them) a 15s grace
// period before reporting them as Failed.
// - Resets the RESTMapper cache automatically when CRDs are modified.
//
// ObjectStatusReporter is NOT repeatable. It will panic if started more than
// once. If you need a repeatable factory, use DefaultStatusWatcher.
//
// TODO: support detection of added/removed api extensions at runtime
// TODO: Watch CRDs & Namespaces, even if not in the set of IDs.
// TODO: Retry with backoff if in namespace-scoped mode, to allow CRDs & namespaces to be created asynchronously
type ObjectStatusReporter struct {
// InformerFactory is used to build informers
InformerFactory *DynamicInformerFactory
// Mapper is used to map from GroupKind to GroupVersionKind.
Mapper meta.RESTMapper
// StatusReader specifies a custom implementation of the
// engine.StatusReader interface that will be used to compute reconcile
// status for resource objects.
StatusReader engine.StatusReader
// ClusterReader is used to look up generated objects on-demand.
// Generated objects (ex: Deployment > ReplicaSet > Pod) are sometimes
// required for computing parent object status, to compensate for
// controllers that aren't following status conventions.
ClusterReader engine.ClusterReader
// GroupKinds is the list of GroupKinds to watch.
Targets []GroupKindNamespace
// ObjectFilter is used to decide which objects to ingore.
ObjectFilter ObjectFilter
// RESTScope specifies whether to ListAndWatch resources at the namespace
// or cluster (root) level. Using root scope is more efficient, but
// namespace scope may require fewer permissions.
RESTScope meta.RESTScope
// lock guards modification of the subsequent stateful fields
lock sync.Mutex
// gk2gkn maps GKs to GKNs to make it easy/cheap to look up.
gk2gkn map[schema.GroupKind]map[GroupKindNamespace]struct{}
// ns2gkn maps Namespaces to GKNs to make it easy/cheap to look up.
ns2gkn map[string]map[GroupKindNamespace]struct{}
// informerRefs tracks which informers have been started and stopped
informerRefs map[GroupKindNamespace]*informerReference
// context will be cancelled when the reporter should stop.
context context.Context
// cancel function that stops the context.
// This should only be called after the terminal error event has been sent.
cancel context.CancelFunc
// funnel multiplexes multiple input channels into one output channel,
// allowing input channels to be added and removed at runtime.
funnel *eventFunnel
// taskManager makes it possible to cancel scheduled tasks.
taskManager *taskManager
started bool
stopped bool
}
func (w *ObjectStatusReporter) Start(ctx context.Context) <-chan event.Event {
w.lock.Lock()
defer w.lock.Unlock()
if w.started {
panic("ObjectStatusInformer cannot be restarted")
}
w.taskManager = &taskManager{}
// Map GroupKinds to sets of GroupKindNamespaces for fast lookups.
// This is the only time we modify the map.
// So it should be safe to read from multiple threads after this.
w.gk2gkn = make(map[schema.GroupKind]map[GroupKindNamespace]struct{})
for _, gkn := range w.Targets {
gk := gkn.GroupKind()
m, found := w.gk2gkn[gk]
if !found {
m = make(map[GroupKindNamespace]struct{})
w.gk2gkn[gk] = m
}
m[gkn] = struct{}{}
}
// Map namespaces to sets of GroupKindNamespaces for fast lookups.
// This is the only time we modify the map.
// So it should be safe to read from multiple threads after this.
w.ns2gkn = make(map[string]map[GroupKindNamespace]struct{})
for _, gkn := range w.Targets {
ns := gkn.Namespace
m, found := w.ns2gkn[ns]
if !found {
m = make(map[GroupKindNamespace]struct{})
w.ns2gkn[ns] = m
}
m[gkn] = struct{}{}
}
// Initialize the informer map with references to track their start/stop.
// This is the only time we modify the map.
// So it should be safe to read from multiple threads after this.
w.informerRefs = make(map[GroupKindNamespace]*informerReference, len(w.Targets))
for _, gkn := range w.Targets {
w.informerRefs[gkn] = &informerReference{}
}
ctx, cancel := context.WithCancel(ctx)
w.context = ctx
w.cancel = cancel
// Use an event funnel to multiplex events through multiple input channels
// into out output channel. We can't use the normal fan-in pattern, because
// we need to be able to add and remove new input channels at runtime, as
// new informers are created and destroyed.
w.funnel = newEventFunnel(ctx)
// Send start requests.
for _, gkn := range w.Targets {
w.startInformer(gkn)
}
w.started = true
// Block until the event funnel is closed.
// The event funnel will close after all the informer channels are closed.
// The informer channels will close after the informers have stopped.
// The informers will stop after their context is cancelled.
go func() {
<-w.funnel.Done()
w.lock.Lock()
defer w.lock.Unlock()
w.stopped = true
}()
// Wait until all informers are synced or stopped, then send a SyncEvent.
syncEventCh := make(chan event.Event)
err := w.funnel.AddInputChannel(syncEventCh)
if err != nil {
// Reporter already stopped.
return handleFatalError(fmt.Errorf("reporter failed to start: %v", err))
}
go func() {
defer close(syncEventCh)
// TODO: should we use something less aggressive, like wait.BackoffUntil?
if cache.WaitForCacheSync(ctx.Done(), w.HasSynced) {
syncEventCh <- event.Event{
Type: event.SyncEvent,
}
}
}()
return w.funnel.OutputChannel()
}
// Stop triggers the cancellation of the reporter context, and closure of the
// event channel without sending an error event.
func (w *ObjectStatusReporter) Stop() {
klog.V(4).Info("Stopping reporter")
w.cancel()
}
// HasSynced returns true if all the started informers have been synced.
//
// Use the following to block waiting for synchronization:
// synced := cache.WaitForCacheSync(stopCh, informer.HasSynced)
func (w *ObjectStatusReporter) HasSynced() bool {
w.lock.Lock()
defer w.lock.Unlock()
if w.stopped || !w.started {
return false
}
pending := make([]GroupKindNamespace, 0, len(w.informerRefs))
for gke, informer := range w.informerRefs {
if informer.HasStarted() && !informer.HasSynced() {
pending = append(pending, gke)
}
}
if len(pending) > 0 {
klog.V(5).Infof("Informers pending synchronization: %v", pending)
return false
}
return true
}
// startInformer adds the specified GroupKindNamespace to the start channel to
// be started asynchronously.
func (w *ObjectStatusReporter) startInformer(gkn GroupKindNamespace) {
ctx, ok := w.informerRefs[gkn].Start(w.context)
if !ok {
klog.V(5).Infof("Watch start skipped (already started): %v", gkn)
// already started
return
}
go w.startInformerWithRetry(ctx, gkn)
}
// stopInformer stops the informer watching the specified GroupKindNamespace.
func (w *ObjectStatusReporter) stopInformer(gkn GroupKindNamespace) {
w.informerRefs[gkn].Stop()
}
func (w *ObjectStatusReporter) startInformerWithRetry(ctx context.Context, gkn GroupKindNamespace) {
realClock := &clock.RealClock{}
// TODO nolint can be removed once https://github.com/kubernetes/kubernetes/issues/118638 is resolved
backoffManager := wait.NewExponentialBackoffManager(800*time.Millisecond, 30*time.Second, 2*time.Minute, 2.0, 1.0, realClock) //nolint:staticcheck
retryCtx, retryCancel := context.WithCancel(ctx)
wait.BackoffUntil(func() {
err := w.startInformerNow(
ctx,
gkn,
)
if err != nil {
if meta.IsNoMatchError(err) {
// CRD (or api extension) not installed
// TODO: retry if CRDs are not being watched
klog.V(3).Infof("Watch start error (blocking until CRD is added): %v: %v", gkn, err)
// Cancel the parent context, which will stop the retries too.
w.stopInformer(gkn)
return
}
// Create a temporary input channel to send the error event.
eventCh := make(chan event.Event)
defer close(eventCh)
err := w.funnel.AddInputChannel(eventCh)
if err != nil {
// Reporter already stopped.
// This is fine. 🔥
klog.V(5).Infof("Informer failed to start: %v", err)
return
}
// Send error event and stop the reporter!
w.handleFatalError(eventCh, err)
return
}
// Success! - Stop retrying
retryCancel()
}, backoffManager, true, retryCtx.Done())
}
// startInformerNow starts an informer to watch for changes to a
// GroupKindNamespace. Changes are filtered and passed by event channel into the
// funnel. Each update event includes the computed status of the object.
// An error is returned if the informer could not be created.
func (w *ObjectStatusReporter) startInformerNow(
ctx context.Context,
gkn GroupKindNamespace,
) error {
// Look up the mapping for this GroupKind.
// If it doesn't exist, either delay watching or emit an error.
gk := gkn.GroupKind()
mapping, err := w.Mapper.RESTMapping(gk)
if err != nil {
// Might be a NoResourceMatchError/NoKindMatchError
return err
}
informer := w.InformerFactory.NewInformer(ctx, mapping, gkn.Namespace)
w.informerRefs[gkn].SetInformer(informer)
eventCh := make(chan event.Event)
// Add this event channel to the output multiplexer
err = w.funnel.AddInputChannel(eventCh)
if err != nil {
// Reporter already stopped.
return fmt.Errorf("informer failed to build event handler: %w", err)
}
// Handler called when ListAndWatch errors.
// Custom handler stops the informer if the resource is NotFound (CRD deleted).
err = informer.SetWatchErrorHandler(func(r *cache.Reflector, err error) {
w.watchErrorHandler(gkn, eventCh, err)
})
if err != nil {
// Should never happen.
// Informer can't have started yet. We just created it.
return fmt.Errorf("failed to set error handler on new informer for %v: %v", mapping.Resource, err)
}
_, err = informer.AddEventHandler(w.eventHandler(ctx, eventCh))
if err != nil {
// Should never happen.
return fmt.Errorf("failed add event handler on new informer for %v: %v", mapping.Resource, err)
}
// Start the informer in the background.
// Informer will be stopped when the context is cancelled.
go func() {
klog.V(3).Infof("Watch starting: %v", gkn)
informer.Run(ctx.Done())
klog.V(3).Infof("Watch stopped: %v", gkn)
// Signal to the caller there will be no more events for this GroupKind.
close(eventCh)
}()
return nil
}
func (w *ObjectStatusReporter) forEachTargetWithGroupKind(gk schema.GroupKind, fn func(GroupKindNamespace)) {
for gkn := range w.gk2gkn[gk] {
fn(gkn)
}
}
func (w *ObjectStatusReporter) forEachTargetWithNamespace(ns string, fn func(GroupKindNamespace)) {
for gkn := range w.ns2gkn[ns] {
fn(gkn)
}
}
// readStatusFromObject is a convenience function to read object status with a
// StatusReader using a ClusterReader to retrieve generated objects.
func (w *ObjectStatusReporter) readStatusFromObject(
ctx context.Context,
obj *unstructured.Unstructured,
) (*event.ResourceStatus, error) {
return w.StatusReader.ReadStatusForObject(ctx, w.ClusterReader, obj)
}
// readStatusFromCluster is a convenience function to read object status with a
// StatusReader using a ClusterReader to retrieve the object and its generated
// objects.
func (w *ObjectStatusReporter) readStatusFromCluster(
ctx context.Context,
id object.ObjMetadata,
) (*event.ResourceStatus, error) {
return w.StatusReader.ReadStatus(ctx, w.ClusterReader, id)
}
// deletedStatus builds a ResourceStatus for a deleted object.
//
// StatusReader.ReadStatusForObject doesn't handle nil objects as input. So
// this builds the status manually.
// TODO: find a way to delegate this back to the status package.
func deletedStatus(id object.ObjMetadata) *event.ResourceStatus {
// Status is always NotFound after deltion.
// Passed obj represents the last known state, not the current state.
result := &event.ResourceStatus{
Identifier: id,
Status: status.NotFoundStatus,
Message: "Resource not found",
}
return &event.ResourceStatus{
Identifier: id,
Resource: nil, // deleted object has no
Status: result.Status,
Message: result.Message,
// If deleted with foreground deletion, a finalizer will have blocked
// deletion until all the generated resources are deleted.
// TODO: Handle lookup of generated resources when not using foreground deletion.
GeneratedResources: nil,
}
}
// eventHandler builds an event handler to compute object status.
// Returns an event channel on which these stats updates will be reported.
func (w *ObjectStatusReporter) eventHandler(
ctx context.Context,
eventCh chan<- event.Event,
) cache.ResourceEventHandler {
var handler cache.ResourceEventHandlerFuncs
handler.AddFunc = func(iobj interface{}) {
// Bail early if the context is cancelled, to avoid unnecessary work.
if ctx.Err() != nil {
return
}
obj, ok := iobj.(*unstructured.Unstructured)
if !ok {
panic(fmt.Sprintf("AddFunc received unexpected object type %T", iobj))
}
id := object.UnstructuredToObjMetadata(obj)
if w.ObjectFilter.Filter(obj) {
klog.V(7).Infof("Watch Event Skipped: AddFunc: %s", id)
return
}
klog.V(5).Infof("AddFunc: Computing status for object: %s", id)
// cancel any scheduled status update for this object
w.taskManager.Cancel(id)
rs, err := w.readStatusFromObject(ctx, obj)
if err != nil {
// Send error event and stop the reporter!
w.handleFatalError(eventCh, fmt.Errorf("failed to compute object status: %s: %w", id, err))
return
}
if object.IsNamespace(obj) {
klog.V(5).Infof("AddFunc: Namespace added: %v", id)
w.onNamespaceAdd(obj)
} else if object.IsCRD(obj) {
klog.V(5).Infof("AddFunc: CRD added: %v", id)
w.onCRDAdd(obj)
}
if isObjectUnschedulable(rs) {
klog.V(5).Infof("AddFunc: object unschedulable: %v", id)
// schedule delayed status update
w.taskManager.Schedule(ctx, id, status.ScheduleWindow,
w.newStatusCheckTaskFunc(ctx, eventCh, id))
}
klog.V(7).Infof("AddFunc: sending update event: %v", rs)
eventCh <- event.Event{
Type: event.ResourceUpdateEvent,
Resource: rs,
}
}
handler.UpdateFunc = func(_, iobj interface{}) {
// Bail early if the context is cancelled, to avoid unnecessary work.
if ctx.Err() != nil {
return
}
obj, ok := iobj.(*unstructured.Unstructured)
if !ok {
panic(fmt.Sprintf("UpdateFunc received unexpected object type %T", iobj))
}
id := object.UnstructuredToObjMetadata(obj)
if w.ObjectFilter.Filter(obj) {
klog.V(7).Infof("UpdateFunc: Watch Event Skipped: %s", id)
return
}
klog.V(5).Infof("UpdateFunc: Computing status for object: %s", id)
// cancel any scheduled status update for this object
w.taskManager.Cancel(id)
rs, err := w.readStatusFromObject(ctx, obj)
if err != nil {
// Send error event and stop the reporter!
w.handleFatalError(eventCh, fmt.Errorf("failed to compute object status: %s: %w", id, err))
return
}
if object.IsNamespace(obj) {
klog.V(5).Infof("UpdateFunc: Namespace updated: %v", id)
w.onNamespaceUpdate(obj)
} else if object.IsCRD(obj) {
klog.V(5).Infof("UpdateFunc: CRD updated: %v", id)
w.onCRDUpdate(obj)
}
if isObjectUnschedulable(rs) {
klog.V(5).Infof("UpdateFunc: object unschedulable: %v", id)
// schedule delayed status update
w.taskManager.Schedule(ctx, id, status.ScheduleWindow,
w.newStatusCheckTaskFunc(ctx, eventCh, id))
}
klog.V(7).Infof("UpdateFunc: sending update event: %v", rs)
eventCh <- event.Event{
Type: event.ResourceUpdateEvent,
Resource: rs,
}
}
handler.DeleteFunc = func(iobj interface{}) {
// Bail early if the context is cancelled, to avoid unnecessary work.
if ctx.Err() != nil {
return
}
if tombstone, ok := iobj.(cache.DeletedFinalStateUnknown); ok {
// Last state unknown. Possibly stale.
// TODO: Should we propegate this uncertainty to the caller?
iobj = tombstone.Obj
}
obj, ok := iobj.(*unstructured.Unstructured)
if !ok {
panic(fmt.Sprintf("DeleteFunc received unexpected object type %T", iobj))
}
id := object.UnstructuredToObjMetadata(obj)
if w.ObjectFilter.Filter(obj) {
klog.V(7).Infof("DeleteFunc: Watch Event Skipped: %s", id)
return
}
klog.V(5).Infof("DeleteFunc: Computing status for object: %s", id)
// cancel any scheduled status update for this object
w.taskManager.Cancel(id)
if object.IsNamespace(obj) {
klog.V(5).Infof("DeleteFunc: Namespace deleted: %v", id)
w.onNamespaceDelete(obj)
} else if object.IsCRD(obj) {
klog.V(5).Infof("DeleteFunc: CRD deleted: %v", id)
w.onCRDDelete(obj)
}
rs := deletedStatus(id)
klog.V(7).Infof("DeleteFunc: sending update event: %v", rs)
eventCh <- event.Event{
Type: event.ResourceUpdateEvent,
Resource: rs,
}
}
return handler
}
// onCRDAdd handles creating a new informer to watch the new resource type.
func (w *ObjectStatusReporter) onCRDAdd(obj *unstructured.Unstructured) {
gk, found := object.GetCRDGroupKind(obj)
if !found {
id := object.UnstructuredToObjMetadata(obj)
klog.Warningf("Invalid CRD added: missing group and/or kind: %v", id)
// Don't return an error, because this should not inturrupt the task queue.
// TODO: Allow non-fatal errors to be reported using a specific error type.
return
}
klog.V(3).Infof("CRD added for %s", gk)
klog.V(3).Info("Resetting RESTMapper")
// Reset mapper to invalidate cache.
meta.MaybeResetRESTMapper(w.Mapper)
w.forEachTargetWithGroupKind(gk, func(gkn GroupKindNamespace) {
w.startInformer(gkn)
})
}
// onCRDUpdate handles creating a new informer to watch the updated resource type.
func (w *ObjectStatusReporter) onCRDUpdate(newObj *unstructured.Unstructured) {
gk, found := object.GetCRDGroupKind(newObj)
if !found {
id := object.UnstructuredToObjMetadata(newObj)
klog.Warningf("Invalid CRD updated: missing group and/or kind: %v", id)
// Don't return an error, because this should not inturrupt the task queue.
// TODO: Allow non-fatal errors to be reported using a specific error type.
return
}
klog.V(3).Infof("CRD updated for %s", gk)
klog.V(3).Info("Resetting RESTMapper")
// Reset mapper to invalidate cache.
meta.MaybeResetRESTMapper(w.Mapper)
w.forEachTargetWithGroupKind(gk, func(gkn GroupKindNamespace) {
w.startInformer(gkn)
})
}
// onCRDDelete handles stopping the informer watching the deleted resource type.
func (w *ObjectStatusReporter) onCRDDelete(oldObj *unstructured.Unstructured) {
gk, found := object.GetCRDGroupKind(oldObj)
if !found {
id := object.UnstructuredToObjMetadata(oldObj)
klog.Warningf("Invalid CRD deleted: missing group and/or kind: %v", id)
// Don't return an error, because this should not inturrupt the task queue.
// TODO: Allow non-fatal errors to be reported using a specific error type.
return
}
klog.V(3).Infof("CRD deleted for %s", gk)
w.forEachTargetWithGroupKind(gk, func(gkn GroupKindNamespace) {
w.stopInformer(gkn)
})
klog.V(3).Info("Resetting RESTMapper")
// Reset mapper to invalidate cache.
meta.MaybeResetRESTMapper(w.Mapper)
}
// onNamespaceAdd handles creating new informers to watch this namespace.
func (w *ObjectStatusReporter) onNamespaceAdd(obj *unstructured.Unstructured) {
if w.RESTScope == meta.RESTScopeRoot {
// When watching resources across all namespaces,
// we don't need to start or stop any
// namespace-specific informers.
return
}
namespace := obj.GetName()
w.forEachTargetWithNamespace(namespace, func(gkn GroupKindNamespace) {
w.startInformer(gkn)
})
}
// onNamespaceUpdate handles creating new informers to watch this namespace.
func (w *ObjectStatusReporter) onNamespaceUpdate(obj *unstructured.Unstructured) {
if w.RESTScope == meta.RESTScopeRoot {
// When watching resources across all namespaces,
// we don't need to start or stop any
// namespace-specific informers.
return
}
namespace := obj.GetName()
w.forEachTargetWithNamespace(namespace, func(gkn GroupKindNamespace) {
w.startInformer(gkn)
})
}
// onNamespaceDelete handles stopping informers watching this namespace.
func (w *ObjectStatusReporter) onNamespaceDelete(obj *unstructured.Unstructured) {
if w.RESTScope == meta.RESTScopeRoot {
// When watching resources across all namespaces,
// we don't need to start or stop any
// namespace-specific informers.
return
}
namespace := obj.GetName()
w.forEachTargetWithNamespace(namespace, func(gkn GroupKindNamespace) {
w.stopInformer(gkn)
})
}
// newStatusCheckTaskFunc returns a taskFund that reads the status of an object
// from the cluster and sends it over the event channel.
//
// This method should only be used for generated resource objects, as it's much
// slower at scale than watching the resource for updates.
func (w *ObjectStatusReporter) newStatusCheckTaskFunc(
ctx context.Context,
eventCh chan<- event.Event,
id object.ObjMetadata,
) taskFunc {
return func() {
klog.V(5).Infof("Re-reading object status: %s", id)
// check again
rs, err := w.readStatusFromCluster(ctx, id)
if err != nil {
// Send error event and stop the reporter!
// TODO: retry N times before terminating
w.handleFatalError(eventCh, err)
return
}
eventCh <- event.Event{
Type: event.ResourceUpdateEvent,
Resource: rs,
}
}
}
func (w *ObjectStatusReporter) handleFatalError(eventCh chan<- event.Event, err error) {
klog.V(5).Infof("Reporter error: %v", err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
eventCh <- event.Event{
Type: event.ErrorEvent,
Error: err,
}
w.Stop()
}
// watchErrorHandler logs errors and cancels the informer for this GroupKind
// if the NotFound error is received, which usually means the CRD was deleted.
// Based on DefaultWatchErrorHandler from k8s.io/client-go@v0.23.2/tools/cache/reflector.go
func (w *ObjectStatusReporter) watchErrorHandler(gkn GroupKindNamespace, eventCh chan<- event.Event, err error) {
switch {
// Stop channel closed
case err == io.EOF:
klog.V(5).Infof("ListAndWatch error (termination expected): %v: %v", gkn, err)
// Watch connection closed
case err == io.ErrUnexpectedEOF:
klog.V(1).Infof("ListAndWatch error (retry expected): %v: %v", gkn, err)
// Context done
case errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded):
klog.V(5).Infof("ListAndWatch error (termination expected): %v: %v", gkn, err)
// resourceVersion too old
case apierrors.IsResourceExpired(err):
// Keep retrying
klog.V(5).Infof("ListAndWatch error (retry expected): %v: %v", gkn, err)
// Resource unregistered (DEPRECATED, see NotFound)
case apierrors.IsGone(err):
klog.V(5).Infof("ListAndWatch error (retry expected): %v: %v", gkn, err)
// Resource not registered
case apierrors.IsNotFound(err):
klog.V(3).Infof("ListAndWatch error (termination expected): %v: stopping all informers for this GroupKind: %v", gkn, err)
w.forEachTargetWithGroupKind(gkn.GroupKind(), func(gkn GroupKindNamespace) {
w.stopInformer(gkn)
})
// Insufficient permissions
case apierrors.IsForbidden(err):
klog.V(3).Infof("ListAndWatch error (termination expected): %v: stopping all informers: %v", gkn, err)
w.handleFatalError(eventCh, err)
// Unexpected error
default:
klog.Warningf("ListAndWatch error (retry expected): %v: %v", gkn, err)
}
}
// informerReference tracks informer lifecycle.
type informerReference struct {
// lock guards the subsequent stateful fields
lock sync.Mutex
informer cache.SharedIndexInformer
context context.Context
cancel context.CancelFunc
started bool
}
// Start returns a wrapped context that can be cancelled.
// Returns nil & false if already started.
func (ir *informerReference) Start(ctx context.Context) (context.Context, bool) {
ir.lock.Lock()
defer ir.lock.Unlock()
if ir.started {
return nil, false
}
ctx, cancel := context.WithCancel(ctx)
ir.context = ctx
ir.cancel = cancel
ir.started = true
return ctx, true
}
func (ir *informerReference) SetInformer(informer cache.SharedIndexInformer) {
ir.lock.Lock()
defer ir.lock.Unlock()
ir.informer = informer
}
func (ir *informerReference) HasSynced() bool {
ir.lock.Lock()
defer ir.lock.Unlock()
if !ir.started {
return false
}
if ir.informer == nil {
return false
}
return ir.informer.HasSynced()
}
func (ir *informerReference) HasStarted() bool {
ir.lock.Lock()
defer ir.lock.Unlock()
return ir.started
}
// Stop cancels the context, if it's been started.
func (ir *informerReference) Stop() {
ir.lock.Lock()
defer ir.lock.Unlock()
if !ir.started {
return
}
ir.cancel()
ir.started = false
ir.context = nil
}
type taskFunc func()
// taskManager manages a set of tasks with object identifiers.
// This makes starting and stopping the tasks thread-safe.
type taskManager struct {
lock sync.Mutex
cancelFuncs map[object.ObjMetadata]context.CancelFunc
}
func (tm *taskManager) Schedule(parentCtx context.Context, id object.ObjMetadata, delay time.Duration, task taskFunc) {
tm.lock.Lock()
defer tm.lock.Unlock()
if tm.cancelFuncs == nil {
tm.cancelFuncs = make(map[object.ObjMetadata]context.CancelFunc)
}
cancel, found := tm.cancelFuncs[id]
if found {
// Cancel the existing scheduled task and replace it.
cancel()
}
taskCtx, cancel := context.WithTimeout(context.Background(), delay)
tm.cancelFuncs[id] = cancel
go func() {
klog.V(5).Infof("Task scheduled (%v) for object (%s)", delay, id)
select {
case <-parentCtx.Done():
// stop waiting
cancel()
case <-taskCtx.Done():
if taskCtx.Err() == context.DeadlineExceeded {
klog.V(5).Infof("Task executing (after %v) for object (%v)", delay, id)
task()
}
// else stop waiting
}
}()
}
func (tm *taskManager) Cancel(id object.ObjMetadata) {
tm.lock.Lock()
defer tm.lock.Unlock()
cancelFunc, found := tm.cancelFuncs[id]
if !found {
// already cancelled or not added
return
}
delete(tm.cancelFuncs, id)
cancelFunc()
if len(tm.cancelFuncs) == 0 {
tm.cancelFuncs = nil
}
}
@@ -0,0 +1,25 @@
// Code generated by "stringer -type=RESTScopeStrategy -linecomment"; DO NOT EDIT.
package watcher
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[RESTScopeAutomatic-0]
_ = x[RESTScopeRoot-1]
_ = x[RESTScopeNamespace-2]
}
const _RESTScopeStrategy_name = "automaticrootnamespace"
var _RESTScopeStrategy_index = [...]uint8{0, 9, 13, 22}
func (i RESTScopeStrategy) String() string {
if i < 0 || i >= RESTScopeStrategy(len(_RESTScopeStrategy_index)-1) {
return "RESTScopeStrategy(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RESTScopeStrategy_name[_RESTScopeStrategy_index[i]:_RESTScopeStrategy_index[i+1]]
}
@@ -0,0 +1,69 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package watcher
import (
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
"github.com/fluxcd/cli-utils/pkg/object"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// isObjectUnschedulable returns true if the object or any of its generated resources
// is an unschedulable pod.
//
// This status is computed recursively, so it can handle objects that generate
// objects that generate pods, as long as the input ResourceStatus has those
// GeneratedResources computed.
func isObjectUnschedulable(rs *event.ResourceStatus) bool {
if rs.Error != nil {
return false
}
if rs.Status != status.InProgressStatus {
return false
}
if isPodUnschedulable(rs.Resource) {
return true
}
// recurse through generated resources
for _, subRS := range rs.GeneratedResources {
if isObjectUnschedulable(subRS) {
return true
}
}
return false
}
// isPodUnschedulable returns true if the object is a pod and is unschedulable
// according to a False PodScheduled condition.
func isPodUnschedulable(obj *unstructured.Unstructured) bool {
if obj == nil {
return false
}
gk := obj.GroupVersionKind().GroupKind()
if gk != (schema.GroupKind{Kind: "Pod"}) {
return false
}
icnds, found, err := object.NestedField(obj.Object, "status", "conditions")
if err != nil || !found {
return false
}
cnds, ok := icnds.([]interface{})
if !ok {
return false
}
for _, icnd := range cnds {
cnd, ok := icnd.(map[string]interface{})
if !ok {
return false
}
if cnd["type"] == "PodScheduled" &&
cnd["status"] == "False" &&
cnd["reason"] == "Unschedulable" {
return true
}
}
return false
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package watcher
import (
"context"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
"github.com/fluxcd/cli-utils/pkg/object"
)
// StatusWatcher watches a set of objects for status updates.
type StatusWatcher interface {
// Watch a set of objects for status updates.
// Watching should stop if the context is cancelled.
// Events should only be sent for the specified objects.
// The event channel should be closed when the watching stops.
Watch(context.Context, object.ObjMetadataSet, Options) <-chan event.Event
}
// Options can be provided when creating a new StatusWatcher to customize the
// behavior.
type Options struct {
// RESTScopeStrategy specifies which strategy to use when listing and
// watching resources. By default, the strategy is selected automatically.
RESTScopeStrategy RESTScopeStrategy
}
//go:generate stringer -type=RESTScopeStrategy -linecomment
type RESTScopeStrategy int
const (
RESTScopeAutomatic RESTScopeStrategy = iota // automatic
RESTScopeRoot // root
RESTScopeNamespace // namespace
)
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package object
import (
"fmt"
)
// InvalidAnnotationError represents an invalid annotation.
// Fields are exposed to allow callers to perform introspection.
type InvalidAnnotationError struct {
Annotation string
Cause error
}
func (iae InvalidAnnotationError) Error() string {
return fmt.Sprintf("invalid %q annotation: %v",
iae.Annotation, iae.Cause)
}
func (iae InvalidAnnotationError) Unwrap() error {
return iae.Cause
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package object
import (
"fmt"
"regexp"
"strings"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// NestedField gets a value from a KRM map, if it exists, otherwise nil.
// Fields can be string (map key) or int (array index).
func NestedField(obj map[string]interface{}, fields ...interface{}) (interface{}, bool, error) {
var val interface{} = obj
for i, field := range fields {
if val == nil {
return nil, false, nil
}
switch typedField := field.(type) {
case string:
if m, ok := val.(map[string]interface{}); ok {
val, ok = m[typedField]
if !ok {
// not in map
return nil, false, nil
}
} else {
return nil, false, InvalidType(fields[:i+1], val, "map[string]interface{}")
}
case int:
if s, ok := val.([]interface{}); ok {
if typedField >= len(s) {
// index out of range
return nil, false, nil
}
val = s[typedField]
} else {
return nil, false, InvalidType(fields[:i+1], val, "[]interface{}")
}
default:
return nil, false, InvalidType(fields[:i+1], val, "string or int")
}
}
return val, true, nil
}
// InvalidType returns a *Error indicating "invalid value type". This is used
// to report malformed values (e.g. found int, expected string).
func InvalidType(fieldPath []interface{}, value interface{}, validTypes string) *field.Error {
return Invalid(fieldPath, value,
fmt.Sprintf("found type %T, expected %s", value, validTypes))
}
// Invalid returns a *Error indicating "invalid value". This is used
// to report malformed values (e.g. failed regex match, too long, out of bounds).
func Invalid(fieldPath []interface{}, value interface{}, detail string) *field.Error {
return &field.Error{
Type: field.ErrorTypeInvalid,
Field: FieldPath(fieldPath),
BadValue: value,
Detail: detail,
}
}
// NotFound returns a *Error indicating "value not found". This is
// used to report failure to find a requested value (e.g. looking up an ID).
func NotFound(fieldPath []interface{}, value interface{}) *field.Error {
return &field.Error{
Type: field.ErrorTypeNotFound,
Field: FieldPath(fieldPath),
BadValue: value,
Detail: "",
}
}
// FieldPath formats a list of KRM field keys as a JSONPath expression.
// The only valid field keys in KRM are strings (map keys) and ints (list keys).
// Simple strings (see isSimpleString) will be delimited with a period.
// Complex strings will be wrapped with square brackets and double quotes.
// Integers will be wrapped with square brackets.
// All other types will be formatted best-effort within square brackets.
func FieldPath(fieldPath []interface{}) string {
var sb strings.Builder
for _, field := range fieldPath {
switch typedField := field.(type) {
case string:
if isSimpleString(typedField) {
_, _ = fmt.Fprintf(&sb, ".%s", typedField)
} else {
_, _ = fmt.Fprintf(&sb, "[%q]", typedField)
}
case int:
_, _ = fmt.Fprintf(&sb, "[%d]", typedField)
default:
// invalid type. try anyway...
_, _ = fmt.Fprintf(&sb, "[%#v]", typedField)
}
}
return sb.String()
}
var simpleStringRegex = regexp.MustCompile(`^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$`)
// isSimpleString returns true if the input follows the following rules:
// - contains only alphanumeric characters, '_' or '-'
// - starts with an alphabetic character
// - ends with an alphanumeric character
func isSimpleString(s string) bool {
return simpleStringRegex.FindString(s) != ""
}
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package object
import (
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
)
// InfosToObjMetas returns object metadata (ObjMetadata) for the
// passed objects (infos); returns an error if one occurs.
func InfosToObjMetas(infos []*resource.Info) ([]ObjMetadata, error) {
objMetas := make([]ObjMetadata, 0, len(infos))
for _, info := range infos {
objMeta, err := InfoToObjMeta(info)
if err != nil {
return nil, err
}
objMetas = append(objMetas, objMeta)
}
return objMetas, nil
}
// InfoToObjMeta takes information from the provided info and
// returns an ObjMetadata that identifies the resource.
func InfoToObjMeta(info *resource.Info) (ObjMetadata, error) {
if info == nil || info.Object == nil {
return ObjMetadata{}, fmt.Errorf("attempting to transform info, but it is empty")
}
id := ObjMetadata{
Namespace: info.Namespace,
Name: info.Name,
GroupKind: info.Object.GetObjectKind().GroupVersionKind().GroupKind(),
}
return id, nil
}
// InfoToUnstructured transforms the passed info object into unstructured format.
func InfoToUnstructured(info *resource.Info) *unstructured.Unstructured {
return info.Object.(*unstructured.Unstructured)
}
// UnstructuredToInfo transforms the passed Unstructured object into Info format,
// or an error if one occurs.
func UnstructuredToInfo(obj *unstructured.Unstructured) (*resource.Info, error) {
// make a copy of the input object to avoid modifying the input
obj = obj.DeepCopy()
annos := obj.GetAnnotations()
source := "unstructured"
path, ok := annos[kioutil.PathAnnotation]
if ok {
source = path
}
StripKyamlAnnotations(obj)
return &resource.Info{
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
Source: source,
Object: obj,
}, nil
}
// InfosToUnstructureds transforms the passed objects in Info format to Unstructured.
func InfosToUnstructureds(infos []*resource.Info) []*unstructured.Unstructured {
var objs []*unstructured.Unstructured
for _, info := range infos {
objs = append(objs, InfoToUnstructured(info))
}
return objs
}
// UnstructuredsToInfos transforms the passed Unstructured objects into Info format
// or an error if one occurs.
func UnstructuredsToInfos(objs []*unstructured.Unstructured) ([]*resource.Info, error) {
var infos []*resource.Info
for _, obj := range objs {
inf, err := UnstructuredToInfo(obj)
if err != nil {
return infos, err
}
infos = append(infos, inf)
}
return infos, nil
}
+147
View File
@@ -0,0 +1,147 @@
// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
//
// ObjMetadata is the minimal set of information to
// uniquely identify an object. The four fields are:
//
// Group/Kind (NOTE: NOT version)
// Namespace
// Name
//
// We specifically do not use the "version", because
// the APIServer does not recognize a version as a
// different resource. This metadata is used to identify
// resources for pruning and teardown.
package object
import (
"fmt"
"strings"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
// Separates inventory fields. This string is allowable as a
// ConfigMap key, but it is not allowed as a character in
// resource name.
fieldSeparator = "_"
// Transform colons in the RBAC resource names to double
// underscore.
colonTranscoded = "__"
)
var (
NilObjMetadata = ObjMetadata{}
)
// RBACGroupKind is a map of the RBAC resources. Needed since name validation
// is different than other k8s resources.
var RBACGroupKind = map[schema.GroupKind]bool{
{Group: rbacv1.GroupName, Kind: "Role"}: true,
{Group: rbacv1.GroupName, Kind: "ClusterRole"}: true,
{Group: rbacv1.GroupName, Kind: "RoleBinding"}: true,
{Group: rbacv1.GroupName, Kind: "ClusterRoleBinding"}: true,
}
// ObjMetadata organizes and stores the indentifying information
// for an object. This struct (as a string) is stored in a
// inventory object to keep track of sets of applied objects.
type ObjMetadata struct {
Namespace string
Name string
GroupKind schema.GroupKind
}
// ParseObjMetadata takes a string, splits it into its four fields,
// and returns an ObjMetadata struct storing the four fields.
// Example inventory string:
//
// test-namespace_test-name_apps_ReplicaSet
//
// Returns an error if unable to parse and create the ObjMetadata struct.
//
// NOTE: name field can contain double underscore (__), which represents
// a colon. RBAC resources can have this additional character (:) in their name.
func ParseObjMetadata(s string) (ObjMetadata, error) {
// Parse first field namespace
index := strings.Index(s, fieldSeparator)
if index == -1 {
return NilObjMetadata, fmt.Errorf("unable to parse stored object metadata: %s", s)
}
namespace := s[:index]
s = s[index+1:]
// Next, parse last field kind
index = strings.LastIndex(s, fieldSeparator)
if index == -1 {
return NilObjMetadata, fmt.Errorf("unable to parse stored object metadata: %s", s)
}
kind := s[index+1:]
s = s[:index]
// Next, parse next to last field group
index = strings.LastIndex(s, fieldSeparator)
if index == -1 {
return NilObjMetadata, fmt.Errorf("unable to parse stored object metadata: %s", s)
}
group := s[index+1:]
// Finally, second field name. Name may contain colon transcoded as double underscore.
name := s[:index]
name = strings.ReplaceAll(name, colonTranscoded, ":")
// Check that there are no extra fields by search for fieldSeparator.
if strings.Contains(name, fieldSeparator) {
return NilObjMetadata, fmt.Errorf("too many fields within: %s", s)
}
// Create the ObjMetadata object from the four parsed fields.
id := ObjMetadata{
Namespace: namespace,
Name: name,
GroupKind: schema.GroupKind{
Group: group,
Kind: kind,
},
}
return id, nil
}
// Equals compares two ObjMetadata and returns true if they are equal. This does
// not contain any special treatment for the extensions API group.
func (o *ObjMetadata) Equals(other *ObjMetadata) bool {
if other == nil {
return false
}
return *o == *other
}
// String create a string version of the ObjMetadata struct. For RBAC resources,
// the "name" field transcodes ":" into double underscore for valid storing
// as the label of a ConfigMap.
func (o ObjMetadata) String() string {
name := o.Name
if _, exists := RBACGroupKind[o.GroupKind]; exists {
name = strings.ReplaceAll(name, ":", colonTranscoded)
}
return fmt.Sprintf("%s%s%s%s%s%s%s",
o.Namespace, fieldSeparator,
name, fieldSeparator,
o.GroupKind.Group, fieldSeparator,
o.GroupKind.Kind)
}
// RuntimeToObjMeta extracts the object metadata information from a
// runtime.Object and returns it as ObjMetadata.
func RuntimeToObjMeta(obj runtime.Object) (ObjMetadata, error) {
accessor, err := meta.Accessor(obj)
if err != nil {
return NilObjMetadata, err
}
id := ObjMetadata{
Namespace: accessor.GetNamespace(),
Name: accessor.GetName(),
GroupKind: obj.GetObjectKind().GroupVersionKind().GroupKind(),
}
return id, nil
}
+211
View File
@@ -0,0 +1,211 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
//
package object
import (
"hash/fnv"
"sort"
"strconv"
)
// ObjMetadataSet is an ordered list of ObjMetadata that acts like an unordered
// set for comparison purposes.
type ObjMetadataSet []ObjMetadata
// UnstructuredSetEquals returns true if the slice of objects in setA equals
// the slice of objects in setB.
func ObjMetadataSetEquals(setA []ObjMetadata, setB []ObjMetadata) bool {
return ObjMetadataSet(setA).Equal(ObjMetadataSet(setB))
}
// ObjMetadataSetFromMap constructs a set from a map
func ObjMetadataSetFromMap(mapA map[ObjMetadata]struct{}) ObjMetadataSet {
setA := make(ObjMetadataSet, 0, len(mapA))
for f := range mapA {
setA = append(setA, f)
}
return setA
}
// Equal returns true if the two sets contain equivalent objects. Duplicates are
// ignored.
// This function satisfies the cmp.Equal interface from github.com/google/go-cmp
func (setA ObjMetadataSet) Equal(setB ObjMetadataSet) bool {
mapA := make(map[ObjMetadata]struct{}, len(setA))
for _, a := range setA {
mapA[a] = struct{}{}
}
mapB := make(map[ObjMetadata]struct{}, len(setB))
for _, b := range setB {
mapB[b] = struct{}{}
}
if len(mapA) != len(mapB) {
return false
}
for b := range mapB {
if _, exists := mapA[b]; !exists {
return false
}
}
return true
}
// Contains checks if the provided ObjMetadata exists in the set.
func (setA ObjMetadataSet) Contains(id ObjMetadata) bool {
for _, om := range setA {
if om == id {
return true
}
}
return false
}
// Remove the object from the set and return the updated set.
func (setA ObjMetadataSet) Remove(obj ObjMetadata) ObjMetadataSet {
for i, a := range setA {
if a == obj {
setA[len(setA)-1], setA[i] = setA[i], setA[len(setA)-1]
return setA[:len(setA)-1]
}
}
return setA
}
// Intersection returns the set of unique objects in both set A and set B.
func (setA ObjMetadataSet) Intersection(setB ObjMetadataSet) ObjMetadataSet {
var maxlen int
if len(setA) > len(setB) {
maxlen = len(setA)
} else {
maxlen = len(setB)
}
mapI := make(map[ObjMetadata]struct{}, maxlen)
mapB := setB.ToMap()
for _, a := range setA {
if _, ok := mapB[a]; ok {
mapI[a] = struct{}{}
}
}
intersection := make(ObjMetadataSet, 0, len(mapI))
// Iterate over setA & setB to retain input order and have stable output
for _, id := range setA {
if _, ok := mapI[id]; ok {
intersection = append(intersection, id)
delete(mapI, id)
}
}
for _, id := range setB {
if _, ok := mapI[id]; ok {
intersection = append(intersection, id)
delete(mapI, id)
}
}
return intersection
}
// Union returns the set of unique objects from the merging of set A and set B.
func (setA ObjMetadataSet) Union(setB ObjMetadataSet) ObjMetadataSet {
m := make(map[ObjMetadata]struct{}, len(setA)+len(setB))
for _, a := range setA {
m[a] = struct{}{}
}
for _, b := range setB {
m[b] = struct{}{}
}
union := make(ObjMetadataSet, 0, len(m))
// Iterate over setA & setB to retain input order and have stable output
for _, id := range setA {
if _, ok := m[id]; ok {
union = append(union, id)
delete(m, id)
}
}
for _, id := range setB {
if _, ok := m[id]; ok {
union = append(union, id)
delete(m, id)
}
}
return union
}
// Diff returns the set of objects that exist in set A, but not in set B (A - B).
func (setA ObjMetadataSet) Diff(setB ObjMetadataSet) ObjMetadataSet {
// Create a map of the elements of A
m := make(map[ObjMetadata]struct{}, len(setA))
for _, a := range setA {
m[a] = struct{}{}
}
// Remove from A each element of B
for _, b := range setB {
delete(m, b) // OK to delete even if b not in m
}
// Create/return slice from the map of remaining items
diff := make(ObjMetadataSet, 0, len(m))
// Iterate over setA to retain input order and have stable output
for _, id := range setA {
if _, ok := m[id]; ok {
diff = append(diff, id)
delete(m, id)
}
}
return diff
}
// Unique returns the set with duplicates removed.
// Order may or may not remain consistent.
func (setA ObjMetadataSet) Unique() ObjMetadataSet {
return ObjMetadataSetFromMap(setA.ToMap())
}
// Hash the objects in the set by serializing, sorting, concatonating, and
// hashing the result with the 32-bit FNV-1a algorithm.
func (setA ObjMetadataSet) Hash() string {
objStrs := make([]string, 0, len(setA))
for _, obj := range setA {
objStrs = append(objStrs, obj.String())
}
sort.Strings(objStrs)
h := fnv.New32a()
for _, obj := range objStrs {
// Hash32.Write never returns an error
// https://pkg.go.dev/hash#pkg-types
_, _ = h.Write([]byte(obj))
}
return strconv.FormatUint(uint64(h.Sum32()), 16)
}
// ToMap returns the set as a map, with objMeta keys and empty struct values.
func (setA ObjMetadataSet) ToMap() map[ObjMetadata]struct{} {
m := make(map[ObjMetadata]struct{}, len(setA))
for _, objMeta := range setA {
m[objMeta] = struct{}{}
}
return m
}
// ToStringMap returns the set as a serializable map, with objMeta keys and
// empty string values.
func (setA ObjMetadataSet) ToStringMap() map[string]string {
stringMap := make(map[string]string, len(setA))
for _, objMeta := range setA {
stringMap[objMeta.String()] = ""
}
return stringMap
}
// FromStringMap returns a set from a serializable map, with objMeta keys and
// empty string values. Errors if parsing fails.
func FromStringMap(in map[string]string) (ObjMetadataSet, error) {
var set ObjMetadataSet
for s := range in {
objMeta, err := ParseObjMetadata(s)
if err != nil {
return nil, err
}
set = append(set, objMeta)
}
return set, nil
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package object
import (
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/yaml"
)
// YamlStringer delays YAML marshalling for logging until String() is called.
type YamlStringer struct {
O *unstructured.Unstructured
}
// String marshals the wrapped object to a YAML string. If serializing errors,
// the error string will be returned instead. This is primarily for use with
// verbose logging.
func (ys YamlStringer) String() string {
yamlBytes, err := yaml.Marshal(ys.O)
if err != nil {
return fmt.Sprintf("<<failed to serialize as yaml: %s>>", err)
}
return string(yamlBytes)
}
+214
View File
@@ -0,0 +1,214 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
//
package object
import (
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
)
var (
namespaceGK = schema.GroupKind{Group: "", Kind: "Namespace"}
crdGK = schema.GroupKind{Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition"}
)
// UnstructuredSetToObjMetadataSet converts a UnstructuredSet to a ObjMetadataSet.
func UnstructuredSetToObjMetadataSet(objs UnstructuredSet) ObjMetadataSet {
objMetas := make([]ObjMetadata, len(objs))
for i, obj := range objs {
objMetas[i] = UnstructuredToObjMetadata(obj)
}
return objMetas
}
// UnstructuredToObjMetadata extracts the identifying information from an
// Unstructured object and returns it as ObjMetadata object.
func UnstructuredToObjMetadata(obj *unstructured.Unstructured) ObjMetadata {
return ObjMetadata{
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
GroupKind: obj.GroupVersionKind().GroupKind(),
}
}
// IsKindNamespace returns true if the passed Unstructured object is
// GroupKind == Core/Namespace (no version checked); false otherwise.
func IsKindNamespace(u *unstructured.Unstructured) bool {
if u == nil {
return false
}
gvk := u.GroupVersionKind()
return namespaceGK == gvk.GroupKind()
}
// IsNamespaced returns true if the passed Unstructured object
// is namespace-scoped (not cluster-scoped); false otherwise.
func IsNamespaced(u *unstructured.Unstructured) bool {
if u == nil {
return false
}
return u.GetNamespace() != ""
}
// IsNamespace returns true if the passed Unstructured object
// is Namespace in the core (empty string) group.
func IsNamespace(u *unstructured.Unstructured) bool {
if u == nil {
return false
}
gvk := u.GroupVersionKind()
// core group, any version
return gvk.Group == "" && gvk.Kind == "Namespace"
}
// IsCRD returns true if the passed Unstructured object has
// GroupKind == Extensions/CustomResourceDefinition; false otherwise.
func IsCRD(u *unstructured.Unstructured) bool {
if u == nil {
return false
}
gvk := u.GroupVersionKind()
return crdGK == gvk.GroupKind()
}
// GetCRDGroupKind returns the GroupKind stored in the passed
// Unstructured CustomResourceDefinition and true if the passed object
// is a CRD.
func GetCRDGroupKind(u *unstructured.Unstructured) (schema.GroupKind, bool) {
emptyGroupKind := schema.GroupKind{Group: "", Kind: ""}
if u == nil {
return emptyGroupKind, false
}
group, found, err := unstructured.NestedString(u.Object, "spec", "group")
if found && err == nil {
kind, found, err := unstructured.NestedString(u.Object, "spec", "names", "kind")
if found && err == nil {
return schema.GroupKind{Group: group, Kind: kind}, true
}
}
return emptyGroupKind, false
}
// UnknownTypeError captures information about a type for which no information
// could be found in the cluster or among the known CRDs.
type UnknownTypeError struct {
GroupVersionKind schema.GroupVersionKind
}
func (e *UnknownTypeError) Error() string {
return fmt.Sprintf("unknown resource type: %q", e.GroupVersionKind.String())
}
// LookupResourceScope tries to look up the scope of the type of the provided
// resource, looking at both the types known to the cluster (through the
// RESTMapper) and the provided CRDs. If no information about the type can
// be found, an UnknownTypeError wil be returned.
func LookupResourceScope(u *unstructured.Unstructured, crds []*unstructured.Unstructured, mapper meta.RESTMapper) (meta.RESTScope, error) {
gvk := u.GroupVersionKind()
// First see if we can find the type (and the scope) in the cluster through
// the RESTMapper.
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err == nil {
// If we find the type in the cluster, we just look up the scope there.
return mapping.Scope, nil
}
// Not finding a match is not an error here, so only error out for other
// error types.
if !meta.IsNoMatchError(err) {
return nil, err
}
// If we couldn't find the type in the cluster, check if we find a
// match in any of the provided CRDs.
for _, crd := range crds {
group, found, err := NestedField(crd.Object, "spec", "group")
if err != nil {
return nil, err
}
if !found || group == "" {
return nil, NotFound([]interface{}{"spec", "group"}, group)
}
kind, found, err := NestedField(crd.Object, "spec", "names", "kind")
if err != nil {
return nil, err
}
if !found || kind == "" {
return nil, NotFound([]interface{}{"spec", "kind"}, group)
}
if gvk.Kind != kind || gvk.Group != group {
continue
}
versionDefined, err := crdDefinesVersion(crd, gvk.Version)
if err != nil {
return nil, err
}
if !versionDefined {
return nil, &UnknownTypeError{
GroupVersionKind: gvk,
}
}
scopeName, _, err := NestedField(crd.Object, "spec", "scope")
if err != nil {
return nil, err
}
switch scopeName {
case "Namespaced":
return meta.RESTScopeNamespace, nil
case "Cluster":
return meta.RESTScopeRoot, nil
default:
return nil, Invalid([]interface{}{"spec", "scope"}, scopeName,
"expected Namespaced or Cluster")
}
}
return nil, &UnknownTypeError{
GroupVersionKind: gvk,
}
}
func crdDefinesVersion(crd *unstructured.Unstructured, version string) (bool, error) {
versions, found, err := NestedField(crd.Object, "spec", "versions")
if err != nil {
return false, err
}
if !found {
return false, NotFound([]interface{}{"spec", "versions"}, versions)
}
versionsSlice, ok := versions.([]interface{})
if !ok {
return false, InvalidType([]interface{}{"spec", "versions"}, versions, "[]interface{}")
}
if len(versionsSlice) == 0 {
return false, Invalid([]interface{}{"spec", "versions"}, versionsSlice, "must not be empty")
}
for i := range versionsSlice {
name, found, err := NestedField(crd.Object, "spec", "versions", i, "name")
if err != nil {
return false, err
}
if !found {
return false, NotFound([]interface{}{"spec", "versions", i, "name"}, name)
}
if name == version {
return true, nil
}
}
return false, nil
}
// StripKyamlAnnotations removes any path and index annotations from the
// unstructured resource.
func StripKyamlAnnotations(u *unstructured.Unstructured) {
annos := u.GetAnnotations()
delete(annos, kioutil.PathAnnotation)
delete(annos, kioutil.LegacyPathAnnotation) //nolint:staticcheck
delete(annos, kioutil.IndexAnnotation)
delete(annos, kioutil.LegacyIndexAnnotation) //nolint:staticcheck
u.SetAnnotations(annos)
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
//
package object
import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// UnstructuredSet is an ordered list of Unstructured that acts like an
// unordered set for comparison purposes.
type UnstructuredSet []*unstructured.Unstructured
// UnstructuredSetEquals returns true if the slice of objects in setA equals
// the slice of objects in setB.
func UnstructuredSetEquals(setA []*unstructured.Unstructured, setB []*unstructured.Unstructured) bool {
return UnstructuredSet(setA).Equal(UnstructuredSet(setB))
}
func (setA UnstructuredSet) Equal(setB UnstructuredSet) bool {
mapA := make(map[string]string, len(setA))
for _, a := range setA {
jsonBytes, err := a.MarshalJSON()
if err != nil {
mapA[string(jsonBytes)] = err.Error()
} else {
mapA[string(jsonBytes)] = ""
}
}
mapB := make(map[string]string, len(setB))
for _, b := range setB {
jsonBytes, err := b.MarshalJSON()
if err != nil {
mapB[string(jsonBytes)] = err.Error()
} else {
mapB[string(jsonBytes)] = ""
}
}
if len(mapA) != len(mapB) {
return false
}
for b, errB := range mapB {
if errA, exists := mapA[b]; !exists {
if !exists {
return false
}
if errA != errB {
return false
}
}
}
return true
}