mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
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:
commit
d852b8aad9
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
139
cmd/kubeadm/app/util/apiclient/clientbacked_dryrun.go
Normal file
139
cmd/kubeadm/app/util/apiclient/clientbacked_dryrun.go
Normal 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
|
||||
}
|
237
cmd/kubeadm/app/util/apiclient/dryrunclient.go
Normal file
237
cmd/kubeadm/app/util/apiclient/dryrunclient.go
Normal 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())
|
||||
}
|
||||
}
|
100
cmd/kubeadm/app/util/apiclient/dryrunclient_test.go
Normal file
100
cmd/kubeadm/app/util/apiclient/dryrunclient_test.go
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
162
cmd/kubeadm/app/util/apiclient/init_dryrun.go
Normal file
162
cmd/kubeadm/app/util/apiclient/init_dryrun.go
Normal 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")
|
||||
}
|
126
cmd/kubeadm/app/util/apiclient/init_dryrun_test.go
Normal file
126
cmd/kubeadm/app/util/apiclient/init_dryrun_test.go
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user