1/*
2Copyright 2017 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 apiclient
18
19import (
20	"bufio"
21	"bytes"
22	"fmt"
23	"io"
24	"strings"
25
26	kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
27
28	"k8s.io/apimachinery/pkg/runtime"
29	"k8s.io/apimachinery/pkg/runtime/schema"
30	clientset "k8s.io/client-go/kubernetes"
31	fakeclientset "k8s.io/client-go/kubernetes/fake"
32	core "k8s.io/client-go/testing"
33
34	"github.com/pkg/errors"
35)
36
37// DryRunGetter is an interface that must be supplied to the NewDryRunClient function in order to construct a fully functional fake dryrun clientset
38type DryRunGetter interface {
39	HandleGetAction(core.GetAction) (bool, runtime.Object, error)
40	HandleListAction(core.ListAction) (bool, runtime.Object, error)
41}
42
43// MarshalFunc takes care of converting any object to a byte array for displaying the object to the user
44type MarshalFunc func(runtime.Object, schema.GroupVersion) ([]byte, error)
45
46// DefaultMarshalFunc is the default MarshalFunc used; uses YAML to print objects to the user
47func DefaultMarshalFunc(obj runtime.Object, gv schema.GroupVersion) ([]byte, error) {
48	return kubeadmutil.MarshalToYaml(obj, gv)
49}
50
51// DryRunClientOptions specifies options to pass to NewDryRunClientWithOpts in order to get a dryrun clientset
52type DryRunClientOptions struct {
53	Writer          io.Writer
54	Getter          DryRunGetter
55	PrependReactors []core.Reactor
56	AppendReactors  []core.Reactor
57	MarshalFunc     MarshalFunc
58	PrintGETAndLIST bool
59}
60
61// GetDefaultDryRunClientOptions returns the default DryRunClientOptions values
62func GetDefaultDryRunClientOptions(drg DryRunGetter, w io.Writer) DryRunClientOptions {
63	return DryRunClientOptions{
64		Writer:          w,
65		Getter:          drg,
66		PrependReactors: []core.Reactor{},
67		AppendReactors:  []core.Reactor{},
68		MarshalFunc:     DefaultMarshalFunc,
69		PrintGETAndLIST: false,
70	}
71}
72
73// actionWithName is the generic interface for an action that has a name associated with it
74// This just makes it easier to catch all actions that has a name; instead of hard-coding all request that has it associated
75type actionWithName interface {
76	core.Action
77	GetName() string
78}
79
80// actionWithObject is the generic interface for an action that has an object associated with it
81// This just makes it easier to catch all actions that has an object; instead of hard-coding all request that has it associated
82type actionWithObject interface {
83	core.Action
84	GetObject() runtime.Object
85}
86
87// NewDryRunClient is a wrapper for NewDryRunClientWithOpts using some default values
88func NewDryRunClient(drg DryRunGetter, w io.Writer) clientset.Interface {
89	return NewDryRunClientWithOpts(GetDefaultDryRunClientOptions(drg, w))
90}
91
92// NewDryRunClientWithOpts returns a clientset.Interface that can be used normally for talking to the Kubernetes API.
93// This client doesn't apply changes to the backend. The client gets GET/LIST values from the DryRunGetter implementation.
94// This client logs all I/O to the writer w in YAML format
95func NewDryRunClientWithOpts(opts DryRunClientOptions) clientset.Interface {
96	// Build a chain of reactors to act like a normal clientset; but log everything that is happening and don't change any state
97	client := fakeclientset.NewSimpleClientset()
98
99	// Build the chain of reactors. Order matters; first item here will be invoked first on match, then the second one will be evaluated, etc.
100	defaultReactorChain := []core.Reactor{
101		// Log everything that happens. Default the object if it's about to be created/updated so that the logged object is representative.
102		&core.SimpleReactor{
103			Verb:     "*",
104			Resource: "*",
105			Reaction: func(action core.Action) (bool, runtime.Object, error) {
106				logDryRunAction(action, opts.Writer, opts.MarshalFunc)
107
108				return false, nil, nil
109			},
110		},
111		// Let the DryRunGetter implementation take care of all GET requests.
112		// The DryRunGetter implementation may call a real API Server behind the scenes or just fake everything
113		&core.SimpleReactor{
114			Verb:     "get",
115			Resource: "*",
116			Reaction: func(action core.Action) (bool, runtime.Object, error) {
117				getAction, ok := action.(core.GetAction)
118				if !ok {
119					// something's wrong, we can't handle this event
120					return true, nil, errors.New("can't cast get reactor event action object to GetAction interface")
121				}
122				handled, obj, err := opts.Getter.HandleGetAction(getAction)
123
124				if opts.PrintGETAndLIST {
125					// Print the marshalled object format with one tab indentation
126					objBytes, err := opts.MarshalFunc(obj, action.GetResource().GroupVersion())
127					if err == nil {
128						fmt.Println("[dryrun] Returning faked GET response:")
129						PrintBytesWithLinePrefix(opts.Writer, objBytes, "\t")
130					}
131				}
132
133				return handled, obj, err
134			},
135		},
136		// Let the DryRunGetter implementation take care of all GET requests.
137		// The DryRunGetter implementation may call a real API Server behind the scenes or just fake everything
138		&core.SimpleReactor{
139			Verb:     "list",
140			Resource: "*",
141			Reaction: func(action core.Action) (bool, runtime.Object, error) {
142				listAction, ok := action.(core.ListAction)
143				if !ok {
144					// something's wrong, we can't handle this event
145					return true, nil, errors.New("can't cast list reactor event action object to ListAction interface")
146				}
147				handled, objs, err := opts.Getter.HandleListAction(listAction)
148
149				if opts.PrintGETAndLIST {
150					// Print the marshalled object format with one tab indentation
151					objBytes, err := opts.MarshalFunc(objs, action.GetResource().GroupVersion())
152					if err == nil {
153						fmt.Println("[dryrun] Returning faked LIST response:")
154						PrintBytesWithLinePrefix(opts.Writer, objBytes, "\t")
155					}
156				}
157
158				return handled, objs, err
159			},
160		},
161		// For the verbs that modify anything on the server; just return the object if present and exit successfully
162		&core.SimpleReactor{
163			Verb:     "create",
164			Resource: "*",
165			Reaction: successfulModificationReactorFunc,
166		},
167		&core.SimpleReactor{
168			Verb:     "update",
169			Resource: "*",
170			Reaction: successfulModificationReactorFunc,
171		},
172		&core.SimpleReactor{
173			Verb:     "delete",
174			Resource: "*",
175			Reaction: successfulModificationReactorFunc,
176		},
177		&core.SimpleReactor{
178			Verb:     "delete-collection",
179			Resource: "*",
180			Reaction: successfulModificationReactorFunc,
181		},
182		&core.SimpleReactor{
183			Verb:     "patch",
184			Resource: "*",
185			Reaction: successfulModificationReactorFunc,
186		},
187	}
188
189	// The chain of reactors will look like this:
190	// opts.PrependReactors | defaultReactorChain | opts.AppendReactors | client.Fake.ReactionChain (default reactors for the fake clientset)
191	fullReactorChain := append(opts.PrependReactors, defaultReactorChain...)
192	fullReactorChain = append(fullReactorChain, opts.AppendReactors...)
193
194	// Prepend the reaction chain with our reactors. Important, these MUST be prepended; not appended due to how the fake clientset works by default
195	client.Fake.ReactionChain = append(fullReactorChain, client.Fake.ReactionChain...)
196	return client
197}
198
199// successfulModificationReactorFunc is a no-op that just returns the POSTed/PUTed value if present; but does nothing to edit any backing data store.
200func successfulModificationReactorFunc(action core.Action) (bool, runtime.Object, error) {
201	objAction, ok := action.(actionWithObject)
202	if ok {
203		return true, objAction.GetObject(), nil
204	}
205	return true, nil, nil
206}
207
208// logDryRunAction logs the action that was recorded by the "catch-all" (*,*) reactor and tells the user what would have happened in an user-friendly way
209func logDryRunAction(action core.Action, w io.Writer, marshalFunc MarshalFunc) {
210
211	group := action.GetResource().Group
212	if len(group) == 0 {
213		group = "core"
214	}
215	fmt.Fprintf(w, "[dryrun] Would perform action %s on resource %q in API group \"%s/%s\"\n", strings.ToUpper(action.GetVerb()), action.GetResource().Resource, group, action.GetResource().Version)
216
217	namedAction, ok := action.(actionWithName)
218	if ok {
219		fmt.Fprintf(w, "[dryrun] Resource name: %q\n", namedAction.GetName())
220	}
221
222	objAction, ok := action.(actionWithObject)
223	if ok && objAction.GetObject() != nil {
224		// Print the marshalled object with a tab indentation
225		objBytes, err := marshalFunc(objAction.GetObject(), action.GetResource().GroupVersion())
226		if err == nil {
227			fmt.Println("[dryrun] Attached object:")
228			PrintBytesWithLinePrefix(w, objBytes, "\t")
229		}
230	}
231
232	patchAction, ok := action.(core.PatchAction)
233	if ok {
234		// Replace all occurrences of \" with a simple " when printing
235		fmt.Fprintf(w, "[dryrun] Attached patch:\n\t%s\n", strings.Replace(string(patchAction.GetPatch()), `\"`, `"`, -1))
236	}
237}
238
239// PrintBytesWithLinePrefix prints objBytes to writer w with linePrefix in the beginning of every line
240func PrintBytesWithLinePrefix(w io.Writer, objBytes []byte, linePrefix string) {
241	scanner := bufio.NewScanner(bytes.NewReader(objBytes))
242	for scanner.Scan() {
243		fmt.Fprintf(w, "%s%s\n", linePrefix, scanner.Text())
244	}
245}
246