Merge pull request #50631 from luxas/kubeadm_dryrun_apiclient

Automatic merge from submit-queue (batch tested with PRs 47896, 50678, 50620, 50631, 51005)

kubeadm: Adds dry-run support for kubeadm using the `--dry-run` option

**What this PR does / why we need it**:

Adds dry-run support to kubeadm by creating a fake clientset that can get totally fake values (like in the init case), or delegate GETs/LISTs to a real API server but discard all edits like POST/PUT/PATCH/DELETE

**Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #

fixes: https://github.com/kubernetes/kubeadm/issues/389

**Special notes for your reviewer**:

This PR depends on https://github.com/kubernetes/kubernetes/pull/50626, first three commits are from there
This PR is a dependency for https://github.com/kubernetes/kubernetes/pull/48899 (kubeadm upgrades)

I have some small things to fixup and I'll yet write unit tests, but PTAL if you think this is going in the right direction

**Release note**:

```release-note
kubeadm: Adds dry-run support for kubeadm using the `--dry-run` option
```
cc @kubernetes/sig-cluster-lifecycle-pr-reviews @kubernetes/sig-api-machinery-pr-reviews
This commit is contained in:
Kubernetes Submit Queue 2017-08-21 08:26:26 -07:00 committed by GitHub
commit d852b8aad9
12 changed files with 863 additions and 17 deletions

View File

