1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package metadata
18
19import (
20	"context"
21	"encoding/json"
22	"fmt"
23	"time"
24
25	"k8s.io/klog"
26
27	metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
28	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29	"k8s.io/apimachinery/pkg/runtime"
30	"k8s.io/apimachinery/pkg/runtime/schema"
31	"k8s.io/apimachinery/pkg/runtime/serializer"
32	"k8s.io/apimachinery/pkg/types"
33	"k8s.io/apimachinery/pkg/watch"
34	"k8s.io/client-go/rest"
35)
36
37var deleteScheme = runtime.NewScheme()
38var parameterScheme = runtime.NewScheme()
39var deleteOptionsCodec = serializer.NewCodecFactory(deleteScheme)
40var dynamicParameterCodec = runtime.NewParameterCodec(parameterScheme)
41
42var versionV1 = schema.GroupVersion{Version: "v1"}
43
44func init() {
45	metav1.AddToGroupVersion(parameterScheme, versionV1)
46	metav1.AddToGroupVersion(deleteScheme, versionV1)
47}
48
49// Client allows callers to retrieve the object metadata for any
50// Kubernetes-compatible API endpoint. The client uses the
51// meta.k8s.io/v1 PartialObjectMetadata resource to more efficiently
52// retrieve just the necessary metadata, but on older servers
53// (Kubernetes 1.14 and before) will retrieve the object and then
54// convert the metadata.
55type Client struct {
56	client *rest.RESTClient
57}
58
59var _ Interface = &Client{}
60
61// ConfigFor returns a copy of the provided config with the
62// appropriate metadata client defaults set.
63func ConfigFor(inConfig *rest.Config) *rest.Config {
64	config := rest.CopyConfig(inConfig)
65	config.AcceptContentTypes = "application/vnd.kubernetes.protobuf,application/json"
66	config.ContentType = "application/vnd.kubernetes.protobuf"
67	config.NegotiatedSerializer = metainternalversionscheme.Codecs.WithoutConversion()
68	if config.UserAgent == "" {
69		config.UserAgent = rest.DefaultKubernetesUserAgent()
70	}
71	return config
72}
73
74// NewForConfigOrDie creates a new metadata client for the given config and
75// panics if there is an error in the config.
76func NewForConfigOrDie(c *rest.Config) Interface {
77	ret, err := NewForConfig(c)
78	if err != nil {
79		panic(err)
80	}
81	return ret
82}
83
84// NewForConfig creates a new metadata client that can retrieve object
85// metadata details about any Kubernetes object (core, aggregated, or custom
86// resource based) in the form of PartialObjectMetadata objects, or returns
87// an error.
88func NewForConfig(inConfig *rest.Config) (Interface, error) {
89	config := ConfigFor(inConfig)
90	// for serializing the options
91	config.GroupVersion = &schema.GroupVersion{}
92	config.APIPath = "/this-value-should-never-be-sent"
93
94	restClient, err := rest.RESTClientFor(config)
95	if err != nil {
96		return nil, err
97	}
98
99	return &Client{client: restClient}, nil
100}
101
102type client struct {
103	client    *Client
104	namespace string
105	resource  schema.GroupVersionResource
106}
107
108// Resource returns an interface that can access cluster or namespace
109// scoped instances of resource.
110func (c *Client) Resource(resource schema.GroupVersionResource) Getter {
111	return &client{client: c, resource: resource}
112}
113
114// Namespace returns an interface that can access namespace-scoped instances of the
115// provided resource.
116func (c *client) Namespace(ns string) ResourceInterface {
117	ret := *c
118	ret.namespace = ns
119	return &ret
120}
121
122// Delete removes the provided resource from the server.
123func (c *client) Delete(ctx context.Context, name string, opts metav1.DeleteOptions, subresources ...string) error {
124	if len(name) == 0 {
125		return fmt.Errorf("name is required")
126	}
127	deleteOptionsByte, err := runtime.Encode(deleteOptionsCodec.LegacyCodec(schema.GroupVersion{Version: "v1"}), &opts)
128	if err != nil {
129		return err
130	}
131
132	result := c.client.client.
133		Delete().
134		AbsPath(append(c.makeURLSegments(name), subresources...)...).
135		Body(deleteOptionsByte).
136		Do(ctx)
137	return result.Error()
138}
139
140// DeleteCollection triggers deletion of all resources in the specified scope (namespace or cluster).
141func (c *client) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOptions metav1.ListOptions) error {
142	deleteOptionsByte, err := runtime.Encode(deleteOptionsCodec.LegacyCodec(schema.GroupVersion{Version: "v1"}), &opts)
143	if err != nil {
144		return err
145	}
146
147	result := c.client.client.
148		Delete().
149		AbsPath(c.makeURLSegments("")...).
150		Body(deleteOptionsByte).
151		SpecificallyVersionedParams(&listOptions, dynamicParameterCodec, versionV1).
152		Do(ctx)
153	return result.Error()
154}
155
156// Get returns the resource with name from the specified scope (namespace or cluster).
157func (c *client) Get(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) {
158	if len(name) == 0 {
159		return nil, fmt.Errorf("name is required")
160	}
161	result := c.client.client.Get().AbsPath(append(c.makeURLSegments(name), subresources...)...).
162		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").
163		SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).
164		Do(ctx)
165	if err := result.Error(); err != nil {
166		return nil, err
167	}
168	obj, err := result.Get()
169	if runtime.IsNotRegisteredError(err) {
170		klog.V(5).Infof("Unable to retrieve PartialObjectMetadata: %#v", err)
171		rawBytes, err := result.Raw()
172		if err != nil {
173			return nil, err
174		}
175		var partial metav1.PartialObjectMetadata
176		if err := json.Unmarshal(rawBytes, &partial); err != nil {
177			return nil, fmt.Errorf("unable to decode returned object as PartialObjectMetadata: %v", err)
178		}
179		if !isLikelyObjectMetadata(&partial) {
180			return nil, fmt.Errorf("object does not appear to match the ObjectMeta schema: %#v", partial)
181		}
182		partial.TypeMeta = metav1.TypeMeta{}
183		return &partial, nil
184	}
185	if err != nil {
186		return nil, err
187	}
188	partial, ok := obj.(*metav1.PartialObjectMetadata)
189	if !ok {
190		return nil, fmt.Errorf("unexpected object, expected PartialObjectMetadata but got %T", obj)
191	}
192	return partial, nil
193}
194
195// List returns all resources within the specified scope (namespace or cluster).
196func (c *client) List(ctx context.Context, opts metav1.ListOptions) (*metav1.PartialObjectMetadataList, error) {
197	result := c.client.client.Get().AbsPath(c.makeURLSegments("")...).
198		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").
199		SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).
200		Do(ctx)
201	if err := result.Error(); err != nil {
202		return nil, err
203	}
204	obj, err := result.Get()
205	if runtime.IsNotRegisteredError(err) {
206		klog.V(5).Infof("Unable to retrieve PartialObjectMetadataList: %#v", err)
207		rawBytes, err := result.Raw()
208		if err != nil {
209			return nil, err
210		}
211		var partial metav1.PartialObjectMetadataList
212		if err := json.Unmarshal(rawBytes, &partial); err != nil {
213			return nil, fmt.Errorf("unable to decode returned object as PartialObjectMetadataList: %v", err)
214		}
215		partial.TypeMeta = metav1.TypeMeta{}
216		return &partial, nil
217	}
218	if err != nil {
219		return nil, err
220	}
221	partial, ok := obj.(*metav1.PartialObjectMetadataList)
222	if !ok {
223		return nil, fmt.Errorf("unexpected object, expected PartialObjectMetadata but got %T", obj)
224	}
225	return partial, nil
226}
227
228// Watch finds all changes to the resources in the specified scope (namespace or cluster).
229func (c *client) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
230	var timeout time.Duration
231	if opts.TimeoutSeconds != nil {
232		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
233	}
234	opts.Watch = true
235	return c.client.client.Get().
236		AbsPath(c.makeURLSegments("")...).
237		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").
238		SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).
239		Timeout(timeout).
240		Watch(ctx)
241}
242
243// Patch modifies the named resource in the specified scope (namespace or cluster).
244func (c *client) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) {
245	if len(name) == 0 {
246		return nil, fmt.Errorf("name is required")
247	}
248	result := c.client.client.
249		Patch(pt).
250		AbsPath(append(c.makeURLSegments(name), subresources...)...).
251		Body(data).
252		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").
253		SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).
254		Do(ctx)
255	if err := result.Error(); err != nil {
256		return nil, err
257	}
258	obj, err := result.Get()
259	if runtime.IsNotRegisteredError(err) {
260		rawBytes, err := result.Raw()
261		if err != nil {
262			return nil, err
263		}
264		var partial metav1.PartialObjectMetadata
265		if err := json.Unmarshal(rawBytes, &partial); err != nil {
266			return nil, fmt.Errorf("unable to decode returned object as PartialObjectMetadata: %v", err)
267		}
268		if !isLikelyObjectMetadata(&partial) {
269			return nil, fmt.Errorf("object does not appear to match the ObjectMeta schema")
270		}
271		partial.TypeMeta = metav1.TypeMeta{}
272		return &partial, nil
273	}
274	if err != nil {
275		return nil, err
276	}
277	partial, ok := obj.(*metav1.PartialObjectMetadata)
278	if !ok {
279		return nil, fmt.Errorf("unexpected object, expected PartialObjectMetadata but got %T", obj)
280	}
281	return partial, nil
282}
283
284func (c *client) makeURLSegments(name string) []string {
285	url := []string{}
286	if len(c.resource.Group) == 0 {
287		url = append(url, "api")
288	} else {
289		url = append(url, "apis", c.resource.Group)
290	}
291	url = append(url, c.resource.Version)
292
293	if len(c.namespace) > 0 {
294		url = append(url, "namespaces", c.namespace)
295	}
296	url = append(url, c.resource.Resource)
297
298	if len(name) > 0 {
299		url = append(url, name)
300	}
301
302	return url
303}
304
305func isLikelyObjectMetadata(meta *metav1.PartialObjectMetadata) bool {
306	return len(meta.UID) > 0 || !meta.CreationTimestamp.IsZero() || len(meta.Name) > 0 || len(meta.GenerateName) > 0
307}
308