working commit

This commit is contained in:
2026-03-13 19:02:42 +02:00
parent bebbf79c7a
commit 5c1da77f4c
1329 changed files with 314708 additions and 39 deletions
@@ -0,0 +1,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
View File
@@ -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
View File
@@ -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
}
@@ -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
View File
@@ -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...)
}
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
File diff suppressed because it is too large Load Diff
+215
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}