@ -284,7 +284,7 @@ func ValidateMixedArguments(flag *pflag.FlagSet) error {
mixedInvalidFlags := []string{}
flag.Visit(func(f *pflag.Flag) {
if f.Name == "config" || strings.HasPrefix(f.Name, "skip-") {
if f.Name == "config" || strings.HasPrefix(f.Name, "skip-") || f.Name == "dry-run" {
// "--skip-*" flags can be set with --config
return
}

View File

@ -20,6 +20,7 @@ import (
"fmt"
"io"
"io/ioutil"
"os"
"text/template"
"time"
@ -27,6 +28,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime"
clientset "k8s.io/client-go/kubernetes"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1"
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/validation"
@ -85,6 +87,7 @@ func NewCmdInit(out io.Writer) *cobra.Command {
var cfgPath string
var skipPreFlight bool
var skipTokenPrint bool
var dryRun bool
cmd := &cobra.Command{
Use: "init",
Short: "Run this in order to set up the Kubernetes master",
@ -93,7 +96,7 @@ func NewCmdInit(out io.Writer) *cobra.Command {
internalcfg := &kubeadmapi.MasterConfiguration{}
api.Scheme.Convert(cfg, internalcfg, nil)
i, err := NewInit(cfgPath, internalcfg, skipPreFlight, skipTokenPrint)
i, err := NewInit(cfgPath, internalcfg, skipPreFlight, skipTokenPrint, dryRun)
kubeadmutil.CheckErr(err)
kubeadmutil.CheckErr(i.Validate(cmd))
@ -155,6 +158,11 @@ func NewCmdInit(out io.Writer) *cobra.Command {
&skipTokenPrint, "skip-token-print", skipTokenPrint,
"Skip printing of the default bootstrap token generated by 'kubeadm init'",
)
// Note: All flags that are not bound to the cfg object should be whitelisted in cmd/kubeadm/app/apis/kubeadm/validation/validation.go
cmd.PersistentFlags().BoolVar(
&dryRun, "dry-run", dryRun,
"Don't apply any changes; just output what would be done",
)
cmd.PersistentFlags().StringVar(
&cfg.Token, "token", cfg.Token,
@ -167,7 +175,7 @@ func NewCmdInit(out io.Writer) *cobra.Command {
return cmd
}
func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration, skipPreFlight, skipTokenPrint bool) (*Init, error) {
func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration, skipPreFlight, skipTokenPrint, dryRun bool) (*Init, error) {
fmt.Println("[kubeadm] WARNING: kubeadm is in beta, please do not use it for production clusters.")
@ -209,12 +217,13 @@ func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration, skipPreFlight,
fmt.Println("[preflight] Skipping pre-flight checks")
}
return &Init{cfg: cfg, skipTokenPrint: skipTokenPrint}, nil
return &Init{cfg: cfg, skipTokenPrint: skipTokenPrint, dryRun: dryRun}, nil
}
type Init struct {
cfg *kubeadmapi.MasterConfiguration
skipTokenPrint bool
dryRun bool
}
// Validate validates configuration passed to "kubeadm init"
@ -255,16 +264,11 @@ func (i *Init) Run(out io.Writer) error {
}
}
client, err := kubeconfigutil.ClientSetFromFile(kubeadmconstants.GetAdminKubeConfigPath())
client, err := createClientsetAndOptionallyWaitForReady(i.cfg, i.dryRun)
if err != nil {
return err
}
fmt.Printf("[init] Waiting for the kubelet to boot up the control plane as Static Pods from directory %q\n", kubeadmconstants.GetStaticPodDirectory())
if err := apiclient.WaitForAPI(client, 30*time.Minute); err != nil {
return err
}
// PHASE 4: Mark the master with the right label/taint
if err := markmasterphase.MarkMaster(client, i.cfg.NodeName); err != nil {
return err
@ -348,3 +352,25 @@ func (i *Init) Run(out io.Writer) error {
return initDoneTempl.Execute(out, ctx)
}
func createClientsetAndOptionallyWaitForReady(cfg *kubeadmapi.MasterConfiguration, dryRun bool) (clientset.Interface, error) {
if dryRun {
// If we're dry-running; we should create a faked client that answers some GETs in order to be able to do the full init flow and just logs the rest of requests
dryRunGetter := apiclient.NewInitDryRunGetter(cfg.NodeName, cfg.Networking.ServiceSubnet)
return apiclient.NewDryRunClient(dryRunGetter, os.Stdout), nil
}
// If we're acting for real,we should create a connection to the API server and wait for it to come up
client, err := kubeconfigutil.ClientSetFromFile(kubeadmconstants.GetAdminKubeConfigPath())
if err != nil {
return nil, err
}
fmt.Printf("[init] Waiting for the kubelet to boot up the control plane as Static Pods from directory %q\n", kubeadmconstants.GetStaticPodDirectory())
// TODO: Adjust this timeout or start polling the kubelet API
// TODO: Make this timeout more realistic when we do create some more complex logic about the interaction with the kubelet
if err := apiclient.WaitForAPI(client, 30*time.Minute); err != nil {
return nil, err
}
return client, nil
}

View File

@ -17,8 +17,6 @@ limitations under the License.
package phases
import (
"fmt"
"github.com/spf13/cobra"
markmasterphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/markmaster"
@ -41,8 +39,6 @@ func NewCmdMarkMaster() *cobra.Command {
kubeadmutil.CheckErr(err)
nodeName := args[0]
fmt.Printf("[markmaster] Will mark node %s as master by adding a label and a taint\n", nodeName)
return markmasterphase.MarkMaster(client, nodeName)
},
}

View File

@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"io"
"os"
"sort"
"strings"
"text/tabwriter"
@ -36,6 +37,7 @@ import (
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
tokenphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/node"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig"
tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token"
"k8s.io/kubernetes/pkg/api"
@ -46,6 +48,7 @@ import (
func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command {
var kubeConfigFile string
var dryRun bool
tokenCmd := &cobra.Command{
Use: "token",
Short: "Manage bootstrap tokens.",
@ -86,6 +89,8 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command {
tokenCmd.PersistentFlags().StringVar(&kubeConfigFile,
"kubeconfig", "/etc/kubernetes/admin.conf", "The KubeConfig file to use for talking to the cluster")
tokenCmd.PersistentFlags().BoolVar(&dryRun,
"dry-run", dryRun, "Whether to enable dry-run mode or not")
var usages []string
var tokenDuration time.Duration
@ -106,7 +111,7 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command {
if len(args) != 0 {
token = args[0]
}
client, err := kubeconfigutil.ClientSetFromFile(kubeConfigFile)
client, err := getClientset(kubeConfigFile, dryRun)
kubeadmutil.CheckErr(err)
// TODO: remove this warning in 1.9
@ -136,7 +141,7 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command {
This command will list all Bootstrap Tokens for you.
`),
Run: func(tokenCmd *cobra.Command, args []string) {
client, err := kubeconfigutil.ClientSetFromFile(kubeConfigFile)
client, err := getClientset(kubeConfigFile, dryRun)
kubeadmutil.CheckErr(err)
err = RunListTokens(out, errW, client)
@ -158,7 +163,7 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command {
if len(args) < 1 {
kubeadmutil.CheckErr(fmt.Errorf("missing subcommand; 'token delete' is missing token of form [%q]", tokenutil.TokenIDRegexpString))
}
client, err := kubeconfigutil.ClientSetFromFile(kubeConfigFile)
client, err := getClientset(kubeConfigFile, dryRun)
kubeadmutil.CheckErr(err)
err = RunDeleteToken(out, client, args[0])
@ -338,3 +343,15 @@ func getSecretString(secret *v1.Secret, key string) string {
}
return ""
}
func getClientset(file string, dryRun bool) (clientset.Interface, error) {
if dryRun {
dryRunGetter, err := apiclient.NewClientBackedDryRunGetterFromKubeconfig(file)
if err != nil {
return nil, err
}
return apiclient.NewDryRunClient(dryRunGetter, os.Stdout), nil
}
client, err := kubeconfigutil.ClientSetFromFile(file)
return client, err
}

View File

@ -34,6 +34,8 @@ import (
// MarkMaster taints the master and sets the master label
func MarkMaster(client clientset.Interface, masterName string) error {
fmt.Printf("[markmaster] Will mark node %s as master by adding a label and a taint\n", masterName)
// Loop on every falsy return. Return with an error if raised. Exit successfully if true is returned.
return wait.Poll(kubeadmconstants.APICallRetryInterval, kubeadmconstants.MarkMasterTimeout, func() (bool, error) {
// First get the node object

View File

@ -3,23 +3,37 @@ package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"clientbacked_dryrun.go",
"dryrunclient.go",
"idempotency.go",
"init_dryrun.go",
"wait.go",
],
deps = [
"//cmd/kubeadm/app/constants:go_default_library",
"//pkg/registry/core/service/ipallocator:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/extensions/v1beta1:go_default_library",
"//vendor/k8s.io/api/rbac/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/scheme:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
],
)
@ -35,3 +49,19 @@ filegroup(
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = [
"dryrunclient_test.go",
"init_dryrun_test.go",
],
library = ":go_default_library",
deps = [
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/rbac/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library",
],
)

View File

@ -0,0 +1,139 @@
/*
Copyright 2017 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 apiclient
import (
"encoding/json"
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
kuberuntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic"
clientsetscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
core "k8s.io/client-go/testing"
"k8s.io/client-go/tools/clientcmd"
)
// ClientBackedDryRunGetter implements the DryRunGetter interface for use in NewDryRunClient() and proxies all GET and LIST requests to the backing API server reachable via rest.Config
type ClientBackedDryRunGetter struct {
baseConfig *rest.Config
dynClientPool dynamic.ClientPool
}
// InitDryRunGetter should implement the DryRunGetter interface
var _ DryRunGetter = &ClientBackedDryRunGetter{}
// NewClientBackedDryRunGetter creates a new ClientBackedDryRunGetter instance based on the rest.Config object
func NewClientBackedDryRunGetter(config *rest.Config) *ClientBackedDryRunGetter {
return &ClientBackedDryRunGetter{
baseConfig: config,
dynClientPool: dynamic.NewDynamicClientPool(config),
}
}
// NewClientBackedDryRunGetterFromKubeconfig creates a new ClientBackedDryRunGetter instance from the given KubeConfig file
func NewClientBackedDryRunGetterFromKubeconfig(file string) (*ClientBackedDryRunGetter, error) {
config, err := clientcmd.LoadFromFile(file)
if err != nil {
return nil, fmt.Errorf("failed to load kubeconfig: %v", err)
}
clientConfig, err := clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{}).ClientConfig()
if err != nil {
return nil, fmt.Errorf("failed to create API client configuration from kubeconfig: %v", err)
}
return NewClientBackedDryRunGetter(clientConfig), nil
}
// HandleGetAction handles GET actions to the dryrun clientset this interface supports
func (clg *ClientBackedDryRunGetter) HandleGetAction(action core.GetAction) (bool, runtime.Object, error) {
rc, err := clg.actionToResourceClient(action)
if err != nil {
return true, nil, err
}
unversionedObj, err := rc.Get(action.GetName(), metav1.GetOptions{})
// If the unversioned object does not have .apiVersion; the inner object is probably nil
if len(unversionedObj.GetAPIVersion()) == 0 {
return true, nil, apierrors.NewNotFound(action.GetResource().GroupResource(), action.GetName())
}
newObj, err := decodeUnversionedIntoAPIObject(action, unversionedObj)
if err != nil {
fmt.Printf("error after decode: %v %v\n", unversionedObj, err)
return true, nil, err
}
return true, newObj, err
}
// HandleListAction handles LIST actions to the dryrun clientset this interface supports
func (clg *ClientBackedDryRunGetter) HandleListAction(action core.ListAction) (bool, runtime.Object, error) {
rc, err := clg.actionToResourceClient(action)
if err != nil {
return true, nil, err
}
listOpts := metav1.ListOptions{
LabelSelector: action.GetListRestrictions().Labels.String(),
FieldSelector: action.GetListRestrictions().Fields.String(),
}
unversionedList, err := rc.List(listOpts)
// If the runtime.Object here is nil, we should return successfully with no result
if unversionedList == nil {
return true, unversionedList, nil
}
newObj, err := decodeUnversionedIntoAPIObject(action, unversionedList)
if err != nil {
fmt.Printf("error after decode: %v %v\n", unversionedList, err)
return true, nil, err
}
return true, newObj, err
}
// actionToResourceClient returns the ResourceInterface for the given action
// First; the function gets the right API group interface from the resource type. The API group struct behind the interface
// returned may be cached in the dynamic client pool. Then, an APIResource object is constructed so that it can be passed to
// dynamic.Interface's Resource() function, which will give us the final ResourceInterface to query
func (clg *ClientBackedDryRunGetter) actionToResourceClient(action core.Action) (dynamic.ResourceInterface, error) {
dynIface, err := clg.dynClientPool.ClientForGroupVersionResource(action.GetResource())
if err != nil {
return nil, err
}
apiResource := &metav1.APIResource{
Name: action.GetResource().Resource,
Namespaced: action.GetNamespace() != "",
}
return dynIface.Resource(apiResource, action.GetNamespace()), nil
}
// decodeUnversionedIntoAPIObject converts the *unversioned.Unversioned object returned from the dynamic client
// to bytes; and then decodes it back _to an external api version (k8s.io/api vs k8s.io/kubernetes/pkg/api*)_ using the normal API machinery
func decodeUnversionedIntoAPIObject(action core.Action, unversionedObj runtime.Object) (runtime.Object, error) {
objBytes, err := json.Marshal(unversionedObj)
if err != nil {
return nil, err
}
newObj, err := kuberuntime.Decode(clientsetscheme.Codecs.UniversalDecoder(action.GetResource().GroupVersion()), objBytes)
if err != nil {
return nil, err
}
return newObj, nil
}

View File

@ -0,0 +1,237 @@
/*
Copyright 2017 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 apiclient
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
"github.com/ghodss/yaml"
"k8s.io/apimachinery/pkg/runtime"
clientset "k8s.io/client-go/kubernetes"
fakeclientset "k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
)
// DryRunGetter is an interface that must be supplied to the NewDryRunClient function in order to contstruct a fully functional fake dryrun clientset
type DryRunGetter interface {
HandleGetAction(core.GetAction) (bool, runtime.Object, error)
HandleListAction(core.ListAction) (bool, runtime.Object, error)
}
// MarshalFunc takes care of converting any object to a byte array for displaying the object to the user
type MarshalFunc func(runtime.Object) ([]byte, error)
// DefaultMarshalFunc is the default MarshalFunc used; uses YAML to print objects to the user
func DefaultMarshalFunc(obj runtime.Object) ([]byte, error) {
b, err := yaml.Marshal(obj)
return b, err
}
// DryRunClientOptions specifies options to pass to NewDryRunClientWithOpts in order to get a dryrun clientset
type DryRunClientOptions struct {
Writer io.Writer
Getter DryRunGetter
PrependReactors []core.Reactor
AppendReactors []core.Reactor
MarshalFunc MarshalFunc
PrintGETAndLIST bool
}
// actionWithName is the generic interface for an action that has a name associated with it
// This just makes it easier to catch all actions that has a name; instead of hard-coding all request that has it associated
type actionWithName interface {
core.Action
GetName() string
}
// actionWithObject is the generic interface for an action that has an object associated with it
// This just makes it easier to catch all actions that has an object; instead of hard-coding all request that has it associated
type actionWithObject interface {
core.Action
GetObject() runtime.Object
}
// NewDryRunClient is a wrapper for NewDryRunClientWithOpts using some default values
func NewDryRunClient(drg DryRunGetter, w io.Writer) clientset.Interface {
return NewDryRunClientWithOpts(DryRunClientOptions{
Writer: w,
Getter: drg,
PrependReactors: []core.Reactor{},
AppendReactors: []core.Reactor{},
MarshalFunc: DefaultMarshalFunc,
PrintGETAndLIST: false,
})
}
// NewDryRunClientWithOpts returns a clientset.Interface that can be used normally for talking to the Kubernetes API.
// This client doesn't apply changes to the backend. The client gets GET/LIST values from the DryRunGetter implementation.
// This client logs all I/O to the writer w in YAML format
func NewDryRunClientWithOpts(opts DryRunClientOptions) clientset.Interface {
// Build a chain of reactors to act like a normal clientset; but log everything's that happening and don't change any state
client := fakeclientset.NewSimpleClientset()
// Build the chain of reactors. Order matters; first item here will be invoked first on match, then the second one will be evaluted, etc.
defaultReactorChain := []core.Reactor{
// Log everything that happens. Default the object if it's about to be created/updated so that the logged object is representative.
&core.SimpleReactor{
Verb: "*",
Resource: "*",
Reaction: func(action core.Action) (bool, runtime.Object, error) {
logDryRunAction(action, opts.Writer, opts.MarshalFunc)
return false, nil, nil
},
},
// Let the DryRunGetter implementation take care of all GET requests.
// The DryRunGetter implementation may call a real API Server behind the scenes or just fake everything
&core.SimpleReactor{
Verb: "get",
Resource: "*",
Reaction: func(action core.Action) (bool, runtime.Object, error) {
getAction, ok := action.(core.GetAction)
if !ok {
// something's wrong, we can't handle this event
return true, nil, fmt.Errorf("can't cast get reactor event action object to GetAction interface")
}
handled, obj, err := opts.Getter.HandleGetAction(getAction)
if opts.PrintGETAndLIST {
// Print the marshalled object format with one tab indentation
objBytes, err := opts.MarshalFunc(obj)
if err == nil {
fmt.Println("[dryrun] Returning faked GET response:")
printBytesWithLinePrefix(opts.Writer, objBytes, "\t")
}
}
return handled, obj, err
},
},
// Let the DryRunGetter implementation take care of all GET requests.
// The DryRunGetter implementation may call a real API Server behind the scenes or just fake everything
&core.SimpleReactor{
Verb: "list",
Resource: "*",
Reaction: func(action core.Action) (bool, runtime.Object, error) {
listAction, ok := action.(core.ListAction)
if !ok {
// something's wrong, we can't handle this event
return true, nil, fmt.Errorf("can't cast list reactor event action object to ListAction interface")
}
handled, objs, err := opts.Getter.HandleListAction(listAction)
if opts.PrintGETAndLIST {
// Print the marshalled object format with one tab indentation
objBytes, err := opts.MarshalFunc(objs)
if err == nil {
fmt.Println("[dryrun] Returning faked LIST response:")
printBytesWithLinePrefix(opts.Writer, objBytes, "\t")
}
}
return handled, objs, err
},
},
// For the verbs that modify anything on the server; just return the object if present and exit successfully
&core.SimpleReactor{
Verb: "create",
Resource: "*",
Reaction: successfulModificationReactorFunc,
},
&core.SimpleReactor{
Verb: "update",
Resource: "*",
Reaction: successfulModificationReactorFunc,
},
&core.SimpleReactor{
Verb: "delete",
Resource: "*",
Reaction: successfulModificationReactorFunc,
},
&core.SimpleReactor{
Verb: "delete-collection",
Resource: "*",
Reaction: successfulModificationReactorFunc,
},
&core.SimpleReactor{
Verb: "patch",
Resource: "*",
Reaction: successfulModificationReactorFunc,
},
}
// The chain of reactors will look like this:
// opts.PrependReactors | defaultReactorChain | opts.AppendReactors | client.Fake.ReactionChain (default reactors for the fake clientset)
fullReactorChain := append(opts.PrependReactors, defaultReactorChain...)
fullReactorChain = append(fullReactorChain, opts.AppendReactors...)
// Prepend the reaction chain with our reactors. Important, these MUST be prepended; not appended due to how the fake clientset works by default
client.Fake.ReactionChain = append(fullReactorChain, client.Fake.ReactionChain...)
return client
}
// successfulModificationReactorFunc is a no-op that just returns the POSTed/PUTed value if present; but does nothing to edit any backing data store.
func successfulModificationReactorFunc(action core.Action) (bool, runtime.Object, error) {
objAction, ok := action.(actionWithObject)
if ok {
return true, objAction.GetObject(), nil
}
return true, nil, nil
}
// 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
func logDryRunAction(action core.Action, w io.Writer, marshalFunc MarshalFunc) {
group := action.GetResource().Group
if len(group) == 0 {
group = "core"
}
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)
namedAction, ok := action.(actionWithName)
if ok {
fmt.Fprintf(w, "[dryrun] Resource name: %q\n", namedAction.GetName())
}
objAction, ok := action.(actionWithObject)
if ok && objAction.GetObject() != nil {
// Print the marshalled object with a tab indentation
objBytes, err := marshalFunc(objAction.GetObject())
if err == nil {
fmt.Println("[dryrun] Attached object:")
printBytesWithLinePrefix(w, objBytes, "\t")
}
}
patchAction, ok := action.(core.PatchAction)
if ok {
// Replace all occurences of \" with a simple " when printing
fmt.Fprintf(w, "[dryrun] Attached patch:\n\t%s\n", strings.Replace(string(patchAction.GetPatch()), `\"`, `"`, -1))
}
}
// printBytesWithLinePrefix prints objBytes to writer w with linePrefix in the beginning of every line
func printBytesWithLinePrefix(w io.Writer, objBytes []byte, linePrefix string) {
scanner := bufio.NewScanner(bytes.NewReader(objBytes))
for scanner.Scan() {
fmt.Fprintf(w, "%s%s\n", linePrefix, scanner.Text())
}
}

View File

@ -0,0 +1,100 @@
/*
Copyright 2017 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 apiclient
import (
"bytes"
"testing"
"k8s.io/api/core/v1"
rbac "k8s.io/api/rbac/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
core "k8s.io/client-go/testing"
)
func TestLogDryRunAction(t *testing.T) {
var tests = []struct {
action core.Action
expectedBytes []byte
buf *bytes.Buffer
}{
{
action: core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "services"}, "default", "kubernetes"),
expectedBytes: []byte(`[dryrun] Would perform action GET on resource "services" in API group "core/v1"
[dryrun] Resource name: "kubernetes"
`),
},
{
action: core.NewRootGetAction(schema.GroupVersionResource{Group: rbac.GroupName, Version: rbac.SchemeGroupVersion.Version, Resource: "clusterrolebindings"}, "system:node"),
expectedBytes: []byte(`[dryrun] Would perform action GET on resource "clusterrolebindings" in API group "rbac.authorization.k8s.io/v1beta1"
[dryrun] Resource name: "system:node"
`),
},
{
action: core.NewListAction(schema.GroupVersionResource{Version: "v1", Resource: "services"}, schema.GroupVersionKind{Version: "v1", Kind: "Service"}, "default", metav1.ListOptions{}),
expectedBytes: []byte(`[dryrun] Would perform action LIST on resource "services" in API group "core/v1"
`),
},
{
action: core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "services"}, "default", &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: v1.ServiceSpec{
ClusterIP: "1.1.1.1",
},
}),
expectedBytes: []byte(`[dryrun] Would perform action CREATE on resource "services" in API group "core/v1"
metadata:
creationTimestamp: null
name: foo
spec:
clusterIP: 1.1.1.1
status:
loadBalancer: {}
`),
},
{
action: core.NewPatchAction(schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "default", "my-node", []byte(`{"spec":{"taints":[{"key": "foo", "value": "bar"}]}}`)),
expectedBytes: []byte(`[dryrun] Would perform action PATCH on resource "nodes" in API group "core/v1"
[dryrun] Resource name: "my-node"
[dryrun] Attached patch:
{"spec":{"taints":[{"key": "foo", "value": "bar"}]}}
`),
},
{
action: core.NewDeleteAction(schema.GroupVersionResource{Version: "v1", Resource: "pods"}, "default", "my-pod"),
expectedBytes: []byte(`[dryrun] Would perform action DELETE on resource "pods" in API group "core/v1"
[dryrun] Resource name: "my-pod"
`),
},
}
for _, rt := range tests {
rt.buf = bytes.NewBufferString("")
logDryRunAction(rt.action, rt.buf, DefaultMarshalFunc)
actualBytes := rt.buf.Bytes()
if !bytes.Equal(actualBytes, rt.expectedBytes) {
t.Errorf(
"failed LogDryRunAction:\n\texpected bytes: %q\n\t actual: %q",
rt.expectedBytes,
actualBytes,
)
}
}
}

View File

@ -0,0 +1,162 @@
/*
Copyright 2017 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 apiclient
import (
"fmt"
"net"
"strings"
"k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
core "k8s.io/client-go/testing"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/pkg/registry/core/service/ipallocator"
)
// InitDryRunGetter implements the DryRunGetter interface and can be used to GET/LIST values in the dryrun fake clientset
// Need to handle these routes in a special manner:
// - GET /default/services/kubernetes -- must return a valid Service
// - GET /clusterrolebindings/system:nodes -- can safely return a NotFound error
// - GET /kube-system/secrets/bootstrap-token-* -- can safely return a NotFound error
// - GET /nodes/<node-name> -- must return a valid Node
// - ...all other, unknown GETs/LISTs will be logged
type InitDryRunGetter struct {
masterName string
serviceSubnet string
}
// InitDryRunGetter should implement the DryRunGetter interface
var _ DryRunGetter = &InitDryRunGetter{}
// NewInitDryRunGetter creates a new instance of the InitDryRunGetter struct
func NewInitDryRunGetter(masterName string, serviceSubnet string) *InitDryRunGetter {
return &InitDryRunGetter{
masterName: masterName,
serviceSubnet: serviceSubnet,
}
}
// HandleGetAction handles GET actions to the dryrun clientset this interface supports
func (idr *InitDryRunGetter) HandleGetAction(action core.GetAction) (bool, runtime.Object, error) {
funcs := []func(core.GetAction) (bool, runtime.Object, error){
idr.handleKubernetesService,
idr.handleGetNode,
idr.handleSystemNodesClusterRoleBinding,
idr.handleGetBootstrapToken,
}
for _, f := range funcs {
handled, obj, err := f(action)
if handled {
return handled, obj, err
}
}
return false, nil, nil
}
// HandleListAction handles GET actions to the dryrun clientset this interface supports.
// Currently there are no known LIST calls during kubeadm init this code has to take care of.
func (idr *InitDryRunGetter) HandleListAction(action core.ListAction) (bool, runtime.Object, error) {
return false, nil, nil
}
// handleKubernetesService returns a faked Kubernetes service in order to be able to continue running kubeadm init.
// The kube-dns addon code GETs the kubernetes service in order to extract the service subnet
func (idr *InitDryRunGetter) handleKubernetesService(action core.GetAction) (bool, runtime.Object, error) {
if action.GetName() != "kubernetes" || action.GetNamespace() != metav1.NamespaceDefault || action.GetResource().Resource != "services" {
// We can't handle this event
return false, nil, nil
}
_, svcSubnet, err := net.ParseCIDR(idr.serviceSubnet)
if err != nil {
return true, nil, fmt.Errorf("error parsing CIDR %q: %v", idr.serviceSubnet, err)
}
internalAPIServerVirtualIP, err := ipallocator.GetIndexedIP(svcSubnet, 1)
if err != nil {
return true, nil, fmt.Errorf("unable to get first IP address from the given CIDR (%s): %v", svcSubnet.String(), err)
}
// The only used field of this Service object is the ClusterIP, which kube-dns uses to calculate its own IP
return true, &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "kubernetes",
Namespace: metav1.NamespaceDefault,
Labels: map[string]string{
"component": "apiserver",
"provider": "kubernetes",
},
},
Spec: v1.ServiceSpec{
ClusterIP: internalAPIServerVirtualIP.String(),
Ports: []v1.ServicePort{
{
Name: "https",
Port: 443,
TargetPort: intstr.FromInt(6443),
},
},
},
}, nil
}
// handleGetNode returns a fake node object for the purpose of moving kubeadm init forwards.
func (idr *InitDryRunGetter) handleGetNode(action core.GetAction) (bool, runtime.Object, error) {
if action.GetName() != idr.masterName || action.GetResource().Resource != "nodes" {
// We can't handle this event
return false, nil, nil
}
return true, &v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: idr.masterName,
Labels: map[string]string{
"kubernetes.io/hostname": idr.masterName,
},
},
Spec: v1.NodeSpec{
ExternalID: idr.masterName,
},
}, nil
}
// handleSystemNodesClusterRoleBinding handles the GET call to the system:nodes clusterrolebinding
func (idr *InitDryRunGetter) handleSystemNodesClusterRoleBinding(action core.GetAction) (bool, runtime.Object, error) {
if action.GetName() != constants.NodesClusterRoleBinding || action.GetResource().Resource != "clusterrolebindings" {
// We can't handle this event
return false, nil, nil
}
// We can safely return a NotFound error here as the code will just proceed normally and don't care about modifying this clusterrolebinding
// This can only happen on an upgrade; and in that case the ClientBackedDryRunGetter impl will be used
return true, nil, apierrors.NewNotFound(action.GetResource().GroupResource(), "clusterrolebinding not found")
}
// handleGetBootstrapToken handles the case where kubeadm init creates the default token; and the token code GETs the
// bootstrap token secret first in order to check if it already exists
func (idr *InitDryRunGetter) handleGetBootstrapToken(action core.GetAction) (bool, runtime.Object, error) {
if !strings.HasPrefix(action.GetName(), "bootstrap-token-") || action.GetNamespace() != metav1.NamespaceSystem || action.GetResource().Resource != "secrets" {
// We can't handle this event
return false, nil, nil
}
// We can safely return a NotFound error here as the code will just proceed normally and create the Bootstrap Token
return true, nil, apierrors.NewNotFound(action.GetResource().GroupResource(), "secret not found")
}

View File

@ -0,0 +1,126 @@
/*
Copyright 2017 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 apiclient
import (
"bytes"
"encoding/json"
"testing"
rbac "k8s.io/api/rbac/v1beta1"
"k8s.io/apimachinery/pkg/runtime/schema"
core "k8s.io/client-go/testing"
)
func TestHandleGetAction(t *testing.T) {
masterName := "master-foo"
serviceSubnet := "10.96.0.1/12"
idr := NewInitDryRunGetter(masterName, serviceSubnet)
var tests = []struct {
action core.GetActionImpl
expectedHandled bool
expectedObjectJSON []byte
expectedErr bool
}{
{
action: core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "services"}, "default", "kubernetes"),
expectedHandled: true,
expectedObjectJSON: []byte(`{"metadata":{"name":"kubernetes","namespace":"default","creationTimestamp":null,"labels":{"component":"apiserver","provider":"kubernetes"}},"spec":{"ports":[{"name":"https","port":443,"targetPort":6443}],"clusterIP":"10.96.0.1"},"status":{"loadBalancer":{}}}`),
expectedErr: false,
},
{
action: core.NewRootGetAction(schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, masterName),
expectedHandled: true,
expectedObjectJSON: []byte(`{"metadata":{"name":"master-foo","creationTimestamp":null,"labels":{"kubernetes.io/hostname":"master-foo"}},"spec":{"externalID":"master-foo"},"status":{"daemonEndpoints":{"kubeletEndpoint":{"Port":0}},"nodeInfo":{"machineID":"","systemUUID":"","bootID":"","kernelVersion":"","osImage":"","containerRuntimeVersion":"","kubeletVersion":"","kubeProxyVersion":"","operatingSystem":"","architecture":""}}}`),
expectedErr: false,
},
{
action: core.NewRootGetAction(schema.GroupVersionResource{Group: rbac.GroupName, Version: rbac.SchemeGroupVersion.Version, Resource: "clusterrolebindings"}, "system:node"),
expectedHandled: true,
expectedObjectJSON: []byte(``),
expectedErr: true, // we expect a NotFound error here
},
{
action: core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, "kube-system", "bootstrap-token-abcdef"),
expectedHandled: true,
expectedObjectJSON: []byte(``),
expectedErr: true, // we expect a NotFound error here
},
{ // an ask for a kubernetes service in the _kube-system_ ns should not be answered
action: core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "services"}, "kube-system", "kubernetes"),
expectedHandled: false,
expectedObjectJSON: []byte(``),
expectedErr: false,
},
{ // an ask for an other service than kubernetes should not be answered
action: core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "services"}, "default", "my-other-service"),
expectedHandled: false,
expectedObjectJSON: []byte(``),
expectedErr: false,
},
{ // an ask for an other node than the master should not be answered
action: core.NewRootGetAction(schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "other-node"),
expectedHandled: false,
expectedObjectJSON: []byte(``),
expectedErr: false,
},
{ // an ask for a secret in any other ns than kube-system should not be answered
action: core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, "default", "bootstrap-token-abcdef"),
expectedHandled: false,
expectedObjectJSON: []byte(``),
expectedErr: false,
},
}
for _, rt := range tests {
handled, obj, actualErr := idr.HandleGetAction(rt.action)
objBytes := []byte(``)
if obj != nil {
var err error
objBytes, err = json.Marshal(obj)
if err != nil {
t.Fatalf("couldn't marshal returned object")
}
}
if handled != rt.expectedHandled {
t.Errorf(
"failed HandleGetAction:\n\texpected handled: %t\n\t actual: %t %v",
rt.expectedHandled,
handled,
rt.action,
)
}
if !bytes.Equal(objBytes, rt.expectedObjectJSON) {
t.Errorf(
"failed HandleGetAction:\n\texpected object: %q\n\t actual: %q",
rt.expectedObjectJSON,
objBytes,
)
}
if (actualErr != nil) != rt.expectedErr {
t.Errorf(
"failed HandleGetAction:\n\texpected error: %t\n\t actual: %t %v",
rt.expectedErr,
(actualErr != nil),
rt.action,
)
}
}
}

View File

@ -319,6 +319,17 @@ type DeleteAction interface {
GetName() string
}
type DeleteCollectionAction interface {
Action
GetListRestrictions() ListRestrictions
}
type PatchAction interface {
Action
GetName() string
GetPatch() []byte
}
type WatchAction interface {
Action
GetWatchRestrictions() WatchRestrictions