/* 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 metadata import ( "context" "encoding/json" "fmt" "time" "k8s.io/klog/v2" metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme" 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/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/rest" ) var deleteScheme = runtime.NewScheme() var parameterScheme = runtime.NewScheme() var deleteOptionsCodec = serializer.NewCodecFactory(deleteScheme) var dynamicParameterCodec = runtime.NewParameterCodec(parameterScheme) var versionV1 = schema.GroupVersion{Version: "v1"} func init() { metav1.AddToGroupVersion(parameterScheme, versionV1) metav1.AddToGroupVersion(deleteScheme, versionV1) } // Client allows callers to retrieve the object metadata for any // Kubernetes-compatible API endpoint. The client uses the // meta.k8s.io/v1 PartialObjectMetadata resource to more efficiently // retrieve just the necessary metadata, but on older servers // (Kubernetes 1.14 and before) will retrieve the object and then // convert the metadata. type Client struct { client *rest.RESTClient } var _ Interface = &Client{} // ConfigFor returns a copy of the provided config with the // appropriate metadata client defaults set. func ConfigFor(inConfig *rest.Config) *rest.Config { config := rest.CopyConfig(inConfig) config.AcceptContentTypes = "application/vnd.kubernetes.protobuf,application/json" config.ContentType = "application/vnd.kubernetes.protobuf" config.NegotiatedSerializer = metainternalversionscheme.Codecs.WithoutConversion() if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() } return config } // NewForConfigOrDie creates a new metadata client for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) Interface { ret, err := NewForConfig(c) if err != nil { panic(err) } return ret } // NewForConfig creates a new metadata client that can retrieve object // metadata details about any Kubernetes object (core, aggregated, or custom // resource based) in the form of PartialObjectMetadata objects, or returns // an error. func NewForConfig(inConfig *rest.Config) (Interface, error) { config := ConfigFor(inConfig) // for serializing the options config.GroupVersion = &schema.GroupVersion{} config.APIPath = "/this-value-should-never-be-sent" restClient, err := rest.RESTClientFor(config) if err != nil { return nil, err } return &Client{client: restClient}, nil } type client struct { client *Client namespace string resource schema.GroupVersionResource } // Resource returns an interface that can access cluster or namespace // scoped instances of resource. func (c *Client) Resource(resource schema.GroupVersionResource) Getter { return &client{client: c, resource: resource} } // Namespace returns an interface that can access namespace-scoped instances of the // provided resource. func (c *client) Namespace(ns string) ResourceInterface { ret := *c ret.namespace = ns return &ret } // Delete removes the provided resource from the server. func (c *client) Delete(ctx context.Context, name string, opts metav1.DeleteOptions, subresources ...string) error { if len(name) == 0 { return fmt.Errorf("name is required") } deleteOptionsByte, err := runtime.Encode(deleteOptionsCodec.LegacyCodec(schema.GroupVersion{Version: "v1"}), &opts) if err != nil { return err } result := c.client.client. Delete(). AbsPath(append(c.makeURLSegments(name), subresources...)...). Body(deleteOptionsByte). Do(ctx) return result.Error() } // DeleteCollection triggers deletion of all resources in the specified scope (namespace or cluster). func (c *client) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOptions metav1.ListOptions) error { deleteOptionsByte, err := runtime.Encode(deleteOptionsCodec.LegacyCodec(schema.GroupVersion{Version: "v1"}), &opts) if err != nil { return err } result := c.client.client. Delete(). AbsPath(c.makeURLSegments("")...). Body(deleteOptionsByte). SpecificallyVersionedParams(&listOptions, dynamicParameterCodec, versionV1). Do(ctx) return result.Error() } // Get returns the resource with name from the specified scope (namespace or cluster). func (c *client) Get(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) { if len(name) == 0 { return nil, fmt.Errorf("name is required") } result := c.client.client.Get().AbsPath(append(c.makeURLSegments(name), subresources...)...). SetHeader("Accept", "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json"). SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1). Do(ctx) if err := result.Error(); err != nil { return nil, err } obj, err := result.Get() if runtime.IsNotRegisteredError(err) { klog.V(5).Infof("Unable to retrieve PartialObjectMetadata: %#v", err) rawBytes, err := result.Raw() if err != nil { return nil, err } var partial metav1.PartialObjectMetadata if err := json.Unmarshal(rawBytes, &partial); err != nil { return nil, fmt.Errorf("unable to decode returned object as PartialObjectMetadata: %v", err) } if !isLikelyObjectMetadata(&partial) { return nil, fmt.Errorf("object does not appear to match the ObjectMeta schema: %#v", partial) } partial.TypeMeta = metav1.TypeMeta{} return &partial, nil } if err != nil { return nil, err } partial, ok := obj.(*metav1.PartialObjectMetadata) if !ok { return nil, fmt.Errorf("unexpected object, expected PartialObjectMetadata but got %T", obj) } return partial, nil } // List returns all resources within the specified scope (namespace or cluster). func (c *client) List(ctx context.Context, opts metav1.ListOptions) (*metav1.PartialObjectMetadataList, error) { result := c.client.client.Get().AbsPath(c.makeURLSegments("")...). SetHeader("Accept", "application/vnd.kubernetes.protobuf;as=PartialObjectMetadataList;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadataList;g=meta.k8s.io;v=v1,application/json"). SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1). Do(ctx) if err := result.Error(); err != nil { return nil, err } obj, err := result.Get() if runtime.IsNotRegisteredError(err) { klog.V(5).Infof("Unable to retrieve PartialObjectMetadataList: %#v", err) rawBytes, err := result.Raw() if err != nil { return nil, err } var partial metav1.PartialObjectMetadataList if err := json.Unmarshal(rawBytes, &partial); err != nil { return nil, fmt.Errorf("unable to decode returned object as PartialObjectMetadataList: %v", err) } partial.TypeMeta = metav1.TypeMeta{} return &partial, nil } if err != nil { return nil, err } partial, ok := obj.(*metav1.PartialObjectMetadataList) if !ok { return nil, fmt.Errorf("unexpected object, expected PartialObjectMetadata but got %T", obj) } return partial, nil } // Watch finds all changes to the resources in the specified scope (namespace or cluster). func (c *client) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { var timeout time.Duration if opts.TimeoutSeconds != nil { timeout = time.Duration(*opts.TimeoutSeconds) * time.Second } opts.Watch = true return c.client.client.Get(). AbsPath(c.makeURLSegments("")...). SetHeader("Accept", "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json"). SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1). Timeout(timeout). Watch(ctx) } // Patch modifies the named resource in the specified scope (namespace or cluster). func (c *client) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) { if len(name) == 0 { return nil, fmt.Errorf("name is required") } result := c.client.client. Patch(pt). AbsPath(append(c.makeURLSegments(name), subresources...)...). Body(data). SetHeader("Accept", "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json"). SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1). Do(ctx) if err := result.Error(); err != nil { return nil, err } obj, err := result.Get() if runtime.IsNotRegisteredError(err) { rawBytes, err := result.Raw() if err != nil { return nil, err } var partial metav1.PartialObjectMetadata if err := json.Unmarshal(rawBytes, &partial); err != nil { return nil, fmt.Errorf("unable to decode returned object as PartialObjectMetadata: %v", err) } if !isLikelyObjectMetadata(&partial) { return nil, fmt.Errorf("object does not appear to match the ObjectMeta schema") } partial.TypeMeta = metav1.TypeMeta{} return &partial, nil } if err != nil { return nil, err } partial, ok := obj.(*metav1.PartialObjectMetadata) if !ok { return nil, fmt.Errorf("unexpected object, expected PartialObjectMetadata but got %T", obj) } return partial, nil } func (c *client) makeURLSegments(name string) []string { url := []string{} if len(c.resource.Group) == 0 { url = append(url, "api") } else { url = append(url, "apis", c.resource.Group) } url = append(url, c.resource.Version) if len(c.namespace) > 0 { url = append(url, "namespaces", c.namespace) } url = append(url, c.resource.Resource) if len(name) > 0 { url = append(url, name) } return url } func isLikelyObjectMetadata(meta *metav1.PartialObjectMetadata) bool { return len(meta.UID) > 0 || !meta.CreationTimestamp.IsZero() || len(meta.Name) > 0 || len(meta.GenerateName) > 0 }