working commit
This commit is contained in:
+201
@@ -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.
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
// Package apiutil contains utilities for working with raw Kubernetes
|
||||
// API machinery, such as creating RESTMappers and raw REST clients,
|
||||
// and extracting the GVK of an object.
|
||||
package apiutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/dynamic"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
var (
|
||||
protobufScheme = runtime.NewScheme()
|
||||
protobufSchemeLock sync.RWMutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Currently only enabled for built-in resources which are guaranteed to implement Protocol Buffers.
|
||||
// For custom resources, CRDs can not support Protocol Buffers but Aggregated API can.
|
||||
// See doc: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#advanced-features-and-flexibility
|
||||
if err := clientgoscheme.AddToScheme(protobufScheme); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// AddToProtobufScheme add the given SchemeBuilder into protobufScheme, which should
|
||||
// be additional types that do support protobuf.
|
||||
func AddToProtobufScheme(addToScheme func(*runtime.Scheme) error) error {
|
||||
protobufSchemeLock.Lock()
|
||||
defer protobufSchemeLock.Unlock()
|
||||
return addToScheme(protobufScheme)
|
||||
}
|
||||
|
||||
// IsObjectNamespaced returns true if the object is namespace scoped.
|
||||
// For unstructured objects the gvk is found from the object itself.
|
||||
func IsObjectNamespaced(obj runtime.Object, scheme *runtime.Scheme, restmapper meta.RESTMapper) (bool, error) {
|
||||
gvk, err := GVKForObject(obj, scheme)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return IsGVKNamespaced(gvk, restmapper)
|
||||
}
|
||||
|
||||
// IsGVKNamespaced returns true if the object having the provided
|
||||
// GVK is namespace scoped.
|
||||
func IsGVKNamespaced(gvk schema.GroupVersionKind, restmapper meta.RESTMapper) (bool, error) {
|
||||
// Fetch the RESTMapping using the complete GVK. If we exclude the Version, the Version set
|
||||
// will be populated using the cached Group if available. This can lead to failures updating
|
||||
// the cache with new Versions of CRDs registered at runtime.
|
||||
restmapping, err := restmapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get restmapping: %w", err)
|
||||
}
|
||||
|
||||
scope := restmapping.Scope.Name()
|
||||
if scope == "" {
|
||||
return false, errors.New("scope cannot be identified, empty scope returned")
|
||||
}
|
||||
|
||||
if scope != meta.RESTScopeNameRoot {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// GVKForObject finds the GroupVersionKind associated with the given object, if there is only a single such GVK.
|
||||
func GVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionKind, error) {
|
||||
// TODO(directxman12): do we want to generalize this to arbitrary container types?
|
||||
// I think we'd need a generalized form of scheme or something. It's a
|
||||
// shame there's not a reliable "GetGVK" interface that works by default
|
||||
// for unpopulated static types and populated "dynamic" types
|
||||
// (unstructured, partial, etc)
|
||||
|
||||
// check for PartialObjectMetadata, which is analogous to unstructured, but isn't handled by ObjectKinds
|
||||
_, isPartial := obj.(*metav1.PartialObjectMetadata)
|
||||
_, isPartialList := obj.(*metav1.PartialObjectMetadataList)
|
||||
if isPartial || isPartialList {
|
||||
// we require that the GVK be populated in order to recognize the object
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
if len(gvk.Kind) == 0 {
|
||||
return schema.GroupVersionKind{}, runtime.NewMissingKindErr("unstructured object has no kind")
|
||||
}
|
||||
if len(gvk.Version) == 0 {
|
||||
return schema.GroupVersionKind{}, runtime.NewMissingVersionErr("unstructured object has no version")
|
||||
}
|
||||
return gvk, nil
|
||||
}
|
||||
|
||||
// Use the given scheme to retrieve all the GVKs for the object.
|
||||
gvks, isUnversioned, err := scheme.ObjectKinds(obj)
|
||||
if err != nil {
|
||||
return schema.GroupVersionKind{}, err
|
||||
}
|
||||
if isUnversioned {
|
||||
return schema.GroupVersionKind{}, fmt.Errorf("cannot create group-version-kind for unversioned type %T", obj)
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(gvks) < 1:
|
||||
// If the object has no GVK, the object might not have been registered with the scheme.
|
||||
// or it's not a valid object.
|
||||
return schema.GroupVersionKind{}, fmt.Errorf("no GroupVersionKind associated with Go type %T, was the type registered with the Scheme?", obj)
|
||||
case len(gvks) > 1:
|
||||
err := fmt.Errorf("multiple GroupVersionKinds associated with Go type %T within the Scheme, this can happen when a type is registered for multiple GVKs at the same time", obj)
|
||||
|
||||
// We've found multiple GVKs for the object.
|
||||
currentGVK := obj.GetObjectKind().GroupVersionKind()
|
||||
if !currentGVK.Empty() {
|
||||
// If the base object has a GVK, check if it's in the list of GVKs before using it.
|
||||
for _, gvk := range gvks {
|
||||
if gvk == currentGVK {
|
||||
return gvk, nil
|
||||
}
|
||||
}
|
||||
|
||||
return schema.GroupVersionKind{}, fmt.Errorf(
|
||||
"%w: the object's supplied GroupVersionKind %q was not found in the Scheme's list; refusing to guess at one: %q", err, currentGVK, gvks)
|
||||
}
|
||||
|
||||
// This should only trigger for things like metav1.XYZ --
|
||||
// normal versioned types should be fine.
|
||||
//
|
||||
// See https://github.com/kubernetes-sigs/controller-runtime/issues/362
|
||||
// for more information.
|
||||
return schema.GroupVersionKind{}, fmt.Errorf(
|
||||
"%w: callers can either fix their type registration to only register it once, or specify the GroupVersionKind to use for object passed in; refusing to guess at one: %q", err, gvks)
|
||||
default:
|
||||
// In any other case, we've found a single GVK for the object.
|
||||
return gvks[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
// RESTClientForGVK constructs a new rest.Interface capable of accessing the resource associated
|
||||
// with the given GroupVersionKind. The REST client will be configured to use the negotiated serializer from
|
||||
// baseConfig, if set, otherwise a default serializer will be set.
|
||||
func RESTClientForGVK(
|
||||
gvk schema.GroupVersionKind,
|
||||
forceDisableProtoBuf bool,
|
||||
isUnstructured bool,
|
||||
baseConfig *rest.Config,
|
||||
codecs serializer.CodecFactory,
|
||||
httpClient *http.Client,
|
||||
) (rest.Interface, error) {
|
||||
if httpClient == nil {
|
||||
return nil, fmt.Errorf("httpClient must not be nil, consider using rest.HTTPClientFor(c) to create a client")
|
||||
}
|
||||
return rest.RESTClientForConfigAndClient(createRestConfig(gvk, forceDisableProtoBuf, isUnstructured, baseConfig, codecs), httpClient)
|
||||
}
|
||||
|
||||
// createRestConfig copies the base config and updates needed fields for a new rest config.
|
||||
func createRestConfig(gvk schema.GroupVersionKind,
|
||||
forceDisableProtoBuf bool,
|
||||
isUnstructured bool,
|
||||
baseConfig *rest.Config,
|
||||
codecs serializer.CodecFactory,
|
||||
) *rest.Config {
|
||||
gv := gvk.GroupVersion()
|
||||
|
||||
cfg := rest.CopyConfig(baseConfig)
|
||||
cfg.GroupVersion = &gv
|
||||
if gvk.Group == "" {
|
||||
cfg.APIPath = "/api"
|
||||
} else {
|
||||
cfg.APIPath = "/apis"
|
||||
}
|
||||
if cfg.UserAgent == "" {
|
||||
cfg.UserAgent = rest.DefaultKubernetesUserAgent()
|
||||
}
|
||||
// TODO(FillZpp): In the long run, we want to check discovery or something to make sure that this is actually true.
|
||||
if cfg.ContentType == "" && !forceDisableProtoBuf {
|
||||
protobufSchemeLock.RLock()
|
||||
if protobufScheme.Recognizes(gvk) {
|
||||
cfg.ContentType = runtime.ContentTypeProtobuf
|
||||
}
|
||||
protobufSchemeLock.RUnlock()
|
||||
}
|
||||
|
||||
if isUnstructured {
|
||||
// If the object is unstructured, we use the client-go dynamic serializer.
|
||||
cfg = dynamic.ConfigFor(cfg)
|
||||
} else {
|
||||
cfg.NegotiatedSerializer = serializerWithTargetZeroingDecode{NegotiatedSerializer: serializer.WithoutConversionCodecFactory{CodecFactory: codecs}}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
type serializerWithTargetZeroingDecode struct {
|
||||
runtime.NegotiatedSerializer
|
||||
}
|
||||
|
||||
func (s serializerWithTargetZeroingDecode) DecoderToVersion(serializer runtime.Decoder, r runtime.GroupVersioner) runtime.Decoder {
|
||||
return targetZeroingDecoder{upstream: s.NegotiatedSerializer.DecoderToVersion(serializer, r)}
|
||||
}
|
||||
|
||||
type targetZeroingDecoder struct {
|
||||
upstream runtime.Decoder
|
||||
}
|
||||
|
||||
func (t targetZeroingDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
|
||||
zero(into)
|
||||
return t.upstream.Decode(data, defaults, into)
|
||||
}
|
||||
|
||||
// zero zeros the value of a pointer.
|
||||
func zero(x any) {
|
||||
if x == nil {
|
||||
return
|
||||
}
|
||||
res := reflect.ValueOf(x).Elem()
|
||||
res.Set(reflect.Zero(res.Type()))
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package apiutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// ErrResourceDiscoveryFailed is returned if the RESTMapper cannot discover supported resources for some GroupVersions.
|
||||
// It wraps the errors encountered, except "NotFound" errors are replaced with meta.NoResourceMatchError, for
|
||||
// backwards compatibility with code that uses meta.IsNoMatchError() to check for unsupported APIs.
|
||||
type ErrResourceDiscoveryFailed map[schema.GroupVersion]error
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *ErrResourceDiscoveryFailed) Error() string {
|
||||
subErrors := []string{}
|
||||
for k, v := range *e {
|
||||
subErrors = append(subErrors, fmt.Sprintf("%s: %v", k, v))
|
||||
}
|
||||
slices.Sort(subErrors)
|
||||
return fmt.Sprintf("unable to retrieve the complete list of server APIs: %s", strings.Join(subErrors, ", "))
|
||||
}
|
||||
|
||||
func (e *ErrResourceDiscoveryFailed) Unwrap() []error {
|
||||
subErrors := []error{}
|
||||
for gv, err := range *e {
|
||||
if apierrors.IsNotFound(err) {
|
||||
err = &meta.NoResourceMatchError{PartialResource: gv.WithResource("")}
|
||||
}
|
||||
subErrors = append(subErrors, err)
|
||||
}
|
||||
return subErrors
|
||||
}
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package apiutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
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/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
// NewDynamicRESTMapper returns a dynamic RESTMapper for cfg. The dynamic
|
||||
// RESTMapper dynamically discovers resource types at runtime.
|
||||
func NewDynamicRESTMapper(cfg *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) {
|
||||
if httpClient == nil {
|
||||
return nil, fmt.Errorf("httpClient must not be nil, consider using rest.HTTPClientFor(c) to create a client")
|
||||
}
|
||||
|
||||
client, err := discovery.NewDiscoveryClientForConfigAndClient(cfg, httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mapper{
|
||||
mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}),
|
||||
client: client,
|
||||
knownGroups: map[string]*restmapper.APIGroupResources{},
|
||||
apiGroups: map[string]*metav1.APIGroup{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// mapper is a RESTMapper that will lazily query the provided
|
||||
// client for discovery information to do REST mappings.
|
||||
type mapper struct {
|
||||
mapper meta.RESTMapper
|
||||
client discovery.AggregatedDiscoveryInterface
|
||||
knownGroups map[string]*restmapper.APIGroupResources
|
||||
apiGroups map[string]*metav1.APIGroup
|
||||
|
||||
initialDiscoveryDone bool
|
||||
|
||||
// mutex to provide thread-safe mapper reloading.
|
||||
// It protects all fields in the mapper as well as methods
|
||||
// that have the `Locked` suffix.
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// KindFor implements Mapper.KindFor.
|
||||
func (m *mapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
|
||||
res, err := m.getMapper().KindFor(resource)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
|
||||
return schema.GroupVersionKind{}, err
|
||||
}
|
||||
res, err = m.getMapper().KindFor(resource)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// KindsFor implements Mapper.KindsFor.
|
||||
func (m *mapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
|
||||
res, err := m.getMapper().KindsFor(resource)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err = m.getMapper().KindsFor(resource)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ResourceFor implements Mapper.ResourceFor.
|
||||
func (m *mapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
|
||||
res, err := m.getMapper().ResourceFor(input)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
|
||||
return schema.GroupVersionResource{}, err
|
||||
}
|
||||
res, err = m.getMapper().ResourceFor(input)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ResourcesFor implements Mapper.ResourcesFor.
|
||||
func (m *mapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
|
||||
res, err := m.getMapper().ResourcesFor(input)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err = m.getMapper().ResourcesFor(input)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// RESTMapping implements Mapper.RESTMapping.
|
||||
func (m *mapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
|
||||
res, err := m.getMapper().RESTMapping(gk, versions...)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err = m.getMapper().RESTMapping(gk, versions...)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// RESTMappings implements Mapper.RESTMappings.
|
||||
func (m *mapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
|
||||
res, err := m.getMapper().RESTMappings(gk, versions...)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err = m.getMapper().RESTMappings(gk, versions...)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ResourceSingularizer implements Mapper.ResourceSingularizer.
|
||||
func (m *mapper) ResourceSingularizer(resource string) (string, error) {
|
||||
return m.getMapper().ResourceSingularizer(resource)
|
||||
}
|
||||
|
||||
func (m *mapper) getMapper() meta.RESTMapper {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.mapper
|
||||
}
|
||||
|
||||
// addKnownGroupAndReload reloads the mapper with updated information about missing API group.
|
||||
// versions can be specified for partial updates, for instance for v1beta1 version only.
|
||||
func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) error {
|
||||
// versions will here be [""] if the forwarded Version value of
|
||||
// GroupVersionResource (in calling method) was not specified.
|
||||
if len(versions) == 1 && versions[0] == "" {
|
||||
versions = nil
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
// If no specific versions are set by user, we will scan all available ones for the API group.
|
||||
// This operation requires 2 requests: /api and /apis, but only once. For all subsequent calls
|
||||
// this data will be taken from cache.
|
||||
//
|
||||
// We always run this once, because if the server supports aggregated discovery, this will
|
||||
// load everything with two api calls which we assume is overall cheaper.
|
||||
if len(versions) == 0 || !m.initialDiscoveryDone {
|
||||
apiGroup, didAggregatedDiscovery, err := m.findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked(groupName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if apiGroup != nil && len(versions) == 0 {
|
||||
for _, version := range apiGroup.Versions {
|
||||
versions = append(versions, version.Version)
|
||||
}
|
||||
}
|
||||
|
||||
// No need to do anything further if aggregatedDiscovery is supported and we did a lookup
|
||||
if didAggregatedDiscovery {
|
||||
failedGroups := make(map[schema.GroupVersion]error)
|
||||
for _, version := range versions {
|
||||
if m.knownGroups[groupName] == nil || m.knownGroups[groupName].VersionedResources[version] == nil {
|
||||
failedGroups[schema.GroupVersion{Group: groupName, Version: version}] = &meta.NoResourceMatchError{
|
||||
PartialResource: schema.GroupVersionResource{
|
||||
Group: groupName,
|
||||
Version: version,
|
||||
}}
|
||||
}
|
||||
}
|
||||
if len(failedGroups) > 0 {
|
||||
return ptr.To(ErrResourceDiscoveryFailed(failedGroups))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Update information for group resources about versioned resources.
|
||||
// The number of API calls is equal to the number of versions: /apis/<group>/<version>.
|
||||
// If we encounter a missing API version (NotFound error), we will remove the group from
|
||||
// the m.apiGroups and m.knownGroups caches.
|
||||
// If this happens, in the next call the group will be added back to apiGroups
|
||||
// and only the existing versions will be loaded in knownGroups.
|
||||
groupVersionResources, err := m.fetchGroupVersionResourcesLocked(groupName, versions...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API group resources: %w", err)
|
||||
}
|
||||
|
||||
m.addGroupVersionResourcesToCacheAndReloadLocked(groupVersionResources)
|
||||
return nil
|
||||
}
|
||||
|
||||
// addGroupVersionResourcesToCacheAndReloadLocked does what the name suggests. The mutex must be held when
|
||||
// calling it.
|
||||
func (m *mapper) addGroupVersionResourcesToCacheAndReloadLocked(gvr map[schema.GroupVersion]*metav1.APIResourceList) {
|
||||
// Update information for group resources about the API group by adding new versions.
|
||||
// Ignore the versions that are already registered
|
||||
for groupVersion, resources := range gvr {
|
||||
var groupResources *restmapper.APIGroupResources
|
||||
if _, ok := m.knownGroups[groupVersion.Group]; ok {
|
||||
groupResources = m.knownGroups[groupVersion.Group]
|
||||
} else {
|
||||
groupResources = &restmapper.APIGroupResources{
|
||||
Group: metav1.APIGroup{Name: groupVersion.Group},
|
||||
VersionedResources: make(map[string][]metav1.APIResource),
|
||||
}
|
||||
}
|
||||
|
||||
version := groupVersion.Version
|
||||
|
||||
groupResources.VersionedResources[version] = resources.APIResources
|
||||
found := false
|
||||
for _, v := range groupResources.Group.Versions {
|
||||
if v.Version == version {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
gv := metav1.GroupVersionForDiscovery{
|
||||
GroupVersion: metav1.GroupVersion{Group: groupVersion.Group, Version: version}.String(),
|
||||
Version: version,
|
||||
}
|
||||
|
||||
// Prepend if preferred version, else append. The upstream DiscoveryRestMappper assumes
|
||||
// the first version is the preferred one: https://github.com/kubernetes/kubernetes/blob/ef54ac803b712137871c1a1f8d635d50e69ffa6c/staging/src/k8s.io/apimachinery/pkg/api/meta/restmapper.go#L458-L461
|
||||
if group, ok := m.apiGroups[groupVersion.Group]; ok && group.PreferredVersion.Version == version {
|
||||
groupResources.Group.Versions = append([]metav1.GroupVersionForDiscovery{gv}, groupResources.Group.Versions...)
|
||||
} else {
|
||||
groupResources.Group.Versions = append(groupResources.Group.Versions, gv)
|
||||
}
|
||||
}
|
||||
|
||||
// Update data in the cache.
|
||||
m.knownGroups[groupVersion.Group] = groupResources
|
||||
}
|
||||
|
||||
// Finally, reload the mapper.
|
||||
updatedGroupResources := make([]*restmapper.APIGroupResources, 0, len(m.knownGroups))
|
||||
for _, agr := range m.knownGroups {
|
||||
updatedGroupResources = append(updatedGroupResources, agr)
|
||||
}
|
||||
|
||||
m.mapper = restmapper.NewDiscoveryRESTMapper(updatedGroupResources)
|
||||
}
|
||||
|
||||
// findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked tries to find the passed apiGroup.
|
||||
// If the server supports aggregated discovery, it will always perform that.
|
||||
func (m *mapper) findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked(groupName string) (_ *metav1.APIGroup, didAggregatedDiscovery bool, _ error) {
|
||||
// Looking in the cache first
|
||||
group, ok := m.apiGroups[groupName]
|
||||
if ok {
|
||||
return group, false, nil
|
||||
}
|
||||
|
||||
// Update the cache if nothing was found.
|
||||
apiGroups, maybeResources, _, err := m.client.GroupsAndMaybeResources()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to get server groups: %w", err)
|
||||
}
|
||||
if len(apiGroups.Groups) == 0 {
|
||||
return nil, false, fmt.Errorf("received an empty API groups list")
|
||||
}
|
||||
|
||||
m.initialDiscoveryDone = true
|
||||
for i := range apiGroups.Groups {
|
||||
group := &apiGroups.Groups[i]
|
||||
m.apiGroups[group.Name] = group
|
||||
}
|
||||
if len(maybeResources) > 0 {
|
||||
didAggregatedDiscovery = true
|
||||
m.addGroupVersionResourcesToCacheAndReloadLocked(maybeResources)
|
||||
}
|
||||
|
||||
// Looking in the cache again.
|
||||
// Don't return an error here if the API group is not present.
|
||||
// The reloaded RESTMapper will take care of returning a NoMatchError.
|
||||
return m.apiGroups[groupName], didAggregatedDiscovery, nil
|
||||
}
|
||||
|
||||
// fetchGroupVersionResourcesLocked fetches the resources for the specified group and its versions.
|
||||
// This method might modify the cache so it needs to be called under the lock.
|
||||
func (m *mapper) fetchGroupVersionResourcesLocked(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) {
|
||||
groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
|
||||
failedGroups := make(map[schema.GroupVersion]error)
|
||||
|
||||
for _, version := range versions {
|
||||
groupVersion := schema.GroupVersion{Group: groupName, Version: version}
|
||||
|
||||
apiResourceList, err := m.client.ServerResourcesForGroupVersion(groupVersion.String())
|
||||
if apierrors.IsNotFound(err) {
|
||||
// If the version is not found, we remove the group from the cache
|
||||
// so it gets refreshed on the next call.
|
||||
if m.isAPIGroupCachedLocked(groupVersion) {
|
||||
delete(m.apiGroups, groupName)
|
||||
}
|
||||
if m.isGroupVersionCachedLocked(groupVersion) {
|
||||
delete(m.knownGroups, groupName)
|
||||
}
|
||||
continue
|
||||
} else if err != nil {
|
||||
failedGroups[groupVersion] = err
|
||||
}
|
||||
|
||||
if apiResourceList != nil {
|
||||
// even in case of error, some fallback might have been returned.
|
||||
groupVersionResources[groupVersion] = apiResourceList
|
||||
}
|
||||
}
|
||||
|
||||
if len(failedGroups) > 0 {
|
||||
err := ErrResourceDiscoveryFailed(failedGroups)
|
||||
return nil, &err
|
||||
}
|
||||
|
||||
return groupVersionResources, nil
|
||||
}
|
||||
|
||||
// isGroupVersionCachedLocked checks if a version for a group is cached in the known groups cache.
|
||||
func (m *mapper) isGroupVersionCachedLocked(gv schema.GroupVersion) bool {
|
||||
if cachedGroup, ok := m.knownGroups[gv.Group]; ok {
|
||||
_, cached := cachedGroup.VersionedResources[gv.Version]
|
||||
return cached
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isAPIGroupCachedLocked checks if a version for a group is cached in the api groups cache.
|
||||
func (m *mapper) isAPIGroupCachedLocked(gv schema.GroupVersion) bool {
|
||||
cachedGroup, ok := m.apiGroups[gv.Group]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, version := range cachedGroup.Versions {
|
||||
if version.Version == gv.Version {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
type unstructuredApplyConfiguration struct {
|
||||
*unstructured.Unstructured
|
||||
}
|
||||
|
||||
func (u *unstructuredApplyConfiguration) IsApplyConfiguration() {}
|
||||
|
||||
// ApplyConfigurationFromUnstructured creates a runtime.ApplyConfiguration from an *unstructured.Unstructured object.
|
||||
//
|
||||
// Do not use Unstructured objects here that were generated from API objects, as its impossible to tell
|
||||
// if a zero value was explicitly set.
|
||||
func ApplyConfigurationFromUnstructured(u *unstructured.Unstructured) runtime.ApplyConfiguration {
|
||||
return &unstructuredApplyConfiguration{Unstructured: u}
|
||||
}
|
||||
|
||||
type applyconfigurationRuntimeObject struct {
|
||||
runtime.ApplyConfiguration
|
||||
}
|
||||
|
||||
func (a *applyconfigurationRuntimeObject) GetObjectKind() schema.ObjectKind {
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *applyconfigurationRuntimeObject) GroupVersionKind() schema.GroupVersionKind {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
|
||||
func (a *applyconfigurationRuntimeObject) SetGroupVersionKind(gvk schema.GroupVersionKind) {}
|
||||
|
||||
func (a *applyconfigurationRuntimeObject) DeepCopyObject() runtime.Object {
|
||||
panic("applyconfigurationRuntimeObject does not support DeepCopyObject")
|
||||
}
|
||||
|
||||
func runtimeObjectFromApplyConfiguration(ac runtime.ApplyConfiguration) runtime.Object {
|
||||
return &applyconfigurationRuntimeObject{ApplyConfiguration: ac}
|
||||
}
|
||||
|
||||
func gvkFromApplyConfiguration(ac applyConfiguration) (schema.GroupVersionKind, error) {
|
||||
var gvk schema.GroupVersionKind
|
||||
gv, err := schema.ParseGroupVersion(ptr.Deref(ac.GetAPIVersion(), ""))
|
||||
if err != nil {
|
||||
return gvk, fmt.Errorf("failed to parse %q as GroupVersion: %w", ptr.Deref(ac.GetAPIVersion(), ""), err)
|
||||
}
|
||||
gvk.Group = gv.Group
|
||||
gvk.Version = gv.Version
|
||||
gvk.Kind = ptr.Deref(ac.GetKind(), "")
|
||||
|
||||
return gvk, nil
|
||||
}
|
||||
+656
@@ -0,0 +1,656 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/metadata"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// Options are creation options for a Client.
|
||||
type Options struct {
|
||||
// HTTPClient is the HTTP client to use for requests.
|
||||
HTTPClient *http.Client
|
||||
|
||||
// Scheme, if provided, will be used to map go structs to GroupVersionKinds
|
||||
Scheme *runtime.Scheme
|
||||
|
||||
// Mapper, if provided, will be used to map GroupVersionKinds to Resources
|
||||
Mapper meta.RESTMapper
|
||||
|
||||
// Cache, if provided, is used to read objects from the cache.
|
||||
Cache *CacheOptions
|
||||
|
||||
// DryRun instructs the client to only perform dry run requests.
|
||||
DryRun *bool
|
||||
|
||||
// FieldOwner, if provided, sets the default field manager for all write operations
|
||||
// (Create, Update, Patch, Apply) performed by this client. The field manager is used by
|
||||
// the server for Server-Side Apply to track field ownership.
|
||||
// For more details, see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#field-management
|
||||
//
|
||||
// This default can be overridden for a specific call by passing a [FieldOwner] option
|
||||
// to the method.
|
||||
FieldOwner string
|
||||
|
||||
// FieldValidation sets the field validation strategy for all mutating operations performed by this client
|
||||
// and subresource clients created from it.
|
||||
// The exception are apply requests which are always strict, regardless of the FieldValidation setting.
|
||||
// Available values for this option can be found in "k8s.io/apimachinery/pkg/apis/meta/v1" package and are:
|
||||
// - FieldValidationIgnore
|
||||
// - FieldValidationWarn
|
||||
// - FieldValidationStrict
|
||||
// For more details, see: https://kubernetes.io/docs/reference/using-api/api-concepts/#field-validation
|
||||
FieldValidation string
|
||||
}
|
||||
|
||||
// CacheOptions are options for creating a cache-backed client.
|
||||
type CacheOptions struct {
|
||||
// Reader is a cache-backed reader that will be used to read objects from the cache.
|
||||
// +required
|
||||
Reader Reader
|
||||
// DisableFor is a list of objects that should never be read from the cache.
|
||||
// Objects configured here always result in a live lookup.
|
||||
DisableFor []Object
|
||||
// Unstructured is a flag that indicates whether the cache-backed client should
|
||||
// read unstructured objects or lists from the cache.
|
||||
// If false, unstructured objects will always result in a live lookup.
|
||||
Unstructured bool
|
||||
}
|
||||
|
||||
// NewClientFunc allows a user to define how to create a client.
|
||||
type NewClientFunc func(config *rest.Config, options Options) (Client, error)
|
||||
|
||||
// New returns a new Client using the provided config and Options.
|
||||
//
|
||||
// By default, the client surfaces warnings returned by the server. To
|
||||
// suppress warnings, set config.WarningHandlerWithContext = rest.NoWarnings{}. To
|
||||
// define custom behavior, implement the rest.WarningHandlerWithContext interface.
|
||||
// See [sigs.k8s.io/controller-runtime/pkg/log.KubeAPIWarningLogger] for
|
||||
// an example.
|
||||
//
|
||||
// The client's read behavior is determined by Options.Cache.
|
||||
// If either Options.Cache or Options.Cache.Reader is nil,
|
||||
// the client reads directly from the API server.
|
||||
// If both Options.Cache and Options.Cache.Reader are non-nil,
|
||||
// the client reads from a local cache. However, specific
|
||||
// resources can still be configured to bypass the cache based
|
||||
// on Options.Cache.Unstructured and Options.Cache.DisableFor.
|
||||
// Write operations are always performed directly on the API server.
|
||||
//
|
||||
// The client understands how to work with normal types (both custom resources
|
||||
// and aggregated/built-in resources), as well as unstructured types.
|
||||
// In the case of normal types, the scheme will be used to look up the
|
||||
// corresponding group, version, and kind for the given type. In the
|
||||
// case of unstructured types, the group, version, and kind will be extracted
|
||||
// from the corresponding fields on the object.
|
||||
func New(config *rest.Config, options Options) (c Client, err error) {
|
||||
c, err = newClient(config, options)
|
||||
if err == nil && options.DryRun != nil && *options.DryRun {
|
||||
c = NewDryRunClient(c)
|
||||
}
|
||||
if fo := options.FieldOwner; fo != "" {
|
||||
c = WithFieldOwner(c, fo)
|
||||
}
|
||||
if fv := options.FieldValidation; fv != "" {
|
||||
c = WithFieldValidation(c, FieldValidation(fv))
|
||||
}
|
||||
|
||||
return c, err
|
||||
}
|
||||
|
||||
func newClient(config *rest.Config, options Options) (*client, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("must provide non-nil rest.Config to client.New")
|
||||
}
|
||||
|
||||
config = rest.CopyConfig(config)
|
||||
if config.UserAgent == "" {
|
||||
config.UserAgent = rest.DefaultKubernetesUserAgent()
|
||||
}
|
||||
|
||||
if config.WarningHandler == nil && config.WarningHandlerWithContext == nil {
|
||||
// By default, we surface warnings.
|
||||
config.WarningHandlerWithContext = log.NewKubeAPIWarningLogger(
|
||||
log.KubeAPIWarningLoggerOptions{
|
||||
Deduplicate: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Use the rest HTTP client for the provided config if unset
|
||||
if options.HTTPClient == nil {
|
||||
var err error
|
||||
options.HTTPClient, err = rest.HTTPClientFor(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Init a scheme if none provided
|
||||
if options.Scheme == nil {
|
||||
options.Scheme = scheme.Scheme
|
||||
}
|
||||
|
||||
// Init a Mapper if none provided
|
||||
if options.Mapper == nil {
|
||||
var err error
|
||||
options.Mapper, err = apiutil.NewDynamicRESTMapper(config, options.HTTPClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resources := &clientRestResources{
|
||||
httpClient: options.HTTPClient,
|
||||
config: config,
|
||||
scheme: options.Scheme,
|
||||
mapper: options.Mapper,
|
||||
codecs: serializer.NewCodecFactory(options.Scheme),
|
||||
|
||||
resourceByType: make(map[cacheKey]*resourceMeta),
|
||||
}
|
||||
|
||||
rawMetaClient, err := metadata.NewForConfigAndClient(metadata.ConfigFor(config), options.HTTPClient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to construct metadata-only client for use as part of client: %w", err)
|
||||
}
|
||||
|
||||
c := &client{
|
||||
typedClient: typedClient{
|
||||
resources: resources,
|
||||
paramCodec: runtime.NewParameterCodec(options.Scheme),
|
||||
},
|
||||
unstructuredClient: unstructuredClient{
|
||||
resources: resources,
|
||||
paramCodec: noConversionParamCodec{},
|
||||
},
|
||||
metadataClient: metadataClient{
|
||||
client: rawMetaClient,
|
||||
restMapper: options.Mapper,
|
||||
},
|
||||
scheme: options.Scheme,
|
||||
mapper: options.Mapper,
|
||||
}
|
||||
if options.Cache == nil || options.Cache.Reader == nil {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// We want a cache if we're here.
|
||||
// Set the cache.
|
||||
c.cache = options.Cache.Reader
|
||||
|
||||
// Load uncached GVKs.
|
||||
c.cacheUnstructured = options.Cache.Unstructured
|
||||
c.uncachedGVKs = map[schema.GroupVersionKind]struct{}{}
|
||||
for _, obj := range options.Cache.DisableFor {
|
||||
gvk, err := c.GroupVersionKindFor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.uncachedGVKs[gvk] = struct{}{}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var _ Client = &client{}
|
||||
|
||||
// client is a client.Client configured to either read from a local cache or directly from the API server.
|
||||
// Write operations are always performed directly on the API server.
|
||||
// It lazily initializes new clients at the time they are used.
|
||||
type client struct {
|
||||
typedClient typedClient
|
||||
unstructuredClient unstructuredClient
|
||||
metadataClient metadataClient
|
||||
scheme *runtime.Scheme
|
||||
mapper meta.RESTMapper
|
||||
|
||||
cache Reader
|
||||
uncachedGVKs map[schema.GroupVersionKind]struct{}
|
||||
cacheUnstructured bool
|
||||
}
|
||||
|
||||
func (c *client) shouldBypassCache(obj runtime.Object) (bool, error) {
|
||||
if c.cache == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
gvk, err := c.GroupVersionKindFor(obj)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// TODO: this is producing unsafe guesses that don't actually work,
|
||||
// but it matches ~99% of the cases out there.
|
||||
if meta.IsListType(obj) {
|
||||
gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")
|
||||
}
|
||||
if _, isUncached := c.uncachedGVKs[gvk]; isUncached {
|
||||
return true, nil
|
||||
}
|
||||
if !c.cacheUnstructured {
|
||||
_, isUnstructured := obj.(runtime.Unstructured)
|
||||
return isUnstructured, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// resetGroupVersionKind is a helper function to restore and preserve GroupVersionKind on an object.
|
||||
func (c *client) resetGroupVersionKind(obj runtime.Object, gvk schema.GroupVersionKind) {
|
||||
if gvk != schema.EmptyObjectKind.GroupVersionKind() {
|
||||
if v, ok := obj.(schema.ObjectKind); ok {
|
||||
v.SetGroupVersionKind(gvk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GroupVersionKindFor returns the GroupVersionKind for the given object.
|
||||
func (c *client) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
|
||||
return apiutil.GVKForObject(obj, c.scheme)
|
||||
}
|
||||
|
||||
// IsObjectNamespaced returns true if the GroupVersionKind of the object is namespaced.
|
||||
func (c *client) IsObjectNamespaced(obj runtime.Object) (bool, error) {
|
||||
return apiutil.IsObjectNamespaced(obj, c.scheme, c.mapper)
|
||||
}
|
||||
|
||||
// Scheme returns the scheme this client is using.
|
||||
func (c *client) Scheme() *runtime.Scheme {
|
||||
return c.scheme
|
||||
}
|
||||
|
||||
// RESTMapper returns the scheme this client is using.
|
||||
func (c *client) RESTMapper() meta.RESTMapper {
|
||||
return c.mapper
|
||||
}
|
||||
|
||||
// Create implements client.Client.
|
||||
func (c *client) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
|
||||
switch obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
return c.unstructuredClient.Create(ctx, obj, opts...)
|
||||
case *metav1.PartialObjectMetadata:
|
||||
return fmt.Errorf("cannot create using only metadata")
|
||||
default:
|
||||
return c.typedClient.Create(ctx, obj, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// Update implements client.Client.
|
||||
func (c *client) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
|
||||
defer c.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
|
||||
switch obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
return c.unstructuredClient.Update(ctx, obj, opts...)
|
||||
case *metav1.PartialObjectMetadata:
|
||||
return fmt.Errorf("cannot update using only metadata -- did you mean to patch?")
|
||||
default:
|
||||
return c.typedClient.Update(ctx, obj, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete implements client.Client.
|
||||
func (c *client) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
|
||||
switch obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
return c.unstructuredClient.Delete(ctx, obj, opts...)
|
||||
case *metav1.PartialObjectMetadata:
|
||||
return c.metadataClient.Delete(ctx, obj, opts...)
|
||||
default:
|
||||
return c.typedClient.Delete(ctx, obj, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAllOf implements client.Client.
|
||||
func (c *client) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
|
||||
switch obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
return c.unstructuredClient.DeleteAllOf(ctx, obj, opts...)
|
||||
case *metav1.PartialObjectMetadata:
|
||||
return c.metadataClient.DeleteAllOf(ctx, obj, opts...)
|
||||
default:
|
||||
return c.typedClient.DeleteAllOf(ctx, obj, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// Patch implements client.Client.
|
||||
func (c *client) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
|
||||
defer c.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
|
||||
switch obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
return c.unstructuredClient.Patch(ctx, obj, patch, opts...)
|
||||
case *metav1.PartialObjectMetadata:
|
||||
return c.metadataClient.Patch(ctx, obj, patch, opts...)
|
||||
default:
|
||||
return c.typedClient.Patch(ctx, obj, patch, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error {
|
||||
switch obj := obj.(type) {
|
||||
case *unstructuredApplyConfiguration:
|
||||
defer c.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
|
||||
return c.unstructuredClient.Apply(ctx, obj, opts...)
|
||||
default:
|
||||
return c.typedClient.Apply(ctx, obj, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// Get implements client.Client.
|
||||
func (c *client) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
|
||||
if isUncached, err := c.shouldBypassCache(obj); err != nil {
|
||||
return err
|
||||
} else if !isUncached {
|
||||
// Attempt to get from the cache.
|
||||
return c.cache.Get(ctx, key, obj, opts...)
|
||||
}
|
||||
|
||||
// Perform a live lookup.
|
||||
switch obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
return c.unstructuredClient.Get(ctx, key, obj, opts...)
|
||||
case *metav1.PartialObjectMetadata:
|
||||
// Metadata only object should always preserve the GVK coming in from the caller.
|
||||
defer c.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
|
||||
return c.metadataClient.Get(ctx, key, obj, opts...)
|
||||
default:
|
||||
return c.typedClient.Get(ctx, key, obj, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// List implements client.Client.
|
||||
func (c *client) List(ctx context.Context, obj ObjectList, opts ...ListOption) error {
|
||||
if isUncached, err := c.shouldBypassCache(obj); err != nil {
|
||||
return err
|
||||
} else if !isUncached {
|
||||
// Attempt to get from the cache.
|
||||
return c.cache.List(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Perform a live lookup.
|
||||
switch x := obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
return c.unstructuredClient.List(ctx, obj, opts...)
|
||||
case *metav1.PartialObjectMetadataList:
|
||||
// Metadata only object should always preserve the GVK.
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
defer c.resetGroupVersionKind(obj, gvk)
|
||||
|
||||
// Call the list client.
|
||||
if err := c.metadataClient.List(ctx, obj, opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restore the GVK for each item in the list.
|
||||
itemGVK := schema.GroupVersionKind{
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
// TODO: this is producing unsafe guesses that don't actually work,
|
||||
// but it matches ~99% of the cases out there.
|
||||
Kind: strings.TrimSuffix(gvk.Kind, "List"),
|
||||
}
|
||||
for i := range x.Items {
|
||||
item := &x.Items[i]
|
||||
item.SetGroupVersionKind(itemGVK)
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
return c.typedClient.List(ctx, obj, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// Status implements client.StatusClient.
|
||||
func (c *client) Status() SubResourceWriter {
|
||||
return c.SubResource("status")
|
||||
}
|
||||
|
||||
func (c *client) SubResource(subResource string) SubResourceClient {
|
||||
return &subResourceClient{client: c, subResource: subResource}
|
||||
}
|
||||
|
||||
// subResourceClient is client.SubResourceWriter that writes to subresources.
|
||||
type subResourceClient struct {
|
||||
client *client
|
||||
subResource string
|
||||
}
|
||||
|
||||
// ensure subResourceClient implements client.SubResourceClient.
|
||||
var _ SubResourceClient = &subResourceClient{}
|
||||
|
||||
// SubResourceGetOptions holds all the possible configuration
|
||||
// for a subresource Get request.
|
||||
type SubResourceGetOptions struct {
|
||||
Raw *metav1.GetOptions
|
||||
}
|
||||
|
||||
// ApplyToSubResourceGet updates the configuaration to the given get options.
|
||||
func (getOpt *SubResourceGetOptions) ApplyToSubResourceGet(o *SubResourceGetOptions) {
|
||||
if getOpt.Raw != nil {
|
||||
o.Raw = getOpt.Raw
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyOptions applues the given options.
|
||||
func (getOpt *SubResourceGetOptions) ApplyOptions(opts []SubResourceGetOption) *SubResourceGetOptions {
|
||||
for _, o := range opts {
|
||||
o.ApplyToSubResourceGet(getOpt)
|
||||
}
|
||||
|
||||
return getOpt
|
||||
}
|
||||
|
||||
// AsGetOptions returns the configured options as *metav1.GetOptions.
|
||||
func (getOpt *SubResourceGetOptions) AsGetOptions() *metav1.GetOptions {
|
||||
if getOpt.Raw == nil {
|
||||
return &metav1.GetOptions{}
|
||||
}
|
||||
return getOpt.Raw
|
||||
}
|
||||
|
||||
// SubResourceUpdateOptions holds all the possible configuration
|
||||
// for a subresource update request.
|
||||
type SubResourceUpdateOptions struct {
|
||||
UpdateOptions
|
||||
SubResourceBody Object
|
||||
}
|
||||
|
||||
// ApplyToSubResourceUpdate updates the configuration on the given create options
|
||||
func (uo *SubResourceUpdateOptions) ApplyToSubResourceUpdate(o *SubResourceUpdateOptions) {
|
||||
uo.UpdateOptions.ApplyToUpdate(&o.UpdateOptions)
|
||||
if uo.SubResourceBody != nil {
|
||||
o.SubResourceBody = uo.SubResourceBody
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyOptions applies the given options.
|
||||
func (uo *SubResourceUpdateOptions) ApplyOptions(opts []SubResourceUpdateOption) *SubResourceUpdateOptions {
|
||||
for _, o := range opts {
|
||||
o.ApplyToSubResourceUpdate(uo)
|
||||
}
|
||||
|
||||
return uo
|
||||
}
|
||||
|
||||
// SubResourceUpdateAndPatchOption is an option that can be used for either
|
||||
// a subresource update or patch request.
|
||||
type SubResourceUpdateAndPatchOption interface {
|
||||
SubResourceUpdateOption
|
||||
SubResourcePatchOption
|
||||
}
|
||||
|
||||
// WithSubResourceBody returns an option that uses the given body
|
||||
// for a subresource Update or Patch operation.
|
||||
func WithSubResourceBody(body Object) SubResourceUpdateAndPatchOption {
|
||||
return &withSubresourceBody{body: body}
|
||||
}
|
||||
|
||||
type withSubresourceBody struct {
|
||||
body Object
|
||||
}
|
||||
|
||||
func (wsr *withSubresourceBody) ApplyToSubResourceUpdate(o *SubResourceUpdateOptions) {
|
||||
o.SubResourceBody = wsr.body
|
||||
}
|
||||
|
||||
func (wsr *withSubresourceBody) ApplyToSubResourcePatch(o *SubResourcePatchOptions) {
|
||||
o.SubResourceBody = wsr.body
|
||||
}
|
||||
|
||||
// SubResourceCreateOptions are all the possible configurations for a subresource
|
||||
// create request.
|
||||
type SubResourceCreateOptions struct {
|
||||
CreateOptions
|
||||
}
|
||||
|
||||
// ApplyOptions applies the given options.
|
||||
func (co *SubResourceCreateOptions) ApplyOptions(opts []SubResourceCreateOption) *SubResourceCreateOptions {
|
||||
for _, o := range opts {
|
||||
o.ApplyToSubResourceCreate(co)
|
||||
}
|
||||
|
||||
return co
|
||||
}
|
||||
|
||||
// ApplyToSubResourceCreate applies the the configuration on the given create options.
|
||||
func (co *SubResourceCreateOptions) ApplyToSubResourceCreate(o *SubResourceCreateOptions) {
|
||||
co.CreateOptions.ApplyToCreate(&co.CreateOptions)
|
||||
}
|
||||
|
||||
// SubResourcePatchOptions holds all possible configurations for a subresource patch
|
||||
// request.
|
||||
type SubResourcePatchOptions struct {
|
||||
PatchOptions
|
||||
SubResourceBody Object
|
||||
}
|
||||
|
||||
// ApplyOptions applies the given options.
|
||||
func (po *SubResourcePatchOptions) ApplyOptions(opts []SubResourcePatchOption) *SubResourcePatchOptions {
|
||||
for _, o := range opts {
|
||||
o.ApplyToSubResourcePatch(po)
|
||||
}
|
||||
|
||||
return po
|
||||
}
|
||||
|
||||
// ApplyToSubResourcePatch applies the configuration on the given patch options.
|
||||
func (po *SubResourcePatchOptions) ApplyToSubResourcePatch(o *SubResourcePatchOptions) {
|
||||
po.PatchOptions.ApplyToPatch(&o.PatchOptions)
|
||||
if po.SubResourceBody != nil {
|
||||
o.SubResourceBody = po.SubResourceBody
|
||||
}
|
||||
}
|
||||
|
||||
// SubResourceApplyOptions are the options for a subresource
|
||||
// apply request.
|
||||
type SubResourceApplyOptions struct {
|
||||
ApplyOptions
|
||||
SubResourceBody runtime.ApplyConfiguration
|
||||
}
|
||||
|
||||
// ApplyOpts applies the given options.
|
||||
func (ao *SubResourceApplyOptions) ApplyOpts(opts []SubResourceApplyOption) *SubResourceApplyOptions {
|
||||
for _, o := range opts {
|
||||
o.ApplyToSubResourceApply(ao)
|
||||
}
|
||||
|
||||
return ao
|
||||
}
|
||||
|
||||
// ApplyToSubResourceApply applies the configuration on the given patch options.
|
||||
func (ao *SubResourceApplyOptions) ApplyToSubResourceApply(o *SubResourceApplyOptions) {
|
||||
ao.ApplyOptions.ApplyToApply(&o.ApplyOptions)
|
||||
if ao.SubResourceBody != nil {
|
||||
o.SubResourceBody = ao.SubResourceBody
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *subResourceClient) Get(ctx context.Context, obj Object, subResource Object, opts ...SubResourceGetOption) error {
|
||||
switch obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
return sc.client.unstructuredClient.GetSubResource(ctx, obj, subResource, sc.subResource, opts...)
|
||||
case *metav1.PartialObjectMetadata:
|
||||
return errors.New("can not get subresource using only metadata")
|
||||
default:
|
||||
return sc.client.typedClient.GetSubResource(ctx, obj, subResource, sc.subResource, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// Create implements client.SubResourceClient
|
||||
func (sc *subResourceClient) Create(ctx context.Context, obj Object, subResource Object, opts ...SubResourceCreateOption) error {
|
||||
defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
|
||||
defer sc.client.resetGroupVersionKind(subResource, subResource.GetObjectKind().GroupVersionKind())
|
||||
|
||||
switch obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
return sc.client.unstructuredClient.CreateSubResource(ctx, obj, subResource, sc.subResource, opts...)
|
||||
case *metav1.PartialObjectMetadata:
|
||||
return fmt.Errorf("cannot update status using only metadata -- did you mean to patch?")
|
||||
default:
|
||||
return sc.client.typedClient.CreateSubResource(ctx, obj, subResource, sc.subResource, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// Update implements client.SubResourceClient
|
||||
func (sc *subResourceClient) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
|
||||
defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
|
||||
switch obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
return sc.client.unstructuredClient.UpdateSubResource(ctx, obj, sc.subResource, opts...)
|
||||
case *metav1.PartialObjectMetadata:
|
||||
return fmt.Errorf("cannot update status using only metadata -- did you mean to patch?")
|
||||
default:
|
||||
return sc.client.typedClient.UpdateSubResource(ctx, obj, sc.subResource, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// Patch implements client.SubResourceWriter.
|
||||
func (sc *subResourceClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
|
||||
defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
|
||||
switch obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
return sc.client.unstructuredClient.PatchSubResource(ctx, obj, sc.subResource, patch, opts...)
|
||||
case *metav1.PartialObjectMetadata:
|
||||
return sc.client.metadataClient.PatchSubResource(ctx, obj, sc.subResource, patch, opts...)
|
||||
default:
|
||||
return sc.client.typedClient.PatchSubResource(ctx, obj, sc.subResource, patch, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *subResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error {
|
||||
switch obj := obj.(type) {
|
||||
case *unstructuredApplyConfiguration:
|
||||
defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
|
||||
return sc.client.unstructuredClient.ApplySubResource(ctx, obj, sc.subResource, opts...)
|
||||
default:
|
||||
return sc.client.typedClient.ApplySubResource(ctx, obj, sc.subResource, opts...)
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/utils/ptr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
)
|
||||
|
||||
// clientRestResources creates and stores rest clients and metadata for Kubernetes types.
|
||||
type clientRestResources struct {
|
||||
// httpClient is the http client to use for requests
|
||||
httpClient *http.Client
|
||||
|
||||
// config is the rest.Config to talk to an apiserver
|
||||
config *rest.Config
|
||||
|
||||
// scheme maps go structs to GroupVersionKinds
|
||||
scheme *runtime.Scheme
|
||||
|
||||
// mapper maps GroupVersionKinds to Resources
|
||||
mapper meta.RESTMapper
|
||||
|
||||
// codecs are used to create a REST client for a gvk
|
||||
codecs serializer.CodecFactory
|
||||
|
||||
// resourceByType stores type metadata
|
||||
resourceByType map[cacheKey]*resourceMeta
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type cacheKey struct {
|
||||
gvk schema.GroupVersionKind
|
||||
forceDisableProtoBuf bool
|
||||
}
|
||||
|
||||
// newResource maps obj to a Kubernetes Resource and constructs a client for that Resource.
|
||||
// If the object is a list, the resource represents the item's type instead.
|
||||
func (c *clientRestResources) newResource(gvk schema.GroupVersionKind,
|
||||
isList bool,
|
||||
forceDisableProtoBuf bool,
|
||||
isUnstructured bool,
|
||||
) (*resourceMeta, error) {
|
||||
if strings.HasSuffix(gvk.Kind, "List") && isList {
|
||||
// if this was a list, treat it as a request for the item's resource
|
||||
gvk.Kind = gvk.Kind[:len(gvk.Kind)-4]
|
||||
}
|
||||
|
||||
client, err := apiutil.RESTClientForGVK(gvk, forceDisableProtoBuf, isUnstructured, c.config, c.codecs, c.httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapping, err := c.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resourceMeta{Interface: client, mapping: mapping, gvk: gvk}, nil
|
||||
}
|
||||
|
||||
type applyConfiguration interface {
|
||||
GetName() *string
|
||||
GetNamespace() *string
|
||||
GetKind() *string
|
||||
GetAPIVersion() *string
|
||||
}
|
||||
|
||||
// getResource returns the resource meta information for the given type of object.
|
||||
// If the object is a list, the resource represents the item's type instead.
|
||||
func (c *clientRestResources) getResource(obj any) (*resourceMeta, error) {
|
||||
var gvk schema.GroupVersionKind
|
||||
var err error
|
||||
var isApplyConfiguration bool
|
||||
switch o := obj.(type) {
|
||||
case runtime.Object:
|
||||
gvk, err = apiutil.GVKForObject(o, c.scheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case runtime.ApplyConfiguration:
|
||||
ac, ok := o.(applyConfiguration)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T is a runtime.ApplyConfiguration but not an applyConfiguration", o)
|
||||
}
|
||||
gvk, err = gvkFromApplyConfiguration(ac)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isApplyConfiguration = true
|
||||
default:
|
||||
return nil, fmt.Errorf("bug: %T is neither a runtime.Object nor a runtime.ApplyConfiguration", o)
|
||||
}
|
||||
|
||||
_, isUnstructured := obj.(runtime.Unstructured)
|
||||
forceDisableProtoBuf := isUnstructured || isApplyConfiguration
|
||||
|
||||
// It's better to do creation work twice than to not let multiple
|
||||
// people make requests at once
|
||||
c.mu.RLock()
|
||||
|
||||
cacheKey := cacheKey{gvk: gvk, forceDisableProtoBuf: forceDisableProtoBuf}
|
||||
|
||||
r, known := c.resourceByType[cacheKey]
|
||||
|
||||
c.mu.RUnlock()
|
||||
|
||||
if known {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
var isList bool
|
||||
if runtimeObject, ok := obj.(runtime.Object); ok && meta.IsListType(runtimeObject) {
|
||||
isList = true
|
||||
}
|
||||
|
||||
// Initialize a new Client
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
r, err = c.newResource(gvk, isList, forceDisableProtoBuf, isUnstructured)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.resourceByType[cacheKey] = r
|
||||
return r, err
|
||||
}
|
||||
|
||||
// getObjMeta returns objMeta containing both type and object metadata and state.
|
||||
func (c *clientRestResources) getObjMeta(obj any) (*objMeta, error) {
|
||||
r, err := c.getResource(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objMeta := &objMeta{resourceMeta: r}
|
||||
|
||||
switch o := obj.(type) {
|
||||
case runtime.Object:
|
||||
m, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objMeta.namespace = m.GetNamespace()
|
||||
objMeta.name = m.GetName()
|
||||
case applyConfiguration:
|
||||
objMeta.namespace = ptr.Deref(o.GetNamespace(), "")
|
||||
objMeta.name = ptr.Deref(o.GetName(), "")
|
||||
default:
|
||||
return nil, fmt.Errorf("object %T is neither a runtime.Object nor a runtime.ApplyConfiguration", obj)
|
||||
}
|
||||
|
||||
return objMeta, nil
|
||||
}
|
||||
|
||||
// resourceMeta stores state for a Kubernetes type.
|
||||
type resourceMeta struct {
|
||||
// client is the rest client used to talk to the apiserver
|
||||
rest.Interface
|
||||
// gvk is the GroupVersionKind of the resourceMeta
|
||||
gvk schema.GroupVersionKind
|
||||
// mapping is the rest mapping
|
||||
mapping *meta.RESTMapping
|
||||
}
|
||||
|
||||
// isNamespaced returns true if the type is namespaced.
|
||||
func (r *resourceMeta) isNamespaced() bool {
|
||||
return r.mapping.Scope.Name() != meta.RESTScopeNameRoot
|
||||
}
|
||||
|
||||
// resource returns the resource name of the type.
|
||||
func (r *resourceMeta) resource() string {
|
||||
return r.mapping.Resource.Resource
|
||||
}
|
||||
|
||||
// objMeta stores type and object information about a Kubernetes type.
|
||||
type objMeta struct {
|
||||
// resourceMeta contains type information for the object
|
||||
*resourceMeta
|
||||
|
||||
namespace string
|
||||
name string
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"k8s.io/apimachinery/pkg/conversion/queryparams"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var _ runtime.ParameterCodec = noConversionParamCodec{}
|
||||
|
||||
// noConversionParamCodec is a no-conversion codec for serializing parameters into URL query strings.
|
||||
// it's useful in scenarios with the unstructured client and arbitrary resources.
|
||||
type noConversionParamCodec struct{}
|
||||
|
||||
func (noConversionParamCodec) EncodeParameters(obj runtime.Object, to schema.GroupVersion) (url.Values, error) {
|
||||
return queryparams.Convert(obj)
|
||||
}
|
||||
|
||||
func (noConversionParamCodec) DecodeParameters(parameters url.Values, from schema.GroupVersion, into runtime.Object) error {
|
||||
return errors.New("DecodeParameters not implemented on noConversionParamCodec")
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
// Package client contains functionality for interacting with Kubernetes API
|
||||
// servers.
|
||||
//
|
||||
// # Clients
|
||||
//
|
||||
// Clients are split into two interfaces -- Readers and Writers. Readers
|
||||
// get and list, while writers create, update, and delete.
|
||||
//
|
||||
// The New function can be used to create a new client that talks directly
|
||||
// to the API server.
|
||||
//
|
||||
// It is a common pattern in Kubernetes to read from a cache and write to the API
|
||||
// server. This pattern is covered by the creating the Client with a Cache.
|
||||
//
|
||||
// # Options
|
||||
//
|
||||
// Many client operations in Kubernetes support options. These options are
|
||||
// represented as variadic arguments at the end of a given method call.
|
||||
// For instance, to use a label selector on list, you can call
|
||||
//
|
||||
// err := someReader.List(context.Background(), &podList, client.MatchingLabels{"somelabel": "someval"})
|
||||
//
|
||||
// # Indexing
|
||||
//
|
||||
// Indexes may be added to caches using a FieldIndexer. This allows you to easily
|
||||
// and efficiently look up objects with certain properties. You can then make
|
||||
// use of the index by specifying a field selector on calls to List on the Reader
|
||||
// corresponding to the given Cache.
|
||||
//
|
||||
// For instance, a Secret controller might have an index on the
|
||||
// `.spec.volumes.secret.secretName` field in Pod objects, so that it could
|
||||
// easily look up all pods that reference a given secret.
|
||||
package client
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// NewDryRunClient wraps an existing client and enforces DryRun mode
|
||||
// on all mutating api calls.
|
||||
func NewDryRunClient(c Client) Client {
|
||||
return &dryRunClient{client: c}
|
||||
}
|
||||
|
||||
var _ Client = &dryRunClient{}
|
||||
|
||||
// dryRunClient is a Client that wraps another Client in order to enforce DryRun mode.
|
||||
type dryRunClient struct {
|
||||
client Client
|
||||
}
|
||||
|
||||
// Scheme returns the scheme this client is using.
|
||||
func (c *dryRunClient) Scheme() *runtime.Scheme {
|
||||
return c.client.Scheme()
|
||||
}
|
||||
|
||||
// RESTMapper returns the rest mapper this client is using.
|
||||
func (c *dryRunClient) RESTMapper() meta.RESTMapper {
|
||||
return c.client.RESTMapper()
|
||||
}
|
||||
|
||||
// GroupVersionKindFor returns the GroupVersionKind for the given object.
|
||||
func (c *dryRunClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
|
||||
return c.client.GroupVersionKindFor(obj)
|
||||
}
|
||||
|
||||
// IsObjectNamespaced returns true if the GroupVersionKind of the object is namespaced.
|
||||
func (c *dryRunClient) IsObjectNamespaced(obj runtime.Object) (bool, error) {
|
||||
return c.client.IsObjectNamespaced(obj)
|
||||
}
|
||||
|
||||
// Create implements client.Client.
|
||||
func (c *dryRunClient) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
|
||||
return c.client.Create(ctx, obj, append(opts, DryRunAll)...)
|
||||
}
|
||||
|
||||
// Update implements client.Client.
|
||||
func (c *dryRunClient) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
|
||||
return c.client.Update(ctx, obj, append(opts, DryRunAll)...)
|
||||
}
|
||||
|
||||
// Delete implements client.Client.
|
||||
func (c *dryRunClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
|
||||
return c.client.Delete(ctx, obj, append(opts, DryRunAll)...)
|
||||
}
|
||||
|
||||
// DeleteAllOf implements client.Client.
|
||||
func (c *dryRunClient) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
|
||||
return c.client.DeleteAllOf(ctx, obj, append(opts, DryRunAll)...)
|
||||
}
|
||||
|
||||
// Patch implements client.Client.
|
||||
func (c *dryRunClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
|
||||
return c.client.Patch(ctx, obj, patch, append(opts, DryRunAll)...)
|
||||
}
|
||||
|
||||
func (c *dryRunClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error {
|
||||
return c.client.Apply(ctx, obj, append(opts, DryRunAll)...)
|
||||
}
|
||||
|
||||
// Get implements client.Client.
|
||||
func (c *dryRunClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
|
||||
return c.client.Get(ctx, key, obj, opts...)
|
||||
}
|
||||
|
||||
// List implements client.Client.
|
||||
func (c *dryRunClient) List(ctx context.Context, obj ObjectList, opts ...ListOption) error {
|
||||
return c.client.List(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Status implements client.StatusClient.
|
||||
func (c *dryRunClient) Status() SubResourceWriter {
|
||||
return c.SubResource("status")
|
||||
}
|
||||
|
||||
// SubResource implements client.SubResourceClient.
|
||||
func (c *dryRunClient) SubResource(subResource string) SubResourceClient {
|
||||
return &dryRunSubResourceClient{client: c.client.SubResource(subResource)}
|
||||
}
|
||||
|
||||
// ensure dryRunSubResourceWriter implements client.SubResourceWriter.
|
||||
var _ SubResourceWriter = &dryRunSubResourceClient{}
|
||||
|
||||
// dryRunSubResourceClient is client.SubResourceWriter that writes status subresource with dryRun mode
|
||||
// enforced.
|
||||
type dryRunSubResourceClient struct {
|
||||
client SubResourceClient
|
||||
}
|
||||
|
||||
func (sw *dryRunSubResourceClient) Get(ctx context.Context, obj, subResource Object, opts ...SubResourceGetOption) error {
|
||||
return sw.client.Get(ctx, obj, subResource, opts...)
|
||||
}
|
||||
|
||||
func (sw *dryRunSubResourceClient) Create(ctx context.Context, obj, subResource Object, opts ...SubResourceCreateOption) error {
|
||||
return sw.client.Create(ctx, obj, subResource, append(opts, DryRunAll)...)
|
||||
}
|
||||
|
||||
// Update implements client.SubResourceWriter.
|
||||
func (sw *dryRunSubResourceClient) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
|
||||
return sw.client.Update(ctx, obj, append(opts, DryRunAll)...)
|
||||
}
|
||||
|
||||
// Patch implements client.SubResourceWriter.
|
||||
func (sw *dryRunSubResourceClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
|
||||
return sw.client.Patch(ctx, obj, patch, append(opts, DryRunAll)...)
|
||||
}
|
||||
|
||||
func (sw *dryRunSubResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error {
|
||||
return sw.client.Apply(ctx, obj, append(opts, DryRunAll)...)
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2024 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// WithFieldOwner wraps a Client and adds the fieldOwner as the field
|
||||
// manager to all write requests from this client. If additional [FieldOwner]
|
||||
// options are specified on methods of this client, the value specified here
|
||||
// will be overridden.
|
||||
func WithFieldOwner(c Client, fieldOwner string) Client {
|
||||
return &clientWithFieldManager{
|
||||
owner: fieldOwner,
|
||||
c: c,
|
||||
Reader: c,
|
||||
}
|
||||
}
|
||||
|
||||
type clientWithFieldManager struct {
|
||||
owner string
|
||||
c Client
|
||||
Reader
|
||||
}
|
||||
|
||||
func (f *clientWithFieldManager) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
|
||||
return f.c.Create(ctx, obj, append([]CreateOption{FieldOwner(f.owner)}, opts...)...)
|
||||
}
|
||||
|
||||
func (f *clientWithFieldManager) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
|
||||
return f.c.Update(ctx, obj, append([]UpdateOption{FieldOwner(f.owner)}, opts...)...)
|
||||
}
|
||||
|
||||
func (f *clientWithFieldManager) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
|
||||
return f.c.Patch(ctx, obj, patch, append([]PatchOption{FieldOwner(f.owner)}, opts...)...)
|
||||
}
|
||||
|
||||
func (f *clientWithFieldManager) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error {
|
||||
return f.c.Apply(ctx, obj, append([]ApplyOption{FieldOwner(f.owner)}, opts...)...)
|
||||
}
|
||||
|
||||
func (f *clientWithFieldManager) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
|
||||
return f.c.Delete(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
func (f *clientWithFieldManager) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
|
||||
return f.c.DeleteAllOf(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
func (f *clientWithFieldManager) Scheme() *runtime.Scheme { return f.c.Scheme() }
|
||||
func (f *clientWithFieldManager) RESTMapper() meta.RESTMapper { return f.c.RESTMapper() }
|
||||
func (f *clientWithFieldManager) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
|
||||
return f.c.GroupVersionKindFor(obj)
|
||||
}
|
||||
func (f *clientWithFieldManager) IsObjectNamespaced(obj runtime.Object) (bool, error) {
|
||||
return f.c.IsObjectNamespaced(obj)
|
||||
}
|
||||
|
||||
func (f *clientWithFieldManager) Status() StatusWriter {
|
||||
return &subresourceClientWithFieldOwner{
|
||||
owner: f.owner,
|
||||
subresourceWriter: f.c.Status(),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *clientWithFieldManager) SubResource(subresource string) SubResourceClient {
|
||||
c := f.c.SubResource(subresource)
|
||||
return &subresourceClientWithFieldOwner{
|
||||
owner: f.owner,
|
||||
subresourceWriter: c,
|
||||
SubResourceReader: c,
|
||||
}
|
||||
}
|
||||
|
||||
type subresourceClientWithFieldOwner struct {
|
||||
owner string
|
||||
subresourceWriter SubResourceWriter
|
||||
SubResourceReader
|
||||
}
|
||||
|
||||
func (f *subresourceClientWithFieldOwner) Create(ctx context.Context, obj Object, subresource Object, opts ...SubResourceCreateOption) error {
|
||||
return f.subresourceWriter.Create(ctx, obj, subresource, append([]SubResourceCreateOption{FieldOwner(f.owner)}, opts...)...)
|
||||
}
|
||||
|
||||
func (f *subresourceClientWithFieldOwner) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
|
||||
return f.subresourceWriter.Update(ctx, obj, append([]SubResourceUpdateOption{FieldOwner(f.owner)}, opts...)...)
|
||||
}
|
||||
|
||||
func (f *subresourceClientWithFieldOwner) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
|
||||
return f.subresourceWriter.Patch(ctx, obj, patch, append([]SubResourcePatchOption{FieldOwner(f.owner)}, opts...)...)
|
||||
}
|
||||
|
||||
func (f *subresourceClientWithFieldOwner) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error {
|
||||
return f.subresourceWriter.Apply(ctx, obj, append([]SubResourceApplyOption{FieldOwner(f.owner)}, opts...)...)
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
Copyright 2024 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// WithFieldValidation wraps a Client and configures field validation, by
|
||||
// default, for all write requests from this client. Users can override field
|
||||
// validation for individual write requests.
|
||||
//
|
||||
// This wrapper has no effect on apply requests, as they do not support a
|
||||
// custom fieldValidation setting, it is always strict.
|
||||
func WithFieldValidation(c Client, validation FieldValidation) Client {
|
||||
return &clientWithFieldValidation{
|
||||
validation: validation,
|
||||
client: c,
|
||||
Reader: c,
|
||||
}
|
||||
}
|
||||
|
||||
type clientWithFieldValidation struct {
|
||||
validation FieldValidation
|
||||
client Client
|
||||
Reader
|
||||
}
|
||||
|
||||
func (c *clientWithFieldValidation) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
|
||||
return c.client.Create(ctx, obj, append([]CreateOption{c.validation}, opts...)...)
|
||||
}
|
||||
|
||||
func (c *clientWithFieldValidation) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
|
||||
return c.client.Update(ctx, obj, append([]UpdateOption{c.validation}, opts...)...)
|
||||
}
|
||||
|
||||
func (c *clientWithFieldValidation) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
|
||||
return c.client.Patch(ctx, obj, patch, append([]PatchOption{c.validation}, opts...)...)
|
||||
}
|
||||
|
||||
func (c *clientWithFieldValidation) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error {
|
||||
return c.client.Apply(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
func (c *clientWithFieldValidation) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
|
||||
return c.client.Delete(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
func (c *clientWithFieldValidation) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
|
||||
return c.client.DeleteAllOf(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
func (c *clientWithFieldValidation) Scheme() *runtime.Scheme { return c.client.Scheme() }
|
||||
func (c *clientWithFieldValidation) RESTMapper() meta.RESTMapper { return c.client.RESTMapper() }
|
||||
func (c *clientWithFieldValidation) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
|
||||
return c.client.GroupVersionKindFor(obj)
|
||||
}
|
||||
|
||||
func (c *clientWithFieldValidation) IsObjectNamespaced(obj runtime.Object) (bool, error) {
|
||||
return c.client.IsObjectNamespaced(obj)
|
||||
}
|
||||
|
||||
func (c *clientWithFieldValidation) Status() StatusWriter {
|
||||
return &subresourceClientWithFieldValidation{
|
||||
validation: c.validation,
|
||||
subresourceWriter: c.client.Status(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clientWithFieldValidation) SubResource(subresource string) SubResourceClient {
|
||||
srClient := c.client.SubResource(subresource)
|
||||
return &subresourceClientWithFieldValidation{
|
||||
validation: c.validation,
|
||||
subresourceWriter: srClient,
|
||||
SubResourceReader: srClient,
|
||||
}
|
||||
}
|
||||
|
||||
type subresourceClientWithFieldValidation struct {
|
||||
validation FieldValidation
|
||||
subresourceWriter SubResourceWriter
|
||||
SubResourceReader
|
||||
}
|
||||
|
||||
func (c *subresourceClientWithFieldValidation) Create(ctx context.Context, obj Object, subresource Object, opts ...SubResourceCreateOption) error {
|
||||
return c.subresourceWriter.Create(ctx, obj, subresource, append([]SubResourceCreateOption{c.validation}, opts...)...)
|
||||
}
|
||||
|
||||
func (c *subresourceClientWithFieldValidation) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
|
||||
return c.subresourceWriter.Update(ctx, obj, append([]SubResourceUpdateOption{c.validation}, opts...)...)
|
||||
}
|
||||
|
||||
func (c *subresourceClientWithFieldValidation) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
|
||||
return c.subresourceWriter.Patch(ctx, obj, patch, append([]SubResourcePatchOption{c.validation}, opts...)...)
|
||||
}
|
||||
|
||||
func (c *subresourceClientWithFieldValidation) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error {
|
||||
return c.subresourceWriter.Apply(ctx, obj, opts...)
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
// ObjectKey identifies a Kubernetes Object.
|
||||
type ObjectKey = types.NamespacedName
|
||||
|
||||
// ObjectKeyFromObject returns the ObjectKey given a runtime.Object.
|
||||
func ObjectKeyFromObject(obj Object) ObjectKey {
|
||||
return ObjectKey{Namespace: obj.GetNamespace(), Name: obj.GetName()}
|
||||
}
|
||||
|
||||
// Patch is a patch that can be applied to a Kubernetes object.
|
||||
type Patch interface {
|
||||
// Type is the PatchType of the patch.
|
||||
Type() types.PatchType
|
||||
// Data is the raw data representing the patch.
|
||||
Data(obj Object) ([]byte, error)
|
||||
}
|
||||
|
||||
// TODO(directxman12): is there a sane way to deal with get/delete options?
|
||||
|
||||
// Reader knows how to read and list Kubernetes objects.
|
||||
type Reader interface {
|
||||
// Get retrieves an obj for the given object key from the Kubernetes Cluster.
|
||||
// obj must be a struct pointer so that obj can be updated with the response
|
||||
// returned by the Server.
|
||||
Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error
|
||||
|
||||
// List retrieves list of objects for a given namespace and list options. On a
|
||||
// successful call, Items field in the list will be populated with the
|
||||
// result returned from the server.
|
||||
List(ctx context.Context, list ObjectList, opts ...ListOption) error
|
||||
}
|
||||
|
||||
// Writer knows how to create, delete, and update Kubernetes objects.
|
||||
type Writer interface {
|
||||
// Apply applies the given apply configuration to the Kubernetes cluster.
|
||||
Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error
|
||||
|
||||
// Create saves the object obj in the Kubernetes cluster. obj must be a
|
||||
// struct pointer so that obj can be updated with the content returned by the Server.
|
||||
Create(ctx context.Context, obj Object, opts ...CreateOption) error
|
||||
|
||||
// Delete deletes the given obj from Kubernetes cluster.
|
||||
Delete(ctx context.Context, obj Object, opts ...DeleteOption) error
|
||||
|
||||
// Update updates the given obj in the Kubernetes cluster. obj must be a
|
||||
// struct pointer so that obj can be updated with the content returned by the Server.
|
||||
Update(ctx context.Context, obj Object, opts ...UpdateOption) error
|
||||
|
||||
// Patch patches the given obj in the Kubernetes cluster. obj must be a
|
||||
// struct pointer so that obj can be updated with the content returned by the Server.
|
||||
Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error
|
||||
|
||||
// DeleteAllOf deletes all objects of the given type matching the given options.
|
||||
DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error
|
||||
}
|
||||
|
||||
// StatusClient knows how to create a client which can update status subresource
|
||||
// for kubernetes objects.
|
||||
type StatusClient interface {
|
||||
Status() SubResourceWriter
|
||||
}
|
||||
|
||||
// SubResourceClientConstructor knows how to create a client which can update subresource
|
||||
// for kubernetes objects.
|
||||
type SubResourceClientConstructor interface {
|
||||
// SubResourceClientConstructor returns a subresource client for the named subResource. Known
|
||||
// upstream subResources usages are:
|
||||
// - ServiceAccount token creation:
|
||||
// sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
|
||||
// token := &authenticationv1.TokenRequest{}
|
||||
// c.SubResource("token").Create(ctx, sa, token)
|
||||
//
|
||||
// - Pod eviction creation:
|
||||
// pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
|
||||
// c.SubResource("eviction").Create(ctx, pod, &policyv1.Eviction{})
|
||||
//
|
||||
// - Pod binding creation:
|
||||
// pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
|
||||
// binding := &corev1.Binding{Target: corev1.ObjectReference{Name: "my-node"}}
|
||||
// c.SubResource("binding").Create(ctx, pod, binding)
|
||||
//
|
||||
// - CertificateSigningRequest approval:
|
||||
// csr := &certificatesv1.CertificateSigningRequest{
|
||||
// ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"},
|
||||
// Status: certificatesv1.CertificateSigningRequestStatus{
|
||||
// Conditions: []certificatesv1.[]CertificateSigningRequestCondition{{
|
||||
// Type: certificatesv1.CertificateApproved,
|
||||
// Status: corev1.ConditionTrue,
|
||||
// }},
|
||||
// },
|
||||
// }
|
||||
// c.SubResource("approval").Update(ctx, csr)
|
||||
//
|
||||
// - Scale retrieval:
|
||||
// dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
|
||||
// scale := &autoscalingv1.Scale{}
|
||||
// c.SubResource("scale").Get(ctx, dep, scale)
|
||||
//
|
||||
// - Scale update:
|
||||
// dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
|
||||
// scale := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 2}}
|
||||
// c.SubResource("scale").Update(ctx, dep, client.WithSubResourceBody(scale))
|
||||
SubResource(subResource string) SubResourceClient
|
||||
}
|
||||
|
||||
// StatusWriter is kept for backward compatibility.
|
||||
type StatusWriter = SubResourceWriter
|
||||
|
||||
// SubResourceReader knows how to read SubResources
|
||||
type SubResourceReader interface {
|
||||
Get(ctx context.Context, obj Object, subResource Object, opts ...SubResourceGetOption) error
|
||||
}
|
||||
|
||||
// SubResourceWriter knows how to update subresource of a Kubernetes object.
|
||||
type SubResourceWriter interface {
|
||||
// Create saves the subResource object in the Kubernetes cluster. obj must be a
|
||||
// struct pointer so that obj can be updated with the content returned by the Server.
|
||||
Create(ctx context.Context, obj Object, subResource Object, opts ...SubResourceCreateOption) error
|
||||
|
||||
// Update updates the fields corresponding to the status subresource for the
|
||||
// given obj. obj must be a struct pointer so that obj can be updated
|
||||
// with the content returned by the Server.
|
||||
Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error
|
||||
|
||||
// Patch patches the given object's subresource. obj must be a struct
|
||||
// pointer so that obj can be updated with the content returned by the
|
||||
// Server.
|
||||
Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error
|
||||
|
||||
// Apply applies the given apply configurations subresource.
|
||||
Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error
|
||||
}
|
||||
|
||||
// SubResourceClient knows how to perform CRU operations on Kubernetes objects.
|
||||
type SubResourceClient interface {
|
||||
SubResourceReader
|
||||
SubResourceWriter
|
||||
}
|
||||
|
||||
// Client knows how to perform CRUD operations on Kubernetes objects.
|
||||
type Client interface {
|
||||
Reader
|
||||
Writer
|
||||
StatusClient
|
||||
SubResourceClientConstructor
|
||||
|
||||
// Scheme returns the scheme this client is using.
|
||||
Scheme() *runtime.Scheme
|
||||
// RESTMapper returns the rest this client is using.
|
||||
RESTMapper() meta.RESTMapper
|
||||
// GroupVersionKindFor returns the GroupVersionKind for the given object.
|
||||
GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error)
|
||||
// IsObjectNamespaced returns true if the GroupVersionKind of the object is namespaced.
|
||||
IsObjectNamespaced(obj runtime.Object) (bool, error)
|
||||
}
|
||||
|
||||
// WithWatch supports Watch on top of the CRUD operations supported by
|
||||
// the normal Client. Its intended use-case are CLI apps that need to wait for
|
||||
// events.
|
||||
type WithWatch interface {
|
||||
Client
|
||||
Watch(ctx context.Context, obj ObjectList, opts ...ListOption) (watch.Interface, error)
|
||||
}
|
||||
|
||||
// IndexerFunc knows how to take an object and turn it into a series
|
||||
// of non-namespaced keys. Namespaced objects are automatically given
|
||||
// namespaced and non-spaced variants, so keys do not need to include namespace.
|
||||
type IndexerFunc func(Object) []string
|
||||
|
||||
// FieldIndexer knows how to index over a particular "field" such that it
|
||||
// can later be used by a field selector.
|
||||
type FieldIndexer interface {
|
||||
// IndexField adds an index with the given field name on the given object type
|
||||
// by using the given function to extract the value for that field. If you want
|
||||
// compatibility with the Kubernetes API server, only return one key, and only use
|
||||
// fields that the API server supports. Otherwise, you can return multiple keys,
|
||||
// and "equality" in the field selector means that at least one key matches the value.
|
||||
// The FieldIndexer will automatically take care of indexing over namespace
|
||||
// and supporting efficient all-namespace queries.
|
||||
IndexField(ctx context.Context, obj Object, field string, extractValue IndexerFunc) error
|
||||
}
|
||||
|
||||
// IgnoreNotFound returns nil on NotFound errors.
|
||||
// All other values that are not NotFound errors or nil are returned unmodified.
|
||||
func IgnoreNotFound(err error) error {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// IgnoreAlreadyExists returns nil on AlreadyExists errors.
|
||||
// All other values that are not AlreadyExists errors or nil are returned unmodified.
|
||||
func IgnoreAlreadyExists(err error) error {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/metadata"
|
||||
)
|
||||
|
||||
// TODO(directxman12): we could rewrite this on top of the low-level REST
|
||||
// client to avoid the extra shallow copy at the end, but I'm not sure it's
|
||||
// worth it -- the metadata client deals with falling back to loading the whole
|
||||
// object on older API servers, etc, and we'd have to reproduce that.
|
||||
|
||||
// metadataClient is a client that reads & writes metadata-only requests to/from the API server.
|
||||
type metadataClient struct {
|
||||
client metadata.Interface
|
||||
restMapper meta.RESTMapper
|
||||
}
|
||||
|
||||
func (mc *metadataClient) getResourceInterface(gvk schema.GroupVersionKind, ns string) (metadata.ResourceInterface, error) {
|
||||
mapping, err := mc.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mapping.Scope.Name() == meta.RESTScopeNameRoot {
|
||||
return mc.client.Resource(mapping.Resource), nil
|
||||
}
|
||||
return mc.client.Resource(mapping.Resource).Namespace(ns), nil
|
||||
}
|
||||
|
||||
// Delete implements client.Client.
|
||||
func (mc *metadataClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
|
||||
metadata, ok := obj.(*metav1.PartialObjectMetadata)
|
||||
if !ok {
|
||||
return fmt.Errorf("metadata client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
resInt, err := mc.getResourceInterface(metadata.GroupVersionKind(), metadata.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteOpts := DeleteOptions{}
|
||||
deleteOpts.ApplyOptions(opts)
|
||||
|
||||
return resInt.Delete(ctx, metadata.Name, *deleteOpts.AsDeleteOptions())
|
||||
}
|
||||
|
||||
// DeleteAllOf implements client.Client.
|
||||
func (mc *metadataClient) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
|
||||
metadata, ok := obj.(*metav1.PartialObjectMetadata)
|
||||
if !ok {
|
||||
return fmt.Errorf("metadata client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
deleteAllOfOpts := DeleteAllOfOptions{}
|
||||
deleteAllOfOpts.ApplyOptions(opts)
|
||||
|
||||
resInt, err := mc.getResourceInterface(metadata.GroupVersionKind(), deleteAllOfOpts.ListOptions.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return resInt.DeleteCollection(ctx, *deleteAllOfOpts.AsDeleteOptions(), *deleteAllOfOpts.AsListOptions())
|
||||
}
|
||||
|
||||
// Patch implements client.Client.
|
||||
func (mc *metadataClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
|
||||
metadata, ok := obj.(*metav1.PartialObjectMetadata)
|
||||
if !ok {
|
||||
return fmt.Errorf("metadata client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
gvk := metadata.GroupVersionKind()
|
||||
resInt, err := mc.getResourceInterface(gvk, metadata.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := patch.Data(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patchOpts := &PatchOptions{}
|
||||
patchOpts.ApplyOptions(opts)
|
||||
|
||||
res, err := resInt.Patch(ctx, metadata.Name, patch.Type(), data, *patchOpts.AsPatchOptions())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*metadata = *res
|
||||
metadata.SetGroupVersionKind(gvk) // restore the GVK, which isn't set on metadata
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get implements client.Client.
|
||||
func (mc *metadataClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
|
||||
metadata, ok := obj.(*metav1.PartialObjectMetadata)
|
||||
if !ok {
|
||||
return fmt.Errorf("metadata client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
gvk := metadata.GroupVersionKind()
|
||||
|
||||
getOpts := GetOptions{}
|
||||
getOpts.ApplyOptions(opts)
|
||||
|
||||
resInt, err := mc.getResourceInterface(gvk, key.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := resInt.Get(ctx, key.Name, *getOpts.AsGetOptions())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*metadata = *res
|
||||
metadata.SetGroupVersionKind(gvk) // restore the GVK, which isn't set on metadata
|
||||
return nil
|
||||
}
|
||||
|
||||
// List implements client.Client.
|
||||
func (mc *metadataClient) List(ctx context.Context, obj ObjectList, opts ...ListOption) error {
|
||||
metadata, ok := obj.(*metav1.PartialObjectMetadataList)
|
||||
if !ok {
|
||||
return fmt.Errorf("metadata client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
gvk := metadata.GroupVersionKind()
|
||||
gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")
|
||||
|
||||
listOpts := ListOptions{}
|
||||
listOpts.ApplyOptions(opts)
|
||||
|
||||
resInt, err := mc.getResourceInterface(gvk, listOpts.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := resInt.List(ctx, *listOpts.AsListOptions())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*metadata = *res
|
||||
metadata.SetGroupVersionKind(gvk) // restore the GVK, which isn't set on metadata
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *metadataClient) PatchSubResource(ctx context.Context, obj Object, subResource string, patch Patch, opts ...SubResourcePatchOption) error {
|
||||
metadata, ok := obj.(*metav1.PartialObjectMetadata)
|
||||
if !ok {
|
||||
return fmt.Errorf("metadata client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
gvk := metadata.GroupVersionKind()
|
||||
resInt, err := mc.getResourceInterface(gvk, metadata.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patchOpts := &SubResourcePatchOptions{}
|
||||
patchOpts.ApplyOptions(opts)
|
||||
|
||||
body := obj
|
||||
if patchOpts.SubResourceBody != nil {
|
||||
body = patchOpts.SubResourceBody
|
||||
}
|
||||
|
||||
data, err := patch.Data(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := resInt.Patch(ctx, metadata.Name, patch.Type(), data, *patchOpts.AsPatchOptions(), subResource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*metadata = *res
|
||||
metadata.SetGroupVersionKind(gvk) // restore the GVK, which isn't set on metadata
|
||||
return nil
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/utils/ptr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
)
|
||||
|
||||
// NewNamespacedClient wraps an existing client enforcing the namespace value.
|
||||
// All functions using this client will have the same namespace declared here.
|
||||
func NewNamespacedClient(c Client, ns string) Client {
|
||||
return &namespacedClient{
|
||||
client: c,
|
||||
namespace: ns,
|
||||
}
|
||||
}
|
||||
|
||||
var _ Client = &namespacedClient{}
|
||||
|
||||
// namespacedClient is a Client that wraps another Client in order to enforce the specified namespace value.
|
||||
type namespacedClient struct {
|
||||
namespace string
|
||||
client Client
|
||||
}
|
||||
|
||||
// Scheme returns the scheme this client is using.
|
||||
func (n *namespacedClient) Scheme() *runtime.Scheme {
|
||||
return n.client.Scheme()
|
||||
}
|
||||
|
||||
// RESTMapper returns the scheme this client is using.
|
||||
func (n *namespacedClient) RESTMapper() meta.RESTMapper {
|
||||
return n.client.RESTMapper()
|
||||
}
|
||||
|
||||
// GroupVersionKindFor returns the GroupVersionKind for the given object.
|
||||
func (n *namespacedClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
|
||||
return n.client.GroupVersionKindFor(obj)
|
||||
}
|
||||
|
||||
// IsObjectNamespaced returns true if the GroupVersionKind of the object is namespaced.
|
||||
func (n *namespacedClient) IsObjectNamespaced(obj runtime.Object) (bool, error) {
|
||||
return n.client.IsObjectNamespaced(obj)
|
||||
}
|
||||
|
||||
// Create implements client.Client.
|
||||
func (n *namespacedClient) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
|
||||
isNamespaceScoped, err := n.IsObjectNamespaced(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
|
||||
objectNamespace := obj.GetNamespace()
|
||||
if objectNamespace != n.namespace && objectNamespace != "" {
|
||||
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace)
|
||||
}
|
||||
|
||||
if isNamespaceScoped && objectNamespace == "" {
|
||||
obj.SetNamespace(n.namespace)
|
||||
}
|
||||
return n.client.Create(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Update implements client.Client.
|
||||
func (n *namespacedClient) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
|
||||
isNamespaceScoped, err := n.IsObjectNamespaced(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
|
||||
objectNamespace := obj.GetNamespace()
|
||||
if objectNamespace != n.namespace && objectNamespace != "" {
|
||||
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace)
|
||||
}
|
||||
|
||||
if isNamespaceScoped && objectNamespace == "" {
|
||||
obj.SetNamespace(n.namespace)
|
||||
}
|
||||
return n.client.Update(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Delete implements client.Client.
|
||||
func (n *namespacedClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
|
||||
isNamespaceScoped, err := n.IsObjectNamespaced(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
|
||||
objectNamespace := obj.GetNamespace()
|
||||
if objectNamespace != n.namespace && objectNamespace != "" {
|
||||
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace)
|
||||
}
|
||||
|
||||
if isNamespaceScoped && objectNamespace == "" {
|
||||
obj.SetNamespace(n.namespace)
|
||||
}
|
||||
return n.client.Delete(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// DeleteAllOf implements client.Client.
|
||||
func (n *namespacedClient) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
|
||||
isNamespaceScoped, err := n.IsObjectNamespaced(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
|
||||
if isNamespaceScoped {
|
||||
opts = append(opts, InNamespace(n.namespace))
|
||||
}
|
||||
return n.client.DeleteAllOf(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Patch implements client.Client.
|
||||
func (n *namespacedClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
|
||||
isNamespaceScoped, err := n.IsObjectNamespaced(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
|
||||
objectNamespace := obj.GetNamespace()
|
||||
if objectNamespace != n.namespace && objectNamespace != "" {
|
||||
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace)
|
||||
}
|
||||
|
||||
if isNamespaceScoped && objectNamespace == "" {
|
||||
obj.SetNamespace(n.namespace)
|
||||
}
|
||||
return n.client.Patch(ctx, obj, patch, opts...)
|
||||
}
|
||||
|
||||
func (n *namespacedClient) setNamespaceForApplyConfigIfNamespaceScoped(obj runtime.ApplyConfiguration) error {
|
||||
var gvk schema.GroupVersionKind
|
||||
switch o := obj.(type) {
|
||||
case applyConfiguration:
|
||||
var err error
|
||||
gvk, err = gvkFromApplyConfiguration(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case *unstructuredApplyConfiguration:
|
||||
gvk = o.GroupVersionKind()
|
||||
default:
|
||||
return fmt.Errorf("object %T is not a valid apply configuration", obj)
|
||||
}
|
||||
isNamespaceScoped, err := apiutil.IsGVKNamespaced(gvk, n.RESTMapper())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
if isNamespaceScoped {
|
||||
switch o := obj.(type) {
|
||||
case applyConfiguration:
|
||||
if o.GetNamespace() != nil && *o.GetNamespace() != "" && *o.GetNamespace() != n.namespace {
|
||||
return fmt.Errorf("namespace %s provided for the object %s does not match the namespace %s on the client",
|
||||
*o.GetNamespace(), ptr.Deref(o.GetName(), ""), n.namespace)
|
||||
}
|
||||
v := reflect.ValueOf(o)
|
||||
withNamespace := v.MethodByName("WithNamespace")
|
||||
if !withNamespace.IsValid() {
|
||||
return fmt.Errorf("ApplyConfiguration %T does not have a WithNamespace method", o)
|
||||
}
|
||||
if tp := withNamespace.Type(); tp.NumIn() != 1 || tp.In(0).Kind() != reflect.String {
|
||||
return fmt.Errorf("WithNamespace method of ApplyConfiguration %T must take a single string argument", o)
|
||||
}
|
||||
withNamespace.Call([]reflect.Value{reflect.ValueOf(n.namespace)})
|
||||
case *unstructuredApplyConfiguration:
|
||||
if o.GetNamespace() != "" && o.GetNamespace() != n.namespace {
|
||||
return fmt.Errorf("namespace %s provided for the object %s does not match the namespace %s on the client",
|
||||
o.GetNamespace(), o.GetName(), n.namespace)
|
||||
}
|
||||
o.SetNamespace(n.namespace)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *namespacedClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error {
|
||||
if err := n.setNamespaceForApplyConfigIfNamespaceScoped(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return n.client.Apply(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Get implements client.Client.
|
||||
func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
|
||||
isNamespaceScoped, err := n.IsObjectNamespaced(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
if isNamespaceScoped {
|
||||
if key.Namespace != "" && key.Namespace != n.namespace {
|
||||
return fmt.Errorf("namespace %s provided for the object %s does not match the namespace %s on the client", key.Namespace, obj.GetName(), n.namespace)
|
||||
}
|
||||
key.Namespace = n.namespace
|
||||
}
|
||||
return n.client.Get(ctx, key, obj, opts...)
|
||||
}
|
||||
|
||||
// List implements client.Client.
|
||||
func (n *namespacedClient) List(ctx context.Context, obj ObjectList, opts ...ListOption) error {
|
||||
isNamespaceScoped, err := n.IsObjectNamespaced(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
|
||||
if isNamespaceScoped && n.namespace != "" {
|
||||
opts = append(opts, InNamespace(n.namespace))
|
||||
}
|
||||
return n.client.List(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Status implements client.StatusClient.
|
||||
func (n *namespacedClient) Status() SubResourceWriter {
|
||||
return n.SubResource("status")
|
||||
}
|
||||
|
||||
// SubResource implements client.SubResourceClient.
|
||||
func (n *namespacedClient) SubResource(subResource string) SubResourceClient {
|
||||
return &namespacedClientSubResourceClient{
|
||||
client: n.client.SubResource(subResource),
|
||||
namespacedclient: n,
|
||||
}
|
||||
}
|
||||
|
||||
// ensure namespacedClientSubResourceClient implements client.SubResourceClient.
|
||||
var _ SubResourceClient = &namespacedClientSubResourceClient{}
|
||||
|
||||
type namespacedClientSubResourceClient struct {
|
||||
client SubResourceClient
|
||||
namespacedclient *namespacedClient
|
||||
}
|
||||
|
||||
func (nsw *namespacedClientSubResourceClient) Get(ctx context.Context, obj, subResource Object, opts ...SubResourceGetOption) error {
|
||||
isNamespaceScoped, err := nsw.namespacedclient.IsObjectNamespaced(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
|
||||
objectNamespace := obj.GetNamespace()
|
||||
if objectNamespace != nsw.namespacedclient.namespace && objectNamespace != "" {
|
||||
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespacedclient.namespace)
|
||||
}
|
||||
|
||||
if isNamespaceScoped && objectNamespace == "" {
|
||||
obj.SetNamespace(nsw.namespacedclient.namespace)
|
||||
}
|
||||
|
||||
return nsw.client.Get(ctx, obj, subResource, opts...)
|
||||
}
|
||||
|
||||
func (nsw *namespacedClientSubResourceClient) Create(ctx context.Context, obj, subResource Object, opts ...SubResourceCreateOption) error {
|
||||
isNamespaceScoped, err := nsw.namespacedclient.IsObjectNamespaced(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
|
||||
objectNamespace := obj.GetNamespace()
|
||||
if objectNamespace != nsw.namespacedclient.namespace && objectNamespace != "" {
|
||||
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespacedclient.namespace)
|
||||
}
|
||||
|
||||
if isNamespaceScoped && objectNamespace == "" {
|
||||
obj.SetNamespace(nsw.namespacedclient.namespace)
|
||||
}
|
||||
|
||||
return nsw.client.Create(ctx, obj, subResource, opts...)
|
||||
}
|
||||
|
||||
// Update implements client.SubResourceWriter.
|
||||
func (nsw *namespacedClientSubResourceClient) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
|
||||
isNamespaceScoped, err := nsw.namespacedclient.IsObjectNamespaced(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
|
||||
objectNamespace := obj.GetNamespace()
|
||||
if objectNamespace != nsw.namespacedclient.namespace && objectNamespace != "" {
|
||||
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespacedclient.namespace)
|
||||
}
|
||||
|
||||
if isNamespaceScoped && objectNamespace == "" {
|
||||
obj.SetNamespace(nsw.namespacedclient.namespace)
|
||||
}
|
||||
return nsw.client.Update(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Patch implements client.SubResourceWriter.
|
||||
func (nsw *namespacedClientSubResourceClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
|
||||
isNamespaceScoped, err := nsw.namespacedclient.IsObjectNamespaced(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding the scope of the object: %w", err)
|
||||
}
|
||||
|
||||
objectNamespace := obj.GetNamespace()
|
||||
if objectNamespace != nsw.namespacedclient.namespace && objectNamespace != "" {
|
||||
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespacedclient.namespace)
|
||||
}
|
||||
|
||||
if isNamespaceScoped && objectNamespace == "" {
|
||||
obj.SetNamespace(nsw.namespacedclient.namespace)
|
||||
}
|
||||
return nsw.client.Patch(ctx, obj, patch, opts...)
|
||||
}
|
||||
|
||||
func (nsw *namespacedClientSubResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error {
|
||||
if err := nsw.namespacedclient.setNamespaceForApplyConfigIfNamespaceScoped(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
return nsw.client.Apply(ctx, obj, opts...)
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// Object is a Kubernetes object, allows functions to work indistinctly with
|
||||
// any resource that implements both Object interfaces.
|
||||
//
|
||||
// Semantically, these are objects which are both serializable (runtime.Object)
|
||||
// and identifiable (metav1.Object) -- think any object which you could write
|
||||
// as YAML or JSON, and then `kubectl create`.
|
||||
//
|
||||
// Code-wise, this means that any object which embeds both ObjectMeta (which
|
||||
// provides metav1.Object) and TypeMeta (which provides half of runtime.Object)
|
||||
// and has a `DeepCopyObject` implementation (the other half of runtime.Object)
|
||||
// will implement this by default.
|
||||
//
|
||||
// For example, nearly all the built-in types are Objects, as well as all
|
||||
// KubeBuilder-generated CRDs (unless you do something real funky to them).
|
||||
//
|
||||
// By and large, most things that implement runtime.Object also implement
|
||||
// Object -- it's very rare to have *just* a runtime.Object implementation (the
|
||||
// cases tend to be funky built-in types like Webhook payloads that don't have
|
||||
// a `metadata` field).
|
||||
//
|
||||
// Notice that XYZList types are distinct: they implement ObjectList instead.
|
||||
type Object interface {
|
||||
metav1.Object
|
||||
runtime.Object
|
||||
}
|
||||
|
||||
// ObjectList is a Kubernetes object list, allows functions to work
|
||||
// indistinctly with any resource that implements both runtime.Object and
|
||||
// metav1.ListInterface interfaces.
|
||||
//
|
||||
// Semantically, this is any object which may be serialized (ObjectMeta), and
|
||||
// is a kubernetes list wrapper (has items, pagination fields, etc) -- think
|
||||
// the wrapper used in a response from a `kubectl list --output yaml` call.
|
||||
//
|
||||
// Code-wise, this means that any object which embedds both ListMeta (which
|
||||
// provides metav1.ListInterface) and TypeMeta (which provides half of
|
||||
// runtime.Object) and has a `DeepCopyObject` implementation (the other half of
|
||||
// runtime.Object) will implement this by default.
|
||||
//
|
||||
// For example, nearly all the built-in XYZList types are ObjectLists, as well
|
||||
// as the XYZList types for all KubeBuilder-generated CRDs (unless you do
|
||||
// something real funky to them).
|
||||
//
|
||||
// By and large, most things that are XYZList and implement runtime.Object also
|
||||
// implement ObjectList -- it's very rare to have *just* a runtime.Object
|
||||
// implementation (the cases tend to be funky built-in types like Webhook
|
||||
// payloads that don't have a `metadata` field).
|
||||
//
|
||||
// This is similar to Object, which is almost always implemented by the items
|
||||
// in the list themselves.
|
||||
type ObjectList interface {
|
||||
metav1.ListInterface
|
||||
runtime.Object
|
||||
}
|
||||
+1055
File diff suppressed because it is too large
Load Diff
+215
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
)
|
||||
|
||||
var (
|
||||
// Apply uses server-side apply to patch the given object.
|
||||
//
|
||||
// Deprecated: Use client.Client.Apply() and client.Client.SubResource("subrsource").Apply() instead.
|
||||
Apply Patch = applyPatch{}
|
||||
|
||||
// Merge uses the raw object as a merge patch, without modifications.
|
||||
// Use MergeFrom if you wish to compute a diff instead.
|
||||
Merge Patch = mergePatch{}
|
||||
)
|
||||
|
||||
type patch struct {
|
||||
patchType types.PatchType
|
||||
data []byte
|
||||
}
|
||||
|
||||
// Type implements Patch.
|
||||
func (s *patch) Type() types.PatchType {
|
||||
return s.patchType
|
||||
}
|
||||
|
||||
// Data implements Patch.
|
||||
func (s *patch) Data(obj Object) ([]byte, error) {
|
||||
return s.data, nil
|
||||
}
|
||||
|
||||
// RawPatch constructs a new Patch with the given PatchType and data.
|
||||
func RawPatch(patchType types.PatchType, data []byte) Patch {
|
||||
return &patch{patchType, data}
|
||||
}
|
||||
|
||||
// MergeFromWithOptimisticLock can be used if clients want to make sure a patch
|
||||
// is being applied to the latest resource version of an object.
|
||||
//
|
||||
// The behavior is similar to what an Update would do, without the need to send the
|
||||
// whole object. Usually this method is useful if you might have multiple clients
|
||||
// acting on the same object and the same API version, but with different versions of the Go structs.
|
||||
//
|
||||
// For example, an "older" copy of a Widget that has fields A and B, and a "newer" copy with A, B, and C.
|
||||
// Sending an update using the older struct definition results in C being dropped, whereas using a patch does not.
|
||||
type MergeFromWithOptimisticLock struct{}
|
||||
|
||||
// ApplyToMergeFrom applies this configuration to the given patch options.
|
||||
func (m MergeFromWithOptimisticLock) ApplyToMergeFrom(in *MergeFromOptions) {
|
||||
in.OptimisticLock = true
|
||||
}
|
||||
|
||||
// MergeFromOption is some configuration that modifies options for a merge-from patch data.
|
||||
type MergeFromOption interface {
|
||||
// ApplyToMergeFrom applies this configuration to the given patch options.
|
||||
ApplyToMergeFrom(*MergeFromOptions)
|
||||
}
|
||||
|
||||
// MergeFromOptions contains options to generate a merge-from patch data.
|
||||
type MergeFromOptions struct {
|
||||
// OptimisticLock, when true, includes `metadata.resourceVersion` into the final
|
||||
// patch data. If the `resourceVersion` field doesn't match what's stored,
|
||||
// the operation results in a conflict and clients will need to try again.
|
||||
OptimisticLock bool
|
||||
}
|
||||
|
||||
type mergeFromPatch struct {
|
||||
patchType types.PatchType
|
||||
createPatch func(originalJSON, modifiedJSON []byte, dataStruct any) ([]byte, error)
|
||||
from Object
|
||||
opts MergeFromOptions
|
||||
}
|
||||
|
||||
// Type implements Patch.
|
||||
func (s *mergeFromPatch) Type() types.PatchType {
|
||||
return s.patchType
|
||||
}
|
||||
|
||||
// Data implements Patch.
|
||||
func (s *mergeFromPatch) Data(obj Object) ([]byte, error) {
|
||||
original := s.from
|
||||
modified := obj
|
||||
|
||||
if s.opts.OptimisticLock {
|
||||
version := original.GetResourceVersion()
|
||||
if len(version) == 0 {
|
||||
return nil, fmt.Errorf("cannot use OptimisticLock, object %q does not have any resource version we can use", original)
|
||||
}
|
||||
|
||||
original = original.DeepCopyObject().(Object)
|
||||
original.SetResourceVersion("")
|
||||
|
||||
modified = modified.DeepCopyObject().(Object)
|
||||
modified.SetResourceVersion(version)
|
||||
}
|
||||
|
||||
originalJSON, err := json.Marshal(original)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modifiedJSON, err := json.Marshal(modified)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := s.createPatch(originalJSON, modifiedJSON, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func createMergePatch(originalJSON, modifiedJSON []byte, _ any) ([]byte, error) {
|
||||
return jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
|
||||
}
|
||||
|
||||
func createStrategicMergePatch(originalJSON, modifiedJSON []byte, dataStruct any) ([]byte, error) {
|
||||
return strategicpatch.CreateTwoWayMergePatch(originalJSON, modifiedJSON, dataStruct)
|
||||
}
|
||||
|
||||
// MergeFrom creates a Patch that patches using the merge-patch strategy with the given object as base.
|
||||
// The difference between MergeFrom and StrategicMergeFrom lays in the handling of modified list fields.
|
||||
// When using MergeFrom, existing lists will be completely replaced by new lists.
|
||||
// When using StrategicMergeFrom, the list field's `patchStrategy` is respected if specified in the API type,
|
||||
// e.g. the existing list is not replaced completely but rather merged with the new one using the list's `patchMergeKey`.
|
||||
// See https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/ for more details on
|
||||
// the difference between merge-patch and strategic-merge-patch.
|
||||
func MergeFrom(obj Object) Patch {
|
||||
return &mergeFromPatch{patchType: types.MergePatchType, createPatch: createMergePatch, from: obj}
|
||||
}
|
||||
|
||||
// MergeFromWithOptions creates a Patch that patches using the merge-patch strategy with the given object as base.
|
||||
// See MergeFrom for more details.
|
||||
func MergeFromWithOptions(obj Object, opts ...MergeFromOption) Patch {
|
||||
options := &MergeFromOptions{}
|
||||
for _, opt := range opts {
|
||||
opt.ApplyToMergeFrom(options)
|
||||
}
|
||||
return &mergeFromPatch{patchType: types.MergePatchType, createPatch: createMergePatch, from: obj, opts: *options}
|
||||
}
|
||||
|
||||
// StrategicMergeFrom creates a Patch that patches using the strategic-merge-patch strategy with the given object as base.
|
||||
// The difference between MergeFrom and StrategicMergeFrom lays in the handling of modified list fields.
|
||||
// When using MergeFrom, existing lists will be completely replaced by new lists.
|
||||
// When using StrategicMergeFrom, the list field's `patchStrategy` is respected if specified in the API type,
|
||||
// e.g. the existing list is not replaced completely but rather merged with the new one using the list's `patchMergeKey`.
|
||||
// See https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/ for more details on
|
||||
// the difference between merge-patch and strategic-merge-patch.
|
||||
// Please note, that CRDs don't support strategic-merge-patch, see
|
||||
// https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#advanced-features-and-flexibility
|
||||
func StrategicMergeFrom(obj Object, opts ...MergeFromOption) Patch {
|
||||
options := &MergeFromOptions{}
|
||||
for _, opt := range opts {
|
||||
opt.ApplyToMergeFrom(options)
|
||||
}
|
||||
return &mergeFromPatch{patchType: types.StrategicMergePatchType, createPatch: createStrategicMergePatch, from: obj, opts: *options}
|
||||
}
|
||||
|
||||
// mergePatch uses a raw merge strategy to patch the object.
|
||||
type mergePatch struct{}
|
||||
|
||||
// Type implements Patch.
|
||||
func (p mergePatch) Type() types.PatchType {
|
||||
return types.MergePatchType
|
||||
}
|
||||
|
||||
// Data implements Patch.
|
||||
func (p mergePatch) Data(obj Object) ([]byte, error) {
|
||||
// NB(directxman12): we might technically want to be using an actual encoder
|
||||
// here (in case some more performant encoder is introduced) but this is
|
||||
// correct and sufficient for our uses (it's what the JSON serializer in
|
||||
// client-go does, more-or-less).
|
||||
return json.Marshal(obj)
|
||||
}
|
||||
|
||||
// applyPatch uses server-side apply to patch the object.
|
||||
type applyPatch struct{}
|
||||
|
||||
// Type implements Patch.
|
||||
func (p applyPatch) Type() types.PatchType {
|
||||
return types.ApplyPatchType
|
||||
}
|
||||
|
||||
// Data implements Patch.
|
||||
func (p applyPatch) Data(obj Object) ([]byte, error) {
|
||||
// NB(directxman12): we might technically want to be using an actual encoder
|
||||
// here (in case some more performant encoder is introduced) but this is
|
||||
// correct and sufficient for our uses (it's what the JSON serializer in
|
||||
// client-go does, more-or-less).
|
||||
return json.Marshal(obj)
|
||||
}
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/util/apply"
|
||||
)
|
||||
|
||||
var _ Reader = &typedClient{}
|
||||
var _ Writer = &typedClient{}
|
||||
|
||||
type typedClient struct {
|
||||
resources *clientRestResources
|
||||
paramCodec runtime.ParameterCodec
|
||||
}
|
||||
|
||||
// Create implements client.Client.
|
||||
func (c *typedClient) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
|
||||
o, err := c.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createOpts := &CreateOptions{}
|
||||
createOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Post().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Body(obj).
|
||||
VersionedParams(createOpts.AsCreateOptions(), c.paramCodec).
|
||||
Do(ctx).
|
||||
Into(obj)
|
||||
}
|
||||
|
||||
// Update implements client.Client.
|
||||
func (c *typedClient) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
|
||||
o, err := c.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateOpts := &UpdateOptions{}
|
||||
updateOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Put().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
Body(obj).
|
||||
VersionedParams(updateOpts.AsUpdateOptions(), c.paramCodec).
|
||||
Do(ctx).
|
||||
Into(obj)
|
||||
}
|
||||
|
||||
// Delete implements client.Client.
|
||||
func (c *typedClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
|
||||
o, err := c.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteOpts := DeleteOptions{}
|
||||
deleteOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Delete().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
Body(deleteOpts.AsDeleteOptions()).
|
||||
Do(ctx).
|
||||
Error()
|
||||
}
|
||||
|
||||
// DeleteAllOf implements client.Client.
|
||||
func (c *typedClient) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
|
||||
o, err := c.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteAllOfOpts := DeleteAllOfOptions{}
|
||||
deleteAllOfOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Delete().
|
||||
NamespaceIfScoped(deleteAllOfOpts.ListOptions.Namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
VersionedParams(deleteAllOfOpts.AsListOptions(), c.paramCodec).
|
||||
Body(deleteAllOfOpts.AsDeleteOptions()).
|
||||
Do(ctx).
|
||||
Error()
|
||||
}
|
||||
|
||||
// Patch implements client.Client.
|
||||
func (c *typedClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
|
||||
o, err := c.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := patch.Data(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patchOpts := &PatchOptions{}
|
||||
patchOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Patch(patch.Type()).
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
VersionedParams(patchOpts.AsPatchOptions(), c.paramCodec).
|
||||
Body(data).
|
||||
Do(ctx).
|
||||
Into(obj)
|
||||
}
|
||||
|
||||
func (c *typedClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error {
|
||||
o, err := c.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := apply.NewRequest(o, obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create apply request: %w", err)
|
||||
}
|
||||
applyOpts := &ApplyOptions{}
|
||||
applyOpts.ApplyOptions(opts)
|
||||
|
||||
return req.
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
VersionedParams(applyOpts.AsPatchOptions(), c.paramCodec).
|
||||
Do(ctx).
|
||||
// This is hacky, it is required because `Into` takes a `runtime.Object` and
|
||||
// that is not implemented by the ApplyConfigurations. The generated clients
|
||||
// don't have this problem because they deserialize into the api type, not the
|
||||
// apply configuration: https://github.com/kubernetes/kubernetes/blob/22f5e01a37c0bc6a5f494dec14dd4e3688ee1d55/staging/src/k8s.io/client-go/gentype/type.go#L296-L317
|
||||
Into(runtimeObjectFromApplyConfiguration(obj))
|
||||
}
|
||||
|
||||
// Get implements client.Client.
|
||||
func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
|
||||
r, err := c.resources.getResource(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
getOpts := GetOptions{}
|
||||
getOpts.ApplyOptions(opts)
|
||||
return r.Get().
|
||||
NamespaceIfScoped(key.Namespace, r.isNamespaced()).
|
||||
Resource(r.resource()).
|
||||
VersionedParams(getOpts.AsGetOptions(), c.paramCodec).
|
||||
Name(key.Name).Do(ctx).Into(obj)
|
||||
}
|
||||
|
||||
// List implements client.Client.
|
||||
func (c *typedClient) List(ctx context.Context, obj ObjectList, opts ...ListOption) error {
|
||||
r, err := c.resources.getResource(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listOpts := ListOptions{}
|
||||
listOpts.ApplyOptions(opts)
|
||||
|
||||
return r.Get().
|
||||
NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()).
|
||||
Resource(r.resource()).
|
||||
VersionedParams(listOpts.AsListOptions(), c.paramCodec).
|
||||
Do(ctx).
|
||||
Into(obj)
|
||||
}
|
||||
|
||||
func (c *typedClient) GetSubResource(ctx context.Context, obj, subResourceObj Object, subResource string, opts ...SubResourceGetOption) error {
|
||||
o, err := c.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if subResourceObj.GetName() == "" {
|
||||
subResourceObj.SetName(obj.GetName())
|
||||
}
|
||||
|
||||
getOpts := &SubResourceGetOptions{}
|
||||
getOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Get().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
SubResource(subResource).
|
||||
VersionedParams(getOpts.AsGetOptions(), c.paramCodec).
|
||||
Do(ctx).
|
||||
Into(subResourceObj)
|
||||
}
|
||||
|
||||
func (c *typedClient) CreateSubResource(ctx context.Context, obj Object, subResourceObj Object, subResource string, opts ...SubResourceCreateOption) error {
|
||||
o, err := c.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if subResourceObj.GetName() == "" {
|
||||
subResourceObj.SetName(obj.GetName())
|
||||
}
|
||||
|
||||
createOpts := &SubResourceCreateOptions{}
|
||||
createOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Post().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
SubResource(subResource).
|
||||
Body(subResourceObj).
|
||||
VersionedParams(createOpts.AsCreateOptions(), c.paramCodec).
|
||||
Do(ctx).
|
||||
Into(subResourceObj)
|
||||
}
|
||||
|
||||
// UpdateSubResource used by SubResourceWriter to write status.
|
||||
func (c *typedClient) UpdateSubResource(ctx context.Context, obj Object, subResource string, opts ...SubResourceUpdateOption) error {
|
||||
o, err := c.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(droot): examine the returned error and check if it error needs to be
|
||||
// wrapped to improve the UX ?
|
||||
// It will be nice to receive an error saying the object doesn't implement
|
||||
// status subresource and check CRD definition
|
||||
updateOpts := &SubResourceUpdateOptions{}
|
||||
updateOpts.ApplyOptions(opts)
|
||||
|
||||
body := obj
|
||||
if updateOpts.SubResourceBody != nil {
|
||||
body = updateOpts.SubResourceBody
|
||||
}
|
||||
if body.GetName() == "" {
|
||||
body.SetName(obj.GetName())
|
||||
}
|
||||
if body.GetNamespace() == "" {
|
||||
body.SetNamespace(obj.GetNamespace())
|
||||
}
|
||||
|
||||
return o.Put().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
SubResource(subResource).
|
||||
Body(body).
|
||||
VersionedParams(updateOpts.AsUpdateOptions(), c.paramCodec).
|
||||
Do(ctx).
|
||||
Into(body)
|
||||
}
|
||||
|
||||
// PatchSubResource used by SubResourceWriter to write subresource.
|
||||
func (c *typedClient) PatchSubResource(ctx context.Context, obj Object, subResource string, patch Patch, opts ...SubResourcePatchOption) error {
|
||||
o, err := c.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patchOpts := &SubResourcePatchOptions{}
|
||||
patchOpts.ApplyOptions(opts)
|
||||
|
||||
body := obj
|
||||
if patchOpts.SubResourceBody != nil {
|
||||
body = patchOpts.SubResourceBody
|
||||
}
|
||||
|
||||
data, err := patch.Data(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return o.Patch(patch.Type()).
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
SubResource(subResource).
|
||||
Body(data).
|
||||
VersionedParams(patchOpts.AsPatchOptions(), c.paramCodec).
|
||||
Do(ctx).
|
||||
Into(body)
|
||||
}
|
||||
|
||||
func (c *typedClient) ApplySubResource(ctx context.Context, obj runtime.ApplyConfiguration, subResource string, opts ...SubResourceApplyOption) error {
|
||||
o, err := c.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
applyOpts := &SubResourceApplyOptions{}
|
||||
applyOpts.ApplyOpts(opts)
|
||||
|
||||
body := obj
|
||||
if applyOpts.SubResourceBody != nil {
|
||||
body = applyOpts.SubResourceBody
|
||||
}
|
||||
|
||||
req, err := apply.NewRequest(o, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create apply request: %w", err)
|
||||
}
|
||||
|
||||
return req.
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
SubResource(subResource).
|
||||
VersionedParams(applyOpts.AsPatchOptions(), c.paramCodec).
|
||||
Do(ctx).
|
||||
// This is hacky, it is required because `Into` takes a `runtime.Object` and
|
||||
// that is not implemented by the ApplyConfigurations. The generated clients
|
||||
// don't have this problem because they deserialize into the api type, not the
|
||||
// apply configuration: https://github.com/kubernetes/kubernetes/blob/22f5e01a37c0bc6a5f494dec14dd4e3688ee1d55/staging/src/k8s.io/client-go/gentype/type.go#L296-L317
|
||||
Into(runtimeObjectFromApplyConfiguration(obj))
|
||||
}
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/util/apply"
|
||||
)
|
||||
|
||||
var _ Reader = &unstructuredClient{}
|
||||
var _ Writer = &unstructuredClient{}
|
||||
|
||||
type unstructuredClient struct {
|
||||
resources *clientRestResources
|
||||
paramCodec runtime.ParameterCodec
|
||||
}
|
||||
|
||||
// Create implements client.Client.
|
||||
func (uc *unstructuredClient) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
|
||||
u, ok := obj.(runtime.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
gvk := u.GetObjectKind().GroupVersionKind()
|
||||
|
||||
o, err := uc.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createOpts := &CreateOptions{}
|
||||
createOpts.ApplyOptions(opts)
|
||||
|
||||
result := o.Post().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Body(obj).
|
||||
VersionedParams(createOpts.AsCreateOptions(), uc.paramCodec).
|
||||
Do(ctx).
|
||||
Into(obj)
|
||||
|
||||
u.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
return result
|
||||
}
|
||||
|
||||
// Update implements client.Client.
|
||||
func (uc *unstructuredClient) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
|
||||
u, ok := obj.(runtime.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
gvk := u.GetObjectKind().GroupVersionKind()
|
||||
|
||||
o, err := uc.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateOpts := UpdateOptions{}
|
||||
updateOpts.ApplyOptions(opts)
|
||||
|
||||
result := o.Put().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
Body(obj).
|
||||
VersionedParams(updateOpts.AsUpdateOptions(), uc.paramCodec).
|
||||
Do(ctx).
|
||||
Into(obj)
|
||||
|
||||
u.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
return result
|
||||
}
|
||||
|
||||
// Delete implements client.Client.
|
||||
func (uc *unstructuredClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
|
||||
if _, ok := obj.(runtime.Unstructured); !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
o, err := uc.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteOpts := DeleteOptions{}
|
||||
deleteOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Delete().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
Body(deleteOpts.AsDeleteOptions()).
|
||||
Do(ctx).
|
||||
Error()
|
||||
}
|
||||
|
||||
// DeleteAllOf implements client.Client.
|
||||
func (uc *unstructuredClient) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
|
||||
if _, ok := obj.(runtime.Unstructured); !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
o, err := uc.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteAllOfOpts := DeleteAllOfOptions{}
|
||||
deleteAllOfOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Delete().
|
||||
NamespaceIfScoped(deleteAllOfOpts.ListOptions.Namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
VersionedParams(deleteAllOfOpts.AsListOptions(), uc.paramCodec).
|
||||
Body(deleteAllOfOpts.AsDeleteOptions()).
|
||||
Do(ctx).
|
||||
Error()
|
||||
}
|
||||
|
||||
// Patch implements client.Client.
|
||||
func (uc *unstructuredClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
|
||||
if _, ok := obj.(runtime.Unstructured); !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
o, err := uc.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := patch.Data(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patchOpts := &PatchOptions{}
|
||||
patchOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Patch(patch.Type()).
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
VersionedParams(patchOpts.AsPatchOptions(), uc.paramCodec).
|
||||
Body(data).
|
||||
Do(ctx).
|
||||
Into(obj)
|
||||
}
|
||||
|
||||
func (uc *unstructuredClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error {
|
||||
unstructuredApplyConfig, ok := obj.(*unstructuredApplyConfiguration)
|
||||
if !ok {
|
||||
return fmt.Errorf("bug: unstructured client got an applyconfiguration that was not %T but %T", &unstructuredApplyConfiguration{}, obj)
|
||||
}
|
||||
o, err := uc.resources.getObjMeta(unstructuredApplyConfig.Unstructured)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := apply.NewRequest(o, obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create apply request: %w", err)
|
||||
}
|
||||
applyOpts := &ApplyOptions{}
|
||||
applyOpts.ApplyOptions(opts)
|
||||
|
||||
return req.
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
VersionedParams(applyOpts.AsPatchOptions(), uc.paramCodec).
|
||||
Do(ctx).
|
||||
Into(unstructuredApplyConfig.Unstructured)
|
||||
}
|
||||
|
||||
// Get implements client.Client.
|
||||
func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
|
||||
u, ok := obj.(runtime.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
gvk := u.GetObjectKind().GroupVersionKind()
|
||||
|
||||
getOpts := GetOptions{}
|
||||
getOpts.ApplyOptions(opts)
|
||||
|
||||
r, err := uc.resources.getResource(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := r.Get().
|
||||
NamespaceIfScoped(key.Namespace, r.isNamespaced()).
|
||||
Resource(r.resource()).
|
||||
VersionedParams(getOpts.AsGetOptions(), uc.paramCodec).
|
||||
Name(key.Name).
|
||||
Do(ctx).
|
||||
Into(obj)
|
||||
|
||||
u.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// List implements client.Client.
|
||||
func (uc *unstructuredClient) List(ctx context.Context, obj ObjectList, opts ...ListOption) error {
|
||||
u, ok := obj.(runtime.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
gvk := u.GetObjectKind().GroupVersionKind()
|
||||
gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")
|
||||
|
||||
r, err := uc.resources.getResource(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listOpts := ListOptions{}
|
||||
listOpts.ApplyOptions(opts)
|
||||
|
||||
return r.Get().
|
||||
NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()).
|
||||
Resource(r.resource()).
|
||||
VersionedParams(listOpts.AsListOptions(), uc.paramCodec).
|
||||
Do(ctx).
|
||||
Into(obj)
|
||||
}
|
||||
|
||||
func (uc *unstructuredClient) GetSubResource(ctx context.Context, obj, subResourceObj Object, subResource string, opts ...SubResourceGetOption) error {
|
||||
if _, ok := obj.(runtime.Unstructured); !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
if _, ok := subResourceObj.(runtime.Unstructured); !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", subResourceObj)
|
||||
}
|
||||
|
||||
if subResourceObj.GetName() == "" {
|
||||
subResourceObj.SetName(obj.GetName())
|
||||
}
|
||||
|
||||
o, err := uc.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
getOpts := &SubResourceGetOptions{}
|
||||
getOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Get().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
SubResource(subResource).
|
||||
VersionedParams(getOpts.AsGetOptions(), uc.paramCodec).
|
||||
Do(ctx).
|
||||
Into(subResourceObj)
|
||||
}
|
||||
|
||||
func (uc *unstructuredClient) CreateSubResource(ctx context.Context, obj, subResourceObj Object, subResource string, opts ...SubResourceCreateOption) error {
|
||||
if _, ok := obj.(runtime.Unstructured); !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
if _, ok := subResourceObj.(runtime.Unstructured); !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", subResourceObj)
|
||||
}
|
||||
|
||||
if subResourceObj.GetName() == "" {
|
||||
subResourceObj.SetName(obj.GetName())
|
||||
}
|
||||
|
||||
o, err := uc.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createOpts := &SubResourceCreateOptions{}
|
||||
createOpts.ApplyOptions(opts)
|
||||
|
||||
return o.Post().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
SubResource(subResource).
|
||||
Body(subResourceObj).
|
||||
VersionedParams(createOpts.AsCreateOptions(), uc.paramCodec).
|
||||
Do(ctx).
|
||||
Into(subResourceObj)
|
||||
}
|
||||
|
||||
func (uc *unstructuredClient) UpdateSubResource(ctx context.Context, obj Object, subResource string, opts ...SubResourceUpdateOption) error {
|
||||
if _, ok := obj.(runtime.Unstructured); !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
o, err := uc.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateOpts := SubResourceUpdateOptions{}
|
||||
updateOpts.ApplyOptions(opts)
|
||||
|
||||
body := obj
|
||||
if updateOpts.SubResourceBody != nil {
|
||||
body = updateOpts.SubResourceBody
|
||||
}
|
||||
if body.GetName() == "" {
|
||||
body.SetName(obj.GetName())
|
||||
}
|
||||
if body.GetNamespace() == "" {
|
||||
body.SetNamespace(obj.GetNamespace())
|
||||
}
|
||||
|
||||
return o.Put().
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
SubResource(subResource).
|
||||
Body(body).
|
||||
VersionedParams(updateOpts.AsUpdateOptions(), uc.paramCodec).
|
||||
Do(ctx).
|
||||
Into(body)
|
||||
}
|
||||
|
||||
func (uc *unstructuredClient) PatchSubResource(ctx context.Context, obj Object, subResource string, patch Patch, opts ...SubResourcePatchOption) error {
|
||||
u, ok := obj.(runtime.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("unstructured client did not understand object: %T", obj)
|
||||
}
|
||||
|
||||
gvk := u.GetObjectKind().GroupVersionKind()
|
||||
|
||||
o, err := uc.resources.getObjMeta(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patchOpts := &SubResourcePatchOptions{}
|
||||
patchOpts.ApplyOptions(opts)
|
||||
|
||||
body := obj
|
||||
if patchOpts.SubResourceBody != nil {
|
||||
body = patchOpts.SubResourceBody
|
||||
}
|
||||
|
||||
data, err := patch.Data(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := o.Patch(patch.Type()).
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
SubResource(subResource).
|
||||
Body(data).
|
||||
VersionedParams(patchOpts.AsPatchOptions(), uc.paramCodec).
|
||||
Do(ctx).
|
||||
Into(body)
|
||||
|
||||
u.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
return result
|
||||
}
|
||||
|
||||
func (uc *unstructuredClient) ApplySubResource(ctx context.Context, obj runtime.ApplyConfiguration, subResource string, opts ...SubResourceApplyOption) error {
|
||||
unstructuredApplyConfig, ok := obj.(*unstructuredApplyConfiguration)
|
||||
if !ok {
|
||||
return fmt.Errorf("bug: unstructured client got an applyconfiguration that was not %T but %T", &unstructuredApplyConfiguration{}, obj)
|
||||
}
|
||||
o, err := uc.resources.getObjMeta(unstructuredApplyConfig.Unstructured)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
applyOpts := &SubResourceApplyOptions{}
|
||||
applyOpts.ApplyOpts(opts)
|
||||
|
||||
body := obj
|
||||
if applyOpts.SubResourceBody != nil {
|
||||
body = applyOpts.SubResourceBody
|
||||
}
|
||||
req, err := apply.NewRequest(o, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create apply request: %w", err)
|
||||
}
|
||||
|
||||
return req.
|
||||
NamespaceIfScoped(o.namespace, o.isNamespaced()).
|
||||
Resource(o.resource()).
|
||||
Name(o.name).
|
||||
SubResource(subResource).
|
||||
VersionedParams(applyOpts.AsPatchOptions(), uc.paramCodec).
|
||||
Do(ctx).
|
||||
Into(unstructuredApplyConfig.Unstructured)
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// NewWithWatch returns a new WithWatch.
|
||||
func NewWithWatch(config *rest.Config, options Options) (WithWatch, error) {
|
||||
client, err := newClient(config, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &watchingClient{client: client}, nil
|
||||
}
|
||||
|
||||
type watchingClient struct {
|
||||
*client
|
||||
}
|
||||
|
||||
func (w *watchingClient) Watch(ctx context.Context, list ObjectList, opts ...ListOption) (watch.Interface, error) {
|
||||
switch l := list.(type) {
|
||||
case runtime.Unstructured:
|
||||
return w.unstructuredWatch(ctx, l, opts...)
|
||||
case *metav1.PartialObjectMetadataList:
|
||||
return w.metadataWatch(ctx, l, opts...)
|
||||
default:
|
||||
return w.typedWatch(ctx, l, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watchingClient) listOpts(opts ...ListOption) ListOptions {
|
||||
listOpts := ListOptions{}
|
||||
listOpts.ApplyOptions(opts)
|
||||
if listOpts.Raw == nil {
|
||||
listOpts.Raw = &metav1.ListOptions{}
|
||||
}
|
||||
listOpts.Raw.Watch = true
|
||||
|
||||
return listOpts
|
||||
}
|
||||
|
||||
func (w *watchingClient) metadataWatch(ctx context.Context, obj *metav1.PartialObjectMetadataList, opts ...ListOption) (watch.Interface, error) {
|
||||
gvk := obj.GroupVersionKind()
|
||||
gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")
|
||||
|
||||
listOpts := w.listOpts(opts...)
|
||||
|
||||
resInt, err := w.client.metadataClient.getResourceInterface(gvk, listOpts.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resInt.Watch(ctx, *listOpts.AsListOptions())
|
||||
}
|
||||
|
||||
func (w *watchingClient) unstructuredWatch(ctx context.Context, obj runtime.Unstructured, opts ...ListOption) (watch.Interface, error) {
|
||||
r, err := w.client.unstructuredClient.resources.getResource(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listOpts := w.listOpts(opts...)
|
||||
|
||||
return r.Get().
|
||||
NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()).
|
||||
Resource(r.resource()).
|
||||
VersionedParams(listOpts.AsListOptions(), w.client.unstructuredClient.paramCodec).
|
||||
Watch(ctx)
|
||||
}
|
||||
|
||||
func (w *watchingClient) typedWatch(ctx context.Context, obj ObjectList, opts ...ListOption) (watch.Interface, error) {
|
||||
r, err := w.client.typedClient.resources.getResource(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listOpts := w.listOpts(opts...)
|
||||
|
||||
return r.Get().
|
||||
NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()).
|
||||
Resource(r.resource()).
|
||||
VersionedParams(listOpts.AsListOptions(), w.client.typedClient.paramCodec).
|
||||
Watch(ctx)
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
// loggerPromise knows how to populate a concrete logr.Logger
|
||||
// with options, given an actual base logger later on down the line.
|
||||
type loggerPromise struct {
|
||||
logger *delegatingLogSink
|
||||
childPromises []*loggerPromise
|
||||
promisesLock sync.Mutex
|
||||
|
||||
name *string
|
||||
tags []any
|
||||
}
|
||||
|
||||
func (p *loggerPromise) WithName(l *delegatingLogSink, name string) *loggerPromise {
|
||||
res := &loggerPromise{
|
||||
logger: l,
|
||||
name: &name,
|
||||
promisesLock: sync.Mutex{},
|
||||
}
|
||||
|
||||
p.promisesLock.Lock()
|
||||
defer p.promisesLock.Unlock()
|
||||
p.childPromises = append(p.childPromises, res)
|
||||
return res
|
||||
}
|
||||
|
||||
// WithValues provides a new Logger with the tags appended.
|
||||
func (p *loggerPromise) WithValues(l *delegatingLogSink, tags ...any) *loggerPromise {
|
||||
res := &loggerPromise{
|
||||
logger: l,
|
||||
tags: tags,
|
||||
promisesLock: sync.Mutex{},
|
||||
}
|
||||
|
||||
p.promisesLock.Lock()
|
||||
defer p.promisesLock.Unlock()
|
||||
p.childPromises = append(p.childPromises, res)
|
||||
return res
|
||||
}
|
||||
|
||||
// Fulfill instantiates the Logger with the provided logger.
|
||||
func (p *loggerPromise) Fulfill(parentLogSink logr.LogSink) {
|
||||
sink := parentLogSink
|
||||
if p.name != nil {
|
||||
sink = sink.WithName(*p.name)
|
||||
}
|
||||
|
||||
if p.tags != nil {
|
||||
sink = sink.WithValues(p.tags...)
|
||||
}
|
||||
|
||||
p.logger.lock.Lock()
|
||||
p.logger.logger = sink
|
||||
if withCallDepth, ok := sink.(logr.CallDepthLogSink); ok {
|
||||
p.logger.logger = withCallDepth.WithCallDepth(1)
|
||||
}
|
||||
p.logger.promise = nil
|
||||
p.logger.lock.Unlock()
|
||||
|
||||
for _, childPromise := range p.childPromises {
|
||||
childPromise.Fulfill(sink)
|
||||
}
|
||||
}
|
||||
|
||||
// delegatingLogSink is a logsink that delegates to another logr.LogSink.
|
||||
// If the underlying promise is not nil, it registers calls to sub-loggers with
|
||||
// the logging factory to be populated later, and returns a new delegating
|
||||
// logger. It expects to have *some* logr.Logger set at all times (generally
|
||||
// a no-op logger before the promises are fulfilled).
|
||||
type delegatingLogSink struct {
|
||||
lock sync.RWMutex
|
||||
logger logr.LogSink
|
||||
promise *loggerPromise
|
||||
info logr.RuntimeInfo
|
||||
}
|
||||
|
||||
// Init implements logr.LogSink.
|
||||
func (l *delegatingLogSink) Init(info logr.RuntimeInfo) {
|
||||
eventuallyFulfillRoot()
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
l.info = info
|
||||
}
|
||||
|
||||
// Enabled tests whether this Logger is enabled. For example, commandline
|
||||
// flags might be used to set the logging verbosity and disable some info
|
||||
// logs.
|
||||
func (l *delegatingLogSink) Enabled(level int) bool {
|
||||
eventuallyFulfillRoot()
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
return l.logger.Enabled(level)
|
||||
}
|
||||
|
||||
// Info logs a non-error message with the given key/value pairs as context.
|
||||
//
|
||||
// The msg argument should be used to add some constant description to
|
||||
// the log line. The key/value pairs can then be used to add additional
|
||||
// variable information. The key/value pairs should alternate string
|
||||
// keys and arbitrary values.
|
||||
func (l *delegatingLogSink) Info(level int, msg string, keysAndValues ...any) {
|
||||
eventuallyFulfillRoot()
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
l.logger.Info(level, msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// Error logs an error, with the given message and key/value pairs as context.
|
||||
// It functions similarly to calling Info with the "error" named value, but may
|
||||
// have unique behavior, and should be preferred for logging errors (see the
|
||||
// package documentations for more information).
|
||||
//
|
||||
// The msg field should be used to add context to any underlying error,
|
||||
// while the err field should be used to attach the actual error that
|
||||
// triggered this log line, if present.
|
||||
func (l *delegatingLogSink) Error(err error, msg string, keysAndValues ...any) {
|
||||
eventuallyFulfillRoot()
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
l.logger.Error(err, msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// WithName provides a new Logger with the name appended.
|
||||
func (l *delegatingLogSink) WithName(name string) logr.LogSink {
|
||||
eventuallyFulfillRoot()
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
|
||||
if l.promise == nil {
|
||||
sink := l.logger.WithName(name)
|
||||
if withCallDepth, ok := sink.(logr.CallDepthLogSink); ok {
|
||||
sink = withCallDepth.WithCallDepth(-1)
|
||||
}
|
||||
return sink
|
||||
}
|
||||
|
||||
res := &delegatingLogSink{logger: l.logger}
|
||||
promise := l.promise.WithName(res, name)
|
||||
res.promise = promise
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// WithValues provides a new Logger with the tags appended.
|
||||
func (l *delegatingLogSink) WithValues(tags ...any) logr.LogSink {
|
||||
eventuallyFulfillRoot()
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
|
||||
if l.promise == nil {
|
||||
sink := l.logger.WithValues(tags...)
|
||||
if withCallDepth, ok := sink.(logr.CallDepthLogSink); ok {
|
||||
sink = withCallDepth.WithCallDepth(-1)
|
||||
}
|
||||
return sink
|
||||
}
|
||||
|
||||
res := &delegatingLogSink{logger: l.logger}
|
||||
promise := l.promise.WithValues(res, tags...)
|
||||
res.promise = promise
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Fulfill switches the logger over to use the actual logger
|
||||
// provided, instead of the temporary initial one, if this method
|
||||
// has not been previously called.
|
||||
func (l *delegatingLogSink) Fulfill(actual logr.LogSink) {
|
||||
if actual == nil {
|
||||
actual = NullLogSink{}
|
||||
}
|
||||
if l.promise != nil {
|
||||
l.promise.Fulfill(actual)
|
||||
}
|
||||
}
|
||||
|
||||
// newDelegatingLogSink constructs a new DelegatingLogSink which uses
|
||||
// the given logger before its promise is fulfilled.
|
||||
func newDelegatingLogSink(initial logr.LogSink) *delegatingLogSink {
|
||||
l := &delegatingLogSink{
|
||||
logger: initial,
|
||||
promise: &loggerPromise{promisesLock: sync.Mutex{}},
|
||||
}
|
||||
l.promise.logger = l
|
||||
return l
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
// Package log contains utilities for fetching a new logger
|
||||
// when one is not already available.
|
||||
//
|
||||
// # The Log Handle
|
||||
//
|
||||
// This package contains a root logr.Logger Log. It may be used to
|
||||
// get a handle to whatever the root logging implementation is. By
|
||||
// default, no implementation exists, and the handle returns "promises"
|
||||
// to loggers. When the implementation is set using SetLogger, these
|
||||
// "promises" will be converted over to real loggers.
|
||||
//
|
||||
// # Logr
|
||||
//
|
||||
// All logging in controller-runtime is structured, using a set of interfaces
|
||||
// defined by a package called logr
|
||||
// (https://pkg.go.dev/github.com/go-logr/logr). The sub-package zap provides
|
||||
// helpers for setting up logr backed by Zap (go.uber.org/zap).
|
||||
package log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
// SetLogger sets a concrete logging implementation for all deferred Loggers.
|
||||
func SetLogger(l logr.Logger) {
|
||||
logFullfilled.Store(true)
|
||||
rootLog.Fulfill(l.GetSink())
|
||||
}
|
||||
|
||||
func eventuallyFulfillRoot() {
|
||||
if logFullfilled.Load() {
|
||||
return
|
||||
}
|
||||
if time.Since(rootLogCreated).Seconds() >= 30 {
|
||||
if logFullfilled.CompareAndSwap(false, true) {
|
||||
stack := debug.Stack()
|
||||
stackLines := bytes.Count(stack, []byte{'\n'})
|
||||
sep := []byte{'\n', '\t', '>', ' ', ' '}
|
||||
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"[controller-runtime] log.SetLogger(...) was never called; logs will not be displayed.\nDetected at:%s%s", sep,
|
||||
// prefix every line, so it's clear this is a stack trace related to the above message
|
||||
bytes.Replace(stack, []byte{'\n'}, sep, stackLines-1),
|
||||
)
|
||||
SetLogger(logr.New(NullLogSink{}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
logFullfilled atomic.Bool
|
||||
)
|
||||
|
||||
// Log is the base logger used by kubebuilder. It delegates
|
||||
// to another logr.Logger. You *must* call SetLogger to
|
||||
// get any actual logging. If SetLogger is not called within
|
||||
// the first 30 seconds of a binaries lifetime, it will get
|
||||
// set to a NullLogSink.
|
||||
var (
|
||||
rootLog, rootLogCreated = func() (*delegatingLogSink, time.Time) {
|
||||
return newDelegatingLogSink(NullLogSink{}), time.Now()
|
||||
}()
|
||||
Log = logr.New(rootLog)
|
||||
)
|
||||
|
||||
// FromContext returns a logger with predefined values from a context.Context.
|
||||
func FromContext(ctx context.Context, keysAndValues ...any) logr.Logger {
|
||||
log := Log
|
||||
if ctx != nil {
|
||||
if logger, err := logr.FromContext(ctx); err == nil {
|
||||
log = logger
|
||||
}
|
||||
}
|
||||
return log.WithValues(keysAndValues...)
|
||||
}
|
||||
|
||||
// IntoContext takes a context and sets the logger as one of its values.
|
||||
// Use FromContext function to retrieve the logger.
|
||||
func IntoContext(ctx context.Context, log logr.Logger) context.Context {
|
||||
return logr.NewContext(ctx, log)
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
// NB: this is the same as the null logger logr/testing,
|
||||
// but avoids accidentally adding the testing flags to
|
||||
// all binaries.
|
||||
|
||||
// NullLogSink is a logr.Logger that does nothing.
|
||||
type NullLogSink struct{}
|
||||
|
||||
var _ logr.LogSink = NullLogSink{}
|
||||
|
||||
// Init implements logr.LogSink.
|
||||
func (log NullLogSink) Init(logr.RuntimeInfo) {
|
||||
}
|
||||
|
||||
// Info implements logr.InfoLogger.
|
||||
func (NullLogSink) Info(_ int, _ string, _ ...any) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// Enabled implements logr.InfoLogger.
|
||||
func (NullLogSink) Enabled(level int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Error implements logr.Logger.
|
||||
func (NullLogSink) Error(_ error, _ string, _ ...any) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// WithName implements logr.Logger.
|
||||
func (log NullLogSink) WithName(_ string) logr.LogSink {
|
||||
return log
|
||||
}
|
||||
|
||||
// WithValues implements logr.Logger.
|
||||
func (log NullLogSink) WithValues(_ ...any) logr.LogSink {
|
||||
return log
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// KubeAPIWarningLoggerOptions controls the behavior
|
||||
// of a rest.WarningHandlerWithContext constructed using NewKubeAPIWarningLogger().
|
||||
type KubeAPIWarningLoggerOptions struct {
|
||||
// Deduplicate indicates a given warning message should only be written once.
|
||||
// Setting this to true in a long-running process handling many warnings can
|
||||
// result in increased memory use.
|
||||
Deduplicate bool
|
||||
}
|
||||
|
||||
// KubeAPIWarningLogger is a wrapper around
|
||||
// a provided logr.Logger that implements the
|
||||
// rest.WarningHandlerWithContext interface.
|
||||
type KubeAPIWarningLogger struct {
|
||||
// opts contain options controlling warning output
|
||||
opts KubeAPIWarningLoggerOptions
|
||||
// writtenLock gurads written
|
||||
writtenLock sync.Mutex
|
||||
// used to keep track of already logged messages
|
||||
// and help in de-duplication.
|
||||
written map[string]struct{}
|
||||
}
|
||||
|
||||
// HandleWarningHeaderWithContext handles logging for responses from API server that are
|
||||
// warnings with code being 299 and uses a logr.Logger from context for its logging purposes.
|
||||
func (l *KubeAPIWarningLogger) HandleWarningHeaderWithContext(ctx context.Context, code int, _ string, message string) {
|
||||
log := FromContext(ctx)
|
||||
|
||||
if code != 299 || len(message) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if l.opts.Deduplicate {
|
||||
l.writtenLock.Lock()
|
||||
defer l.writtenLock.Unlock()
|
||||
|
||||
if _, alreadyLogged := l.written[message]; alreadyLogged {
|
||||
return
|
||||
}
|
||||
l.written[message] = struct{}{}
|
||||
}
|
||||
log.Info(message)
|
||||
}
|
||||
|
||||
// NewKubeAPIWarningLogger returns an implementation of rest.WarningHandlerWithContext that logs warnings
|
||||
// with code = 299 to the logger passed into HandleWarningHeaderWithContext via the context.
|
||||
func NewKubeAPIWarningLogger(opts KubeAPIWarningLoggerOptions) *KubeAPIWarningLogger {
|
||||
h := &KubeAPIWarningLogger{opts: opts}
|
||||
if opts.Deduplicate {
|
||||
h.written = map[string]struct{}{}
|
||||
}
|
||||
return h
|
||||
}
|
||||
Reference in New Issue
Block a user