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
|
||||
}
|
||||
+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.
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/filters/filtersutil"
|
||||
"sigs.k8s.io/kustomize/api/filters/fsslice"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
type annoMap map[string]string
|
||||
|
||||
type Filter struct {
|
||||
// Annotations is the set of annotations to apply to the inputs
|
||||
Annotations annoMap `yaml:"annotations,omitempty"`
|
||||
|
||||
// FsSlice contains the FieldSpecs to locate the namespace field
|
||||
FsSlice types.FsSlice
|
||||
|
||||
trackableSetter filtersutil.TrackableSetter
|
||||
}
|
||||
|
||||
var _ kio.Filter = Filter{}
|
||||
var _ kio.TrackableFilter = &Filter{}
|
||||
|
||||
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
|
||||
func (f *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
|
||||
f.trackableSetter.WithMutationTracker(callback)
|
||||
}
|
||||
|
||||
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
keys := yaml.SortedMapKeys(f.Annotations)
|
||||
_, err := kio.FilterAll(yaml.FilterFunc(
|
||||
func(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
for _, k := range keys {
|
||||
if err := node.PipeE(fsslice.Filter{
|
||||
FsSlice: f.FsSlice,
|
||||
SetValue: f.trackableSetter.SetEntry(
|
||||
k, f.Annotations[k], yaml.NodeTagString),
|
||||
CreateKind: yaml.MappingNode, // Annotations are MappingNodes.
|
||||
CreateTag: yaml.NodeTagMap,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return node, nil
|
||||
})).Filter(nodes)
|
||||
return nodes, err
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package annotations contains a kio.Filter implementation of the kustomize
|
||||
// annotations transformer.
|
||||
package annotations
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package fieldspec contains a yaml.Filter to modify a resource
|
||||
// that matches the FieldSpec.
|
||||
package fieldspec
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package fieldspec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/filtersutil"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/resid"
|
||||
"sigs.k8s.io/kustomize/kyaml/utils"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
var _ yaml.Filter = Filter{}
|
||||
|
||||
// Filter possibly mutates its object argument using a FieldSpec.
|
||||
// If the object matches the FieldSpec, and the node found
|
||||
// by following the fieldSpec's path is non-null, this filter calls
|
||||
// the setValue function on the node at the end of the path.
|
||||
// If any part of the path doesn't exist, the filter returns
|
||||
// without doing anything and without error, unless it was set
|
||||
// to create the path. If set to create, it creates a tree of maps
|
||||
// along the path, and the leaf node gets the setValue called on it.
|
||||
// Error on GVK mismatch, empty or poorly formed path.
|
||||
// Filter expect kustomize style paths, not JSON paths.
|
||||
// Filter stores internal state and should not be reused
|
||||
type Filter struct {
|
||||
// FieldSpec contains the path to the value to set.
|
||||
FieldSpec types.FieldSpec `yaml:"fieldSpec"`
|
||||
|
||||
// Set the field using this function
|
||||
SetValue filtersutil.SetFn
|
||||
|
||||
// CreateKind defines the type of node to create if the field is not found
|
||||
CreateKind yaml.Kind
|
||||
|
||||
CreateTag string
|
||||
|
||||
// path keeps internal state about the current path
|
||||
path []string
|
||||
}
|
||||
|
||||
func (fltr Filter) Filter(obj *yaml.RNode) (*yaml.RNode, error) {
|
||||
// check if the FieldSpec applies to the object
|
||||
if match := isMatchGVK(fltr.FieldSpec, obj); !match {
|
||||
return obj, nil
|
||||
}
|
||||
fltr.path = utils.PathSplitter(fltr.FieldSpec.Path, "/")
|
||||
if err := fltr.filter(obj); err != nil {
|
||||
return nil, errors.WrapPrefixf(err,
|
||||
"considering field '%s' of object %s", fltr.FieldSpec.Path, resid.FromRNode(obj))
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// Recursively called.
|
||||
func (fltr Filter) filter(obj *yaml.RNode) error {
|
||||
if len(fltr.path) == 0 {
|
||||
// found the field -- set its value
|
||||
return fltr.SetValue(obj)
|
||||
}
|
||||
if obj.IsTaggedNull() || obj.IsNil() {
|
||||
return nil
|
||||
}
|
||||
switch obj.YNode().Kind {
|
||||
case yaml.SequenceNode:
|
||||
return fltr.handleSequence(obj)
|
||||
case yaml.MappingNode:
|
||||
return fltr.handleMap(obj)
|
||||
case yaml.AliasNode:
|
||||
return fltr.filter(yaml.NewRNode(obj.YNode().Alias))
|
||||
default:
|
||||
return errors.Errorf("expected sequence or mapping node")
|
||||
}
|
||||
}
|
||||
|
||||
// handleMap calls filter on the map field matching the next path element
|
||||
func (fltr Filter) handleMap(obj *yaml.RNode) error {
|
||||
fieldName, isSeq := isSequenceField(fltr.path[0])
|
||||
if fieldName == "" {
|
||||
return fmt.Errorf("cannot set or create an empty field name")
|
||||
}
|
||||
// lookup the field matching the next path element
|
||||
var operation yaml.Filter
|
||||
var kind yaml.Kind
|
||||
tag := yaml.NodeTagEmpty
|
||||
switch {
|
||||
case !fltr.FieldSpec.CreateIfNotPresent || fltr.CreateKind == 0 || isSeq:
|
||||
// don't create the field if we don't find it
|
||||
operation = yaml.Lookup(fieldName)
|
||||
if isSeq {
|
||||
// The query path thinks this field should be a sequence;
|
||||
// accept this hint for use later if the tag is NodeTagNull.
|
||||
kind = yaml.SequenceNode
|
||||
}
|
||||
case len(fltr.path) <= 1:
|
||||
// create the field if it is missing: use the provided node kind
|
||||
operation = yaml.LookupCreate(fltr.CreateKind, fieldName)
|
||||
kind = fltr.CreateKind
|
||||
tag = fltr.CreateTag
|
||||
default:
|
||||
// create the field if it is missing: must be a mapping node
|
||||
operation = yaml.LookupCreate(yaml.MappingNode, fieldName)
|
||||
kind = yaml.MappingNode
|
||||
tag = yaml.NodeTagMap
|
||||
}
|
||||
|
||||
// locate (or maybe create) the field
|
||||
field, err := obj.Pipe(operation)
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "fieldName: %s", fieldName)
|
||||
}
|
||||
if field == nil {
|
||||
// No error if field not found.
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the value exists, but is null and kind is set,
|
||||
// then change it to the creation type
|
||||
// TODO: update yaml.LookupCreate to support this
|
||||
if field.YNode().Tag == yaml.NodeTagNull && yaml.IsCreate(kind) {
|
||||
field.YNode().Kind = kind
|
||||
field.YNode().Tag = tag
|
||||
}
|
||||
|
||||
// copy the current fltr and change the path on the copy
|
||||
var next = fltr
|
||||
// call filter for the next path element on the matching field
|
||||
next.path = fltr.path[1:]
|
||||
return next.filter(field)
|
||||
}
|
||||
|
||||
// seq calls filter on all sequence elements
|
||||
func (fltr Filter) handleSequence(obj *yaml.RNode) error {
|
||||
if err := obj.VisitElements(func(node *yaml.RNode) error {
|
||||
// set an accurate FieldPath for nested elements
|
||||
node.AppendToFieldPath(obj.FieldPath()...)
|
||||
// recurse on each element -- re-allocating a Filter is
|
||||
// not strictly required, but is more consistent with field
|
||||
// and less likely to have side effects
|
||||
// keep the entire path -- it does not contain parts for sequences
|
||||
return fltr.filter(node)
|
||||
}); err != nil {
|
||||
return errors.WrapPrefixf(err,
|
||||
"visit traversal on path: %v", fltr.path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isSequenceField returns true if the path element is for a sequence field.
|
||||
// isSequence also returns the path element with the '[]' suffix trimmed
|
||||
func isSequenceField(name string) (string, bool) {
|
||||
shorter := strings.TrimSuffix(name, "[]")
|
||||
return shorter, shorter != name
|
||||
}
|
||||
|
||||
// isMatchGVK returns true if the fs.GVK matches the obj GVK.
|
||||
func isMatchGVK(fs types.FieldSpec, obj *yaml.RNode) bool {
|
||||
if kind := obj.GetKind(); fs.Kind != "" && fs.Kind != kind {
|
||||
// kind doesn't match
|
||||
return false
|
||||
}
|
||||
|
||||
// parse the group and version from the apiVersion field
|
||||
group, version := resid.ParseGroupVersion(obj.GetApiVersion())
|
||||
|
||||
if fs.Group != "" && fs.Group != group {
|
||||
// group doesn't match
|
||||
return false
|
||||
}
|
||||
|
||||
if fs.Version != "" && fs.Version != version {
|
||||
// version doesn't match
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filtersutil
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// SetFn is a function that accepts an RNode to possibly modify.
|
||||
type SetFn func(*yaml.RNode) error
|
||||
|
||||
// SetScalar returns a SetFn to set a scalar value
|
||||
func SetScalar(value string) SetFn {
|
||||
return SetEntry("", value, yaml.NodeTagEmpty)
|
||||
}
|
||||
|
||||
// SetEntry returns a SetFn to set a field or a map entry to a value.
|
||||
// It can be used with an empty name to set both a value and a tag on a scalar node.
|
||||
// When setting only a value on a scalar node, use SetScalar instead.
|
||||
func SetEntry(name, value, tag string) SetFn {
|
||||
n := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: value,
|
||||
Tag: tag,
|
||||
}
|
||||
return func(node *yaml.RNode) error {
|
||||
return node.PipeE(yaml.FieldSetter{
|
||||
Name: name,
|
||||
Value: yaml.NewRNode(n),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type TrackableSetter struct {
|
||||
// SetValueCallback will be invoked each time a field is set
|
||||
setValueCallback func(name, value, tag string, node *yaml.RNode)
|
||||
}
|
||||
|
||||
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
|
||||
func (s *TrackableSetter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) *TrackableSetter {
|
||||
s.setValueCallback = callback
|
||||
return s
|
||||
}
|
||||
|
||||
// SetScalar returns a SetFn to set a scalar value.
|
||||
// if a mutation tracker has been registered, the tracker will be invoked each
|
||||
// time a scalar is set
|
||||
func (s TrackableSetter) SetScalar(value string) SetFn {
|
||||
return s.SetEntry("", value, yaml.NodeTagEmpty)
|
||||
}
|
||||
|
||||
// SetScalarIfEmpty returns a SetFn to set a scalar value only if it isn't already set.
|
||||
// If a mutation tracker has been registered, the tracker will be invoked each
|
||||
// time a scalar is actually set.
|
||||
func (s TrackableSetter) SetScalarIfEmpty(value string) SetFn {
|
||||
return s.SetEntryIfEmpty("", value, yaml.NodeTagEmpty)
|
||||
}
|
||||
|
||||
// SetEntry returns a SetFn to set a field or a map entry to a value.
|
||||
// It can be used with an empty name to set both a value and a tag on a scalar node.
|
||||
// When setting only a value on a scalar node, use SetScalar instead.
|
||||
// If a mutation tracker has been registered, the tracker will be invoked each
|
||||
// time an entry is set.
|
||||
func (s TrackableSetter) SetEntry(name, value, tag string) SetFn {
|
||||
origSetEntry := SetEntry(name, value, tag)
|
||||
return func(node *yaml.RNode) error {
|
||||
if s.setValueCallback != nil {
|
||||
s.setValueCallback(name, value, tag, node)
|
||||
}
|
||||
return origSetEntry(node)
|
||||
}
|
||||
}
|
||||
|
||||
// SetEntryIfEmpty returns a SetFn to set a field or a map entry to a value only if it isn't already set.
|
||||
// It can be used with an empty name to set both a value and a tag on a scalar node.
|
||||
// When setting only a value on a scalar node, use SetScalar instead.
|
||||
// If a mutation tracker has been registered, the tracker will be invoked each
|
||||
// time an entry is actually set.
|
||||
func (s TrackableSetter) SetEntryIfEmpty(key, value, tag string) SetFn {
|
||||
origSetEntry := SetEntry(key, value, tag)
|
||||
return func(node *yaml.RNode) error {
|
||||
if hasExistingValue(node, key) {
|
||||
return nil
|
||||
}
|
||||
if s.setValueCallback != nil {
|
||||
s.setValueCallback(key, value, tag, node)
|
||||
}
|
||||
return origSetEntry(node)
|
||||
}
|
||||
}
|
||||
|
||||
func hasExistingValue(node *yaml.RNode, key string) bool {
|
||||
if node.IsNilOrEmpty() {
|
||||
return false
|
||||
}
|
||||
if err := yaml.ErrorIfInvalid(node, yaml.ScalarNode); err == nil {
|
||||
return yaml.GetValue(node) != ""
|
||||
}
|
||||
entry := node.Field(key)
|
||||
if entry.IsNilOrEmpty() {
|
||||
return false
|
||||
}
|
||||
return yaml.GetValue(entry.Value) != ""
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package fsslice contains a yaml.Filter to modify a resource if
|
||||
// it matches one or more FieldSpec entries.
|
||||
package fsslice
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package fsslice
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/filters/fieldspec"
|
||||
"sigs.k8s.io/kustomize/api/filters/filtersutil"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
var _ yaml.Filter = Filter{}
|
||||
|
||||
// Filter ranges over an FsSlice to modify fields on a single object.
|
||||
// An FsSlice is a range of FieldSpecs. A FieldSpec is a GVK plus a path.
|
||||
type Filter struct {
|
||||
// FieldSpecList list of FieldSpecs to set
|
||||
FsSlice types.FsSlice `yaml:"fsSlice"`
|
||||
|
||||
// SetValue is called on each field that matches one of the FieldSpecs
|
||||
SetValue filtersutil.SetFn
|
||||
|
||||
// CreateKind is used to create fields that do not exist
|
||||
CreateKind yaml.Kind
|
||||
|
||||
// CreateTag is used to set the tag if encountering a null field
|
||||
CreateTag string
|
||||
}
|
||||
|
||||
func (fltr Filter) Filter(obj *yaml.RNode) (*yaml.RNode, error) {
|
||||
for i := range fltr.FsSlice {
|
||||
// apply this FieldSpec
|
||||
// create a new filter for each iteration because they
|
||||
// store internal state about the field paths
|
||||
_, err := (&fieldspec.Filter{
|
||||
FieldSpec: fltr.FsSlice[i],
|
||||
SetValue: fltr.SetValue,
|
||||
CreateKind: fltr.CreateKind,
|
||||
CreateTag: fltr.CreateTag,
|
||||
}).Filter(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package gkesagenerator contains a kio.Filter that that generates a
|
||||
// iampolicy-related resources for a given cloud provider
|
||||
package iampolicygenerator
|
||||
Generated
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package iampolicygenerator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
IAMPolicyGenerator types.IAMPolicyGeneratorArgs `json:",inline,omitempty" yaml:",inline,omitempty"`
|
||||
}
|
||||
|
||||
// Filter adds a GKE service account object to nodes
|
||||
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
switch f.IAMPolicyGenerator.Cloud {
|
||||
case types.GKE:
|
||||
IAMPolicyResources, err := f.generateGkeIAMPolicyResources()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes = append(nodes, IAMPolicyResources...)
|
||||
default:
|
||||
return nil, fmt.Errorf("cloud provider %s not supported yet", f.IAMPolicyGenerator.Cloud)
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (f Filter) generateGkeIAMPolicyResources() ([]*yaml.RNode, error) {
|
||||
var result []*yaml.RNode
|
||||
input := fmt.Sprintf(`
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
annotations:
|
||||
iam.gke.io/gcp-service-account: %s@%s.iam.gserviceaccount.com
|
||||
name: %s
|
||||
`, f.IAMPolicyGenerator.ServiceAccount.Name,
|
||||
f.IAMPolicyGenerator.ProjectId,
|
||||
f.IAMPolicyGenerator.KubernetesService.Name)
|
||||
|
||||
if f.IAMPolicyGenerator.Namespace != "" {
|
||||
input += fmt.Sprintf("\n namespace: %s", f.IAMPolicyGenerator.Namespace)
|
||||
}
|
||||
|
||||
sa, err := yaml.Parse(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(result, sa), nil
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package imagetag contains two kio.Filter implementations to cover the
|
||||
// functionality of the kustomize imagetag transformer.
|
||||
//
|
||||
// Filter updates fields based on a FieldSpec and an ImageTag.
|
||||
//
|
||||
// LegacyFilter doesn't use a FieldSpec, and instead only updates image
|
||||
// references if the field is name image and it is underneath a field called
|
||||
// either containers or initContainers.
|
||||
package imagetag
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package imagetag
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/filters/filtersutil"
|
||||
"sigs.k8s.io/kustomize/api/filters/fsslice"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Filter modifies an "image tag", the value used to specify the
|
||||
// name, tag, version digest etc. of (docker) container images
|
||||
// used by a pod template.
|
||||
type Filter struct {
|
||||
// imageTag is the tag we want to apply to the inputs
|
||||
// The name of the image is used as a key, and other fields
|
||||
// can specify a new name, tag, etc.
|
||||
ImageTag types.Image `json:"imageTag,omitempty" yaml:"imageTag,omitempty"`
|
||||
|
||||
// FsSlice contains the FieldSpecs to locate an image field,
|
||||
// e.g. Path: "spec/myContainers[]/image"
|
||||
FsSlice types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
|
||||
|
||||
trackableSetter filtersutil.TrackableSetter
|
||||
}
|
||||
|
||||
var _ kio.Filter = Filter{}
|
||||
var _ kio.TrackableFilter = &Filter{}
|
||||
|
||||
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
|
||||
func (f *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
|
||||
f.trackableSetter.WithMutationTracker(callback)
|
||||
}
|
||||
|
||||
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
_, err := kio.FilterAll(yaml.FilterFunc(f.filter)).Filter(nodes)
|
||||
return nodes, err
|
||||
}
|
||||
|
||||
func (f Filter) filter(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
// FsSlice is an allowlist, not a denyList, so to deny
|
||||
// something via configuration a new config mechanism is
|
||||
// needed. Until then, hardcode it.
|
||||
if f.isOnDenyList(node) {
|
||||
return node, nil
|
||||
}
|
||||
if err := node.PipeE(fsslice.Filter{
|
||||
FsSlice: f.FsSlice,
|
||||
SetValue: imageTagUpdater{
|
||||
ImageTag: f.ImageTag,
|
||||
trackableSetter: f.trackableSetter,
|
||||
}.SetImageValue,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (f Filter) isOnDenyList(node *yaml.RNode) bool {
|
||||
meta, err := node.GetMeta()
|
||||
if err != nil {
|
||||
// A missing 'meta' field will cause problems elsewhere;
|
||||
// ignore it here to keep the signature simple.
|
||||
return false
|
||||
}
|
||||
// Ignore CRDs
|
||||
// https://github.com/kubernetes-sigs/kustomize/issues/890
|
||||
return meta.Kind == `CustomResourceDefinition`
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package imagetag
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/internal/utils"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// LegacyFilter is an implementation of the kio.Filter interface
|
||||
// that scans through the provided kyaml data structure and updates
|
||||
// any values of any image fields that is inside a sequence under
|
||||
// a field called either containers or initContainers. The field is only
|
||||
// update if it has a value that matches and image reference and the name
|
||||
// of the image is a match with the provided ImageTag.
|
||||
type LegacyFilter struct {
|
||||
ImageTag types.Image `json:"imageTag,omitempty" yaml:"imageTag,omitempty"`
|
||||
}
|
||||
|
||||
var _ kio.Filter = LegacyFilter{}
|
||||
|
||||
func (lf LegacyFilter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
return kio.FilterAll(yaml.FilterFunc(lf.filter)).Filter(nodes)
|
||||
}
|
||||
|
||||
func (lf LegacyFilter) filter(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
meta, err := node.GetMeta()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We do not make any changes if the type of the resource
|
||||
// is CustomResourceDefinition.
|
||||
if meta.Kind == `CustomResourceDefinition` {
|
||||
return node, nil
|
||||
}
|
||||
|
||||
fff := findFieldsFilter{
|
||||
fields: []string{"containers", "initContainers"},
|
||||
fieldCallback: checkImageTagsFn(lf.ImageTag),
|
||||
}
|
||||
if err := node.PipeE(fff); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
type fieldCallback func(node *yaml.RNode) error
|
||||
|
||||
// findFieldsFilter is an implementation of the kio.Filter
|
||||
// interface. It will walk the data structure and look for fields
|
||||
// that matches the provided list of field names. For each match,
|
||||
// the value of the field will be passed in as a parameter to the
|
||||
// provided fieldCallback.
|
||||
// TODO: move this to kyaml/filterutils
|
||||
type findFieldsFilter struct {
|
||||
fields []string
|
||||
|
||||
fieldCallback fieldCallback
|
||||
}
|
||||
|
||||
func (f findFieldsFilter) Filter(obj *yaml.RNode) (*yaml.RNode, error) {
|
||||
return obj, f.walk(obj)
|
||||
}
|
||||
|
||||
func (f findFieldsFilter) walk(node *yaml.RNode) error {
|
||||
switch node.YNode().Kind {
|
||||
case yaml.MappingNode:
|
||||
return node.VisitFields(func(n *yaml.MapNode) error {
|
||||
err := f.walk(n.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := n.Key.YNode().Value
|
||||
if utils.StringSliceContains(f.fields, key) {
|
||||
return f.fieldCallback(n.Value)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
case yaml.SequenceNode:
|
||||
return errors.Wrap(node.VisitElements(f.walk))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkImageTagsFn(imageTag types.Image) fieldCallback {
|
||||
return func(node *yaml.RNode) error {
|
||||
if node.YNode().Kind != yaml.SequenceNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
return node.VisitElements(func(n *yaml.RNode) error {
|
||||
// Look up any fields on the provided node that is named
|
||||
// image.
|
||||
return n.PipeE(yaml.Get("image"), imageTagUpdater{
|
||||
ImageTag: imageTag,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package imagetag
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/filters/filtersutil"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/internal/image"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// imageTagUpdater is an implementation of the kio.Filter interface
|
||||
// that will update the value of the yaml node based on the provided
|
||||
// ImageTag if the current value matches the format of an image reference.
|
||||
type imageTagUpdater struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
ImageTag types.Image `yaml:"imageTag,omitempty"`
|
||||
trackableSetter filtersutil.TrackableSetter
|
||||
}
|
||||
|
||||
func (u imageTagUpdater) SetImageValue(rn *yaml.RNode) error {
|
||||
if err := yaml.ErrorIfInvalid(rn, yaml.ScalarNode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value := rn.YNode().Value
|
||||
|
||||
if !image.IsImageMatched(value, u.ImageTag.Name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
name, tag, digest := image.Split(value)
|
||||
if u.ImageTag.NewName != "" {
|
||||
name = u.ImageTag.NewName
|
||||
}
|
||||
|
||||
// overriding tag or digest will replace both original tag and digest values
|
||||
switch {
|
||||
case u.ImageTag.NewTag != "" && u.ImageTag.Digest != "":
|
||||
tag = u.ImageTag.NewTag
|
||||
digest = u.ImageTag.Digest
|
||||
case u.ImageTag.NewTag != "":
|
||||
tag = u.ImageTag.NewTag
|
||||
digest = ""
|
||||
case u.ImageTag.Digest != "":
|
||||
tag = ""
|
||||
digest = u.ImageTag.Digest
|
||||
case u.ImageTag.TagSuffix != "":
|
||||
tag += u.ImageTag.TagSuffix
|
||||
digest = ""
|
||||
}
|
||||
|
||||
// build final image name
|
||||
if tag != "" {
|
||||
name += ":" + tag
|
||||
}
|
||||
if digest != "" {
|
||||
name += "@" + digest
|
||||
}
|
||||
|
||||
return u.trackableSetter.SetScalar(name)(rn)
|
||||
}
|
||||
|
||||
func (u imageTagUpdater) Filter(rn *yaml.RNode) (*yaml.RNode, error) {
|
||||
if err := u.SetImageValue(rn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rn, nil
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package labels contains a kio.Filter implementation of the kustomize
|
||||
// labels transformer.
|
||||
package labels
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package labels
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/filters/filtersutil"
|
||||
"sigs.k8s.io/kustomize/api/filters/fsslice"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
type labelMap map[string]string
|
||||
|
||||
// Filter sets labels.
|
||||
type Filter struct {
|
||||
// Labels is the set of labels to apply to the inputs
|
||||
Labels labelMap `yaml:"labels,omitempty"`
|
||||
|
||||
// FsSlice identifies the label fields.
|
||||
FsSlice types.FsSlice
|
||||
|
||||
trackableSetter filtersutil.TrackableSetter
|
||||
}
|
||||
|
||||
var _ kio.Filter = Filter{}
|
||||
var _ kio.TrackableFilter = &Filter{}
|
||||
|
||||
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
|
||||
func (f *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
|
||||
f.trackableSetter.WithMutationTracker(callback)
|
||||
}
|
||||
|
||||
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
keys := yaml.SortedMapKeys(f.Labels)
|
||||
_, err := kio.FilterAll(yaml.FilterFunc(
|
||||
func(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
for _, k := range keys {
|
||||
if err := node.PipeE(fsslice.Filter{
|
||||
FsSlice: f.FsSlice,
|
||||
SetValue: f.trackableSetter.SetEntry(
|
||||
k, f.Labels[k], yaml.NodeTagString),
|
||||
CreateKind: yaml.MappingNode, // Labels are MappingNodes.
|
||||
CreateTag: yaml.NodeTagMap,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return node, nil
|
||||
})).Filter(nodes)
|
||||
return nodes, err
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package nameref contains a kio.Filter implementation of the kustomize
|
||||
// name reference transformer.
|
||||
package nameref
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package nameref
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/fieldspec"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/resource"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/resid"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Filter updates a name references.
|
||||
type Filter struct {
|
||||
// Referrer refers to another resource X by X's name.
|
||||
// E.g. A Deployment can refer to a ConfigMap.
|
||||
// The Deployment is the Referrer,
|
||||
// the ConfigMap is the ReferralTarget.
|
||||
// This filter seeks to repair the reference in Deployment, given
|
||||
// that the ConfigMap's name may have changed.
|
||||
Referrer *resource.Resource
|
||||
|
||||
// NameFieldToUpdate is the field in the Referrer
|
||||
// that holds the name requiring an update.
|
||||
// This is the field to write.
|
||||
NameFieldToUpdate types.FieldSpec
|
||||
|
||||
// ReferralTarget is the source of the new value for
|
||||
// the name, always in the 'metadata/name' field.
|
||||
// This is the field to read.
|
||||
ReferralTarget resid.Gvk
|
||||
|
||||
// Set of resources to scan to find the ReferralTarget.
|
||||
ReferralCandidates resmap.ResMap
|
||||
}
|
||||
|
||||
// At time of writing, in practice this is called with a slice with only
|
||||
// one entry, the node also referred to be the resource in the Referrer field.
|
||||
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
return kio.FilterAll(yaml.FilterFunc(f.run)).Filter(nodes)
|
||||
}
|
||||
|
||||
// The node passed in here is the same node as held in Referrer;
|
||||
// that's how the referrer's name field is updated.
|
||||
// Currently, however, this filter still needs the extra methods on Referrer
|
||||
// to consult things like the resource Id, its namespace, etc.
|
||||
// TODO(3455): No filter should use the Resource api; all information
|
||||
// about names should come from annotations, with helper methods
|
||||
// on the RNode object. Resource should get stupider, RNode smarter.
|
||||
func (f Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
if err := f.confirmNodeMatchesReferrer(node); err != nil {
|
||||
// sanity check.
|
||||
return nil, err
|
||||
}
|
||||
f.NameFieldToUpdate.Gvk = f.Referrer.GetGvk()
|
||||
if err := node.PipeE(fieldspec.Filter{
|
||||
FieldSpec: f.NameFieldToUpdate,
|
||||
SetValue: f.set,
|
||||
}); err != nil {
|
||||
return nil, errors.WrapPrefixf(
|
||||
err, "updating name reference in '%s' field of '%s'",
|
||||
f.NameFieldToUpdate.Path, f.Referrer.CurId().String())
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// This function is called on the node found at FieldSpec.Path.
|
||||
// It's some node in the Referrer.
|
||||
func (f Filter) set(node *yaml.RNode) error {
|
||||
if yaml.IsMissingOrNull(node) {
|
||||
return nil
|
||||
}
|
||||
switch node.YNode().Kind {
|
||||
case yaml.ScalarNode:
|
||||
return f.setScalar(node)
|
||||
case yaml.MappingNode:
|
||||
return f.setMapping(node)
|
||||
case yaml.SequenceNode:
|
||||
return applyFilterToSeq(seqFilter{
|
||||
setScalarFn: f.setScalar,
|
||||
setMappingFn: f.setMapping,
|
||||
}, node)
|
||||
default:
|
||||
return fmt.Errorf("node must be a scalar, sequence or map")
|
||||
}
|
||||
}
|
||||
|
||||
// This method used when NameFieldToUpdate doesn't lead to
|
||||
// one scalar field (typically called 'name'), but rather
|
||||
// leads to a map field (called anything). In this case we
|
||||
// must complete the field path, looking for both a 'name'
|
||||
// and a 'namespace' field to help select the proper
|
||||
// ReferralTarget to read the name and namespace from.
|
||||
func (f Filter) setMapping(node *yaml.RNode) error {
|
||||
if node.YNode().Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("expect a mapping node")
|
||||
}
|
||||
nameNode, err := node.Pipe(yaml.FieldMatcher{Name: "name"})
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "trying to match 'name' field")
|
||||
}
|
||||
if nameNode == nil {
|
||||
// This is a _configuration_ error; the field path
|
||||
// specified in NameFieldToUpdate.Path doesn't resolve
|
||||
// to a map with a 'name' field, so we have no idea what
|
||||
// field to update with a new name.
|
||||
return fmt.Errorf("path config error; no 'name' field in node")
|
||||
}
|
||||
candidates, err := f.filterMapCandidatesByNamespace(node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldName := nameNode.YNode().Value
|
||||
// use allNamesAndNamespacesAreTheSame to compare referral candidates for functional identity,
|
||||
// because we source both name and namespace values from the referral in this case.
|
||||
referral, err := f.selectReferral(oldName, candidates, allNamesAndNamespacesAreTheSame)
|
||||
if err != nil || referral == nil {
|
||||
// Nil referral means nothing to do.
|
||||
return err
|
||||
}
|
||||
f.recordTheReferral(referral)
|
||||
if referral.GetName() == oldName && referral.GetNamespace() == "" {
|
||||
// The name has not changed, nothing to do.
|
||||
return nil
|
||||
}
|
||||
if err = node.PipeE(yaml.FieldSetter{
|
||||
Name: "name",
|
||||
StringValue: referral.GetName(),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if referral.GetNamespace() == "" {
|
||||
// Don't write an empty string into the namespace field, as
|
||||
// it should not replace the value "default". The empty
|
||||
// string is handled as a wild card here, not as an implicit
|
||||
// specification of the "default" k8s namespace.
|
||||
return nil
|
||||
}
|
||||
return node.PipeE(yaml.FieldSetter{
|
||||
Name: "namespace",
|
||||
StringValue: referral.GetNamespace(),
|
||||
})
|
||||
}
|
||||
|
||||
func (f Filter) filterMapCandidatesByNamespace(
|
||||
node *yaml.RNode) ([]*resource.Resource, error) {
|
||||
namespaceNode, err := node.Pipe(yaml.FieldMatcher{Name: "namespace"})
|
||||
if err != nil {
|
||||
return nil, errors.WrapPrefixf(err, "trying to match 'namespace' field")
|
||||
}
|
||||
if namespaceNode == nil {
|
||||
return f.ReferralCandidates.Resources(), nil
|
||||
}
|
||||
namespace := namespaceNode.YNode().Value
|
||||
nsMap := f.ReferralCandidates.GroupedByOriginalNamespace()
|
||||
if candidates, ok := nsMap[namespace]; ok {
|
||||
return candidates, nil
|
||||
}
|
||||
nsMap = f.ReferralCandidates.GroupedByCurrentNamespace()
|
||||
// This could be nil, or an empty list.
|
||||
return nsMap[namespace], nil
|
||||
}
|
||||
|
||||
func (f Filter) setScalar(node *yaml.RNode) error {
|
||||
// use allNamesAreTheSame to compare referral candidates for functional identity,
|
||||
// because we only source the name from the referral in this case.
|
||||
referral, err := f.selectReferral(
|
||||
node.YNode().Value, f.ReferralCandidates.Resources(), allNamesAreTheSame)
|
||||
if err != nil || referral == nil {
|
||||
// Nil referral means nothing to do.
|
||||
return err
|
||||
}
|
||||
f.recordTheReferral(referral)
|
||||
if referral.GetName() == node.YNode().Value {
|
||||
// The name has not changed, nothing to do.
|
||||
return nil
|
||||
}
|
||||
return node.PipeE(yaml.FieldSetter{StringValue: referral.GetName()})
|
||||
}
|
||||
|
||||
// In the resource, make a note that it is referred to by the Referrer.
|
||||
func (f Filter) recordTheReferral(referral *resource.Resource) {
|
||||
referral.AppendRefBy(f.Referrer.CurId())
|
||||
}
|
||||
|
||||
// getRoleRefGvk returns a Gvk in the roleRef field. Return error
|
||||
// if the roleRef, roleRef/apiGroup or roleRef/kind is missing.
|
||||
func getRoleRefGvk(n *resource.Resource) (*resid.Gvk, error) {
|
||||
roleRef, err := n.Pipe(yaml.Lookup("roleRef"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if roleRef.IsNil() {
|
||||
return nil, fmt.Errorf("roleRef cannot be found in %s", n.MustString())
|
||||
}
|
||||
apiGroup, err := roleRef.Pipe(yaml.Lookup("apiGroup"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apiGroup.IsNil() {
|
||||
return nil, fmt.Errorf("apiGroup cannot be found in roleRef %s", roleRef.MustString())
|
||||
}
|
||||
kind, err := roleRef.Pipe(yaml.Lookup("kind"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if kind.IsNil() {
|
||||
return nil, fmt.Errorf("kind cannot be found in roleRef %s", roleRef.MustString())
|
||||
}
|
||||
return &resid.Gvk{
|
||||
Group: apiGroup.YNode().Value,
|
||||
Kind: kind.YNode().Value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// sieveFunc returns true if the resource argument satisfies some criteria.
|
||||
type sieveFunc func(*resource.Resource) bool
|
||||
|
||||
// doSieve uses a function to accept or ignore resources from a list.
|
||||
// If list is nil, returns immediately.
|
||||
// It's a filter obviously, but that term is overloaded here.
|
||||
func doSieve(list []*resource.Resource, fn sieveFunc) (s []*resource.Resource) {
|
||||
for _, r := range list {
|
||||
if fn(r) {
|
||||
s = append(s, r)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func acceptAll(r *resource.Resource) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func previousNameMatches(name string) sieveFunc {
|
||||
return func(r *resource.Resource) bool {
|
||||
for _, id := range r.PrevIds() {
|
||||
if id.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func previousIdSelectedByGvk(gvk *resid.Gvk) sieveFunc {
|
||||
return func(r *resource.Resource) bool {
|
||||
for _, id := range r.PrevIds() {
|
||||
if id.IsSelected(gvk) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// If the we are updating a 'roleRef/name' field, the 'apiGroup' and 'kind'
|
||||
// fields in the same 'roleRef' map must be considered.
|
||||
// If either object is cluster-scoped, there can be a referral.
|
||||
// E.g. a RoleBinding (which exists in a namespace) can refer
|
||||
// to a ClusterRole (cluster-scoped) object.
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole
|
||||
// Likewise, a ClusterRole can refer to a Secret (in a namespace).
|
||||
// Objects in different namespaces generally cannot refer to other
|
||||
// with some exceptions (e.g. RoleBinding and ServiceAccount are both
|
||||
// namespaceable, but the former can refer to accounts in other namespaces).
|
||||
func (f Filter) roleRefFilter() sieveFunc {
|
||||
if !strings.HasSuffix(f.NameFieldToUpdate.Path, "roleRef/name") {
|
||||
return acceptAll
|
||||
}
|
||||
roleRefGvk, err := getRoleRefGvk(f.Referrer)
|
||||
if err != nil {
|
||||
return acceptAll
|
||||
}
|
||||
return previousIdSelectedByGvk(roleRefGvk)
|
||||
}
|
||||
|
||||
func prefixSuffixEquals(other resource.ResCtx, allowEmpty bool) sieveFunc {
|
||||
return func(r *resource.Resource) bool {
|
||||
return r.PrefixesSuffixesEquals(other, allowEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
func (f Filter) sameCurrentNamespaceAsReferrer() sieveFunc {
|
||||
referrerCurId := f.Referrer.CurId()
|
||||
if referrerCurId.IsClusterScoped() {
|
||||
// If the referrer is cluster-scoped, let anything through.
|
||||
return acceptAll
|
||||
}
|
||||
return func(r *resource.Resource) bool {
|
||||
if r.CurId().IsClusterScoped() {
|
||||
// Allow cluster-scoped through.
|
||||
return true
|
||||
}
|
||||
if r.GetKind() == "ServiceAccount" {
|
||||
// Allow service accounts through, even though they
|
||||
// are in a namespace. A RoleBinding in another namespace
|
||||
// can reference them.
|
||||
return true
|
||||
}
|
||||
return referrerCurId.IsNsEquals(r.CurId())
|
||||
}
|
||||
}
|
||||
|
||||
// selectReferral picks the best referral from a list of candidates.
|
||||
func (f Filter) selectReferral(
|
||||
// The name referral that may need to be updated.
|
||||
oldName string,
|
||||
candidates []*resource.Resource,
|
||||
// function that returns whether two referrals are identical for the purposes of the transformation
|
||||
candidatesIdentical func(resources []*resource.Resource) bool) (*resource.Resource, error) {
|
||||
candidates = doSieve(candidates, previousNameMatches(oldName))
|
||||
candidates = doSieve(candidates, previousIdSelectedByGvk(&f.ReferralTarget))
|
||||
candidates = doSieve(candidates, f.roleRefFilter())
|
||||
candidates = doSieve(candidates, f.sameCurrentNamespaceAsReferrer())
|
||||
if len(candidates) == 1 {
|
||||
return candidates[0], nil
|
||||
}
|
||||
candidates = doSieve(candidates, prefixSuffixEquals(f.Referrer, true))
|
||||
if len(candidates) > 1 {
|
||||
candidates = doSieve(candidates, prefixSuffixEquals(f.Referrer, false))
|
||||
}
|
||||
if len(candidates) == 1 {
|
||||
return candidates[0], nil
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if candidatesIdentical(candidates) {
|
||||
// Just take the first one.
|
||||
return candidates[0], nil
|
||||
}
|
||||
ids := getIds(candidates)
|
||||
return nil, fmt.Errorf("found multiple possible referrals: %s\n%s", ids, f.failureDetails(candidates))
|
||||
}
|
||||
|
||||
func (f Filter) failureDetails(resources []*resource.Resource) string {
|
||||
msg := strings.Builder{}
|
||||
msg.WriteString(fmt.Sprintf("\n**** Too many possible referral targets to referrer:\n%s\n", f.Referrer.MustYaml()))
|
||||
for i, r := range resources {
|
||||
msg.WriteString(fmt.Sprintf("--- possible referral %d:\n%s\n", i, r.MustYaml()))
|
||||
}
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
func allNamesAreTheSame(resources []*resource.Resource) bool {
|
||||
name := resources[0].GetName()
|
||||
for i := 1; i < len(resources); i++ {
|
||||
if name != resources[i].GetName() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func allNamesAndNamespacesAreTheSame(resources []*resource.Resource) bool {
|
||||
name := resources[0].GetName()
|
||||
namespace := resources[0].GetNamespace()
|
||||
for i := 1; i < len(resources); i++ {
|
||||
if name != resources[i].GetName() || namespace != resources[i].GetNamespace() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getIds(rs []*resource.Resource) string {
|
||||
var result []string
|
||||
for _, r := range rs {
|
||||
result = append(result, r.CurId().String())
|
||||
}
|
||||
return strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
func checkEqual(k, a, b string) error {
|
||||
if a != b {
|
||||
return fmt.Errorf(
|
||||
"node-referrerOriginal '%s' mismatch '%s' != '%s'",
|
||||
k, a, b)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f Filter) confirmNodeMatchesReferrer(node *yaml.RNode) error {
|
||||
meta, err := node.GetMeta()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gvk := f.Referrer.GetGvk()
|
||||
if err = checkEqual(
|
||||
"APIVersion", meta.APIVersion, gvk.ApiVersion()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = checkEqual(
|
||||
"Kind", meta.Kind, gvk.Kind); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = checkEqual(
|
||||
"Name", meta.Name, f.Referrer.GetName()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = checkEqual(
|
||||
"Namespace", meta.Namespace, f.Referrer.GetNamespace()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package nameref
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
type setFn func(*yaml.RNode) error
|
||||
|
||||
type seqFilter struct {
|
||||
setScalarFn setFn
|
||||
setMappingFn setFn
|
||||
}
|
||||
|
||||
func (sf seqFilter) Filter(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
if yaml.IsMissingOrNull(node) {
|
||||
return node, nil
|
||||
}
|
||||
switch node.YNode().Kind {
|
||||
case yaml.ScalarNode:
|
||||
// Kind: Role/ClusterRole
|
||||
// FieldSpec is rules.resourceNames
|
||||
err := sf.setScalarFn(node)
|
||||
return node, err
|
||||
case yaml.MappingNode:
|
||||
// Kind: RoleBinding/ClusterRoleBinding
|
||||
// FieldSpec is subjects
|
||||
// Note: The corresponding fieldSpec had been changed from
|
||||
// from path: subjects/name to just path: subjects. This is
|
||||
// what get mutatefield to request the mapping of the whole
|
||||
// map containing namespace and name instead of just a simple
|
||||
// string field containing the name
|
||||
err := sf.setMappingFn(node)
|
||||
return node, err
|
||||
default:
|
||||
return node, fmt.Errorf(
|
||||
"%#v is expected to be either a string or a map of string", node)
|
||||
}
|
||||
}
|
||||
|
||||
// applyFilterToSeq will apply the filter to each element in the sequence node
|
||||
func applyFilterToSeq(filter yaml.Filter, node *yaml.RNode) error {
|
||||
if node.YNode().Kind != yaml.SequenceNode {
|
||||
return fmt.Errorf("expect a sequence node but got %v", node.YNode().Kind)
|
||||
}
|
||||
|
||||
for _, elem := range node.Content() {
|
||||
rnode := yaml.NewRNode(elem)
|
||||
err := rnode.PipeE(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package namespace contains a kio.Filter implementation of the kustomize
|
||||
// namespace transformer.
|
||||
//
|
||||
// Special cases for known Kubernetes resources have been hardcoded in addition
|
||||
// to those defined by the FsSlice.
|
||||
package namespace
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package namespace
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/filters/filtersutil"
|
||||
"sigs.k8s.io/kustomize/api/filters/fsslice"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/resid"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
// Namespace is the namespace to apply to the inputs
|
||||
Namespace string `yaml:"namespace,omitempty"`
|
||||
|
||||
// FsSlice contains the FieldSpecs to locate the namespace field
|
||||
FsSlice types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
|
||||
|
||||
// UnsetOnly means only blank namespace fields will be set
|
||||
UnsetOnly bool `json:"unsetOnly" yaml:"unsetOnly"`
|
||||
|
||||
// SetRoleBindingSubjects determines which subject fields in RoleBinding and ClusterRoleBinding
|
||||
// objects will have their namespace fields set. Overrides field specs provided for these types, if any.
|
||||
// - defaultOnly (default): namespace will be set only on subjects named "default".
|
||||
// - allServiceAccounts: namespace will be set on all subjects with "kind: ServiceAccount"
|
||||
// - none: all subjects will be skipped.
|
||||
SetRoleBindingSubjects RoleBindingSubjectMode `json:"setRoleBindingSubjects" yaml:"setRoleBindingSubjects"`
|
||||
|
||||
trackableSetter filtersutil.TrackableSetter
|
||||
}
|
||||
|
||||
type RoleBindingSubjectMode string
|
||||
|
||||
const (
|
||||
DefaultSubjectsOnly RoleBindingSubjectMode = "defaultOnly"
|
||||
SubjectModeUnspecified RoleBindingSubjectMode = ""
|
||||
AllServiceAccountSubjects RoleBindingSubjectMode = "allServiceAccounts"
|
||||
NoSubjects RoleBindingSubjectMode = "none"
|
||||
)
|
||||
|
||||
var _ kio.Filter = Filter{}
|
||||
var _ kio.TrackableFilter = &Filter{}
|
||||
|
||||
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
|
||||
func (ns *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
|
||||
ns.trackableSetter.WithMutationTracker(callback)
|
||||
}
|
||||
|
||||
func (ns Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
return kio.FilterAll(yaml.FilterFunc(ns.run)).Filter(nodes)
|
||||
}
|
||||
|
||||
// Run runs the filter on a single node rather than a slice
|
||||
func (ns Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
// Special handling for metadata.namespace and metadata.name -- :(
|
||||
// never let SetEntry handle metadata.namespace--it will incorrectly include cluster-scoped resources
|
||||
// only update metadata.name if api version is expected one--so-as it leaves other resources of kind namespace alone
|
||||
apiVersion := node.GetApiVersion()
|
||||
ns.FsSlice = ns.removeUnneededMetaFieldSpecs(apiVersion, ns.FsSlice)
|
||||
gvk := resid.GvkFromNode(node)
|
||||
if err := ns.metaNamespaceHack(node, gvk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Special handling for (cluster) role binding subjects -- :(
|
||||
if isRoleBinding(gvk.Kind) {
|
||||
ns.FsSlice = ns.removeRoleBindingSubjectFieldSpecs(ns.FsSlice)
|
||||
if err := ns.roleBindingHack(node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// transformations based on data -- :)
|
||||
err := node.PipeE(fsslice.Filter{
|
||||
FsSlice: ns.FsSlice,
|
||||
SetValue: ns.fieldSetter(),
|
||||
CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode
|
||||
CreateTag: yaml.NodeTagString,
|
||||
})
|
||||
invalidKindErr := &yaml.InvalidNodeKindError{}
|
||||
if err != nil && errors.As(err, &invalidKindErr) && invalidKindErr.ActualNodeKind() != yaml.ScalarNode {
|
||||
return nil, errors.WrapPrefixf(err, "namespace field specs must target scalar nodes")
|
||||
}
|
||||
return node, errors.WrapPrefixf(err, "namespace transformation failed")
|
||||
}
|
||||
|
||||
// metaNamespaceHack is a hack for implementing the namespace transform
|
||||
// for the metadata.namespace field on namespace scoped resources.
|
||||
func (ns Filter) metaNamespaceHack(obj *yaml.RNode, gvk resid.Gvk) error {
|
||||
if gvk.IsClusterScoped() {
|
||||
return nil
|
||||
}
|
||||
f := fsslice.Filter{
|
||||
FsSlice: []types.FieldSpec{
|
||||
{Path: types.MetadataNamespacePath, CreateIfNotPresent: true},
|
||||
},
|
||||
SetValue: ns.fieldSetter(),
|
||||
CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode
|
||||
}
|
||||
_, err := f.Filter(obj)
|
||||
return err
|
||||
}
|
||||
|
||||
// roleBindingHack is a hack for implementing the transformer's SetRoleBindingSubjects option
|
||||
// for RoleBinding and ClusterRoleBinding resource types.
|
||||
//
|
||||
// In NoSubjects mode, it does nothing.
|
||||
//
|
||||
// In AllServiceAccountSubjects mode, it sets the namespace on subjects with "kind: ServiceAccount".
|
||||
//
|
||||
// In DefaultSubjectsOnly mode (default mode), RoleBinding and ClusterRoleBinding have namespace set on
|
||||
// elements of the "subjects" field if and only if the subject elements
|
||||
// "name" is "default". Otherwise the namespace is not set.
|
||||
// Example:
|
||||
//
|
||||
// kind: RoleBinding
|
||||
// subjects:
|
||||
// - name: "default" # this will have the namespace set
|
||||
// ...
|
||||
// - name: "something-else" # this will not have the namespace set
|
||||
// ...
|
||||
func (ns Filter) roleBindingHack(obj *yaml.RNode) error {
|
||||
var visitor filtersutil.SetFn
|
||||
switch ns.SetRoleBindingSubjects {
|
||||
case NoSubjects:
|
||||
return nil
|
||||
case DefaultSubjectsOnly, SubjectModeUnspecified:
|
||||
visitor = ns.setSubjectsNamedDefault
|
||||
case AllServiceAccountSubjects:
|
||||
visitor = ns.setServiceAccountNamespaces
|
||||
default:
|
||||
return errors.Errorf("invalid value %q for setRoleBindingSubjects: "+
|
||||
"must be one of %q, %q or %q", ns.SetRoleBindingSubjects,
|
||||
DefaultSubjectsOnly, NoSubjects, AllServiceAccountSubjects)
|
||||
}
|
||||
|
||||
// Lookup the subjects field on all elements.
|
||||
obj, err := obj.Pipe(yaml.Lookup(subjectsField))
|
||||
if err != nil || yaml.IsMissingOrNull(obj) {
|
||||
return err
|
||||
}
|
||||
// Use the appropriate visitor to set the namespace field on the correct subset of subjects
|
||||
return errors.WrapPrefixf(obj.VisitElements(visitor), "setting namespace on (cluster)role binding subjects")
|
||||
}
|
||||
|
||||
func isRoleBinding(kind string) bool {
|
||||
return kind == roleBindingKind || kind == clusterRoleBindingKind
|
||||
}
|
||||
|
||||
func (ns Filter) setServiceAccountNamespaces(o *yaml.RNode) error {
|
||||
name, err := o.Pipe(yaml.Lookup("kind"), yaml.Match("ServiceAccount"))
|
||||
if err != nil || yaml.IsMissingOrNull(name) {
|
||||
return errors.WrapPrefixf(err, "looking up kind on (cluster)role binding subject")
|
||||
}
|
||||
return setNamespaceField(o, ns.fieldSetter())
|
||||
}
|
||||
|
||||
func (ns Filter) setSubjectsNamedDefault(o *yaml.RNode) error {
|
||||
name, err := o.Pipe(yaml.Lookup("name"), yaml.Match("default"))
|
||||
if err != nil || yaml.IsMissingOrNull(name) {
|
||||
return errors.WrapPrefixf(err, "looking up name on (cluster)role binding subject")
|
||||
}
|
||||
return setNamespaceField(o, ns.fieldSetter())
|
||||
}
|
||||
|
||||
func setNamespaceField(node *yaml.RNode, setter filtersutil.SetFn) error {
|
||||
node, err := node.Pipe(yaml.LookupCreate(yaml.ScalarNode, "namespace"))
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "setting namespace field on (cluster)role binding subject")
|
||||
}
|
||||
return setter(node)
|
||||
}
|
||||
|
||||
// removeRoleBindingSubjectFieldSpecs removes from the list fieldspecs that
|
||||
// have hardcoded implementations
|
||||
func (ns Filter) removeRoleBindingSubjectFieldSpecs(fs types.FsSlice) types.FsSlice {
|
||||
var val types.FsSlice
|
||||
for i := range fs {
|
||||
if isRoleBinding(fs[i].Kind) && fs[i].Path == subjectsNamespacePath {
|
||||
continue
|
||||
}
|
||||
val = append(val, fs[i])
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (ns Filter) removeUnneededMetaFieldSpecs(apiVersion string, fs types.FsSlice) types.FsSlice {
|
||||
var val types.FsSlice
|
||||
for i := range fs {
|
||||
if fs[i].Path == types.MetadataNamespacePath {
|
||||
continue
|
||||
}
|
||||
if apiVersion != types.MetadataNamespaceApiVersion && fs[i].Path == types.MetadataNamePath {
|
||||
continue
|
||||
}
|
||||
val = append(val, fs[i])
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (ns *Filter) fieldSetter() filtersutil.SetFn {
|
||||
if ns.UnsetOnly {
|
||||
return ns.trackableSetter.SetEntryIfEmpty("", ns.Namespace, yaml.NodeTagString)
|
||||
}
|
||||
return ns.trackableSetter.SetEntry("", ns.Namespace, yaml.NodeTagString)
|
||||
}
|
||||
|
||||
const (
|
||||
subjectsField = "subjects"
|
||||
subjectsNamespacePath = "subjects/namespace"
|
||||
roleBindingKind = "RoleBinding"
|
||||
clusterRoleBindingKind = "ClusterRoleBinding"
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package namespace contains a kio.Filter implementation of the kustomize
|
||||
// patchjson6902 transformer
|
||||
package patchjson6902
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package patchjson6902
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
k8syaml "sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
Patch string
|
||||
|
||||
decodedPatch jsonpatch.Patch
|
||||
}
|
||||
|
||||
var _ kio.Filter = Filter{}
|
||||
|
||||
func (pf Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
decodedPatch, err := pf.decodePatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pf.decodedPatch = decodedPatch
|
||||
return kio.FilterAll(yaml.FilterFunc(pf.run)).Filter(nodes)
|
||||
}
|
||||
|
||||
func (pf Filter) decodePatch() (jsonpatch.Patch, error) {
|
||||
patch := pf.Patch
|
||||
// If the patch doesn't look like a JSON6902 patch, we
|
||||
// try to parse it to json.
|
||||
if !strings.HasPrefix(pf.Patch, "[") {
|
||||
p, err := k8syaml.YAMLToJSON([]byte(patch))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patch = string(p)
|
||||
}
|
||||
decodedPatch, err := jsonpatch.DecodePatch([]byte(patch))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodedPatch, nil
|
||||
}
|
||||
|
||||
func (pf Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
// We don't actually use the kyaml library for manipulating the
|
||||
// yaml here. We just marshal it to json and rely on the
|
||||
// jsonpatch library to take care of applying the patch.
|
||||
// This means ordering might not be preserved with this filter.
|
||||
b, err := node.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := pf.decodedPatch.Apply(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = node.UnmarshalJSON(res)
|
||||
return node, err
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package patchstrategicmerge contains a kio.Filter implementation of the
|
||||
// kustomize strategic merge patch transformer.
|
||||
package patchstrategicmerge
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package patchstrategicmerge
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/merge2"
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
Patch *yaml.RNode
|
||||
}
|
||||
|
||||
var _ kio.Filter = Filter{}
|
||||
|
||||
// Filter does a strategic merge patch, which can delete nodes.
|
||||
func (pf Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
var result []*yaml.RNode
|
||||
for i := range nodes {
|
||||
r, err := merge2.Merge(
|
||||
pf.Patch, nodes[i],
|
||||
yaml.MergeOptions{
|
||||
ListIncreaseDirection: yaml.MergeOptionsListPrepend,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r != nil {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package prefix contains a kio.Filter implementation of the kustomize
|
||||
// PrefixTransformer.
|
||||
package prefix
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package prefix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/fieldspec"
|
||||
"sigs.k8s.io/kustomize/api/filters/filtersutil"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Filter applies resource name prefix's using the fieldSpecs
|
||||
type Filter struct {
|
||||
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
|
||||
|
||||
FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"`
|
||||
|
||||
trackableSetter filtersutil.TrackableSetter
|
||||
}
|
||||
|
||||
var _ kio.Filter = Filter{}
|
||||
var _ kio.TrackableFilter = &Filter{}
|
||||
|
||||
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
|
||||
func (f *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
|
||||
f.trackableSetter.WithMutationTracker(callback)
|
||||
}
|
||||
|
||||
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
return kio.FilterAll(yaml.FilterFunc(f.run)).Filter(nodes)
|
||||
}
|
||||
|
||||
func (f Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
err := node.PipeE(fieldspec.Filter{
|
||||
FieldSpec: f.FieldSpec,
|
||||
SetValue: f.evaluateField,
|
||||
CreateKind: yaml.ScalarNode, // Name is a ScalarNode
|
||||
CreateTag: yaml.NodeTagString,
|
||||
})
|
||||
return node, err
|
||||
}
|
||||
|
||||
func (f Filter) evaluateField(node *yaml.RNode) error {
|
||||
return f.trackableSetter.SetScalar(fmt.Sprintf(
|
||||
"%s%s", f.Prefix, node.YNode().Value))(node)
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package refvar contains a kio.Filter implementation of the kustomize
|
||||
// refvar transformer (find and replace $(FOO) style variables in strings).
|
||||
package refvar
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package refvar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
operator = '$'
|
||||
referenceOpener = '('
|
||||
referenceCloser = ')'
|
||||
)
|
||||
|
||||
// syntaxWrap returns the input string wrapped by the expansion syntax.
|
||||
func syntaxWrap(input string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteByte(operator)
|
||||
sb.WriteByte(referenceOpener)
|
||||
sb.WriteString(input)
|
||||
sb.WriteByte(referenceCloser)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// MappingFunc maps a string to anything.
|
||||
type MappingFunc func(string) interface{}
|
||||
|
||||
// MakePrimitiveReplacer returns a MappingFunc that uses a map to do
|
||||
// replacements, and a histogram to count map hits.
|
||||
//
|
||||
// Func behavior:
|
||||
//
|
||||
// If the input key is NOT found in the map, the key is wrapped up as
|
||||
// as a variable declaration string and returned, e.g. key FOO becomes $(FOO).
|
||||
// This string is presumably put back where it was found, and might get replaced
|
||||
// later.
|
||||
//
|
||||
// If the key is found in the map, the value is returned if it is a primitive
|
||||
// type (string, bool, number), and the hit is counted.
|
||||
//
|
||||
// If it's not a primitive type (e.g. a map, struct, func, etc.) then this
|
||||
// function doesn't know what to do with it and it returns the key wrapped up
|
||||
// again as if it had not been replaced. This should probably be an error.
|
||||
func MakePrimitiveReplacer(
|
||||
counts map[string]int, someMap map[string]interface{}) MappingFunc {
|
||||
return func(key string) interface{} {
|
||||
if value, ok := someMap[key]; ok {
|
||||
switch typedV := value.(type) {
|
||||
case string, int, int32, int64, float32, float64, bool:
|
||||
counts[key]++
|
||||
return typedV
|
||||
default:
|
||||
// If the value is some complicated type (e.g. a map or struct),
|
||||
// this function doesn't know how to jam it into a string,
|
||||
// so just pretend it was a cache miss.
|
||||
// Likely this should be an error instead of a silent failure,
|
||||
// since the programmer passed an impossible value.
|
||||
log.Printf(
|
||||
"MakePrimitiveReplacer: bad replacement type=%T val=%v",
|
||||
typedV, typedV)
|
||||
return syntaxWrap(key)
|
||||
}
|
||||
}
|
||||
// If unable to return the mapped variable, return it
|
||||
// as it was found, and a later mapping might be able to
|
||||
// replace it.
|
||||
return syntaxWrap(key)
|
||||
}
|
||||
}
|
||||
|
||||
// DoReplacements replaces variable references in the input string
|
||||
// using the mapping function.
|
||||
func DoReplacements(input string, mapping MappingFunc) interface{} {
|
||||
var buf strings.Builder
|
||||
checkpoint := 0
|
||||
for cursor := 0; cursor < len(input); cursor++ {
|
||||
if input[cursor] == operator && cursor+1 < len(input) {
|
||||
// Copy the portion of the input string since the last
|
||||
// checkpoint into the buffer
|
||||
buf.WriteString(input[checkpoint:cursor])
|
||||
|
||||
// Attempt to read the variable name as defined by the
|
||||
// syntax from the input string
|
||||
read, isVar, advance := tryReadVariableName(input[cursor+1:])
|
||||
|
||||
if isVar {
|
||||
// We were able to read a variable name correctly;
|
||||
// apply the mapping to the variable name and copy the
|
||||
// bytes into the buffer
|
||||
mapped := mapping(read)
|
||||
if input == syntaxWrap(read) {
|
||||
// Preserve the type of variable
|
||||
return mapped
|
||||
}
|
||||
|
||||
// Variable is used in a middle of a string
|
||||
buf.WriteString(fmt.Sprintf("%v", mapped))
|
||||
} else {
|
||||
// Not a variable name; copy the read bytes into the buffer
|
||||
buf.WriteString(read)
|
||||
}
|
||||
|
||||
// Advance the cursor in the input string to account for
|
||||
// bytes consumed to read the variable name expression
|
||||
cursor += advance
|
||||
|
||||
// Advance the checkpoint in the input string
|
||||
checkpoint = cursor + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Return the buffer and any remaining unwritten bytes in the
|
||||
// input string.
|
||||
return buf.String() + input[checkpoint:]
|
||||
}
|
||||
|
||||
// tryReadVariableName attempts to read a variable name from the input
|
||||
// string and returns the content read from the input, whether that content
|
||||
// represents a variable name to perform mapping on, and the number of bytes
|
||||
// consumed in the input string.
|
||||
//
|
||||
// The input string is assumed not to contain the initial operator.
|
||||
func tryReadVariableName(input string) (string, bool, int) {
|
||||
switch input[0] {
|
||||
case operator:
|
||||
// Escaped operator; return it.
|
||||
return input[0:1], false, 1
|
||||
case referenceOpener:
|
||||
// Scan to expression closer
|
||||
for i := 1; i < len(input); i++ {
|
||||
if input[i] == referenceCloser {
|
||||
return input[1:i], true, i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Incomplete reference; return it.
|
||||
return string(operator) + string(referenceOpener), false, 1
|
||||
default:
|
||||
// Not the beginning of an expression, ie, an operator
|
||||
// that doesn't begin an expression. Return the operator
|
||||
// and the first rune in the string.
|
||||
return string(operator) + string(input[0]), false, 1
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package refvar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/fieldspec"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Filter updates $(VAR) style variables with values.
|
||||
// The fieldSpecs are the places to look for occurrences of $(VAR).
|
||||
type Filter struct {
|
||||
MappingFunc MappingFunc `json:"mappingFunc,omitempty" yaml:"mappingFunc,omitempty"`
|
||||
FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"`
|
||||
}
|
||||
|
||||
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
return kio.FilterAll(yaml.FilterFunc(f.run)).Filter(nodes)
|
||||
}
|
||||
|
||||
func (f Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
err := node.PipeE(fieldspec.Filter{
|
||||
FieldSpec: f.FieldSpec,
|
||||
SetValue: f.set,
|
||||
})
|
||||
return node, err
|
||||
}
|
||||
|
||||
func (f Filter) set(node *yaml.RNode) error {
|
||||
if yaml.IsMissingOrNull(node) {
|
||||
return nil
|
||||
}
|
||||
switch node.YNode().Kind {
|
||||
case yaml.ScalarNode:
|
||||
return f.setScalar(node)
|
||||
case yaml.MappingNode:
|
||||
return f.setMap(node)
|
||||
case yaml.SequenceNode:
|
||||
return f.setSeq(node)
|
||||
default:
|
||||
return fmt.Errorf("invalid type encountered %v", node.YNode().Kind)
|
||||
}
|
||||
}
|
||||
|
||||
func updateNodeValue(node *yaml.Node, newValue interface{}) {
|
||||
switch newValue := newValue.(type) {
|
||||
case int:
|
||||
node.Value = strconv.FormatInt(int64(newValue), 10)
|
||||
node.Tag = yaml.NodeTagInt
|
||||
case int32:
|
||||
node.Value = strconv.FormatInt(int64(newValue), 10)
|
||||
node.Tag = yaml.NodeTagInt
|
||||
case int64:
|
||||
node.Value = strconv.FormatInt(newValue, 10)
|
||||
node.Tag = yaml.NodeTagInt
|
||||
case bool:
|
||||
node.SetString(strconv.FormatBool(newValue))
|
||||
node.Tag = yaml.NodeTagBool
|
||||
case float32:
|
||||
node.SetString(strconv.FormatFloat(float64(newValue), 'f', -1, 32))
|
||||
node.Tag = yaml.NodeTagFloat
|
||||
case float64:
|
||||
node.SetString(strconv.FormatFloat(newValue, 'f', -1, 64))
|
||||
node.Tag = yaml.NodeTagFloat
|
||||
default:
|
||||
node.SetString(newValue.(string))
|
||||
node.Tag = yaml.NodeTagString
|
||||
}
|
||||
node.Style = 0
|
||||
}
|
||||
|
||||
func (f Filter) setScalar(node *yaml.RNode) error {
|
||||
if !yaml.IsYNodeString(node.YNode()) {
|
||||
return nil
|
||||
}
|
||||
v := DoReplacements(node.YNode().Value, f.MappingFunc)
|
||||
updateNodeValue(node.YNode(), v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f Filter) setMap(node *yaml.RNode) error {
|
||||
contents := node.YNode().Content
|
||||
for i := 0; i < len(contents); i += 2 {
|
||||
if !yaml.IsYNodeString(contents[i]) {
|
||||
return fmt.Errorf(
|
||||
"invalid map key: value='%s', tag='%s'",
|
||||
contents[i].Value, contents[i].Tag)
|
||||
}
|
||||
if !yaml.IsYNodeString(contents[i+1]) {
|
||||
continue
|
||||
}
|
||||
newValue := DoReplacements(contents[i+1].Value, f.MappingFunc)
|
||||
updateNodeValue(contents[i+1], newValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f Filter) setSeq(node *yaml.RNode) error {
|
||||
for _, item := range node.YNode().Content {
|
||||
if !yaml.IsYNodeString(item) {
|
||||
return fmt.Errorf("invalid value type expect a string")
|
||||
}
|
||||
newValue := DoReplacements(item.Value, f.MappingFunc)
|
||||
updateNodeValue(item, newValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package replacement contains a kio.Filter implementation of the kustomize
|
||||
// replacement transformer (accepts sources and looks for targets to replace
|
||||
// their values with values from the sources).
|
||||
package replacement
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package replacement
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/internal/utils"
|
||||
"sigs.k8s.io/kustomize/api/resource"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
kyaml_utils "sigs.k8s.io/kustomize/kyaml/utils"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
Replacements []types.Replacement `json:"replacements,omitempty" yaml:"replacements,omitempty"`
|
||||
}
|
||||
|
||||
// Filter replaces values of targets with values from sources
|
||||
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
for i, r := range f.Replacements {
|
||||
if (r.SourceValue == nil && r.Source == nil) || r.Targets == nil {
|
||||
return nil, fmt.Errorf("replacements must specify a source and at least one target")
|
||||
}
|
||||
value, err := getReplacement(nodes, &f.Replacements[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes, err = applyReplacement(nodes, value, r.Targets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func getReplacement(nodes []*yaml.RNode, r *types.Replacement) (*yaml.RNode, error) {
|
||||
if r.SourceValue != nil && r.Source != nil {
|
||||
return nil, fmt.Errorf("value and resource selectors are mutually exclusive")
|
||||
}
|
||||
if r.SourceValue != nil {
|
||||
return yaml.NewScalarRNode(*r.SourceValue), nil
|
||||
}
|
||||
|
||||
source, err := selectSourceNode(nodes, r.Source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.Source.FieldPath == "" {
|
||||
r.Source.FieldPath = types.DefaultReplacementFieldPath
|
||||
}
|
||||
fieldPath := kyaml_utils.SmarterPathSplitter(r.Source.FieldPath, ".")
|
||||
|
||||
rn, err := source.Pipe(yaml.Lookup(fieldPath...))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error looking up replacement source: %w", err)
|
||||
}
|
||||
if rn.IsNilOrEmpty() {
|
||||
return nil, fmt.Errorf("fieldPath `%s` is missing for replacement source %s", r.Source.FieldPath, r.Source.ResId)
|
||||
}
|
||||
|
||||
return getRefinedValue(r.Source.Options, rn)
|
||||
}
|
||||
|
||||
// selectSourceNode finds the node that matches the selector, returning
|
||||
// an error if multiple or none are found
|
||||
func selectSourceNode(nodes []*yaml.RNode, selector *types.SourceSelector) (*yaml.RNode, error) {
|
||||
var matches []*yaml.RNode
|
||||
for _, n := range nodes {
|
||||
ids, err := utils.MakeResIds(n)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting node IDs: %w", err)
|
||||
}
|
||||
for _, id := range ids {
|
||||
if id.IsSelectedBy(selector.ResId) {
|
||||
if len(matches) > 0 {
|
||||
return nil, fmt.Errorf(
|
||||
"multiple matches for selector %s", selector)
|
||||
}
|
||||
matches = append(matches, n)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return nil, fmt.Errorf("nothing selected by %s", selector)
|
||||
}
|
||||
return matches[0], nil
|
||||
}
|
||||
|
||||
func getRefinedValue(options *types.FieldOptions, rn *yaml.RNode) (*yaml.RNode, error) {
|
||||
if options == nil || options.Delimiter == "" {
|
||||
return rn, nil
|
||||
}
|
||||
if rn.YNode().Kind != yaml.ScalarNode {
|
||||
return nil, fmt.Errorf("delimiter option can only be used with scalar nodes")
|
||||
}
|
||||
value := strings.Split(yaml.GetValue(rn), options.Delimiter)
|
||||
if options.Index >= len(value) || options.Index < 0 {
|
||||
return nil, fmt.Errorf("options.index %d is out of bounds for value %s", options.Index, yaml.GetValue(rn))
|
||||
}
|
||||
n := rn.Copy()
|
||||
n.YNode().Value = value[options.Index]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func applyReplacement(nodes []*yaml.RNode, value *yaml.RNode, targetSelectors []*types.TargetSelector) ([]*yaml.RNode, error) {
|
||||
for _, selector := range targetSelectors {
|
||||
if selector.Select == nil {
|
||||
return nil, errors.Errorf("target must specify resources to select")
|
||||
}
|
||||
if len(selector.FieldPaths) == 0 {
|
||||
selector.FieldPaths = []string{types.DefaultReplacementFieldPath}
|
||||
}
|
||||
tsr, err := types.NewTargetSelectorRegex(selector)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating target selector: %w", err)
|
||||
}
|
||||
for _, possibleTarget := range nodes {
|
||||
ids, err := utils.MakeResIds(possibleTarget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// filter targets by label and annotation selectors
|
||||
selectByAnnoAndLabel, err := selectByAnnoAndLabel(possibleTarget, selector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !selectByAnnoAndLabel {
|
||||
continue
|
||||
}
|
||||
|
||||
if tsr.RejectsAny(ids) {
|
||||
continue
|
||||
}
|
||||
|
||||
// filter targets by matching resource IDs
|
||||
for _, id := range ids {
|
||||
if tsr.Selects(id) {
|
||||
err := copyValueToTarget(possibleTarget, value, selector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func selectByAnnoAndLabel(n *yaml.RNode, t *types.TargetSelector) (bool, error) {
|
||||
if matchesSelect, err := matchesAnnoAndLabelSelector(n, t.Select); !matchesSelect || err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, reject := range t.Reject {
|
||||
if reject.AnnotationSelector == "" && reject.LabelSelector == "" {
|
||||
continue
|
||||
}
|
||||
if m, err := matchesAnnoAndLabelSelector(n, reject); m || err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func matchesAnnoAndLabelSelector(n *yaml.RNode, selector *types.Selector) (bool, error) {
|
||||
r := resource.Resource{RNode: *n}
|
||||
annoMatch, err := r.MatchesAnnotationSelector(selector.AnnotationSelector)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
labelMatch, err := r.MatchesLabelSelector(selector.LabelSelector)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return annoMatch && labelMatch, nil
|
||||
}
|
||||
|
||||
func copyValueToTarget(target *yaml.RNode, value *yaml.RNode, selector *types.TargetSelector) error {
|
||||
for _, fp := range selector.FieldPaths {
|
||||
createKind := yaml.Kind(0) // do not create
|
||||
if selector.Options != nil && selector.Options.Create {
|
||||
createKind = value.YNode().Kind
|
||||
}
|
||||
|
||||
// Check if this fieldPath contains structured data access
|
||||
if err := setValueInStructuredData(target, value, fp, createKind); err == nil {
|
||||
// Successfully handled as structured data
|
||||
continue
|
||||
}
|
||||
|
||||
// Fall back to normal path handling
|
||||
targetFieldList, err := target.Pipe(&yaml.PathMatcher{
|
||||
Path: kyaml_utils.SmarterPathSplitter(fp, "."),
|
||||
Create: createKind})
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "%s", fieldRetrievalError(fp, createKind != 0))
|
||||
}
|
||||
targetFields, err := targetFieldList.Elements()
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "%s", fieldRetrievalError(fp, createKind != 0))
|
||||
}
|
||||
if len(targetFields) == 0 {
|
||||
return errors.Errorf("%s", fieldRetrievalError(fp, createKind != 0))
|
||||
}
|
||||
|
||||
for _, t := range targetFields {
|
||||
if err := setFieldValue(selector.Options, t, value); err != nil {
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fieldRetrievalError(fieldPath string, isCreate bool) string {
|
||||
if isCreate {
|
||||
return fmt.Sprintf("unable to find or create field %q in replacement target", fieldPath)
|
||||
}
|
||||
return fmt.Sprintf("unable to find field %q in replacement target", fieldPath)
|
||||
}
|
||||
|
||||
func setFieldValue(options *types.FieldOptions, targetField *yaml.RNode, value *yaml.RNode) error {
|
||||
value = value.Copy()
|
||||
if options != nil && options.Delimiter != "" {
|
||||
if targetField.YNode().Kind != yaml.ScalarNode {
|
||||
return fmt.Errorf("delimiter option can only be used with scalar nodes")
|
||||
}
|
||||
tv := strings.Split(targetField.YNode().Value, options.Delimiter)
|
||||
v := yaml.GetValue(value)
|
||||
// TODO: Add a way to remove an element
|
||||
switch {
|
||||
case options.Index < 0: // prefix
|
||||
tv = append([]string{v}, tv...)
|
||||
case options.Index >= len(tv): // suffix
|
||||
tv = append(tv, v)
|
||||
default: // replace an element
|
||||
tv[options.Index] = v
|
||||
}
|
||||
value.YNode().Value = strings.Join(tv, options.Delimiter)
|
||||
}
|
||||
|
||||
if targetField.YNode().Kind == yaml.ScalarNode {
|
||||
// For scalar, only copy the value (leave any type intact to auto-convert int->string or string->int)
|
||||
targetField.YNode().Value = value.YNode().Value
|
||||
} else {
|
||||
targetField.SetYNode(value.YNode())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setValueInStructuredData handles setting values within structured data (JSON/YAML) in scalar fields
|
||||
func setValueInStructuredData(target *yaml.RNode, value *yaml.RNode, fieldPath string, createKind yaml.Kind) error {
|
||||
pathParts := kyaml_utils.SmarterPathSplitter(fieldPath, ".")
|
||||
if len(pathParts) < 2 {
|
||||
return fmt.Errorf("not a structured data path")
|
||||
}
|
||||
|
||||
// Find the potential scalar field that might contain structured data
|
||||
var scalarFieldPath []string
|
||||
var structuredDataPath []string
|
||||
var foundScalar = false
|
||||
|
||||
// Try to find where the scalar field ends and structured data begins
|
||||
for i := 1; i <= len(pathParts); i++ {
|
||||
potentialScalarPath := pathParts[:i]
|
||||
scalarField, err := target.Pipe(yaml.Lookup(potentialScalarPath...))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if scalarField != nil && scalarField.YNode().Kind == yaml.ScalarNode && i < len(pathParts) {
|
||||
// Try to parse the scalar value as structured data
|
||||
scalarValue := scalarField.YNode().Value
|
||||
var parsedNode yaml.Node
|
||||
if err := yaml.Unmarshal([]byte(scalarValue), &parsedNode); err == nil {
|
||||
// Successfully parsed - this is structured data
|
||||
scalarFieldPath = potentialScalarPath
|
||||
structuredDataPath = pathParts[i:]
|
||||
foundScalar = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundScalar {
|
||||
return fmt.Errorf("no structured data found in path")
|
||||
}
|
||||
|
||||
// Get the scalar field containing structured data
|
||||
scalarField, err := target.Pipe(yaml.Lookup(scalarFieldPath...))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
// Parse the structured data
|
||||
scalarValue := scalarField.YNode().Value
|
||||
var parsedNode yaml.Node
|
||||
if err := yaml.Unmarshal([]byte(scalarValue), &parsedNode); err != nil {
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
structuredData := yaml.NewRNode(&parsedNode)
|
||||
|
||||
// Navigate to the target location within the structured data
|
||||
targetInStructured, err := structuredData.Pipe(&yaml.PathMatcher{
|
||||
Path: structuredDataPath,
|
||||
Create: createKind,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
targetFields, err := targetInStructured.Elements()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
if len(targetFields) == 0 {
|
||||
return fmt.Errorf("unable to find field in structured data")
|
||||
}
|
||||
|
||||
// Set the value in the structured data
|
||||
for _, t := range targetFields {
|
||||
if t.YNode().Kind == yaml.ScalarNode {
|
||||
t.YNode().Value = value.YNode().Value
|
||||
} else {
|
||||
t.SetYNode(value.YNode())
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the modified structured data back to the scalar field
|
||||
// Try to detect if original was JSON or YAML and preserve formatting
|
||||
serializedData, err := serializeStructuredData(structuredData, scalarValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
// Update the original scalar field
|
||||
scalarField.YNode().Value = serializedData
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// serializeStructuredData handles the serialization of structured data back to string format
|
||||
// preserving the original format (JSON vs YAML) and style (pretty vs compact)
|
||||
func serializeStructuredData(structuredData *yaml.RNode, originalValue string) (string, error) {
|
||||
firstChar := rune(strings.TrimSpace(originalValue)[0])
|
||||
if firstChar == '{' || firstChar == '[' {
|
||||
return serializeAsJSON(structuredData, originalValue)
|
||||
}
|
||||
|
||||
// Fallback to YAML format
|
||||
return serializeAsYAML(structuredData)
|
||||
}
|
||||
|
||||
// serializeAsJSON converts structured data back to JSON format
|
||||
func serializeAsJSON(structuredData *yaml.RNode, originalValue string) (string, error) {
|
||||
modifiedData, err := structuredData.String()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize structured data: %w", err)
|
||||
}
|
||||
|
||||
// Parse the YAML output as JSON
|
||||
var jsonData interface{}
|
||||
if err := yaml.Unmarshal([]byte(modifiedData), &jsonData); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal YAML data: %w", err)
|
||||
}
|
||||
|
||||
// Check if original was pretty-printed by looking for newlines and indentation
|
||||
if strings.Contains(originalValue, "\n") && strings.Contains(originalValue, " ") {
|
||||
// Pretty-print the JSON to match original formatting
|
||||
if prettyJSON, err := json.MarshalIndent(jsonData, "", " "); err == nil {
|
||||
return string(prettyJSON), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Compact JSON
|
||||
if compactJSON, err := json.Marshal(jsonData); err == nil {
|
||||
return string(compactJSON), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed to marshal JSON data")
|
||||
}
|
||||
|
||||
// serializeAsYAML converts structured data back to YAML format
|
||||
func serializeAsYAML(structuredData *yaml.RNode) (string, error) {
|
||||
modifiedData, err := structuredData.String()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize YAML data: %w", err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(modifiedData), nil
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package replicacount contains a kio.Filter implementation of the kustomize
|
||||
// ReplicaCountTransformer.
|
||||
package replicacount
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package replicacount
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/fieldspec"
|
||||
"sigs.k8s.io/kustomize/api/filters/filtersutil"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Filter updates/sets replicas fields using the fieldSpecs
|
||||
type Filter struct {
|
||||
Replica types.Replica `json:"replica,omitempty" yaml:"replica,omitempty"`
|
||||
FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"`
|
||||
|
||||
trackableSetter filtersutil.TrackableSetter
|
||||
}
|
||||
|
||||
var _ kio.Filter = Filter{}
|
||||
var _ kio.TrackableFilter = &Filter{}
|
||||
|
||||
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
|
||||
func (rc *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
|
||||
rc.trackableSetter.WithMutationTracker(callback)
|
||||
}
|
||||
|
||||
func (rc Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
return kio.FilterAll(yaml.FilterFunc(rc.run)).Filter(nodes)
|
||||
}
|
||||
|
||||
func (rc Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
err := node.PipeE(fieldspec.Filter{
|
||||
FieldSpec: rc.FieldSpec,
|
||||
SetValue: rc.set,
|
||||
CreateKind: yaml.ScalarNode, // replicas is a ScalarNode
|
||||
CreateTag: yaml.NodeTagInt,
|
||||
})
|
||||
return node, err
|
||||
}
|
||||
|
||||
func (rc Filter) set(node *yaml.RNode) error {
|
||||
return rc.trackableSetter.SetEntry("", strconv.FormatInt(rc.Replica.Count, 10), yaml.NodeTagInt)(node)
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package suffix contains a kio.Filter implementation of the kustomize
|
||||
// SuffixTransformer.
|
||||
package suffix
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package suffix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/fieldspec"
|
||||
"sigs.k8s.io/kustomize/api/filters/filtersutil"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Filter applies resource name suffix's using the fieldSpecs
|
||||
type Filter struct {
|
||||
Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"`
|
||||
|
||||
FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"`
|
||||
|
||||
trackableSetter filtersutil.TrackableSetter
|
||||
}
|
||||
|
||||
var _ kio.Filter = Filter{}
|
||||
var _ kio.TrackableFilter = &Filter{}
|
||||
|
||||
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
|
||||
func (f *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
|
||||
f.trackableSetter.WithMutationTracker(callback)
|
||||
}
|
||||
|
||||
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
return kio.FilterAll(yaml.FilterFunc(f.run)).Filter(nodes)
|
||||
}
|
||||
|
||||
func (f Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
err := node.PipeE(fieldspec.Filter{
|
||||
FieldSpec: f.FieldSpec,
|
||||
SetValue: f.evaluateField,
|
||||
CreateKind: yaml.ScalarNode, // Name is a ScalarNode
|
||||
CreateTag: yaml.NodeTagString,
|
||||
})
|
||||
return node, err
|
||||
}
|
||||
|
||||
func (f Filter) evaluateField(node *yaml.RNode) error {
|
||||
return f.trackableSetter.SetScalar(fmt.Sprintf(
|
||||
"%s%s", node.YNode().Value, f.Suffix))(node)
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package valueadd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// An 'Add' operation aspiring to IETF RFC 6902 JSON.
|
||||
//
|
||||
// The filter tries to add a value to a node at a particular field path.
|
||||
//
|
||||
// Kinds of target fields:
|
||||
//
|
||||
// - Non-existent target field.
|
||||
//
|
||||
// The field will be added and the value inserted.
|
||||
//
|
||||
// - Existing field, scalar or map.
|
||||
//
|
||||
// E.g. 'spec/template/spec/containers/[name:nginx]/image'
|
||||
//
|
||||
// This behaves like an IETF RFC 6902 Replace operation would;
|
||||
// the existing value is replaced without complaint, even though
|
||||
// this is an Add operation. In contrast, a Replace operation
|
||||
// must fail (report an error) if the field doesn't exist.
|
||||
//
|
||||
// - Existing field, list (array)
|
||||
// Not supported yet.
|
||||
// TODO: Honor fields with RFC-6902-style array indices
|
||||
// TODO: like 'spec/template/spec/containers/2'
|
||||
// TODO: Modify kyaml/yaml/PathGetter to allow this.
|
||||
// The value will be inserted into the array at the given position,
|
||||
// shifting other contents. To instead replace an array entry, use
|
||||
// an implementation of an IETF RFC 6902 Replace operation.
|
||||
//
|
||||
// For the common case of a filepath in the field value, and a desire
|
||||
// to add the value to the filepath (rather than replace the filepath),
|
||||
// use a non-zero value of FilePathPosition (see below).
|
||||
type Filter struct {
|
||||
// Value is the value to add.
|
||||
//
|
||||
// Empty values are disallowed, i.e. this filter isn't intended
|
||||
// for use in erasing or removing fields. For that, use a filter
|
||||
// more aligned with the IETF RFC 6902 JSON Remove operation.
|
||||
//
|
||||
// At the time of writing, Value's value should be a simple string,
|
||||
// not a JSON document. This particular filter focuses on easing
|
||||
// injection of a single-sourced cloud project and/or cluster name
|
||||
// into various fields, especially namespace and various filepath
|
||||
// specifications.
|
||||
Value string
|
||||
|
||||
// FieldPath is a JSON-style path to the field intended to hold the value.
|
||||
FieldPath string
|
||||
|
||||
// FilePathPosition is a filepath field index.
|
||||
//
|
||||
// Call the value of this field _i_.
|
||||
//
|
||||
// If _i_ is zero, negative or unspecified, this field has no effect.
|
||||
//
|
||||
// If _i_ is > 0, then it's assumed that
|
||||
// - 'Value' is a string that can work as a directory or file name,
|
||||
// - the field value intended for replacement holds a filepath.
|
||||
//
|
||||
// The filepath is split into a string slice, the value is inserted
|
||||
// at position [i-1], shifting the rest of the path to the right.
|
||||
// A value of i==1 puts the new value at the start of the path.
|
||||
// This change never converts an absolute path to a relative path,
|
||||
// meaning adding a new field at position i==1 will preserve a
|
||||
// leading slash. E.g. if Value == 'PEACH'
|
||||
//
|
||||
// OLD : NEW : FilePathPosition
|
||||
// --------------------------------------------------------
|
||||
// {empty} : PEACH : irrelevant
|
||||
// / : /PEACH : irrelevant
|
||||
// pie : PEACH/pie : 1 (or less to prefix)
|
||||
// /pie : /PEACH/pie : 1 (or less to prefix)
|
||||
// raw : raw/PEACH : 2 (or more to postfix)
|
||||
// /raw : /raw/PEACH : 2 (or more to postfix)
|
||||
// a/nice/warm/pie : a/nice/warm/PEACH/pie : 4
|
||||
// /a/nice/warm/pie : /a/nice/warm/PEACH/pie : 4
|
||||
//
|
||||
// For robustness (liberal input, conservative output) FilePathPosition
|
||||
// values that that are too large to index the split filepath result in a
|
||||
// postfix rather than an error. So use 1 to prefix, 9999 to postfix.
|
||||
FilePathPosition int `json:"filePathPosition,omitempty" yaml:"filePathPosition,omitempty"`
|
||||
}
|
||||
|
||||
var _ kio.Filter = Filter{}
|
||||
|
||||
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
_, err := kio.FilterAll(yaml.FilterFunc(
|
||||
func(node *yaml.RNode) (*yaml.RNode, error) {
|
||||
var fields []string
|
||||
// if there is forward slash '/' in the field name, a back slash '\'
|
||||
// will be used to escape it.
|
||||
for _, f := range strings.Split(f.FieldPath, "/") {
|
||||
if len(fields) > 0 && strings.HasSuffix(fields[len(fields)-1], "\\") {
|
||||
concatField := strings.TrimSuffix(fields[len(fields)-1], "\\") + "/" + f
|
||||
fields = append(fields[:len(fields)-1], concatField)
|
||||
} else {
|
||||
fields = append(fields, f)
|
||||
}
|
||||
}
|
||||
// TODO: support SequenceNode.
|
||||
// Presumably here one could look for array indices (digits) at
|
||||
// the end of the field path (as described in IETF RFC 6902 JSON),
|
||||
// and if found, take it as a signal that this should be a
|
||||
// SequenceNode instead of a ScalarNode, and insert the value
|
||||
// into the proper slot, shifting every over.
|
||||
n, err := node.Pipe(yaml.LookupCreate(yaml.ScalarNode, fields...))
|
||||
if err != nil {
|
||||
return node, err
|
||||
}
|
||||
// TODO: allow more kinds
|
||||
if err := yaml.ErrorIfInvalid(n, yaml.ScalarNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newValue := f.Value
|
||||
if f.FilePathPosition > 0 {
|
||||
newValue = filesys.InsertPathPart(
|
||||
n.YNode().Value, f.FilePathPosition-1, newValue)
|
||||
}
|
||||
return n.Pipe(yaml.FieldSetter{StringValue: newValue})
|
||||
})).Filter(nodes)
|
||||
return nodes, err
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package hasher
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// SortArrayAndComputeHash sorts a string array and
|
||||
// returns a hash for it
|
||||
func SortArrayAndComputeHash(s []string) (string, error) {
|
||||
sort.Strings(s)
|
||||
data, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return encode(hex256(string(data)))
|
||||
}
|
||||
|
||||
// Copied from https://github.com/kubernetes/kubernetes
|
||||
// /blob/master/pkg/kubectl/util/hash/hash.go
|
||||
func encode(hex string) (string, error) {
|
||||
if len(hex) < 10 {
|
||||
return "", fmt.Errorf(
|
||||
"input length must be at least 10")
|
||||
}
|
||||
enc := []rune(hex[:10])
|
||||
for i := range enc {
|
||||
switch enc[i] {
|
||||
case '0':
|
||||
enc[i] = 'g'
|
||||
case '1':
|
||||
enc[i] = 'h'
|
||||
case '3':
|
||||
enc[i] = 'k'
|
||||
case 'a':
|
||||
enc[i] = 'm'
|
||||
case 'e':
|
||||
enc[i] = 't'
|
||||
}
|
||||
}
|
||||
return string(enc), nil
|
||||
}
|
||||
|
||||
// hex256 returns the hex form of the sha256 of the argument.
|
||||
func hex256(data string) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(data)))
|
||||
}
|
||||
|
||||
// Hasher computes the hash of an RNode.
|
||||
type Hasher struct{}
|
||||
|
||||
// Hash returns a hash of the argument.
|
||||
func (h *Hasher) Hash(node *yaml.RNode) (r string, err error) {
|
||||
var encoded string
|
||||
switch node.GetKind() {
|
||||
case "ConfigMap":
|
||||
encoded, err = encodeConfigMap(node)
|
||||
case "Secret":
|
||||
encoded, err = encodeSecret(node)
|
||||
default:
|
||||
var encodedBytes []byte
|
||||
encodedBytes, err = json.Marshal(node.YNode())
|
||||
encoded = string(encodedBytes)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return encode(hex256(encoded))
|
||||
}
|
||||
|
||||
func getNodeValues(
|
||||
node *yaml.RNode, paths []string) (map[string]interface{}, error) {
|
||||
values := make(map[string]interface{})
|
||||
for _, p := range paths {
|
||||
vn, err := node.Pipe(yaml.Lookup(p))
|
||||
if err != nil {
|
||||
return map[string]interface{}{}, err
|
||||
}
|
||||
if vn == nil {
|
||||
values[p] = ""
|
||||
continue
|
||||
}
|
||||
if vn.YNode().Kind != yaml.ScalarNode {
|
||||
vs, err := vn.MarshalJSON()
|
||||
if err != nil {
|
||||
return map[string]interface{}{}, err
|
||||
}
|
||||
// data, binaryData and stringData are all maps
|
||||
var v map[string]interface{}
|
||||
json.Unmarshal(vs, &v)
|
||||
values[p] = v
|
||||
} else {
|
||||
values[p] = vn.YNode().Value
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// encodeConfigMap encodes a ConfigMap.
|
||||
// Data, Kind, and Name are taken into account.
|
||||
// BinaryData is included if it's not empty to avoid useless key in output.
|
||||
func encodeConfigMap(node *yaml.RNode) (string, error) {
|
||||
// get fields
|
||||
paths := []string{"metadata/name", "data", "binaryData"}
|
||||
values, err := getNodeValues(node, paths)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m := map[string]interface{}{
|
||||
"kind": "ConfigMap",
|
||||
"name": values["metadata/name"],
|
||||
"data": values["data"],
|
||||
}
|
||||
if _, ok := values["binaryData"].(map[string]interface{}); ok {
|
||||
m["binaryData"] = values["binaryData"]
|
||||
}
|
||||
|
||||
// json.Marshal sorts the keys in a stable order in the encoding
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// encodeSecret encodes a Secret.
|
||||
// Data, Kind, Name, and Type are taken into account.
|
||||
// StringData is included if it's not empty to avoid useless key in output.
|
||||
func encodeSecret(node *yaml.RNode) (string, error) {
|
||||
// get fields
|
||||
paths := []string{"type", "metadata/name", "data", "stringData"}
|
||||
values, err := getNodeValues(node, paths)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m := map[string]interface{}{"kind": "Secret", "type": values["type"],
|
||||
"name": values["metadata/name"], "data": values["data"]}
|
||||
if _, ok := values["stringData"].(map[string]interface{}); ok {
|
||||
m["stringData"] = values["stringData"]
|
||||
}
|
||||
|
||||
// json.Marshal sorts the keys in a stable order in the encoding
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package ifc holds miscellaneous interfaces used by kustomize.
|
||||
package ifc
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Validator provides functions to validate annotations and labels
|
||||
type Validator interface {
|
||||
MakeAnnotationValidator() func(map[string]string) error
|
||||
MakeAnnotationNameValidator() func([]string) error
|
||||
MakeLabelValidator() func(map[string]string) error
|
||||
MakeLabelNameValidator() func([]string) error
|
||||
ValidateNamespace(string) []string
|
||||
ErrIfInvalidKey(string) error
|
||||
IsEnvVarName(k string) error
|
||||
}
|
||||
|
||||
// KvLoader reads and validates KV pairs.
|
||||
type KvLoader interface {
|
||||
Validator() Validator
|
||||
Load(args types.KvPairSources) (all []types.Pair, err error)
|
||||
}
|
||||
|
||||
// Loader interface exposes methods to read bytes.
|
||||
type Loader interface {
|
||||
|
||||
// Repo returns the repo location if this Loader was created from a url
|
||||
// or the empty string otherwise.
|
||||
Repo() string
|
||||
|
||||
// Root returns the root location for this Loader.
|
||||
Root() string
|
||||
|
||||
// New returns Loader located at newRoot.
|
||||
New(newRoot string) (Loader, error)
|
||||
|
||||
// Load returns the bytes read from the location or an error.
|
||||
Load(location string) ([]byte, error)
|
||||
|
||||
// Cleanup cleans the loader
|
||||
Cleanup() error
|
||||
}
|
||||
|
||||
// KustHasher returns a hash of the argument
|
||||
// or an error.
|
||||
type KustHasher interface {
|
||||
Hash(*yaml.RNode) (string, error)
|
||||
}
|
||||
|
||||
// See core.v1.SecretTypeOpaque
|
||||
const SecretTypeOpaque = "Opaque"
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package accumulator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
"sigs.k8s.io/kustomize/api/ifc"
|
||||
"sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||
"sigs.k8s.io/kustomize/kyaml/resid"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// OpenAPIDefinition describes single type.
|
||||
// Normally these definitions are auto-generated using gen-openapi.
|
||||
// Same as in k8s.io / kube-openapi / pkg / common.
|
||||
type OpenAPIDefinition struct {
|
||||
Schema spec.Schema
|
||||
Dependencies []string
|
||||
}
|
||||
|
||||
type myProperties = map[string]spec.Schema
|
||||
type nameToApiMap map[string]OpenAPIDefinition
|
||||
|
||||
// LoadConfigFromCRDs parse CRD schemas from paths into a TransformerConfig
|
||||
func LoadConfigFromCRDs(
|
||||
ldr ifc.Loader, paths []string) (*builtinconfig.TransformerConfig, error) {
|
||||
tc := builtinconfig.MakeEmptyConfig()
|
||||
for _, path := range paths {
|
||||
content, err := ldr.Load(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := makeNameToApiMap(content)
|
||||
if err != nil {
|
||||
return nil, errors.WrapPrefixf(err, "unable to parse open API definition from '%s'", path)
|
||||
}
|
||||
otherTc, err := makeConfigFromApiMap(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tc, err = tc.Merge(otherTc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
func makeNameToApiMap(content []byte) (result nameToApiMap, err error) {
|
||||
if content[0] == '{' {
|
||||
err = json.Unmarshal(content, &result)
|
||||
} else {
|
||||
err = yaml.Unmarshal(content, &result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func makeConfigFromApiMap(m nameToApiMap) (*builtinconfig.TransformerConfig, error) {
|
||||
result := builtinconfig.MakeEmptyConfig()
|
||||
for name, api := range m {
|
||||
if !looksLikeAk8sType(api.Schema.SchemaProps.Properties) {
|
||||
continue
|
||||
}
|
||||
tc := builtinconfig.MakeEmptyConfig()
|
||||
err := loadCrdIntoConfig(
|
||||
tc, makeGvkFromTypeName(name), m, name, []string{})
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result, err = result.Merge(tc)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TODO: Get Group and Version for CRD from the
|
||||
// openAPI definition once
|
||||
// "x-kubernetes-group-version-kind" is available in CRD
|
||||
func makeGvkFromTypeName(n string) resid.Gvk {
|
||||
names := strings.Split(n, filesys.SelfDir)
|
||||
kind := names[len(names)-1]
|
||||
return resid.Gvk{Kind: kind}
|
||||
}
|
||||
|
||||
func looksLikeAk8sType(properties myProperties) bool {
|
||||
_, ok := properties["kind"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, ok = properties["apiVersion"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, ok = properties["metadata"]
|
||||
return ok
|
||||
}
|
||||
|
||||
const (
|
||||
// "x-kubernetes-annotation": ""
|
||||
xAnnotation = "x-kubernetes-annotation"
|
||||
|
||||
// "x-kubernetes-label-selector": ""
|
||||
xLabelSelector = "x-kubernetes-label-selector"
|
||||
|
||||
// "x-kubernetes-identity": ""
|
||||
xIdentity = "x-kubernetes-identity"
|
||||
|
||||
// "x-kubernetes-object-ref-api-version": <apiVersion name>
|
||||
xVersion = "x-kubernetes-object-ref-api-version"
|
||||
|
||||
// "x-kubernetes-object-ref-kind": <kind name>
|
||||
xKind = "x-kubernetes-object-ref-kind"
|
||||
|
||||
// "x-kubernetes-object-ref-name-key": "name"
|
||||
// default is "name"
|
||||
xNameKey = "x-kubernetes-object-ref-name-key"
|
||||
)
|
||||
|
||||
// loadCrdIntoConfig loads a CRD spec into a TransformerConfig
|
||||
func loadCrdIntoConfig(
|
||||
theConfig *builtinconfig.TransformerConfig, theGvk resid.Gvk, theMap nameToApiMap,
|
||||
typeName string, path []string) (err error) {
|
||||
api, ok := theMap[typeName]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for propName, property := range api.Schema.SchemaProps.Properties {
|
||||
_, annotate := property.Extensions.GetString(xAnnotation)
|
||||
if annotate {
|
||||
err = theConfig.AddAnnotationFieldSpec(
|
||||
makeFs(theGvk, append(path, propName)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_, label := property.Extensions.GetString(xLabelSelector)
|
||||
if label {
|
||||
err = theConfig.AddCommonLabelsFieldSpec(
|
||||
makeFs(theGvk, append(path, propName)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_, identity := property.Extensions.GetString(xIdentity)
|
||||
if identity {
|
||||
err = theConfig.AddPrefixFieldSpec(
|
||||
makeFs(theGvk, append(path, propName)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
version, ok := property.Extensions.GetString(xVersion)
|
||||
if ok {
|
||||
kind, ok := property.Extensions.GetString(xKind)
|
||||
if ok {
|
||||
nameKey, ok := property.Extensions.GetString(xNameKey)
|
||||
if !ok {
|
||||
nameKey = "name"
|
||||
}
|
||||
err = theConfig.AddNamereferenceFieldSpec(
|
||||
builtinconfig.NameBackReferences{
|
||||
Gvk: resid.Gvk{Kind: kind, Version: version},
|
||||
Referrers: []types.FieldSpec{
|
||||
makeFs(theGvk, append(path, propName, nameKey))},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if property.Ref.GetURL() != nil {
|
||||
err = loadCrdIntoConfig(
|
||||
theConfig, theGvk, theMap,
|
||||
property.Ref.String(), append(path, propName))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeFs(in resid.Gvk, path []string) types.FieldSpec {
|
||||
return types.FieldSpec{
|
||||
CreateIfNotPresent: false,
|
||||
Gvk: in,
|
||||
Path: strings.Join(path, "/"),
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+164
@@ -0,0 +1,164 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package accumulator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/nameref"
|
||||
"sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/resource"
|
||||
"sigs.k8s.io/kustomize/kyaml/resid"
|
||||
)
|
||||
|
||||
type nameReferenceTransformer struct {
|
||||
backRefs []builtinconfig.NameBackReferences
|
||||
}
|
||||
|
||||
const doDebug = false
|
||||
|
||||
var _ resmap.Transformer = &nameReferenceTransformer{}
|
||||
|
||||
type filterMap map[*resource.Resource][]nameref.Filter
|
||||
|
||||
// newNameReferenceTransformer constructs a nameReferenceTransformer
|
||||
// with a given slice of NameBackReferences.
|
||||
func newNameReferenceTransformer(
|
||||
br []builtinconfig.NameBackReferences) resmap.Transformer {
|
||||
if br == nil {
|
||||
log.Fatal("backrefs not expected to be nil")
|
||||
}
|
||||
return &nameReferenceTransformer{backRefs: br}
|
||||
}
|
||||
|
||||
// Transform updates name references in resource A that
|
||||
// refer to resource B, given that B's name may have
|
||||
// changed.
|
||||
//
|
||||
// For example, a HorizontalPodAutoscaler (HPA)
|
||||
// necessarily refers to a Deployment, the thing that
|
||||
// an HPA scales. In this case:
|
||||
//
|
||||
// - the HPA instance is the Referrer,
|
||||
// - the Deployment instance is the ReferralTarget.
|
||||
//
|
||||
// If the Deployment's name changes, e.g. a prefix is added,
|
||||
// then the HPA's reference to the Deployment must be fixed.
|
||||
//
|
||||
func (t *nameReferenceTransformer) Transform(m resmap.ResMap) error {
|
||||
fMap := t.determineFilters(m.Resources())
|
||||
debug(fMap)
|
||||
for r, fList := range fMap {
|
||||
c, err := m.SubsetThatCouldBeReferencedByResource(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range fList {
|
||||
f.Referrer = r
|
||||
f.ReferralCandidates = c
|
||||
if err := f.Referrer.ApplyFilter(f); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func debug(fMap filterMap) {
|
||||
if !doDebug {
|
||||
return
|
||||
}
|
||||
fmt.Printf("filterMap has %d entries:\n", len(fMap))
|
||||
rCount := 0
|
||||
for r, fList := range fMap {
|
||||
yml, _ := r.AsYAML()
|
||||
rCount++
|
||||
fmt.Printf(`
|
||||
---- %3d. possible referrer -------------
|
||||
%s
|
||||
---------`, rCount, string(yml),
|
||||
)
|
||||
for i, f := range fList {
|
||||
fmt.Printf(`
|
||||
%3d/%3d update: %s
|
||||
from: %s
|
||||
`, rCount, i+1, f.NameFieldToUpdate.Path, f.ReferralTarget,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produce a map from referrer resources that might need to be fixed
|
||||
// to filters that might fix them. The keys to this map are potential
|
||||
// referrers, so won't include resources like ConfigMap or Secret.
|
||||
//
|
||||
// In the inner loop over the resources below, say we
|
||||
// encounter an HPA instance. Then, in scanning the set
|
||||
// of all known backrefs, we encounter an entry like
|
||||
//
|
||||
// - kind: Deployment
|
||||
// fieldSpecs:
|
||||
// - kind: HorizontalPodAutoscaler
|
||||
// path: spec/scaleTargetRef/name
|
||||
//
|
||||
// This entry says that an HPA, via its
|
||||
// 'spec/scaleTargetRef/name' field, may refer to a
|
||||
// Deployment.
|
||||
//
|
||||
// This means that a filter will need to hunt for the right Deployment,
|
||||
// obtain it's new name, and write that name into the HPA's
|
||||
// 'spec/scaleTargetRef/name' field. Return a filter that can do that.
|
||||
func (t *nameReferenceTransformer) determineFilters(
|
||||
resources []*resource.Resource) (fMap filterMap) {
|
||||
// We cache the resource OrgId values because they don't change and otherwise are very visible in a memory pprof
|
||||
resourceOrgIds := make([]resid.ResId, len(resources))
|
||||
for i, resource := range resources {
|
||||
resourceOrgIds[i] = resource.OrgId()
|
||||
}
|
||||
|
||||
fMap = make(filterMap)
|
||||
for _, backReference := range t.backRefs {
|
||||
for _, referrerSpec := range backReference.Referrers {
|
||||
for i, res := range resources {
|
||||
if resourceOrgIds[i].IsSelected(&referrerSpec.Gvk) {
|
||||
// If this is true, the res might be a referrer, and if
|
||||
// so, the name reference it holds might need an update.
|
||||
if resHasField(res, referrerSpec.Path) {
|
||||
// Optimization - the referrer has the field
|
||||
// that might need updating.
|
||||
fMap[res] = append(fMap[res], nameref.Filter{
|
||||
// Name field to write in the Referrer.
|
||||
// If the path specified here isn't found in
|
||||
// the Referrer, nothing happens (no error,
|
||||
// no field creation).
|
||||
NameFieldToUpdate: referrerSpec,
|
||||
// Specification of object class to read from.
|
||||
// Always read from metadata/name field.
|
||||
ReferralTarget: backReference.Gvk,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fMap
|
||||
}
|
||||
|
||||
// TODO: check res for field existence here to avoid extra work.
|
||||
// res.GetFieldValue, which uses yaml.Lookup under the hood, doesn't know
|
||||
// how to parse fieldspec-style paths that make no distinction
|
||||
// between maps and sequences. This means it cannot lookup commonly
|
||||
// used "indeterminate" paths like
|
||||
// spec/containers/env/valueFrom/configMapKeyRef/name
|
||||
// ('containers' is a list, not a map).
|
||||
// However, the fieldspec filter does know how to handle this;
|
||||
// extract that code and call it here?
|
||||
func resHasField(res *resource.Resource, path string) bool {
|
||||
return true
|
||||
// fld := strings.Join(utils.PathSplitter(path), ".")
|
||||
// _, e := res.GetFieldValue(fld)
|
||||
// return e == nil
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package accumulator
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/filters/refvar"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
)
|
||||
|
||||
type refVarTransformer struct {
|
||||
varMap map[string]interface{}
|
||||
replacementCounts map[string]int
|
||||
fieldSpecs []types.FieldSpec
|
||||
}
|
||||
|
||||
// newRefVarTransformer returns a new refVarTransformer
|
||||
// that replaces $(VAR) style variables with values.
|
||||
// The fieldSpecs are the places to look for occurrences of $(VAR).
|
||||
func newRefVarTransformer(
|
||||
varMap map[string]interface{}, fs []types.FieldSpec) *refVarTransformer {
|
||||
return &refVarTransformer{
|
||||
varMap: varMap,
|
||||
fieldSpecs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
// UnusedVars returns slice of Var names that were unused
|
||||
// after a Transform run.
|
||||
func (rv *refVarTransformer) UnusedVars() []string {
|
||||
var unused []string
|
||||
for k := range rv.varMap {
|
||||
if _, ok := rv.replacementCounts[k]; !ok {
|
||||
unused = append(unused, k)
|
||||
}
|
||||
}
|
||||
return unused
|
||||
}
|
||||
|
||||
// Transform replaces $(VAR) style variables with values.
|
||||
func (rv *refVarTransformer) Transform(m resmap.ResMap) error {
|
||||
rv.replacementCounts = make(map[string]int)
|
||||
mf := refvar.MakePrimitiveReplacer(rv.replacementCounts, rv.varMap)
|
||||
for _, res := range m.Resources() {
|
||||
for _, fieldSpec := range rv.fieldSpecs {
|
||||
err := res.ApplyFilter(refvar.Filter{
|
||||
MappingFunc: mf,
|
||||
FieldSpec: fieldSpec,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package accumulator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/resid"
|
||||
)
|
||||
|
||||
// ResAccumulator accumulates resources and the rules
|
||||
// used to customize those resources. It's a ResMap
|
||||
// plus stuff needed to modify the ResMap.
|
||||
type ResAccumulator struct {
|
||||
resMap resmap.ResMap
|
||||
tConfig *builtinconfig.TransformerConfig
|
||||
varSet types.VarSet
|
||||
}
|
||||
|
||||
func MakeEmptyAccumulator() *ResAccumulator {
|
||||
ra := &ResAccumulator{}
|
||||
ra.resMap = resmap.New()
|
||||
ra.tConfig = &builtinconfig.TransformerConfig{}
|
||||
ra.varSet = types.NewVarSet()
|
||||
return ra
|
||||
}
|
||||
|
||||
// ResMap returns a copy of the internal resMap.
|
||||
func (ra *ResAccumulator) ResMap() resmap.ResMap {
|
||||
return ra.resMap.ShallowCopy()
|
||||
}
|
||||
|
||||
// Vars returns a copy of underlying vars.
|
||||
func (ra *ResAccumulator) Vars() []types.Var {
|
||||
return ra.varSet.AsSlice()
|
||||
}
|
||||
|
||||
func (ra *ResAccumulator) AppendAll(resources resmap.ResMap) error {
|
||||
return ra.resMap.AppendAll(resources)
|
||||
}
|
||||
|
||||
func (ra *ResAccumulator) AbsorbAll(resources resmap.ResMap) error {
|
||||
return ra.resMap.AbsorbAll(resources)
|
||||
}
|
||||
|
||||
func (ra *ResAccumulator) MergeConfig(
|
||||
tConfig *builtinconfig.TransformerConfig) (err error) {
|
||||
ra.tConfig, err = ra.tConfig.Merge(tConfig)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ra *ResAccumulator) GetTransformerConfig() *builtinconfig.TransformerConfig {
|
||||
return ra.tConfig
|
||||
}
|
||||
|
||||
// MergeVars accumulates vars into ResAccumulator.
|
||||
// A Var is a tuple of name, object reference and field reference.
|
||||
// This func takes a list of vars from the current kustomization file and
|
||||
// annotates the accumulated resources with the names of the vars that match
|
||||
// those resources. E.g. if there's a var named "sam" that wants to get
|
||||
// its data from a ConfigMap named "james", and the resource list contains a
|
||||
// ConfigMap named "james", then that ConfigMap will be annotated with the
|
||||
// var name "sam". Later this annotation is used to find the data for "sam"
|
||||
// by digging into a particular fieldpath of "james".
|
||||
func (ra *ResAccumulator) MergeVars(incoming []types.Var) error {
|
||||
for _, v := range incoming {
|
||||
targetId := resid.NewResIdWithNamespace(v.ObjRef.GVK(), v.ObjRef.Name, v.ObjRef.Namespace)
|
||||
idMatcher := targetId.GvknEquals
|
||||
if targetId.Namespace != "" || targetId.IsClusterScoped() {
|
||||
// Preserve backward compatibility. An empty namespace means
|
||||
// wildcard search on the namespace hence we still use GvknEquals
|
||||
idMatcher = targetId.Equals
|
||||
}
|
||||
matched := ra.resMap.GetMatchingResourcesByAnyId(idMatcher)
|
||||
if len(matched) > 1 {
|
||||
return fmt.Errorf(
|
||||
"found %d resId matches for var %s "+
|
||||
"(unable to disambiguate)",
|
||||
len(matched), v)
|
||||
}
|
||||
if len(matched) == 1 {
|
||||
matched[0].AppendRefVarName(v)
|
||||
}
|
||||
}
|
||||
return ra.varSet.MergeSlice(incoming)
|
||||
}
|
||||
|
||||
func (ra *ResAccumulator) MergeAccumulator(other *ResAccumulator) (err error) {
|
||||
err = ra.AppendAll(other.resMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ra.MergeConfig(other.tConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ra.varSet.MergeSet(other.varSet)
|
||||
}
|
||||
|
||||
func (ra *ResAccumulator) findVarValueFromResources(v types.Var) (interface{}, error) {
|
||||
for _, res := range ra.resMap.Resources() {
|
||||
for _, varName := range res.GetRefVarNames() {
|
||||
if varName == v.Name {
|
||||
s, err := res.GetFieldValue(v.FieldRef.FieldPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"field specified in var '%v' "+
|
||||
"not found in corresponding resource", v)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf(
|
||||
"var '%v' cannot be mapped to a field "+
|
||||
"in the set of known resources", v)
|
||||
}
|
||||
|
||||
// makeVarReplacementMap returns a map of Var names to
|
||||
// their final values. The values are strings intended
|
||||
// for substitution wherever the $(var.Name) occurs.
|
||||
func (ra *ResAccumulator) makeVarReplacementMap() (map[string]interface{}, error) {
|
||||
result := map[string]interface{}{}
|
||||
for _, v := range ra.Vars() {
|
||||
s, err := ra.findVarValueFromResources(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[v.Name] = s
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ra *ResAccumulator) Transform(t resmap.Transformer) error {
|
||||
return t.Transform(ra.resMap)
|
||||
}
|
||||
|
||||
func (ra *ResAccumulator) ResolveVars() error {
|
||||
replacementMap, err := ra.makeVarReplacementMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(replacementMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
t := newRefVarTransformer(
|
||||
replacementMap, ra.tConfig.VarReference)
|
||||
err = ra.Transform(t)
|
||||
if len(t.UnusedVars()) > 0 {
|
||||
log.Printf(
|
||||
"well-defined vars that were never replaced: %s\n",
|
||||
strings.Join(t.UnusedVars(), ","))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (ra *ResAccumulator) FixBackReferences() (err error) {
|
||||
if ra.tConfig.NameReference == nil {
|
||||
return nil
|
||||
}
|
||||
return ra.Transform(
|
||||
newNameReferenceTransformer(ra.tConfig.NameReference))
|
||||
}
|
||||
|
||||
// Intersection drops the resources which "other" does not have.
|
||||
func (ra *ResAccumulator) Intersection(other resmap.ResMap) error {
|
||||
otherIds := other.AllIds() //nolint:revive
|
||||
for _, curId := range ra.resMap.AllIds() {
|
||||
toDelete := true
|
||||
for _, otherId := range otherIds {
|
||||
if otherId == curId {
|
||||
toDelete = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if toDelete {
|
||||
err := ra.resMap.Remove(curId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Code generated by pluginator on AnnotationsTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/filters/annotations"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Add the given annotations to the given field specifications.
|
||||
type AnnotationsTransformerPlugin struct {
|
||||
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
|
||||
FieldSpecs []types.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
|
||||
}
|
||||
|
||||
func (p *AnnotationsTransformerPlugin) Config(
|
||||
_ *resmap.PluginHelpers, c []byte) (err error) {
|
||||
p.Annotations = nil
|
||||
p.FieldSpecs = nil
|
||||
return yaml.Unmarshal(c, p)
|
||||
}
|
||||
|
||||
func (p *AnnotationsTransformerPlugin) Transform(m resmap.ResMap) error {
|
||||
if len(p.Annotations) == 0 {
|
||||
return nil
|
||||
}
|
||||
return m.ApplyFilter(annotations.Filter{
|
||||
Annotations: p.Annotations,
|
||||
FsSlice: p.FieldSpecs,
|
||||
})
|
||||
}
|
||||
|
||||
func NewAnnotationsTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &AnnotationsTransformerPlugin{}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Code generated by pluginator on ConfigMapGenerator; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/kv"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type ConfigMapGeneratorPlugin struct {
|
||||
h *resmap.PluginHelpers
|
||||
types.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||
types.ConfigMapArgs
|
||||
}
|
||||
|
||||
func (p *ConfigMapGeneratorPlugin) Config(h *resmap.PluginHelpers, config []byte) (err error) {
|
||||
p.ConfigMapArgs = types.ConfigMapArgs{}
|
||||
err = yaml.Unmarshal(config, p)
|
||||
if p.ConfigMapArgs.Name == "" {
|
||||
p.ConfigMapArgs.Name = p.Name
|
||||
}
|
||||
if p.ConfigMapArgs.Namespace == "" {
|
||||
p.ConfigMapArgs.Namespace = p.Namespace
|
||||
}
|
||||
p.h = h
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ConfigMapGeneratorPlugin) Generate() (resmap.ResMap, error) {
|
||||
return p.h.ResmapFactory().FromConfigMapArgs(
|
||||
kv.NewLoader(p.h.Loader(), p.h.Validator()), p.ConfigMapArgs)
|
||||
}
|
||||
|
||||
func NewConfigMapGeneratorPlugin() resmap.GeneratorPlugin {
|
||||
return &ConfigMapGeneratorPlugin{}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Code generated by pluginator on HashTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/ifc"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
)
|
||||
|
||||
type HashTransformerPlugin struct {
|
||||
hasher ifc.KustHasher
|
||||
}
|
||||
|
||||
func (p *HashTransformerPlugin) Config(
|
||||
h *resmap.PluginHelpers, _ []byte) (err error) {
|
||||
p.hasher = h.ResmapFactory().RF().Hasher()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Transform appends hash to generated resources.
|
||||
func (p *HashTransformerPlugin) Transform(m resmap.ResMap) error {
|
||||
for _, res := range m.Resources() {
|
||||
if res.NeedHashSuffix() {
|
||||
h, err := res.Hash(p.hasher)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res.StorePreviousId()
|
||||
res.SetName(fmt.Sprintf("%s-%s", res.GetName(), h))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewHashTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &HashTransformerPlugin{}
|
||||
}
|
||||
Generated
Vendored
+396
@@ -0,0 +1,396 @@
|
||||
// Code generated by pluginator on HelmChartInflationGenerator; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/konfig"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/merge2"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Generate resources from a remote or local helm chart.
|
||||
type HelmChartInflationGeneratorPlugin struct {
|
||||
h *resmap.PluginHelpers
|
||||
types.HelmGlobals
|
||||
types.HelmChart
|
||||
tmpDir string
|
||||
}
|
||||
|
||||
const (
|
||||
valuesMergeOptionMerge = "merge"
|
||||
valuesMergeOptionOverride = "override"
|
||||
valuesMergeOptionReplace = "replace"
|
||||
)
|
||||
|
||||
var legalMergeOptions = []string{
|
||||
valuesMergeOptionMerge,
|
||||
valuesMergeOptionOverride,
|
||||
valuesMergeOptionReplace,
|
||||
}
|
||||
|
||||
// Config uses the input plugin configurations `config` to setup the generator
|
||||
// options
|
||||
func (p *HelmChartInflationGeneratorPlugin) Config(
|
||||
h *resmap.PluginHelpers, config []byte) (err error) {
|
||||
if h.GeneralConfig() == nil {
|
||||
return fmt.Errorf("unable to access general config")
|
||||
}
|
||||
if !h.GeneralConfig().HelmConfig.Enabled {
|
||||
return fmt.Errorf("must specify --enable-helm")
|
||||
}
|
||||
if h.GeneralConfig().HelmConfig.Command == "" {
|
||||
return fmt.Errorf("must specify --helm-command")
|
||||
}
|
||||
|
||||
// CLI args takes precedence
|
||||
if h.GeneralConfig().HelmConfig.KubeVersion != "" {
|
||||
p.HelmChart.KubeVersion = h.GeneralConfig().HelmConfig.KubeVersion
|
||||
}
|
||||
if len(h.GeneralConfig().HelmConfig.ApiVersions) != 0 {
|
||||
p.HelmChart.ApiVersions = h.GeneralConfig().HelmConfig.ApiVersions
|
||||
}
|
||||
if h.GeneralConfig().HelmConfig.Debug {
|
||||
p.HelmChart.Debug = h.GeneralConfig().HelmConfig.Debug
|
||||
}
|
||||
|
||||
p.h = h
|
||||
if err = yaml.Unmarshal(config, p); err != nil {
|
||||
return
|
||||
}
|
||||
return p.validateArgs()
|
||||
}
|
||||
|
||||
// This uses the real file system since tmpDir may be used
|
||||
// by the helm subprocess. Cannot use a chroot jail or fake
|
||||
// filesystem since we allow the user to use previously
|
||||
// downloaded charts. This is safe since this plugin is
|
||||
// owned by kustomize.
|
||||
func (p *HelmChartInflationGeneratorPlugin) establishTmpDir() (err error) {
|
||||
if p.tmpDir != "" {
|
||||
// already done.
|
||||
return nil
|
||||
}
|
||||
p.tmpDir, err = os.MkdirTemp("", "kustomize-helm-")
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *HelmChartInflationGeneratorPlugin) validateArgs() (err error) {
|
||||
if p.Name == "" {
|
||||
return fmt.Errorf("chart name cannot be empty")
|
||||
}
|
||||
|
||||
// ChartHome might be consulted by the plugin (to read
|
||||
// values files below it), so it must be located under
|
||||
// the loader root (unless root restrictions are
|
||||
// disabled, in which case this can be an absolute path).
|
||||
if p.ChartHome == "" {
|
||||
p.ChartHome = types.HelmDefaultHome
|
||||
}
|
||||
|
||||
// The ValuesFile(s) may be consulted by the plugin, so it must
|
||||
// be under the loader root (unless root restrictions are
|
||||
// disabled).
|
||||
if p.ValuesFile == "" {
|
||||
p.ValuesFile = filepath.Join(p.absChartHome(), p.Name, "values.yaml")
|
||||
}
|
||||
for i, file := range p.AdditionalValuesFiles {
|
||||
// use Load() to enforce root restrictions
|
||||
if _, err := p.h.Loader().Load(file); err != nil {
|
||||
return errors.WrapPrefixf(err, "could not load additionalValuesFile")
|
||||
}
|
||||
// the additional values filepaths must be relative to the kust root
|
||||
p.AdditionalValuesFiles[i] = filepath.Join(p.h.Loader().Root(), file)
|
||||
}
|
||||
|
||||
if err = p.errIfIllegalValuesMerge(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ConfigHome is not loaded by the plugin, and can be located anywhere.
|
||||
if p.ConfigHome == "" {
|
||||
if err = p.establishTmpDir(); err != nil {
|
||||
return errors.WrapPrefixf(
|
||||
err, "unable to create tmp dir for HELM_CONFIG_HOME")
|
||||
}
|
||||
p.ConfigHome = filepath.Join(p.tmpDir, "helm")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HelmChartInflationGeneratorPlugin) errIfIllegalValuesMerge() error {
|
||||
if p.ValuesMerge == "" {
|
||||
// Use the default.
|
||||
p.ValuesMerge = valuesMergeOptionOverride
|
||||
return nil
|
||||
}
|
||||
for _, opt := range legalMergeOptions {
|
||||
if p.ValuesMerge == opt {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("valuesMerge must be one of %v", legalMergeOptions)
|
||||
}
|
||||
|
||||
func (p *HelmChartInflationGeneratorPlugin) absChartHome() string {
|
||||
var chartHome string
|
||||
if filepath.IsAbs(p.ChartHome) {
|
||||
chartHome = p.ChartHome
|
||||
} else {
|
||||
chartHome = filepath.Join(p.h.Loader().Root(), p.ChartHome)
|
||||
}
|
||||
|
||||
if p.Version != "" && p.Repo != "" {
|
||||
return filepath.Join(chartHome, fmt.Sprintf("%s-%s", p.Name, p.Version))
|
||||
}
|
||||
return chartHome
|
||||
}
|
||||
|
||||
func (p *HelmChartInflationGeneratorPlugin) runHelmCommand(
|
||||
args []string) ([]byte, error) {
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
cmd := exec.Command(p.h.GeneralConfig().HelmConfig.Command, args...)
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
env := []string{
|
||||
fmt.Sprintf("HELM_CONFIG_HOME=%s", p.ConfigHome),
|
||||
fmt.Sprintf("HELM_CACHE_HOME=%s/.cache", p.ConfigHome),
|
||||
fmt.Sprintf("HELM_DATA_HOME=%s/.data", p.ConfigHome)}
|
||||
cmd.Env = append(os.Environ(), env...)
|
||||
err := cmd.Run()
|
||||
errorOutput := stderr.String()
|
||||
if slices.Contains(args, "--debug") {
|
||||
errorOutput = " Helm stack trace:\n" + errorOutput + "\nHelm template:\n" + stdout.String() + "\n"
|
||||
}
|
||||
if err != nil {
|
||||
helm := p.h.GeneralConfig().HelmConfig.Command
|
||||
err = errors.WrapPrefixf(
|
||||
fmt.Errorf(
|
||||
"unable to run: '%s %s' with env=%s (is '%s' installed?): %w",
|
||||
helm, strings.Join(args, " "), env, helm, err),
|
||||
"%s", errorOutput,
|
||||
)
|
||||
}
|
||||
return stdout.Bytes(), err
|
||||
}
|
||||
|
||||
// createNewMergedValuesFile replaces/merges original values file with ValuesInline.
|
||||
func (p *HelmChartInflationGeneratorPlugin) createNewMergedValuesFile() (
|
||||
path string, err error) {
|
||||
if p.ValuesMerge == valuesMergeOptionMerge ||
|
||||
p.ValuesMerge == valuesMergeOptionOverride {
|
||||
if err = p.replaceValuesInline(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
var b []byte
|
||||
b, err = yaml.Marshal(p.ValuesInline)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return p.writeValuesBytes(b)
|
||||
}
|
||||
|
||||
func (p *HelmChartInflationGeneratorPlugin) replaceValuesInline() error {
|
||||
pValues, err := p.h.Loader().Load(p.ValuesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chValues, err := kyaml.Parse(string(pValues))
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "could not parse values file into rnode")
|
||||
}
|
||||
inlineValues, err := kyaml.FromMap(p.ValuesInline)
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "could not parse values inline into rnode")
|
||||
}
|
||||
var outValues *kyaml.RNode
|
||||
switch p.ValuesMerge {
|
||||
// Function `merge2.Merge` overrides values in dest with values from src.
|
||||
// To achieve override or merge behavior, we pass parameters in different order.
|
||||
// Object passed as dest will be modified, so we copy it just in case someone
|
||||
// decides to use it after this is called.
|
||||
case valuesMergeOptionOverride:
|
||||
outValues, err = merge2.Merge(inlineValues, chValues.Copy(), kyaml.MergeOptions{})
|
||||
case valuesMergeOptionMerge:
|
||||
outValues, err = merge2.Merge(chValues, inlineValues.Copy(), kyaml.MergeOptions{})
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "could not merge values")
|
||||
}
|
||||
mapValues, err := outValues.Map()
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "could not parse merged values into map")
|
||||
}
|
||||
p.ValuesInline = mapValues
|
||||
return err
|
||||
}
|
||||
|
||||
// copyValuesFile to avoid branching. TODO: get rid of this.
|
||||
func (p *HelmChartInflationGeneratorPlugin) copyValuesFile() (string, error) {
|
||||
b, err := p.h.Loader().Load(p.ValuesFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return p.writeValuesBytes(b)
|
||||
}
|
||||
|
||||
// Write a absolute path file in the tmp file system.
|
||||
func (p *HelmChartInflationGeneratorPlugin) writeValuesBytes(
|
||||
b []byte) (string, error) {
|
||||
if err := p.establishTmpDir(); err != nil {
|
||||
return "", fmt.Errorf("cannot create tmp dir to write helm values")
|
||||
}
|
||||
path := filepath.Join(p.tmpDir, p.Name+"-kustomize-values.yaml")
|
||||
return path, errors.WrapPrefixf(os.WriteFile(path, b, 0644), "failed to write values file")
|
||||
}
|
||||
|
||||
func (p *HelmChartInflationGeneratorPlugin) cleanup() {
|
||||
if p.tmpDir != "" {
|
||||
os.RemoveAll(p.tmpDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate implements generator
|
||||
func (p *HelmChartInflationGeneratorPlugin) Generate() (rm resmap.ResMap, err error) {
|
||||
defer p.cleanup()
|
||||
if err = p.checkHelmVersion(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if path, exists := p.chartExistsLocally(); !exists {
|
||||
if p.Repo == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"no repo specified for pull, no chart found at '%s'", path)
|
||||
}
|
||||
if _, err := p.runHelmCommand(p.pullCommand()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(p.ValuesInline) > 0 {
|
||||
p.ValuesFile, err = p.createNewMergedValuesFile()
|
||||
} else {
|
||||
p.ValuesFile, err = p.copyValuesFile()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var stdout []byte
|
||||
stdout, err = p.runHelmCommand(p.AsHelmArgs(p.absChartHome()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rm, resMapErr := p.h.ResmapFactory().NewResMapFromBytes(stdout)
|
||||
if resMapErr == nil {
|
||||
if err := p.markHelmGeneratedResources(rm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rm, nil
|
||||
}
|
||||
// try to remove the contents before first "---" because
|
||||
// helm may produce messages to stdout before it
|
||||
r := &kio.ByteReader{Reader: bytes.NewBuffer(stdout), OmitReaderAnnotations: true}
|
||||
nodes, err := r.Read()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading helm output: %w", err)
|
||||
}
|
||||
|
||||
if len(nodes) != 0 {
|
||||
rm, err = p.h.ResmapFactory().NewResMapFromRNodeSlice(nodes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse rnode slice into resource map: %w", err)
|
||||
}
|
||||
if err := p.markHelmGeneratedResources(rm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rm, nil
|
||||
}
|
||||
return nil, fmt.Errorf("could not parse bytes into resource map: %w", resMapErr)
|
||||
}
|
||||
|
||||
func (p *HelmChartInflationGeneratorPlugin) pullCommand() []string {
|
||||
args := []string{
|
||||
"pull",
|
||||
"--untar",
|
||||
"--untardir", p.absChartHome(),
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(p.Repo, "oci://"):
|
||||
args = append(args, strings.TrimSuffix(p.Repo, "/")+"/"+p.Name)
|
||||
case p.Repo != "":
|
||||
args = append(args, "--repo", p.Repo)
|
||||
fallthrough
|
||||
default:
|
||||
args = append(args, p.Name)
|
||||
}
|
||||
|
||||
if p.Version != "" {
|
||||
args = append(args, "--version", p.Version)
|
||||
}
|
||||
if p.Devel {
|
||||
args = append(args, "--devel")
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// chartExistsLocally will return true if the chart does exist in
|
||||
// local chart home.
|
||||
func (p *HelmChartInflationGeneratorPlugin) chartExistsLocally() (string, bool) {
|
||||
path := filepath.Join(p.absChartHome(), p.Name)
|
||||
s, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return path, s.IsDir()
|
||||
}
|
||||
|
||||
func (p *HelmChartInflationGeneratorPlugin) markHelmGeneratedResources(rm resmap.ResMap) error {
|
||||
for _, r := range rm.Resources() {
|
||||
if err := r.RNode.PipeE(kyaml.SetAnnotation(konfig.HelmGeneratedAnnotation, "true")); err != nil {
|
||||
return fmt.Errorf("failed to set helm annotation: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkHelmVersion will return an error if the helm version is not V3 or V4
|
||||
func (p *HelmChartInflationGeneratorPlugin) checkHelmVersion() error {
|
||||
stdout, err := p.runHelmCommand([]string{"version", "--short"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r, err := regexp.Compile(`v?\d+(\.\d+)+`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v := r.FindString(string(stdout))
|
||||
if v == "" {
|
||||
return fmt.Errorf("cannot find version string in %s", string(stdout))
|
||||
}
|
||||
if v[0] == 'v' {
|
||||
v = v[1:]
|
||||
}
|
||||
majorVersion := strings.Split(v, ".")[0]
|
||||
if majorVersion != "3" && majorVersion != "4" {
|
||||
return fmt.Errorf("this plugin requires helm V3 or V4 but got v%s", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewHelmChartInflationGeneratorPlugin() resmap.GeneratorPlugin {
|
||||
return &HelmChartInflationGeneratorPlugin{}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Code generated by pluginator on IAMPolicyGenerator; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/filters/iampolicygenerator"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type IAMPolicyGeneratorPlugin struct {
|
||||
types.IAMPolicyGeneratorArgs
|
||||
}
|
||||
|
||||
func (p *IAMPolicyGeneratorPlugin) Config(h *resmap.PluginHelpers, config []byte) (err error) {
|
||||
p.IAMPolicyGeneratorArgs = types.IAMPolicyGeneratorArgs{}
|
||||
err = yaml.Unmarshal(config, p)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *IAMPolicyGeneratorPlugin) Generate() (resmap.ResMap, error) {
|
||||
r := resmap.New()
|
||||
err := r.ApplyFilter(iampolicygenerator.Filter{
|
||||
IAMPolicyGenerator: p.IAMPolicyGeneratorArgs,
|
||||
})
|
||||
return r, err
|
||||
}
|
||||
|
||||
func NewIAMPolicyGeneratorPlugin() resmap.GeneratorPlugin {
|
||||
return &IAMPolicyGeneratorPlugin{}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Code generated by pluginator on ImageTagTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/filters/imagetag"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Find matching image declarations and replace
|
||||
// the name, tag and/or digest.
|
||||
type ImageTagTransformerPlugin struct {
|
||||
ImageTag types.Image `json:"imageTag,omitempty" yaml:"imageTag,omitempty"`
|
||||
FieldSpecs []types.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
|
||||
}
|
||||
|
||||
func (p *ImageTagTransformerPlugin) Config(
|
||||
_ *resmap.PluginHelpers, c []byte) (err error) {
|
||||
p.ImageTag = types.Image{}
|
||||
p.FieldSpecs = nil
|
||||
return yaml.Unmarshal(c, p)
|
||||
}
|
||||
|
||||
func (p *ImageTagTransformerPlugin) Transform(m resmap.ResMap) error {
|
||||
if err := m.ApplyFilter(imagetag.LegacyFilter{
|
||||
ImageTag: p.ImageTag,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.ApplyFilter(imagetag.Filter{
|
||||
ImageTag: p.ImageTag,
|
||||
FsSlice: p.FieldSpecs,
|
||||
})
|
||||
}
|
||||
|
||||
func NewImageTagTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &ImageTagTransformerPlugin{}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Code generated by pluginator on LabelTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/filters/labels"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Add the given labels to the given field specifications.
|
||||
type LabelTransformerPlugin struct {
|
||||
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
|
||||
FieldSpecs []types.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
|
||||
}
|
||||
|
||||
func (p *LabelTransformerPlugin) Config(
|
||||
_ *resmap.PluginHelpers, c []byte) (err error) {
|
||||
p.Labels = nil
|
||||
p.FieldSpecs = nil
|
||||
return yaml.Unmarshal(c, p)
|
||||
}
|
||||
|
||||
func (p *LabelTransformerPlugin) Transform(m resmap.ResMap) error {
|
||||
if len(p.Labels) == 0 {
|
||||
return nil
|
||||
}
|
||||
return m.ApplyFilter(labels.Filter{
|
||||
Labels: p.Labels,
|
||||
FsSlice: p.FieldSpecs,
|
||||
})
|
||||
}
|
||||
|
||||
func NewLabelTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &LabelTransformerPlugin{}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// Code generated by pluginator on NamespaceTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/namespace"
|
||||
"sigs.k8s.io/kustomize/api/konfig"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Change or set the namespace of non-cluster level resources.
|
||||
//
|
||||
//nolint:tagalign
|
||||
type NamespaceTransformerPlugin struct {
|
||||
types.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||
FieldSpecs []types.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
|
||||
UnsetOnly bool `json:"unsetOnly" yaml:"unsetOnly"`
|
||||
SetRoleBindingSubjects namespace.RoleBindingSubjectMode `json:"setRoleBindingSubjects" yaml:"setRoleBindingSubjects"`
|
||||
}
|
||||
|
||||
func (p *NamespaceTransformerPlugin) Config(
|
||||
_ *resmap.PluginHelpers, c []byte) (err error) {
|
||||
p.Namespace = ""
|
||||
p.FieldSpecs = nil
|
||||
if err := yaml.Unmarshal(c, p); err != nil {
|
||||
return errors.WrapPrefixf(err, "unmarshalling NamespaceTransformer config")
|
||||
}
|
||||
switch p.SetRoleBindingSubjects {
|
||||
case namespace.AllServiceAccountSubjects, namespace.DefaultSubjectsOnly, namespace.NoSubjects:
|
||||
// valid
|
||||
case namespace.SubjectModeUnspecified:
|
||||
p.SetRoleBindingSubjects = namespace.DefaultSubjectsOnly
|
||||
default:
|
||||
return errors.Errorf("invalid value %q for setRoleBindingSubjects: "+
|
||||
"must be one of %q, %q or %q", p.SetRoleBindingSubjects,
|
||||
namespace.DefaultSubjectsOnly, namespace.NoSubjects, namespace.AllServiceAccountSubjects)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *NamespaceTransformerPlugin) Transform(m resmap.ResMap) error {
|
||||
if len(p.Namespace) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, r := range m.Resources() {
|
||||
if r.IsNilOrEmpty() {
|
||||
// Don't mutate empty objects?
|
||||
continue
|
||||
}
|
||||
if annotations := r.GetAnnotations(konfig.HelmGeneratedAnnotation); annotations[konfig.HelmGeneratedAnnotation] == "true" {
|
||||
// Don't apply namespace on Helm generated manifest. Helm should take care of it.
|
||||
continue
|
||||
}
|
||||
r.StorePreviousId()
|
||||
if err := r.ApplyFilter(namespace.Filter{
|
||||
Namespace: p.Namespace,
|
||||
FsSlice: p.FieldSpecs,
|
||||
SetRoleBindingSubjects: p.SetRoleBindingSubjects,
|
||||
UnsetOnly: p.UnsetOnly,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
matches := m.GetMatchingResourcesByCurrentId(r.CurId().Equals)
|
||||
if len(matches) != 1 {
|
||||
return fmt.Errorf(
|
||||
"namespace transformation produces ID conflict: %+v", matches)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewNamespaceTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &NamespaceTransformerPlugin{}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// Code generated by pluginator on PatchJson6902Transformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
"sigs.k8s.io/kustomize/api/filters/patchjson6902"
|
||||
"sigs.k8s.io/kustomize/api/ifc"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type PatchJson6902TransformerPlugin struct {
|
||||
ldr ifc.Loader
|
||||
decodedPatch jsonpatch.Patch
|
||||
Target *types.Selector `json:"target,omitempty" yaml:"target,omitempty"`
|
||||
Path string `json:"path,omitempty" yaml:"path,omitempty"`
|
||||
JsonOp string `json:"jsonOp,omitempty" yaml:"jsonOp,omitempty"`
|
||||
}
|
||||
|
||||
func (p *PatchJson6902TransformerPlugin) Config(
|
||||
h *resmap.PluginHelpers, c []byte) (err error) {
|
||||
p.ldr = h.Loader()
|
||||
err = yaml.Unmarshal(c, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if p.Target.Name == "" {
|
||||
return fmt.Errorf("must specify the target name")
|
||||
}
|
||||
if p.Path == "" && p.JsonOp == "" {
|
||||
return fmt.Errorf("empty file path and empty jsonOp")
|
||||
}
|
||||
if p.Path != "" {
|
||||
if p.JsonOp != "" {
|
||||
return fmt.Errorf("must specify a file path or jsonOp, not both")
|
||||
}
|
||||
rawOp, err := p.ldr.Load(p.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.JsonOp = string(rawOp)
|
||||
if p.JsonOp == "" {
|
||||
return fmt.Errorf("patch file '%s' empty seems to be empty", p.Path)
|
||||
}
|
||||
}
|
||||
if p.JsonOp[0] != '[' {
|
||||
// if it doesn't seem to be JSON, imagine
|
||||
// it is YAML, and convert to JSON.
|
||||
op, err := yaml.YAMLToJSON([]byte(p.JsonOp))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.JsonOp = string(op)
|
||||
}
|
||||
p.decodedPatch, err = jsonpatch.DecodePatch([]byte(p.JsonOp))
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "decoding %s", p.JsonOp)
|
||||
}
|
||||
if len(p.decodedPatch) == 0 {
|
||||
return fmt.Errorf(
|
||||
"patch appears to be empty; file=%s, JsonOp=%s", p.Path, p.JsonOp)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *PatchJson6902TransformerPlugin) Transform(m resmap.ResMap) error {
|
||||
if p.Target == nil {
|
||||
return fmt.Errorf("must specify a target for patch %s", p.JsonOp)
|
||||
}
|
||||
resources, err := m.Select(*p.Target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, res := range resources {
|
||||
internalAnnotations := kioutil.GetInternalAnnotations(&res.RNode)
|
||||
|
||||
err = res.ApplyFilter(patchjson6902.Filter{
|
||||
Patch: p.JsonOp,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
annotations := res.GetAnnotations()
|
||||
for key, value := range internalAnnotations {
|
||||
annotations[key] = value
|
||||
}
|
||||
err = res.SetAnnotations(annotations)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPatchJson6902TransformerPlugin() resmap.TransformerPlugin {
|
||||
return &PatchJson6902TransformerPlugin{}
|
||||
}
|
||||
Generated
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
// Code generated by pluginator on PatchStrategicMergeTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/resource"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type PatchStrategicMergeTransformerPlugin struct {
|
||||
loadedPatches []*resource.Resource
|
||||
Paths []types.PatchStrategicMerge `json:"paths,omitempty" yaml:"paths,omitempty"`
|
||||
Patches string `json:"patches,omitempty" yaml:"patches,omitempty"`
|
||||
}
|
||||
|
||||
func (p *PatchStrategicMergeTransformerPlugin) Config(
|
||||
h *resmap.PluginHelpers, c []byte) (err error) {
|
||||
err = yaml.Unmarshal(c, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(p.Paths) == 0 && p.Patches == "" {
|
||||
return fmt.Errorf("empty file path and empty patch content")
|
||||
}
|
||||
if len(p.Paths) != 0 {
|
||||
patches, err := loadFromPaths(h, p.Paths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.loadedPatches = append(p.loadedPatches, patches...)
|
||||
}
|
||||
if p.Patches != "" {
|
||||
patches, err := h.ResmapFactory().RF().SliceFromBytes([]byte(p.Patches))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.loadedPatches = append(p.loadedPatches, patches...)
|
||||
}
|
||||
if len(p.loadedPatches) == 0 {
|
||||
return fmt.Errorf(
|
||||
"patch appears to be empty; files=%v, Patch=%s", p.Paths, p.Patches)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadFromPaths(
|
||||
h *resmap.PluginHelpers,
|
||||
paths []types.PatchStrategicMerge) (
|
||||
result []*resource.Resource, err error) {
|
||||
var patches []*resource.Resource
|
||||
for _, path := range paths {
|
||||
// For legacy reasons, attempt to treat the path string as
|
||||
// actual patch content.
|
||||
patches, err = h.ResmapFactory().RF().SliceFromBytes([]byte(path))
|
||||
if err != nil {
|
||||
// Failing that, treat it as a file path.
|
||||
patches, err = h.ResmapFactory().RF().SliceFromPatches(
|
||||
h.Loader(), []types.PatchStrategicMerge{path})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
result = append(result, patches...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *PatchStrategicMergeTransformerPlugin) Transform(m resmap.ResMap) error {
|
||||
for _, patch := range p.loadedPatches {
|
||||
target, err := m.GetById(patch.OrgId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = m.ApplySmPatch(
|
||||
resource.MakeIdSet([]*resource.Resource{target}), patch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPatchStrategicMergeTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &PatchStrategicMergeTransformerPlugin{}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
// Code generated by pluginator on PatchTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
"sigs.k8s.io/kustomize/api/filters/patchjson6902"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/resource"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type PatchTransformerPlugin struct {
|
||||
smPatches []*resource.Resource // strategic-merge patches
|
||||
jsonPatches jsonpatch.Patch // json6902 patch
|
||||
// patchText is pure patch text created by Path or Patch
|
||||
patchText string
|
||||
// patchSource is patch source message
|
||||
patchSource string
|
||||
Path string `json:"path,omitempty" yaml:"path,omitempty"`
|
||||
Patch string `json:"patch,omitempty" yaml:"patch,omitempty"`
|
||||
Target *types.Selector `json:"target,omitempty" yaml:"target,omitempty"`
|
||||
Options *types.PatchArgs `json:"options,omitempty" yaml:"options,omitempty"`
|
||||
}
|
||||
|
||||
func (p *PatchTransformerPlugin) Config(h *resmap.PluginHelpers, c []byte) error {
|
||||
if err := yaml.Unmarshal(c, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Patch = strings.TrimSpace(p.Patch)
|
||||
switch {
|
||||
case p.Patch == "" && p.Path == "":
|
||||
return fmt.Errorf("must specify one of patch and path in\n%s", string(c))
|
||||
case p.Patch != "" && p.Path != "":
|
||||
return fmt.Errorf("patch and path can't be set at the same time\n%s", string(c))
|
||||
case p.Patch != "":
|
||||
p.patchText = p.Patch
|
||||
p.patchSource = fmt.Sprintf("[patch: %q]", p.patchText)
|
||||
case p.Path != "":
|
||||
loaded, err := h.Loader().Load(p.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get the patch file from path(%s): %w", p.Path, err)
|
||||
}
|
||||
p.patchText = string(loaded)
|
||||
p.patchSource = fmt.Sprintf("[path: %q]", p.Path)
|
||||
}
|
||||
|
||||
patchesSM, errSM := h.ResmapFactory().RF().SliceFromBytes([]byte(p.patchText))
|
||||
patchesJson, errJson := jsonPatchFromBytes([]byte(p.patchText))
|
||||
|
||||
if ((errSM == nil && errJson == nil) ||
|
||||
(patchesSM != nil && patchesJson != nil)) &&
|
||||
(len(patchesSM) > 0 && len(patchesJson) > 0) {
|
||||
return fmt.Errorf(
|
||||
"illegally qualifies as both an SM and JSON patch: %s",
|
||||
p.patchSource)
|
||||
}
|
||||
if errSM != nil && errJson != nil {
|
||||
return fmt.Errorf(
|
||||
"unable to parse SM or JSON patch from %s", p.patchSource)
|
||||
}
|
||||
if errSM == nil {
|
||||
p.smPatches = patchesSM
|
||||
for _, loadedPatch := range p.smPatches {
|
||||
if p.Options == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Options.AllowNameChange {
|
||||
loadedPatch.AllowNameChange()
|
||||
}
|
||||
if p.Options.AllowKindChange {
|
||||
loadedPatch.AllowKindChange()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p.jsonPatches = patchesJson
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchTransformerPlugin) Transform(m resmap.ResMap) error {
|
||||
if p.smPatches != nil {
|
||||
return p.transformStrategicMerge(m)
|
||||
}
|
||||
if p.jsonPatches != nil {
|
||||
return p.transformJson6902(m)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// transformStrategicMerge applies each loaded strategic merge patch
|
||||
// to the resource in the ResMap that matches the identifier of the patch.
|
||||
// If only one patch is specified, the Target can be used instead.
|
||||
func (p *PatchTransformerPlugin) transformStrategicMerge(m resmap.ResMap) error {
|
||||
if p.Target != nil {
|
||||
if len(p.smPatches) > 1 {
|
||||
// detail: https://github.com/kubernetes-sigs/kustomize/issues/5049#issuecomment-1440604403
|
||||
return fmt.Errorf("Multiple Strategic-Merge Patches in one `patches` entry is not allowed to set `patches.target` field: %s", p.patchSource)
|
||||
}
|
||||
|
||||
// single patch
|
||||
patch := p.smPatches[0]
|
||||
selected, err := m.Select(*p.Target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to find patch target %q in `resources`: %w", p.Target, err)
|
||||
}
|
||||
return errors.Wrap(m.ApplySmPatch(resource.MakeIdSet(selected), patch))
|
||||
}
|
||||
|
||||
for _, patch := range p.smPatches {
|
||||
target, err := m.GetById(patch.OrgId())
|
||||
if err != nil {
|
||||
return fmt.Errorf("no resource matches strategic merge patch %q: %w", patch.OrgId(), err)
|
||||
}
|
||||
if err := target.ApplySmPatch(patch); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// transformJson6902 applies json6902 Patch to all the resources in the ResMap that match Target.
|
||||
func (p *PatchTransformerPlugin) transformJson6902(m resmap.ResMap) error {
|
||||
if p.Target == nil {
|
||||
return fmt.Errorf("must specify a target for JSON patch %s", p.patchSource)
|
||||
}
|
||||
resources, err := m.Select(*p.Target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, res := range resources {
|
||||
res.StorePreviousId()
|
||||
internalAnnotations := kioutil.GetInternalAnnotations(&res.RNode)
|
||||
err = res.ApplyFilter(patchjson6902.Filter{
|
||||
Patch: p.patchText,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
annotations := res.GetAnnotations()
|
||||
for key, value := range internalAnnotations {
|
||||
annotations[key] = value
|
||||
}
|
||||
err = res.SetAnnotations(annotations)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// jsonPatchFromBytes loads a Json 6902 patch from a bytes input
|
||||
func jsonPatchFromBytes(in []byte) (jsonpatch.Patch, error) {
|
||||
ops := string(in)
|
||||
if ops == "" {
|
||||
return nil, fmt.Errorf("empty json patch operations")
|
||||
}
|
||||
|
||||
if ops[0] != '[' {
|
||||
// TODO(5049):
|
||||
// In the case of multiple yaml documents, return error instead of ignoring all but first.
|
||||
// Details: https://github.com/kubernetes-sigs/kustomize/pull/5194#discussion_r1256686728
|
||||
jsonOps, err := yaml.YAMLToJSON(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops = string(jsonOps)
|
||||
}
|
||||
return jsonpatch.DecodePatch([]byte(ops))
|
||||
}
|
||||
|
||||
func NewPatchTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &PatchTransformerPlugin{}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Code generated by pluginator on PrefixTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/prefix"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/resid"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Add the given prefix to the field
|
||||
type PrefixTransformerPlugin struct {
|
||||
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
|
||||
FieldSpecs types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
|
||||
}
|
||||
|
||||
// TODO: Make this gvk skip list part of the config.
|
||||
var prefixFieldSpecsToSkip = types.FsSlice{
|
||||
{Gvk: resid.Gvk{Kind: "CustomResourceDefinition"}},
|
||||
{Gvk: resid.Gvk{Group: "apiregistration.k8s.io", Kind: "APIService"}},
|
||||
{Gvk: resid.Gvk{Kind: "Namespace"}},
|
||||
}
|
||||
|
||||
func (p *PrefixTransformerPlugin) Config(
|
||||
_ *resmap.PluginHelpers, c []byte) (err error) {
|
||||
p.Prefix = ""
|
||||
p.FieldSpecs = nil
|
||||
err = yaml.Unmarshal(c, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if p.FieldSpecs == nil {
|
||||
return errors.New("fieldSpecs is not expected to be nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *PrefixTransformerPlugin) Transform(m resmap.ResMap) error {
|
||||
// Even if the Prefix is empty we want to proceed with the
|
||||
// transformation. This allows to add contextual information
|
||||
// to the resources (AddNamePrefix).
|
||||
for _, r := range m.Resources() {
|
||||
// TODO: move this test into the filter (i.e. make a better filter)
|
||||
if p.shouldSkip(r.OrgId()) {
|
||||
continue
|
||||
}
|
||||
id := r.OrgId()
|
||||
// current default configuration contains
|
||||
// only one entry: "metadata/name" with no GVK
|
||||
for _, fs := range p.FieldSpecs {
|
||||
// TODO: this is redundant to filter (but needed for now)
|
||||
if !id.IsSelected(&fs.Gvk) {
|
||||
continue
|
||||
}
|
||||
// TODO: move this test into the filter.
|
||||
if fs.Path == "metadata/name" {
|
||||
// "metadata/name" is the only field.
|
||||
// this will add a prefix to the resource
|
||||
// even if it is empty
|
||||
|
||||
r.AddNamePrefix(p.Prefix)
|
||||
if p.Prefix != "" {
|
||||
// TODO: There are multiple transformers that can change a resource's name, and each makes a call to
|
||||
// StorePreviousID(). We should make it so that we only call StorePreviousID once per kustomization layer
|
||||
// to avoid storing intermediate names between transformations, to prevent intermediate name conflicts.
|
||||
r.StorePreviousId()
|
||||
}
|
||||
}
|
||||
if err := r.ApplyFilter(prefix.Filter{
|
||||
Prefix: p.Prefix,
|
||||
FieldSpec: fs,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PrefixTransformerPlugin) shouldSkip(id resid.ResId) bool {
|
||||
for _, path := range prefixFieldSpecsToSkip {
|
||||
if id.IsSelected(&path.Gvk) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewPrefixTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &PrefixTransformerPlugin{}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
// Code generated by pluginator on ReplacementTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/replacement"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Replace values in targets with values from a source
|
||||
type ReplacementTransformerPlugin struct {
|
||||
ReplacementList []types.ReplacementField `json:"replacements,omitempty" yaml:"replacements,omitempty"`
|
||||
replacements []types.Replacement
|
||||
}
|
||||
|
||||
func (p *ReplacementTransformerPlugin) Config(
|
||||
h *resmap.PluginHelpers, c []byte) (err error) {
|
||||
p.ReplacementList = []types.ReplacementField{}
|
||||
if err := yaml.Unmarshal(c, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range p.ReplacementList {
|
||||
if r.Path != "" && (r.Source != nil || len(r.Targets) != 0) {
|
||||
return fmt.Errorf("cannot specify both path and inline replacement")
|
||||
}
|
||||
if r.Path != "" {
|
||||
// load the replacement from the path
|
||||
content, err := h.Loader().Load(r.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// find if the path contains a a list of replacements or a single replacement
|
||||
var replacement interface{}
|
||||
err = yaml.Unmarshal(content, &replacement)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := reflect.ValueOf(replacement)
|
||||
switch items.Kind() {
|
||||
case reflect.Slice:
|
||||
repl := []types.Replacement{}
|
||||
if err := yaml.Unmarshal(content, &repl); err != nil {
|
||||
return err
|
||||
}
|
||||
p.replacements = append(p.replacements, repl...)
|
||||
case reflect.Map:
|
||||
repl := types.Replacement{}
|
||||
if err := yaml.Unmarshal(content, &repl); err != nil {
|
||||
return err
|
||||
}
|
||||
p.replacements = append(p.replacements, repl)
|
||||
default:
|
||||
return fmt.Errorf("unsupported replacement type encountered within replacement path: %v", items.Kind())
|
||||
}
|
||||
} else {
|
||||
// replacement information is already loaded
|
||||
p.replacements = append(p.replacements, r.Replacement)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ReplacementTransformerPlugin) Transform(m resmap.ResMap) (err error) {
|
||||
return m.ApplyFilter(replacement.Filter{
|
||||
Replacements: p.replacements,
|
||||
})
|
||||
}
|
||||
|
||||
func NewReplacementTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &ReplacementTransformerPlugin{}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
// Code generated by pluginator on ReplicaCountTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/replicacount"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/resid"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Find matching replicas declarations and replace the count.
|
||||
// Eases the kustomization configuration of replica changes.
|
||||
type ReplicaCountTransformerPlugin struct {
|
||||
Replica types.Replica `json:"replica,omitempty" yaml:"replica,omitempty"`
|
||||
FieldSpecs []types.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
|
||||
}
|
||||
|
||||
func (p *ReplicaCountTransformerPlugin) Config(
|
||||
_ *resmap.PluginHelpers, c []byte) (err error) {
|
||||
p.Replica = types.Replica{}
|
||||
p.FieldSpecs = nil
|
||||
return yaml.Unmarshal(c, p)
|
||||
}
|
||||
|
||||
func (p *ReplicaCountTransformerPlugin) Transform(m resmap.ResMap) error {
|
||||
found := false
|
||||
for _, fs := range p.FieldSpecs {
|
||||
matcher := p.createMatcher(fs)
|
||||
resList := m.GetMatchingResourcesByAnyId(matcher)
|
||||
if len(resList) > 0 {
|
||||
found = true
|
||||
for _, r := range resList {
|
||||
// There are redundant checks in the filter
|
||||
// that we'll live with until resolution of
|
||||
// https://github.com/kubernetes-sigs/kustomize/issues/2506
|
||||
err := r.ApplyFilter(replicacount.Filter{
|
||||
Replica: p.Replica,
|
||||
FieldSpec: fs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
gvks := make([]string, len(p.FieldSpecs))
|
||||
for i, replicaSpec := range p.FieldSpecs {
|
||||
gvks[i] = replicaSpec.Gvk.String()
|
||||
}
|
||||
return fmt.Errorf("resource with name %s does not match a config with the following GVK %v",
|
||||
p.Replica.Name, gvks)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match Replica.Name and FieldSpec
|
||||
func (p *ReplicaCountTransformerPlugin) createMatcher(fs types.FieldSpec) resmap.IdMatcher {
|
||||
return func(r resid.ResId) bool {
|
||||
return r.Name == p.Replica.Name && r.Gvk.IsSelected(&fs.Gvk)
|
||||
}
|
||||
}
|
||||
|
||||
func NewReplicaCountTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &ReplicaCountTransformerPlugin{}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Code generated by pluginator on SecretGenerator; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/kv"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type SecretGeneratorPlugin struct {
|
||||
h *resmap.PluginHelpers
|
||||
types.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||
types.SecretArgs
|
||||
}
|
||||
|
||||
func (p *SecretGeneratorPlugin) Config(h *resmap.PluginHelpers, config []byte) (err error) {
|
||||
p.SecretArgs = types.SecretArgs{}
|
||||
err = yaml.Unmarshal(config, p)
|
||||
if p.SecretArgs.Name == "" {
|
||||
p.SecretArgs.Name = p.Name
|
||||
}
|
||||
if p.SecretArgs.Namespace == "" {
|
||||
p.SecretArgs.Namespace = p.Namespace
|
||||
}
|
||||
p.h = h
|
||||
return
|
||||
}
|
||||
|
||||
func (p *SecretGeneratorPlugin) Generate() (resmap.ResMap, error) {
|
||||
return p.h.ResmapFactory().FromSecretArgs(
|
||||
kv.NewLoader(p.h.Loader(), p.h.Validator()), p.SecretArgs)
|
||||
}
|
||||
|
||||
func NewSecretGeneratorPlugin() resmap.GeneratorPlugin {
|
||||
return &SecretGeneratorPlugin{}
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
// Code generated by pluginator on SortOrderTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/resource"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/resid"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Sort the resources using a customizable ordering based of Kind.
|
||||
// Defaults to the ordering of the GVK struct, which puts cluster-wide basic
|
||||
// resources with no dependencies (like Namespace, StorageClass, etc.) first,
|
||||
// and resources with a high number of dependencies
|
||||
// (like ValidatingWebhookConfiguration) last.
|
||||
type SortOrderTransformerPlugin struct {
|
||||
SortOptions *types.SortOptions `json:"sortOptions,omitempty" yaml:"sortOptions,omitempty"`
|
||||
}
|
||||
|
||||
func (p *SortOrderTransformerPlugin) Config(
|
||||
_ *resmap.PluginHelpers, c []byte) error {
|
||||
return errors.WrapPrefixf(yaml.Unmarshal(c, p), "Failed to unmarshal SortOrderTransformer config")
|
||||
}
|
||||
|
||||
func (p *SortOrderTransformerPlugin) applyDefaults() {
|
||||
// Default to FIFO sort, aka no-op.
|
||||
if p.SortOptions == nil {
|
||||
p.SortOptions = &types.SortOptions{
|
||||
Order: types.FIFOSortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
// If legacy sort is selected and no options are given, default to
|
||||
// hardcoded order.
|
||||
if p.SortOptions.Order == types.LegacySortOrder && p.SortOptions.LegacySortOptions == nil {
|
||||
p.SortOptions.LegacySortOptions = &types.LegacySortOptions{
|
||||
OrderFirst: defaultOrderFirst,
|
||||
OrderLast: defaultOrderLast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SortOrderTransformerPlugin) validate() error {
|
||||
// Check valid values for SortOrder
|
||||
if p.SortOptions.Order != types.FIFOSortOrder && p.SortOptions.Order != types.LegacySortOrder {
|
||||
return errors.Errorf("the field 'sortOptions.order' must be one of [%s, %s]",
|
||||
types.FIFOSortOrder, types.LegacySortOrder)
|
||||
}
|
||||
|
||||
// Validate that the only options set are the ones corresponding to the
|
||||
// selected sort order.
|
||||
if p.SortOptions.Order == types.FIFOSortOrder &&
|
||||
p.SortOptions.LegacySortOptions != nil {
|
||||
return errors.Errorf("the field 'sortOptions.legacySortOptions' is"+
|
||||
" set but the selected sort order is '%v', not 'legacy'",
|
||||
p.SortOptions.Order)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SortOrderTransformerPlugin) Transform(m resmap.ResMap) (err error) {
|
||||
p.applyDefaults()
|
||||
err = p.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sort
|
||||
if p.SortOptions.Order == types.LegacySortOrder {
|
||||
s := newLegacyIDSorter(m.Resources(), p.SortOptions.LegacySortOptions)
|
||||
sort.Sort(s)
|
||||
|
||||
// Clear the map and re-add the resources in the sorted order.
|
||||
m.Clear()
|
||||
for _, r := range s.resources {
|
||||
err := m.Append(r)
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "SortOrderTransformer: Failed to append to resources")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Code for legacy sorting.
|
||||
// Legacy sorting is a "fixed" order sorting maintained for backwards
|
||||
// compatibility.
|
||||
|
||||
// legacyIDSorter sorts resources based on two priority lists:
|
||||
// - orderFirst: Resources that should be placed in the start, in the given order.
|
||||
// - orderLast: Resources that should be placed in the end, in the given order.
|
||||
type legacyIDSorter struct {
|
||||
// resids only stores the metadata of the object. This is an optimization as
|
||||
// it's expensive to compute these again and again during ordering.
|
||||
resids []resid.ResId
|
||||
// Initially, we sorted the metadata (ResId) of each object and then called GetByCurrentId on each to construct the final list.
|
||||
// The problem is that GetByCurrentId is inefficient and does a linear scan in a list every time we do that.
|
||||
// So instead, we sort resources alongside the ResIds.
|
||||
resources []*resource.Resource
|
||||
|
||||
typeOrders map[string]int
|
||||
}
|
||||
|
||||
func newLegacyIDSorter(
|
||||
resources []*resource.Resource,
|
||||
options *types.LegacySortOptions) *legacyIDSorter {
|
||||
// Precalculate a resource ranking based on the priority lists.
|
||||
var typeOrders = func() map[string]int {
|
||||
m := map[string]int{}
|
||||
for i, n := range options.OrderFirst {
|
||||
m[n] = -len(options.OrderFirst) + i
|
||||
}
|
||||
for i, n := range options.OrderLast {
|
||||
m[n] = 1 + i
|
||||
}
|
||||
return m
|
||||
}()
|
||||
|
||||
ret := &legacyIDSorter{typeOrders: typeOrders}
|
||||
for _, res := range resources {
|
||||
ret.resids = append(ret.resids, res.CurId())
|
||||
ret.resources = append(ret.resources, res)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
var _ sort.Interface = legacyIDSorter{}
|
||||
|
||||
func (a legacyIDSorter) Len() int { return len(a.resids) }
|
||||
func (a legacyIDSorter) Swap(i, j int) {
|
||||
a.resids[i], a.resids[j] = a.resids[j], a.resids[i]
|
||||
a.resources[i], a.resources[j] = a.resources[j], a.resources[i]
|
||||
}
|
||||
func (a legacyIDSorter) Less(i, j int) bool {
|
||||
if !a.resids[i].Gvk.Equals(a.resids[j].Gvk) {
|
||||
return gvkLessThan(a.resids[i].Gvk, a.resids[j].Gvk, a.typeOrders)
|
||||
}
|
||||
return legacyResIDSortString(a.resids[i]) < legacyResIDSortString(a.resids[j])
|
||||
}
|
||||
|
||||
func gvkLessThan(gvk1, gvk2 resid.Gvk, typeOrders map[string]int) bool {
|
||||
index1 := typeOrders[gvk1.Kind]
|
||||
index2 := typeOrders[gvk2.Kind]
|
||||
if index1 != index2 {
|
||||
return index1 < index2
|
||||
}
|
||||
if (gvk1.Kind == types.NamespaceKind && gvk2.Kind == types.NamespaceKind) && (gvk1.Group == "" || gvk2.Group == "") {
|
||||
return legacyGVKSortString(gvk1) > legacyGVKSortString(gvk2)
|
||||
}
|
||||
return legacyGVKSortString(gvk1) < legacyGVKSortString(gvk2)
|
||||
}
|
||||
|
||||
// legacyGVKSortString returns a string representation of given GVK used for
|
||||
// stable sorting.
|
||||
func legacyGVKSortString(x resid.Gvk) string {
|
||||
legacyNoGroup := "~G"
|
||||
legacyNoVersion := "~V"
|
||||
legacyNoKind := "~K"
|
||||
legacyFieldSeparator := "_"
|
||||
|
||||
g := x.Group
|
||||
if g == "" {
|
||||
g = legacyNoGroup
|
||||
}
|
||||
v := x.Version
|
||||
if v == "" {
|
||||
v = legacyNoVersion
|
||||
}
|
||||
k := x.Kind
|
||||
if k == "" {
|
||||
k = legacyNoKind
|
||||
}
|
||||
return strings.Join([]string{g, v, k}, legacyFieldSeparator)
|
||||
}
|
||||
|
||||
// legacyResIDSortString returns a string representation of given ResID used for
|
||||
// stable sorting.
|
||||
func legacyResIDSortString(id resid.ResId) string {
|
||||
legacyNoNamespace := "~X"
|
||||
legacyNoName := "~N"
|
||||
legacySeparator := "|"
|
||||
|
||||
ns := id.Namespace
|
||||
if ns == "" {
|
||||
ns = legacyNoNamespace
|
||||
}
|
||||
nm := id.Name
|
||||
if nm == "" {
|
||||
nm = legacyNoName
|
||||
}
|
||||
return strings.Join(
|
||||
[]string{id.Gvk.String(), ns, nm}, legacySeparator)
|
||||
}
|
||||
|
||||
// DO NOT CHANGE!
|
||||
// Final legacy ordering provided as a default by kustomize.
|
||||
// Originally an attempt to apply resources in the correct order, an effort
|
||||
// which later proved impossible as not all types are known beforehand.
|
||||
// See: https://github.com/kubernetes-sigs/kustomize/issues/3913
|
||||
var defaultOrderFirst = []string{ //nolint:gochecknoglobals
|
||||
"Namespace",
|
||||
"ResourceQuota",
|
||||
"StorageClass",
|
||||
"CustomResourceDefinition",
|
||||
"ServiceAccount",
|
||||
"PodSecurityPolicy",
|
||||
"Role",
|
||||
"ClusterRole",
|
||||
"RoleBinding",
|
||||
"ClusterRoleBinding",
|
||||
"ConfigMap",
|
||||
"Secret",
|
||||
"Endpoints",
|
||||
"Service",
|
||||
"LimitRange",
|
||||
"PriorityClass",
|
||||
"PersistentVolume",
|
||||
"PersistentVolumeClaim",
|
||||
"Deployment",
|
||||
"StatefulSet",
|
||||
"CronJob",
|
||||
"PodDisruptionBudget",
|
||||
}
|
||||
var defaultOrderLast = []string{ //nolint:gochecknoglobals
|
||||
"MutatingWebhookConfiguration",
|
||||
"ValidatingWebhookConfiguration",
|
||||
}
|
||||
|
||||
func NewSortOrderTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &SortOrderTransformerPlugin{}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Code generated by pluginator on SuffixTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/suffix"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/resid"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Add the given suffix to the field
|
||||
type SuffixTransformerPlugin struct {
|
||||
Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"`
|
||||
FieldSpecs types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
|
||||
}
|
||||
|
||||
// TODO: Make this gvk skip list part of the config.
|
||||
var suffixFieldSpecsToSkip = types.FsSlice{
|
||||
{Gvk: resid.Gvk{Kind: "CustomResourceDefinition"}},
|
||||
{Gvk: resid.Gvk{Group: "apiregistration.k8s.io", Kind: "APIService"}},
|
||||
{Gvk: resid.Gvk{Kind: "Namespace"}},
|
||||
}
|
||||
|
||||
func (p *SuffixTransformerPlugin) Config(
|
||||
_ *resmap.PluginHelpers, c []byte) (err error) {
|
||||
p.Suffix = ""
|
||||
p.FieldSpecs = nil
|
||||
err = yaml.Unmarshal(c, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if p.FieldSpecs == nil {
|
||||
return errors.New("fieldSpecs is not expected to be nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *SuffixTransformerPlugin) Transform(m resmap.ResMap) error {
|
||||
// Even if the Suffix is empty we want to proceed with the
|
||||
// transformation. This allows to add contextual information
|
||||
// to the resources (AddNameSuffix).
|
||||
for _, r := range m.Resources() {
|
||||
// TODO: move this test into the filter (i.e. make a better filter)
|
||||
if p.shouldSkip(r.OrgId()) {
|
||||
continue
|
||||
}
|
||||
id := r.OrgId()
|
||||
// current default configuration contains
|
||||
// only one entry: "metadata/name" with no GVK
|
||||
for _, fs := range p.FieldSpecs {
|
||||
// TODO: this is redundant to filter (but needed for now)
|
||||
if !id.IsSelected(&fs.Gvk) {
|
||||
continue
|
||||
}
|
||||
// TODO: move this test into the filter.
|
||||
if fs.Path == "metadata/name" {
|
||||
// "metadata/name" is the only field.
|
||||
// this will add a suffix to the resource
|
||||
// even if it is empty
|
||||
|
||||
r.AddNameSuffix(p.Suffix)
|
||||
if p.Suffix != "" {
|
||||
// TODO: There are multiple transformers that can change a resource's name, and each makes a call to
|
||||
// StorePreviousID(). We should make it so that we only call StorePreviousID once per kustomization layer
|
||||
// to avoid storing intermediate names between transformations, to prevent intermediate name conflicts.
|
||||
r.StorePreviousId()
|
||||
}
|
||||
}
|
||||
if err := r.ApplyFilter(suffix.Filter{
|
||||
Suffix: p.Suffix,
|
||||
FieldSpec: fs,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SuffixTransformerPlugin) shouldSkip(id resid.ResId) bool {
|
||||
for _, path := range suffixFieldSpecsToSkip {
|
||||
if id.IsSelected(&path.Gvk) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewSuffixTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &SuffixTransformerPlugin{}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
// Code generated by pluginator on ValueAddTransformer; DO NOT EDIT.
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/filters/namespace"
|
||||
"sigs.k8s.io/kustomize/api/filters/valueadd"
|
||||
"sigs.k8s.io/kustomize/api/resmap"
|
||||
"sigs.k8s.io/kustomize/api/resource"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// An 'Add' transformer inspired by the IETF RFC 6902 JSON spec Add operation.
|
||||
type ValueAddTransformerPlugin struct {
|
||||
// Value is the value to add.
|
||||
// Defaults to base name of encompassing kustomization root.
|
||||
Value string `json:"value,omitempty" yaml:"value,omitempty"`
|
||||
|
||||
// Targets is a slice of targets that should have the value added.
|
||||
Targets []Target `json:"targets,omitempty" yaml:"targets,omitempty"`
|
||||
|
||||
// TargetFilePath is a file path. If specified, the file will be parsed into
|
||||
// a slice of Target, and appended to anything that was specified in the
|
||||
// Targets field. This is just a means to share common target specifications.
|
||||
TargetFilePath string `json:"targetFilePath,omitempty" yaml:"targetFilePath,omitempty"`
|
||||
}
|
||||
|
||||
// Target describes where to put the value.
|
||||
type Target struct {
|
||||
// Selector selects the resources to modify.
|
||||
Selector *types.Selector `json:"selector,omitempty" yaml:"selector,omitempty"`
|
||||
|
||||
// NotSelector selects the resources to exclude
|
||||
// from those included by overly broad selectors.
|
||||
// TODO: implement this?
|
||||
// NotSelector *types.Selector `json:"notSelector,omitempty" yaml:"notSelector,omitempty"`
|
||||
|
||||
// FieldPath is a JSON-style path to the field intended to hold the value.
|
||||
FieldPath string `json:"fieldPath,omitempty" yaml:"fieldPath,omitempty"`
|
||||
|
||||
// FilePathPosition is passed to the filter directly. Look there for doc.
|
||||
FilePathPosition int `json:"filePathPosition,omitempty" yaml:"filePathPosition,omitempty"`
|
||||
}
|
||||
|
||||
func (p *ValueAddTransformerPlugin) Config(h *resmap.PluginHelpers, c []byte) error {
|
||||
err := yaml.Unmarshal(c, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Value = strings.TrimSpace(p.Value)
|
||||
if p.Value == "" {
|
||||
p.Value = filepath.Base(h.Loader().Root())
|
||||
}
|
||||
if p.TargetFilePath != "" {
|
||||
bytes, err := h.Loader().Load(p.TargetFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var targets struct {
|
||||
Targets []Target `json:"targets,omitempty" yaml:"targets,omitempty"`
|
||||
}
|
||||
err = yaml.Unmarshal(bytes, &targets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Targets = append(p.Targets, targets.Targets...)
|
||||
}
|
||||
if len(p.Targets) == 0 {
|
||||
return fmt.Errorf("must specify at least one target")
|
||||
}
|
||||
for _, target := range p.Targets {
|
||||
if err = validateSelector(target.Selector); err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: call validateSelector(target.NotSelector) if field added.
|
||||
if err = validateJsonFieldPath(target.FieldPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if target.FilePathPosition < 0 {
|
||||
return fmt.Errorf(
|
||||
"value of FilePathPosition (%d) cannot be negative",
|
||||
target.FilePathPosition)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: implement
|
||||
func validateSelector(_ *types.Selector) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Enforce RFC 6902?
|
||||
func validateJsonFieldPath(p string) error {
|
||||
if len(p) == 0 {
|
||||
return fmt.Errorf("fieldPath cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ValueAddTransformerPlugin) Transform(m resmap.ResMap) (err error) {
|
||||
for _, t := range p.Targets {
|
||||
var resources []*resource.Resource
|
||||
if t.Selector == nil {
|
||||
resources = m.Resources()
|
||||
} else {
|
||||
resources, err = m.Select(*t.Selector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// TODO: consider t.NotSelector if implemented
|
||||
for _, res := range resources {
|
||||
if t.FieldPath == types.MetadataNamespacePath {
|
||||
err = res.ApplyFilter(namespace.Filter{
|
||||
Namespace: p.Value,
|
||||
})
|
||||
} else {
|
||||
err = res.ApplyFilter(valueadd.Filter{
|
||||
Value: p.Value,
|
||||
FieldPath: t.FieldPath,
|
||||
FilePathPosition: t.FilePathPosition,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewValueAddTransformerPlugin() resmap.TransformerPlugin {
|
||||
return &ValueAddTransformerPlugin{}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package builtins holds code generated from the builtin plugins.
|
||||
// The "builtin" plugins are written as normal plugins and can
|
||||
// be used as such, but they are also used to generate the code
|
||||
// in this package so they can be statically linked to client code.
|
||||
package builtins
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package generators
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/ifc"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// MakeConfigMap makes a configmap.
|
||||
//
|
||||
// ConfigMap: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#configmap-v1-core
|
||||
//
|
||||
// ConfigMaps and Secrets are similar.
|
||||
//
|
||||
// Both objects have a `data` field, which contains a map from keys to
|
||||
// values that must be UTF-8 valid strings. Such data might be simple text,
|
||||
// or whoever made the data may have done so by performing a base64 encoding
|
||||
// on binary data. Regardless, k8s has no means to know this, so it treats
|
||||
// the data field as a string.
|
||||
//
|
||||
// The ConfigMap has an additional field `binaryData`, also a map, but its
|
||||
// values are _intended_ to be interpreted as a base64 encoding of []byte,
|
||||
// by whatever makes use of the ConfigMap.
|
||||
//
|
||||
// In a ConfigMap, any key used in `data` cannot also be used in `binaryData`
|
||||
// and vice-versa. A key must be unique across both maps.
|
||||
func MakeConfigMap(
|
||||
ldr ifc.KvLoader, args *types.ConfigMapArgs) (rn *yaml.RNode, err error) {
|
||||
rn, err = makeBaseNode("ConfigMap", args.Name, args.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := makeValidatedDataMap(ldr, args.Name, args.KvPairSources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = rn.LoadMapIntoConfigMapData(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = copyLabelsAndAnnotations(rn, args.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = setImmutable(rn, args.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rn, nil
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package generators
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/api/ifc"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// MakeSecret makes a kubernetes Secret.
|
||||
//
|
||||
// Secret: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#secret-v1-core
|
||||
//
|
||||
// ConfigMaps and Secrets are similar.
|
||||
//
|
||||
// Like a ConfigMap, a Secret has a `data` field, but unlike a ConfigMap it has
|
||||
// no `binaryData` field.
|
||||
//
|
||||
// All of a Secret's data is assumed to be opaque in nature, and assumed to be
|
||||
// base64 encoded from its original representation, regardless of whether the
|
||||
// original data was UTF-8 text or binary.
|
||||
//
|
||||
// This encoding provides no secrecy. It's just a neutral, common means to
|
||||
// represent opaque text and binary data. Beneath the base64 encoding
|
||||
// is presumably further encoding under control of the Secret's consumer.
|
||||
//
|
||||
// A Secret has string field `type` which holds an identifier, used by the
|
||||
// client, to choose the algorithm to interpret the `data` field. Kubernetes
|
||||
// cannot make use of this data; it's up to a controller or some pod's service
|
||||
// to interpret the value, using `type` as a clue as to how to do this.
|
||||
func MakeSecret(
|
||||
ldr ifc.KvLoader, args *types.SecretArgs) (rn *yaml.RNode, err error) {
|
||||
rn, err = makeBaseNode("Secret", args.Name, args.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := "Opaque"
|
||||
if args.Type != "" {
|
||||
t = args.Type
|
||||
}
|
||||
if _, err := rn.Pipe(
|
||||
yaml.FieldSetter{
|
||||
Name: "type",
|
||||
Value: yaml.NewStringRNode(t)}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := makeValidatedDataMap(ldr, args.Name, args.KvPairSources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = rn.LoadMapIntoSecretData(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copyLabelsAndAnnotations(rn, args.Options)
|
||||
setImmutable(rn, args.Options)
|
||||
return rn, nil
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package generators
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"sigs.k8s.io/kustomize/api/ifc"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
func makeBaseNode(kind, name, namespace string) (*yaml.RNode, error) {
|
||||
rn, err := yaml.Parse(fmt.Sprintf(`
|
||||
apiVersion: v1
|
||||
kind: %s
|
||||
`, kind))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name == "" {
|
||||
return nil, errors.Errorf("a configmap must have a name")
|
||||
}
|
||||
if _, err := rn.Pipe(yaml.SetK8sName(name)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if namespace != "" {
|
||||
if _, err := rn.Pipe(yaml.SetK8sNamespace(namespace)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return rn, nil
|
||||
}
|
||||
|
||||
func makeValidatedDataMap(
|
||||
ldr ifc.KvLoader, name string, sources types.KvPairSources) (map[string]string, error) {
|
||||
pairs, err := ldr.Load(sources)
|
||||
if err != nil {
|
||||
return nil, errors.WrapPrefix(err, "loading KV pairs", 0)
|
||||
}
|
||||
knownKeys := make(map[string]string)
|
||||
for _, p := range pairs {
|
||||
// legal key: alphanumeric characters, '-', '_' or '.'
|
||||
if err := ldr.Validator().ErrIfInvalidKey(p.Key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := knownKeys[p.Key]; ok {
|
||||
return nil, errors.Errorf(
|
||||
"configmap %s illegally repeats the key `%s`", name, p.Key)
|
||||
}
|
||||
knownKeys[p.Key] = p.Value
|
||||
}
|
||||
return knownKeys, nil
|
||||
}
|
||||
|
||||
// copyLabelsAndAnnotations copies labels and annotations from
|
||||
// GeneratorOptions into the given object.
|
||||
func copyLabelsAndAnnotations(
|
||||
rn *yaml.RNode, opts *types.GeneratorOptions) error {
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
for _, k := range yaml.SortedMapKeys(opts.Labels) {
|
||||
v := opts.Labels[k]
|
||||
if _, err := rn.Pipe(yaml.SetLabel(k, v)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, k := range yaml.SortedMapKeys(opts.Annotations) {
|
||||
v := opts.Annotations[k]
|
||||
if _, err := rn.Pipe(yaml.SetAnnotation(k, v)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setImmutable(
|
||||
rn *yaml.RNode, opts *types.GeneratorOptions) error {
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
if opts.Immutable {
|
||||
n := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: "true",
|
||||
Tag: yaml.NodeTagBool,
|
||||
}
|
||||
if _, err := rn.Pipe(yaml.FieldSetter{Name: "immutable", Value: yaml.NewRNode(n)}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseFileSource parses the source given.
|
||||
//
|
||||
// Acceptable formats include:
|
||||
// 1. source-path: the basename will become the key name
|
||||
// 2. source-name=source-path: the source-name will become the key name and
|
||||
// source-path is the path to the key file.
|
||||
//
|
||||
// Key names cannot include '='.
|
||||
func ParseFileSource(source string) (keyName, filePath string, err error) {
|
||||
numSeparators := strings.Count(source, "=")
|
||||
switch {
|
||||
case numSeparators == 0:
|
||||
return path.Base(source), source, nil
|
||||
case numSeparators == 1 && strings.HasPrefix(source, "="):
|
||||
return "", "", errors.Errorf("missing key name for file path %q in source %q", strings.TrimPrefix(source, "="), source)
|
||||
case numSeparators == 1 && strings.HasSuffix(source, "="):
|
||||
return "", "", errors.Errorf("missing file path for key name %q in source %q", strings.TrimSuffix(source, "="), source)
|
||||
case numSeparators > 1:
|
||||
return "", "", errors.Errorf("source %q key name or file path contains '='", source)
|
||||
default:
|
||||
components := strings.Split(source, "=")
|
||||
return components[0], components[1], nil
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||
)
|
||||
|
||||
// Cloner is a function that can clone a git repo.
|
||||
type Cloner func(repoSpec *RepoSpec) error
|
||||
|
||||
// ClonerUsingGitExec uses a local git install, as opposed
|
||||
// to say, some remote API, to obtain a local clone of
|
||||
// a remote repo.
|
||||
func ClonerUsingGitExec(repoSpec *RepoSpec) error {
|
||||
r, err := newCmdRunner(repoSpec.Timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repoSpec.Dir = r.dir
|
||||
if err = r.run("init"); err != nil {
|
||||
return err
|
||||
}
|
||||
// git relative submodule need origin, see https://github.com/kubernetes-sigs/kustomize/issues/5131
|
||||
if err = r.run("remote", "add", "origin", repoSpec.CloneSpec()); err != nil {
|
||||
return err
|
||||
}
|
||||
ref := "HEAD"
|
||||
if repoSpec.Ref != "" {
|
||||
ref = repoSpec.Ref
|
||||
}
|
||||
// we use repoSpec.CloneSpec() instead of origin because on error,
|
||||
// the prior prints the actual repo url for the user.
|
||||
if err = r.run("fetch", "--depth=1", repoSpec.CloneSpec(), ref); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = r.run("checkout", "FETCH_HEAD"); err != nil {
|
||||
return err
|
||||
}
|
||||
if repoSpec.Submodules {
|
||||
return r.run("submodule", "update", "--init", "--recursive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DoNothingCloner returns a cloner that only sets
|
||||
// cloneDir field in the repoSpec. It's assumed that
|
||||
// the cloneDir is associated with some fake filesystem
|
||||
// used in a test.
|
||||
func DoNothingCloner(dir filesys.ConfirmedDir) Cloner {
|
||||
return func(rs *RepoSpec) error {
|
||||
rs.Dir = dir
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/internal/utils"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||
)
|
||||
|
||||
// gitRunner runs the external git binary.
|
||||
type gitRunner struct {
|
||||
gitProgram string
|
||||
duration time.Duration
|
||||
dir filesys.ConfirmedDir
|
||||
}
|
||||
|
||||
// newCmdRunner returns a gitRunner if it can find the binary.
|
||||
// It also creats a temp directory for cloning repos.
|
||||
func newCmdRunner(timeout time.Duration) (*gitRunner, error) {
|
||||
gitProgram, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
return nil, errors.WrapPrefixf(err, "no 'git' program on path")
|
||||
}
|
||||
dir, err := filesys.NewTmpConfirmedDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &gitRunner{
|
||||
gitProgram: gitProgram,
|
||||
duration: timeout,
|
||||
dir: dir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// run a command with a timeout.
|
||||
func (r gitRunner) run(args ...string) error {
|
||||
//nolint: gosec
|
||||
cmd := exec.Command(r.gitProgram, args...)
|
||||
cmd.Dir = r.dir.String()
|
||||
return utils.TimedCall(
|
||||
cmd.String(),
|
||||
r.duration,
|
||||
func() error {
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "failed to run '%s': %s", cmd.String(), string(out))
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||
)
|
||||
|
||||
// Used as a temporary non-empty occupant of the cloneDir
|
||||
// field, as something distinguishable from the empty string
|
||||
// in various outputs (especially tests). Not using an
|
||||
// actual directory name here, as that's a temporary directory
|
||||
// with a unique name that isn't created until clone time.
|
||||
const notCloned = filesys.ConfirmedDir("/notCloned")
|
||||
|
||||
// RepoSpec specifies a git repository and a branch and path therein.
|
||||
type RepoSpec struct {
|
||||
// Raw, original spec, used to look for cycles.
|
||||
// TODO(monopole): Drop raw, use processed fields instead.
|
||||
raw string
|
||||
|
||||
// Host, e.g. https://github.com/
|
||||
Host string
|
||||
|
||||
// RepoPath name (Path to repository),
|
||||
// e.g. kubernetes-sigs/kustomize
|
||||
RepoPath string
|
||||
|
||||
// Dir is where the repository is cloned to.
|
||||
Dir filesys.ConfirmedDir
|
||||
|
||||
// Relative path in the repository, and in the cloneDir,
|
||||
// to a Kustomization.
|
||||
KustRootPath string
|
||||
|
||||
// Branch or tag reference.
|
||||
Ref string
|
||||
|
||||
// Submodules indicates whether or not to clone git submodules.
|
||||
Submodules bool
|
||||
|
||||
// Timeout is the maximum duration allowed for execing git commands.
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// CloneSpec returns a string suitable for "git clone {spec}".
|
||||
func (x *RepoSpec) CloneSpec() string {
|
||||
return x.Host + x.RepoPath
|
||||
}
|
||||
|
||||
func (x *RepoSpec) CloneDir() filesys.ConfirmedDir {
|
||||
return x.Dir
|
||||
}
|
||||
|
||||
func (x *RepoSpec) Raw() string {
|
||||
return x.raw
|
||||
}
|
||||
|
||||
func (x *RepoSpec) AbsPath() string {
|
||||
return x.Dir.Join(x.KustRootPath)
|
||||
}
|
||||
|
||||
func (x *RepoSpec) Cleaner(fSys filesys.FileSystem) func() error {
|
||||
return func() error { return fSys.RemoveAll(x.Dir.String()) }
|
||||
}
|
||||
|
||||
const (
|
||||
refQuery = "?ref="
|
||||
gitSuffix = ".git"
|
||||
gitRootDelimiter = "_git/"
|
||||
pathSeparator = "/" // do not use filepath.Separator, as this is a URL
|
||||
)
|
||||
|
||||
// NewRepoSpecFromURL parses git-like urls.
|
||||
// From strings like git@github.com:someOrg/someRepo.git or
|
||||
// https://github.com/someOrg/someRepo?ref=someHash, extract
|
||||
// the different parts of URL, set into a RepoSpec object and return RepoSpec object.
|
||||
// It MUST return an error if the input is not a git-like URL, as this is used by some code paths
|
||||
// to distinguish between local and remote paths.
|
||||
//
|
||||
// In particular, NewRepoSpecFromURL separates the URL used to clone the repo from the
|
||||
// elements Kustomize uses for other purposes (e.g. query params that turn into args, and
|
||||
// the path to the kustomization root within the repo).
|
||||
func NewRepoSpecFromURL(n string) (*RepoSpec, error) {
|
||||
repoSpec := &RepoSpec{raw: n, Dir: notCloned, Timeout: defaultTimeout, Submodules: defaultSubmodules}
|
||||
if filepath.IsAbs(n) {
|
||||
return nil, fmt.Errorf("uri looks like abs path: %s", n)
|
||||
}
|
||||
|
||||
// Parse the query first. This is safe because according to rfc3986 "?" is only allowed in the
|
||||
// query and is not recognized %-encoded.
|
||||
// Note that parseQuery returns default values for empty parameters.
|
||||
n, query, _ := strings.Cut(n, "?")
|
||||
repoSpec.Ref, repoSpec.Timeout, repoSpec.Submodules = parseQuery(query)
|
||||
|
||||
var err error
|
||||
|
||||
// Parse the host (e.g. scheme, username, domain) segment.
|
||||
repoSpec.Host, n, err = extractHost(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// In some cases, we're given a path to a git repo + a path to the kustomization root within
|
||||
// that repo. We need to split them so that we can ultimately give the repo only to the cloner.
|
||||
repoSpec.RepoPath, repoSpec.KustRootPath, err = parsePathParts(n, defaultRepoPathLength(repoSpec.Host))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return repoSpec, nil
|
||||
}
|
||||
|
||||
const allSegments = -999999
|
||||
const orgRepoSegments = 2
|
||||
|
||||
func defaultRepoPathLength(host string) int {
|
||||
if strings.HasPrefix(host, fileScheme) {
|
||||
return allSegments
|
||||
}
|
||||
return orgRepoSegments
|
||||
}
|
||||
|
||||
// parsePathParts splits the repo path that will ultimately be passed to git to clone the
|
||||
// repo from the kustomization root path, which Kustomize will execute the build in after the repo
|
||||
// is cloned.
|
||||
//
|
||||
// We first try to do this based on explicit markers in the URL (e.g. _git, .git or //).
|
||||
// If none are present, we try to apply a historical default repo path length that is derived from
|
||||
// Github URLs. If there aren't enough segments, we have historically considered the URL invalid.
|
||||
func parsePathParts(n string, defaultSegmentLength int) (string, string, error) {
|
||||
repoPath, kustRootPath, success := tryExplicitMarkerSplit(n)
|
||||
if !success {
|
||||
repoPath, kustRootPath, success = tryDefaultLengthSplit(n, defaultSegmentLength)
|
||||
}
|
||||
|
||||
// Validate the result
|
||||
if !success || len(repoPath) == 0 {
|
||||
return "", "", fmt.Errorf("failed to parse repo path segment")
|
||||
}
|
||||
if kustRootPathExitsRepo(kustRootPath) {
|
||||
return "", "", fmt.Errorf("url path exits repo: %s", n)
|
||||
}
|
||||
|
||||
return repoPath, strings.TrimPrefix(kustRootPath, pathSeparator), nil
|
||||
}
|
||||
|
||||
func tryExplicitMarkerSplit(n string) (string, string, bool) {
|
||||
// Look for the _git delimiter, which by convention is expected to be ONE directory above the repo root.
|
||||
// If found, split on the NEXT path element, which is the repo root.
|
||||
// Example: https://username@dev.azure.com/org/project/_git/repo/path/to/kustomization/root
|
||||
if gitRootIdx := strings.Index(n, gitRootDelimiter); gitRootIdx >= 0 {
|
||||
gitRootPath := n[:gitRootIdx+len(gitRootDelimiter)]
|
||||
subpathSegments := strings.Split(n[gitRootIdx+len(gitRootDelimiter):], pathSeparator)
|
||||
return gitRootPath + subpathSegments[0], strings.Join(subpathSegments[1:], pathSeparator), true
|
||||
|
||||
// Look for a double-slash in the path, which if present separates the repo root from the kust path.
|
||||
// It is a convention, not a real path element, so do not preserve it in the returned value.
|
||||
// Example: https://github.com/org/repo//path/to/kustomozation/root
|
||||
} else if repoRootIdx := strings.Index(n, "//"); repoRootIdx >= 0 {
|
||||
return n[:repoRootIdx], n[repoRootIdx+2:], true
|
||||
|
||||
// Look for .git in the path, which if present is part of the directory name of the git repo.
|
||||
// This means we want to grab everything up to and including that suffix
|
||||
// Example: https://github.com/org/repo.git/path/to/kustomozation/root
|
||||
} else if gitSuffixIdx := strings.Index(n, gitSuffix); gitSuffixIdx >= 0 {
|
||||
upToGitSuffix := n[:gitSuffixIdx+len(gitSuffix)]
|
||||
afterGitSuffix := n[gitSuffixIdx+len(gitSuffix):]
|
||||
return upToGitSuffix, afterGitSuffix, true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func tryDefaultLengthSplit(n string, defaultSegmentLength int) (string, string, bool) {
|
||||
// If the default is to take all segments, do so.
|
||||
if defaultSegmentLength == allSegments {
|
||||
return n, "", true
|
||||
|
||||
// If the default is N segments, make sure we have at least that many and take them if so.
|
||||
// If we have less than N, we have historically considered the URL invalid.
|
||||
} else if segments := strings.Split(n, pathSeparator); len(segments) >= defaultSegmentLength {
|
||||
firstNSegments := strings.Join(segments[:defaultSegmentLength], pathSeparator)
|
||||
rest := strings.Join(segments[defaultSegmentLength:], pathSeparator)
|
||||
return firstNSegments, rest, true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func kustRootPathExitsRepo(kustRootPath string) bool {
|
||||
cleanedPath := filepath.Clean(strings.TrimPrefix(kustRootPath, string(filepath.Separator)))
|
||||
pathElements := strings.Split(cleanedPath, string(filepath.Separator))
|
||||
return len(pathElements) > 0 &&
|
||||
pathElements[0] == filesys.ParentDir
|
||||
}
|
||||
|
||||
// Clone git submodules by default.
|
||||
const defaultSubmodules = true
|
||||
|
||||
// Arbitrary, but non-infinite, timeout for running commands.
|
||||
const defaultTimeout = 27 * time.Second
|
||||
|
||||
func parseQuery(query string) (string, time.Duration, bool) {
|
||||
values, err := url.ParseQuery(query)
|
||||
// in event of parse failure, return defaults
|
||||
if err != nil {
|
||||
return "", defaultTimeout, defaultSubmodules
|
||||
}
|
||||
|
||||
// ref is the desired git ref to target. Can be specified by in a git URL
|
||||
// with ?ref=<string> or ?version=<string>, although ref takes precedence.
|
||||
ref := values.Get("version")
|
||||
if queryValue := values.Get("ref"); queryValue != "" {
|
||||
ref = queryValue
|
||||
}
|
||||
|
||||
// depth is the desired git exec timeout. Can be specified by in a git URL
|
||||
// with ?timeout=<duration>.
|
||||
duration := defaultTimeout
|
||||
if queryValue := values.Get("timeout"); queryValue != "" {
|
||||
// Attempt to first parse as a number of integer seconds (like "61"),
|
||||
// and then attempt to parse as a suffixed duration (like "61s").
|
||||
if intValue, err := strconv.Atoi(queryValue); err == nil && intValue > 0 {
|
||||
duration = time.Duration(intValue) * time.Second
|
||||
} else if durationValue, err := time.ParseDuration(queryValue); err == nil && durationValue > 0 {
|
||||
duration = durationValue
|
||||
}
|
||||
}
|
||||
|
||||
// submodules indicates if git submodule cloning is desired. Can be
|
||||
// specified by in a git URL with ?submodules=<bool>.
|
||||
submodules := defaultSubmodules
|
||||
if queryValue := values.Get("submodules"); queryValue != "" {
|
||||
if boolValue, err := strconv.ParseBool(queryValue); err == nil {
|
||||
submodules = boolValue
|
||||
}
|
||||
}
|
||||
|
||||
return ref, duration, submodules
|
||||
}
|
||||
|
||||
func extractHost(n string) (string, string, error) {
|
||||
n = ignoreForcedGitProtocol(n)
|
||||
scheme, n := extractScheme(n)
|
||||
username, n := extractUsername(n)
|
||||
stdGithub := isStandardGithubHost(n)
|
||||
acceptSCP := acceptSCPStyle(scheme, username, stdGithub)
|
||||
|
||||
// Validate the username and scheme before attempting host/path parsing, because if the parsing
|
||||
// so far has not succeeded, we will not be able to extract the host and path correctly.
|
||||
if err := validateScheme(scheme, acceptSCP); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Now that we have extracted a valid scheme+username, we can parse host itself.
|
||||
|
||||
// The file protocol specifies an absolute path to a local git repo.
|
||||
// Everything after the scheme (including any 'username' we found) is actually part of that path.
|
||||
if scheme == fileScheme {
|
||||
return scheme, username + n, nil
|
||||
}
|
||||
var host, rest = n, ""
|
||||
if sepIndex := findPathSeparator(n, acceptSCP); sepIndex >= 0 {
|
||||
host, rest = n[:sepIndex+1], n[sepIndex+1:]
|
||||
}
|
||||
|
||||
// Github URLs are strictly normalized in a way that may discard scheme and username components.
|
||||
if stdGithub {
|
||||
scheme, username, host = normalizeGithubHostParts(scheme, username)
|
||||
}
|
||||
|
||||
// Host is required, so do not concat the scheme and username if we didn't find one.
|
||||
if host == "" {
|
||||
return "", "", errors.Errorf("failed to parse host segment")
|
||||
}
|
||||
return scheme + username + host, rest, nil
|
||||
}
|
||||
|
||||
// ignoreForcedGitProtocol strips the "git::" prefix from URLs.
|
||||
// We used to use go-getter to handle our urls: https://github.com/hashicorp/go-getter.
|
||||
// The git:: prefix signaled go-getter to use the git protocol to fetch the url's contents.
|
||||
// We silently strip this prefix to allow these go-getter-style urls to continue to work,
|
||||
// although the git protocol (which is insecure and unsupported on many platforms, including Github)
|
||||
// will not actually be used as intended.
|
||||
func ignoreForcedGitProtocol(n string) string {
|
||||
n, found := trimPrefixIgnoreCase(n, "git::")
|
||||
if found {
|
||||
log.Println("Warning: Forcing the git protocol using the 'git::' URL prefix is not supported. " +
|
||||
"Kustomize currently strips this invalid prefix, but will stop doing so in a future release. " +
|
||||
"Please remove the 'git::' prefix from your configuration.")
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// acceptSCPStyle returns true if the scheme and username indicate potential use of an SCP-style URL.
|
||||
// With this style, the scheme is not explicit and the path is delimited by a colon.
|
||||
// Strictly speaking the username is optional in SCP-like syntax, but Kustomize has always
|
||||
// required it for non-Github URLs.
|
||||
// Example: user@host.xz:path/to/repo.git/
|
||||
func acceptSCPStyle(scheme, username string, isGithubURL bool) bool {
|
||||
return scheme == "" && (username != "" || isGithubURL)
|
||||
}
|
||||
|
||||
func validateScheme(scheme string, acceptSCPStyle bool) error {
|
||||
// see https://git-scm.com/docs/git-fetch#_git_urls for info relevant to these validations
|
||||
switch scheme {
|
||||
case "":
|
||||
// Empty scheme is only ok if it's a Github URL or if it looks like SCP-style syntax
|
||||
if !acceptSCPStyle {
|
||||
return fmt.Errorf("failed to parse scheme")
|
||||
}
|
||||
case sshScheme, fileScheme, httpsScheme, httpScheme:
|
||||
// These are all supported schemes
|
||||
default:
|
||||
// At time of writing, we should never end up here because we do not parse out
|
||||
// unsupported schemes to begin with.
|
||||
return fmt.Errorf("unsupported scheme %q", scheme)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const fileScheme = "file://"
|
||||
const httpScheme = "http://"
|
||||
const httpsScheme = "https://"
|
||||
const sshScheme = "ssh://"
|
||||
|
||||
func extractScheme(s string) (string, string) {
|
||||
for _, prefix := range []string{sshScheme, httpsScheme, httpScheme, fileScheme} {
|
||||
if rest, found := trimPrefixIgnoreCase(s, prefix); found {
|
||||
return prefix, rest
|
||||
}
|
||||
}
|
||||
return "", s
|
||||
}
|
||||
|
||||
func extractUsername(s string) (string, string) {
|
||||
var userRegexp = regexp.MustCompile(`^([a-zA-Z][a-zA-Z0-9-]*)@`)
|
||||
if m := userRegexp.FindStringSubmatch(s); m != nil {
|
||||
username := m[1] + "@"
|
||||
return username, s[len(username):]
|
||||
}
|
||||
return "", s
|
||||
}
|
||||
|
||||
func isStandardGithubHost(s string) bool {
|
||||
lowerCased := strings.ToLower(s)
|
||||
return strings.HasPrefix(lowerCased, "github.com/") ||
|
||||
strings.HasPrefix(lowerCased, "github.com:")
|
||||
}
|
||||
|
||||
// trimPrefixIgnoreCase returns the rest of s and true if prefix, ignoring case, prefixes s.
|
||||
// Otherwise, trimPrefixIgnoreCase returns s and false.
|
||||
func trimPrefixIgnoreCase(s, prefix string) (string, bool) {
|
||||
if len(prefix) <= len(s) && strings.ToLower(s[:len(prefix)]) == prefix {
|
||||
return s[len(prefix):], true
|
||||
}
|
||||
return s, false
|
||||
}
|
||||
|
||||
func findPathSeparator(hostPath string, acceptSCP bool) int {
|
||||
sepIndex := strings.Index(hostPath, pathSeparator)
|
||||
if acceptSCP {
|
||||
colonIndex := strings.Index(hostPath, ":")
|
||||
// The colon acts as a delimiter in scp-style ssh URLs only if not prefixed by '/'.
|
||||
if sepIndex == -1 || (colonIndex > 0 && colonIndex < sepIndex) {
|
||||
sepIndex = colonIndex
|
||||
}
|
||||
}
|
||||
return sepIndex
|
||||
}
|
||||
|
||||
func normalizeGithubHostParts(scheme, username string) (string, string, string) {
|
||||
if strings.HasPrefix(scheme, sshScheme) || username != "" {
|
||||
return "", username, "github.com:"
|
||||
}
|
||||
return httpsScheme, "", "github.com/"
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package image
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsImageMatched returns true if the value of t is identical to the
|
||||
// image name in the full image name and tag as given by s.
|
||||
func IsImageMatched(s, t string) bool {
|
||||
// Tag values are limited to [a-zA-Z0-9_.{}-].
|
||||
// Some tools like Bazel rules_k8s allow tag patterns with {} characters.
|
||||
// More info: https://github.com/bazelbuild/rules_k8s/pull/423
|
||||
pattern, _ := regexp.Compile("^" + t + "(:[a-zA-Z0-9_.{}-]*)?(@sha256:[a-zA-Z0-9_.{}-]*)?$")
|
||||
return pattern.MatchString(s)
|
||||
}
|
||||
|
||||
// Split separates and returns the name and tag parts
|
||||
// from the image string using either colon `:` or at `@` separators.
|
||||
// image reference pattern: [[host[:port]/]component/]component[:tag][@digest]
|
||||
func Split(imageName string) (name string, tag string, digest string) {
|
||||
// check if image name contains a domain
|
||||
// if domain is present, ignore domain and check for `:`
|
||||
searchName := imageName
|
||||
slashIndex := strings.Index(imageName, "/")
|
||||
if slashIndex > 0 {
|
||||
searchName = imageName[slashIndex:]
|
||||
} else {
|
||||
slashIndex = 0
|
||||
}
|
||||
|
||||
id := strings.Index(searchName, "@")
|
||||
ic := strings.Index(searchName, ":")
|
||||
|
||||
// no tag or digest
|
||||
if ic < 0 && id < 0 {
|
||||
return imageName, "", ""
|
||||
}
|
||||
|
||||
// digest only
|
||||
if id >= 0 && (id < ic || ic < 0) {
|
||||
id += slashIndex
|
||||
name = imageName[:id]
|
||||
digest = strings.TrimPrefix(imageName[id:], "@")
|
||||
return name, "", digest
|
||||
}
|
||||
|
||||
// tag and digest
|
||||
if id >= 0 && ic >= 0 {
|
||||
id += slashIndex
|
||||
ic += slashIndex
|
||||
name = imageName[:ic]
|
||||
tag = strings.TrimPrefix(imageName[ic:id], ":")
|
||||
digest = strings.TrimPrefix(imageName[id:], "@")
|
||||
return name, tag, digest
|
||||
}
|
||||
|
||||
// tag only
|
||||
ic += slashIndex
|
||||
name = imageName[:ic]
|
||||
tag = strings.TrimPrefix(imageName[ic:], ":")
|
||||
return name, tag, ""
|
||||
}
|
||||
Generated
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package builtinpluginconsts
|
||||
|
||||
const commonAnnotationFieldSpecs = `
|
||||
commonAnnotations:
|
||||
- path: metadata/annotations
|
||||
create: true
|
||||
|
||||
- path: spec/template/metadata/annotations
|
||||
create: true
|
||||
version: v1
|
||||
kind: ReplicationController
|
||||
|
||||
- path: spec/template/metadata/annotations
|
||||
create: true
|
||||
kind: Deployment
|
||||
|
||||
- path: spec/template/metadata/annotations
|
||||
create: true
|
||||
kind: ReplicaSet
|
||||
|
||||
- path: spec/template/metadata/annotations
|
||||
create: true
|
||||
kind: DaemonSet
|
||||
|
||||
- path: spec/template/metadata/annotations
|
||||
create: true
|
||||
kind: StatefulSet
|
||||
|
||||
- path: spec/template/metadata/annotations
|
||||
create: true
|
||||
group: batch
|
||||
kind: Job
|
||||
|
||||
- path: spec/jobTemplate/metadata/annotations
|
||||
create: true
|
||||
group: batch
|
||||
kind: CronJob
|
||||
|
||||
- path: spec/jobTemplate/spec/template/metadata/annotations
|
||||
create: true
|
||||
group: batch
|
||||
kind: CronJob
|
||||
|
||||
`
|
||||
Generated
Vendored
+113
@@ -0,0 +1,113 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package builtinpluginconsts
|
||||
|
||||
const commonLabelFieldSpecs = `
|
||||
commonLabels:
|
||||
- path: spec/selector
|
||||
create: true
|
||||
version: v1
|
||||
kind: Service
|
||||
|
||||
- path: spec/selector
|
||||
create: true
|
||||
version: v1
|
||||
kind: ReplicationController
|
||||
- path: spec/selector/matchLabels
|
||||
create: true
|
||||
kind: Deployment
|
||||
|
||||
- path: spec/template/spec/affinity/podAffinity/preferredDuringSchedulingIgnoredDuringExecution/podAffinityTerm/labelSelector/matchLabels
|
||||
create: false
|
||||
group: apps
|
||||
kind: Deployment
|
||||
|
||||
- path: spec/template/spec/affinity/podAffinity/requiredDuringSchedulingIgnoredDuringExecution/labelSelector/matchLabels
|
||||
create: false
|
||||
group: apps
|
||||
kind: Deployment
|
||||
|
||||
- path: spec/template/spec/affinity/podAntiAffinity/preferredDuringSchedulingIgnoredDuringExecution/podAffinityTerm/labelSelector/matchLabels
|
||||
create: false
|
||||
group: apps
|
||||
kind: Deployment
|
||||
|
||||
- path: spec/template/spec/affinity/podAntiAffinity/requiredDuringSchedulingIgnoredDuringExecution/labelSelector/matchLabels
|
||||
create: false
|
||||
group: apps
|
||||
kind: Deployment
|
||||
|
||||
- path: spec/template/spec/topologySpreadConstraints/labelSelector/matchLabels
|
||||
create: false
|
||||
group: apps
|
||||
kind: Deployment
|
||||
|
||||
- path: spec/selector/matchLabels
|
||||
create: true
|
||||
kind: ReplicaSet
|
||||
|
||||
- path: spec/selector/matchLabels
|
||||
create: true
|
||||
kind: DaemonSet
|
||||
|
||||
- path: spec/selector/matchLabels
|
||||
create: true
|
||||
group: apps
|
||||
kind: StatefulSet
|
||||
|
||||
- path: spec/template/spec/affinity/podAffinity/preferredDuringSchedulingIgnoredDuringExecution/podAffinityTerm/labelSelector/matchLabels
|
||||
create: false
|
||||
group: apps
|
||||
kind: StatefulSet
|
||||
|
||||
- path: spec/template/spec/affinity/podAffinity/requiredDuringSchedulingIgnoredDuringExecution/labelSelector/matchLabels
|
||||
create: false
|
||||
group: apps
|
||||
kind: StatefulSet
|
||||
|
||||
- path: spec/template/spec/affinity/podAntiAffinity/preferredDuringSchedulingIgnoredDuringExecution/podAffinityTerm/labelSelector/matchLabels
|
||||
create: false
|
||||
group: apps
|
||||
kind: StatefulSet
|
||||
|
||||
- path: spec/template/spec/affinity/podAntiAffinity/requiredDuringSchedulingIgnoredDuringExecution/labelSelector/matchLabels
|
||||
create: false
|
||||
group: apps
|
||||
kind: StatefulSet
|
||||
|
||||
- path: spec/template/spec/topologySpreadConstraints/labelSelector/matchLabels
|
||||
create: false
|
||||
group: apps
|
||||
kind: StatefulSet
|
||||
|
||||
- path: spec/selector/matchLabels
|
||||
create: false
|
||||
group: batch
|
||||
kind: Job
|
||||
|
||||
- path: spec/jobTemplate/spec/selector/matchLabels
|
||||
create: false
|
||||
group: batch
|
||||
kind: CronJob
|
||||
|
||||
- path: spec/selector/matchLabels
|
||||
create: false
|
||||
group: policy
|
||||
kind: PodDisruptionBudget
|
||||
|
||||
- path: spec/podSelector/matchLabels
|
||||
create: false
|
||||
group: networking.k8s.io
|
||||
kind: NetworkPolicy
|
||||
|
||||
- path: spec/ingress/from/podSelector/matchLabels
|
||||
create: false
|
||||
group: networking.k8s.io
|
||||
kind: NetworkPolicy
|
||||
|
||||
- path: spec/egress/to/podSelector/matchLabels
|
||||
create: false
|
||||
group: networking.k8s.io
|
||||
kind: NetworkPolicy
|
||||
` + metadataLabelsFieldSpecs
|
||||
Generated
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package builtinpluginconsts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// GetDefaultFieldSpecs returns default fieldSpecs.
|
||||
func GetDefaultFieldSpecs() []byte {
|
||||
configData := [][]byte{
|
||||
[]byte(namePrefixFieldSpecs),
|
||||
[]byte(nameSuffixFieldSpecs),
|
||||
[]byte(commonLabelFieldSpecs),
|
||||
[]byte(templateLabelFieldSpecs),
|
||||
[]byte(commonAnnotationFieldSpecs),
|
||||
[]byte(namespaceFieldSpecs),
|
||||
[]byte(varReferenceFieldSpecs),
|
||||
[]byte(nameReferenceFieldSpecs),
|
||||
[]byte(imagesFieldSpecs),
|
||||
[]byte(replicasFieldSpecs),
|
||||
}
|
||||
return bytes.Join(configData, []byte("\n"))
|
||||
}
|
||||
|
||||
// GetDefaultFieldSpecsAsMap returns default fieldSpecs
|
||||
// as a string->string map.
|
||||
func GetDefaultFieldSpecsAsMap() map[string]string {
|
||||
result := make(map[string]string)
|
||||
result["nameprefix"] = namePrefixFieldSpecs
|
||||
result["namesuffix"] = nameSuffixFieldSpecs
|
||||
result["commonlabels"] = commonLabelFieldSpecs
|
||||
result["templatelabels"] = templateLabelFieldSpecs
|
||||
result["commonannotations"] = commonAnnotationFieldSpecs
|
||||
result["namespace"] = namespaceFieldSpecs
|
||||
result["varreference"] = varReferenceFieldSpecs
|
||||
result["namereference"] = nameReferenceFieldSpecs
|
||||
result["images"] = imagesFieldSpecs
|
||||
result["replicas"] = replicasFieldSpecs
|
||||
return result
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package builtinpluginconsts provides builtin plugin
|
||||
// configuration data. Builtin plugins can also be
|
||||
// configured individually with plugin config files,
|
||||
// in which case the constants in this package are ignored.
|
||||
package builtinpluginconsts
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package builtinpluginconsts
|
||||
|
||||
const (
|
||||
imagesFieldSpecs = `
|
||||
images:
|
||||
- path: spec/containers[]/image
|
||||
create: true
|
||||
- path: spec/initContainers[]/image
|
||||
create: true
|
||||
- path: spec/volumes[]/image/reference
|
||||
create: true
|
||||
- path: spec/template/spec/containers[]/image
|
||||
create: true
|
||||
- path: spec/template/spec/initContainers[]/image
|
||||
create: true
|
||||
- path: spec/template/spec/volumes[]/image/reference
|
||||
create: true
|
||||
`
|
||||
)
|
||||
Generated
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package builtinpluginconsts
|
||||
|
||||
const metadataLabelsFieldSpecs = `
|
||||
- path: metadata/labels
|
||||
create: true
|
||||
|
||||
- path: spec/template/metadata/labels
|
||||
create: true
|
||||
version: v1
|
||||
kind: ReplicationController
|
||||
|
||||
- path: spec/template/metadata/labels
|
||||
create: true
|
||||
kind: Deployment
|
||||
|
||||
- path: spec/template/metadata/labels
|
||||
create: true
|
||||
kind: ReplicaSet
|
||||
|
||||
- path: spec/template/metadata/labels
|
||||
create: true
|
||||
kind: DaemonSet
|
||||
|
||||
- path: spec/template/metadata/labels
|
||||
create: true
|
||||
group: apps
|
||||
kind: StatefulSet
|
||||
|
||||
- path: spec/volumeClaimTemplates[]/metadata/labels
|
||||
create: true
|
||||
group: apps
|
||||
kind: StatefulSet
|
||||
|
||||
- path: spec/template/metadata/labels
|
||||
create: true
|
||||
group: batch
|
||||
kind: Job
|
||||
|
||||
- path: spec/jobTemplate/metadata/labels
|
||||
create: true
|
||||
group: batch
|
||||
kind: CronJob
|
||||
|
||||
- path: spec/jobTemplate/spec/template/metadata/labels
|
||||
create: true
|
||||
group: batch
|
||||
kind: CronJob
|
||||
`
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user