k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/util/apiclient/dryrunclient.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package apiclient 18 19 import ( 20 "bufio" 21 "bytes" 22 "fmt" 23 "io" 24 "strings" 25 26 "github.com/pkg/errors" 27 28 v1 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 clientset "k8s.io/client-go/kubernetes" 33 fakeclientset "k8s.io/client-go/kubernetes/fake" 34 core "k8s.io/client-go/testing" 35 bootstrapapi "k8s.io/cluster-bootstrap/token/api" 36 37 kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" 38 ) 39 40 // DryRunGetter is an interface that must be supplied to the NewDryRunClient function in order to construct a fully functional fake dryrun clientset 41 type DryRunGetter interface { 42 HandleGetAction(core.GetAction) (bool, runtime.Object, error) 43 HandleListAction(core.ListAction) (bool, runtime.Object, error) 44 } 45 46 // MarshalFunc takes care of converting any object to a byte array for displaying the object to the user 47 type MarshalFunc func(runtime.Object, schema.GroupVersion) ([]byte, error) 48 49 // DefaultMarshalFunc is the default MarshalFunc used; uses YAML to print objects to the user 50 func DefaultMarshalFunc(obj runtime.Object, gv schema.GroupVersion) ([]byte, error) { 51 return kubeadmutil.MarshalToYaml(obj, gv) 52 } 53 54 // DryRunClientOptions specifies options to pass to NewDryRunClientWithOpts in order to get a dryrun clientset 55 type DryRunClientOptions struct { 56 Writer io.Writer 57 Getter DryRunGetter 58 PrependReactors []core.Reactor 59 AppendReactors []core.Reactor 60 MarshalFunc MarshalFunc 61 PrintGETAndLIST bool 62 } 63 64 // GetDefaultDryRunClientOptions returns the default DryRunClientOptions values 65 func GetDefaultDryRunClientOptions(drg DryRunGetter, w io.Writer) DryRunClientOptions { 66 return DryRunClientOptions{ 67 Writer: w, 68 Getter: drg, 69 PrependReactors: []core.Reactor{}, 70 AppendReactors: []core.Reactor{}, 71 MarshalFunc: DefaultMarshalFunc, 72 PrintGETAndLIST: false, 73 } 74 } 75 76 // actionWithName is the generic interface for an action that has a name associated with it 77 // This just makes it easier to catch all actions that has a name; instead of hard-coding all request that has it associated 78 type actionWithName interface { 79 core.Action 80 GetName() string 81 } 82 83 // actionWithObject is the generic interface for an action that has an object associated with it 84 // This just makes it easier to catch all actions that has an object; instead of hard-coding all request that has it associated 85 type actionWithObject interface { 86 core.Action 87 GetObject() runtime.Object 88 } 89 90 // NewDryRunClient is a wrapper for NewDryRunClientWithOpts using some default values 91 func NewDryRunClient(drg DryRunGetter, w io.Writer) clientset.Interface { 92 return NewDryRunClientWithOpts(GetDefaultDryRunClientOptions(drg, w)) 93 } 94 95 // NewDryRunClientWithOpts returns a clientset.Interface that can be used normally for talking to the Kubernetes API. 96 // This client doesn't apply changes to the backend. The client gets GET/LIST values from the DryRunGetter implementation. 97 // This client logs all I/O to the writer w in YAML format 98 func NewDryRunClientWithOpts(opts DryRunClientOptions) clientset.Interface { 99 // Build a chain of reactors to act like a normal clientset; but log everything that is happening and don't change any state 100 client := fakeclientset.NewSimpleClientset() 101 102 // Build the chain of reactors. Order matters; first item here will be invoked first on match, then the second one will be evaluated, etc. 103 defaultReactorChain := []core.Reactor{ 104 // Log everything that happens. Default the object if it's about to be created/updated so that the logged object is representative. 105 &core.SimpleReactor{ 106 Verb: "*", 107 Resource: "*", 108 Reaction: func(action core.Action) (bool, runtime.Object, error) { 109 logDryRunAction(action, opts.Writer, opts.MarshalFunc) 110 111 return false, nil, nil 112 }, 113 }, 114 // Let the DryRunGetter implementation take care of all GET requests. 115 // The DryRunGetter implementation may call a real API Server behind the scenes or just fake everything 116 &core.SimpleReactor{ 117 Verb: "get", 118 Resource: "*", 119 Reaction: func(action core.Action) (bool, runtime.Object, error) { 120 getAction, ok := action.(core.GetAction) 121 if !ok { 122 // If the GetAction cast fails, this could be an ActionImpl with a "get" verb. 123 // Such actions could be invoked from any of the fake discovery calls, such as ServerVersion(). 124 // Attempt the cast to ActionImpl and construct a GetActionImpl from it. 125 actionImpl, ok := action.(core.ActionImpl) 126 if ok { 127 getAction = core.GetActionImpl{ActionImpl: actionImpl} 128 } else { 129 // something's wrong, we can't handle this event 130 return true, nil, errors.New("can't cast get reactor event action object to GetAction interface") 131 } 132 } 133 handled, obj, err := opts.Getter.HandleGetAction(getAction) 134 135 if opts.PrintGETAndLIST { 136 // Print the marshalled object format with one tab indentation 137 objBytes, err := opts.MarshalFunc(obj, action.GetResource().GroupVersion()) 138 if err == nil { 139 fmt.Println("[dryrun] Returning faked GET response:") 140 PrintBytesWithLinePrefix(opts.Writer, objBytes, "\t") 141 } 142 } 143 144 return handled, obj, err 145 }, 146 }, 147 // Let the DryRunGetter implementation take care of all GET requests. 148 // The DryRunGetter implementation may call a real API Server behind the scenes or just fake everything 149 &core.SimpleReactor{ 150 Verb: "list", 151 Resource: "*", 152 Reaction: func(action core.Action) (bool, runtime.Object, error) { 153 listAction, ok := action.(core.ListAction) 154 if !ok { 155 // something's wrong, we can't handle this event 156 return true, nil, errors.New("can't cast list reactor event action object to ListAction interface") 157 } 158 handled, objs, err := opts.Getter.HandleListAction(listAction) 159 160 if opts.PrintGETAndLIST { 161 // Print the marshalled object format with one tab indentation 162 objBytes, err := opts.MarshalFunc(objs, action.GetResource().GroupVersion()) 163 if err == nil { 164 fmt.Println("[dryrun] Returning faked LIST response:") 165 PrintBytesWithLinePrefix(opts.Writer, objBytes, "\t") 166 } 167 } 168 169 return handled, objs, err 170 }, 171 }, 172 // For the verbs that modify anything on the server; just return the object if present and exit successfully 173 &core.SimpleReactor{ 174 Verb: "create", 175 Resource: "*", 176 Reaction: func(action core.Action) (bool, runtime.Object, error) { 177 objAction, ok := action.(actionWithObject) 178 if obj := objAction.GetObject(); ok && obj != nil { 179 if secret, ok := obj.(*v1.Secret); ok { 180 if secret.Namespace == metav1.NamespaceSystem && strings.HasPrefix(secret.Name, bootstrapapi.BootstrapTokenSecretPrefix) { 181 // bypass bootstrap token secret create event so that it can be persisted to the backing data store 182 // this secret should be readable during the uploadcerts init phase if it has already been created 183 return false, nil, nil 184 } 185 } 186 } 187 return successfulModificationReactorFunc(action) 188 }, 189 }, 190 &core.SimpleReactor{ 191 Verb: "update", 192 Resource: "*", 193 Reaction: successfulModificationReactorFunc, 194 }, 195 &core.SimpleReactor{ 196 Verb: "delete", 197 Resource: "*", 198 Reaction: successfulModificationReactorFunc, 199 }, 200 &core.SimpleReactor{ 201 Verb: "delete-collection", 202 Resource: "*", 203 Reaction: successfulModificationReactorFunc, 204 }, 205 &core.SimpleReactor{ 206 Verb: "patch", 207 Resource: "*", 208 Reaction: successfulModificationReactorFunc, 209 }, 210 } 211 212 // The chain of reactors will look like this: 213 // opts.PrependReactors | defaultReactorChain | opts.AppendReactors | client.Fake.ReactionChain (default reactors for the fake clientset) 214 fullReactorChain := append(opts.PrependReactors, defaultReactorChain...) 215 fullReactorChain = append(fullReactorChain, opts.AppendReactors...) 216 217 // Prepend the reaction chain with our reactors. Important, these MUST be prepended; not appended due to how the fake clientset works by default 218 client.Fake.ReactionChain = append(fullReactorChain, client.Fake.ReactionChain...) 219 return client 220 } 221 222 // successfulModificationReactorFunc is a no-op that just returns the POSTed/PUTed value if present; but does nothing to edit any backing data store. 223 func successfulModificationReactorFunc(action core.Action) (bool, runtime.Object, error) { 224 objAction, ok := action.(actionWithObject) 225 if ok { 226 return true, objAction.GetObject(), nil 227 } 228 return true, nil, nil 229 } 230 231 // 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 232 func logDryRunAction(action core.Action, w io.Writer, marshalFunc MarshalFunc) { 233 234 group := action.GetResource().Group 235 if len(group) == 0 { 236 group = "core" 237 } 238 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) 239 240 namedAction, ok := action.(actionWithName) 241 if ok { 242 fmt.Fprintf(w, "[dryrun] Resource name: %q\n", namedAction.GetName()) 243 } 244 245 objAction, ok := action.(actionWithObject) 246 if ok && objAction.GetObject() != nil { 247 // Print the marshalled object with a tab indentation 248 objBytes, err := marshalFunc(objAction.GetObject(), action.GetResource().GroupVersion()) 249 if err == nil { 250 fmt.Println("[dryrun] Attached object:") 251 PrintBytesWithLinePrefix(w, objBytes, "\t") 252 } 253 } 254 255 patchAction, ok := action.(core.PatchAction) 256 if ok { 257 // Replace all occurrences of \" with a simple " when printing 258 fmt.Fprintf(w, "[dryrun] Attached patch:\n\t%s\n", strings.Replace(string(patchAction.GetPatch()), `\"`, `"`, -1)) 259 } 260 } 261 262 // PrintBytesWithLinePrefix prints objBytes to writer w with linePrefix in the beginning of every line 263 func PrintBytesWithLinePrefix(w io.Writer, objBytes []byte, linePrefix string) { 264 scanner := bufio.NewScanner(bytes.NewReader(objBytes)) 265 for scanner.Scan() { 266 fmt.Fprintf(w, "%s%s\n", linePrefix, scanner.Text()) 267 } 268 }