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