Merge pull request #50302 from fabriziopandini/kubeadm-phases-controlplane2

Automatic merge from submit-queue

kubeadm: Implementing the controlplane phase

**What this PR does / why we need it:**
This contains implementation of controlplane phases in kubeadm, which is part of the wider effort of implementing phases in kubeadm, previously in alpha stage.

The original proposal for this activity can be found [here](https://github.com/kubernetes/kubeadm/pull/156/files) and related comments.

Kubeadm phase implementation checklist is defined [here](https://github.com/kubernetes/kubeadm/issues/267)

Common implementation guidelines and principles for all phases are defined [here](https://docs.google.com/document/d/1VQMyFIVMfRGQPP3oCUpfjiWtOr3pLxp4g7cP-hXQFXc/edit?usp=sharing)

This PR implements:

- [x] kubeadm phase controlplane: wip by @fabriziopandini
  - [x] kubeadm phase controlplane all
  - [x] kubeadm phase controlplane etcd
  - [x] kubeadm phase controlplane apiserver
  - [x] kubeadm phase controlplane scheduler
  - [x] kubeadm phase controlplane controller-manager

**Which issue this PR fixes:**
https://github.com/kubernetes/kubeadm/issues/349

**Special notes for your reviewer:**
This PR implements the same approach of #49419, thus minimising rework/impacts on existing codebase.
pull/6/head
Kubernetes Submit Queue 2017-08-14 20:35:14 -07:00 committed by GitHub
commit c85aea35cb
23 changed files with 1269 additions and 499 deletions

View File

@ -74,7 +74,6 @@ package_group(
packages = [
"//cmd/kubeadm/app",
"//cmd/kubeadm/app/cmd",
"//cmd/kubeadm/app/phases/controlplane",
],
)

View File

@ -40,6 +40,7 @@ filegroup(
"//cmd/kubeadm/app/phases/bootstraptoken/node:all-srcs",
"//cmd/kubeadm/app/phases/certs:all-srcs",
"//cmd/kubeadm/app/phases/controlplane:all-srcs",
"//cmd/kubeadm/app/phases/etcd:all-srcs",
"//cmd/kubeadm/app/phases/kubeconfig:all-srcs",
"//cmd/kubeadm/app/phases/markmaster:all-srcs",
"//cmd/kubeadm/app/phases/selfhosting:all-srcs",

View File

@ -32,6 +32,7 @@ go_library(
"//cmd/kubeadm/app/phases/bootstraptoken/node:go_default_library",
"//cmd/kubeadm/app/phases/certs/pkiutil:go_default_library",
"//cmd/kubeadm/app/phases/controlplane:go_default_library",
"//cmd/kubeadm/app/phases/etcd:go_default_library",
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",
"//cmd/kubeadm/app/phases/markmaster:go_default_library",
"//cmd/kubeadm/app/phases/selfhosting:go_default_library",

View File

@ -20,7 +20,6 @@ import (
"fmt"
"io"
"io/ioutil"
"path/filepath"
"strconv"
"text/template"
@ -41,6 +40,7 @@ import (
nodebootstraptokenphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/node"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil"
controlplanephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane"
etcdphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/etcd"
kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
markmasterphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/markmaster"
selfhostingphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/selfhosting"
@ -242,9 +242,16 @@ func (i *Init) Run(out io.Writer) error {
}
// PHASE 3: Bootstrap the control plane
if err := controlplanephase.WriteStaticPodManifests(i.cfg, k8sVersion, kubeadmconstants.GetStaticPodDirectory()); err != nil {
manifestPath := kubeadmconstants.GetStaticPodDirectory()
if err := controlplanephase.CreateInitStaticPodManifestFiles(manifestPath, i.cfg); err != nil {
return err
}
// Add etcd static pod spec only if external etcd is not configured
if len(i.cfg.Etcd.Endpoints) == 0 {
if err := etcdphase.CreateLocalEtcdStaticPodManifestFile(manifestPath, i.cfg); err != nil {
return err
}
}
client, err := kubeadmutil.CreateClientAndWaitForAPI(kubeadmconstants.GetAdminKubeConfigPath())
if err != nil {
@ -319,7 +326,7 @@ func (i *Init) Run(out io.Writer) error {
}
ctx := map[string]string{
"KubeConfigPath": filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.AdminKubeConfigFileName),
"KubeConfigPath": kubeadmconstants.GetAdminKubeConfigPath(),
"KubeConfigName": kubeadmconstants.AdminKubeConfigFileName,
"Token": i.cfg.Token,
"CAPubKeyPin": pubkeypin.Hash(caCert),

View File

@ -11,21 +11,27 @@ go_library(
srcs = [
"bootstraptoken.go",
"certs.go",
"controlplane.go",
"etcd.go",
"kubeconfig.go",
"markmaster.go",
"phase.go",
"preflight.go",
"selfhosting.go",
"uploadconfig.go",
"util.go",
],
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/validation:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/phases/bootstraptoken/clusterinfo:go_default_library",
"//cmd/kubeadm/app/phases/bootstraptoken/node:go_default_library",
"//cmd/kubeadm/app/phases/certs:go_default_library",
"//cmd/kubeadm/app/phases/certs/pkiutil:go_default_library",
"//cmd/kubeadm/app/phases/controlplane:go_default_library",
"//cmd/kubeadm/app/phases/etcd:go_default_library",
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",
"//cmd/kubeadm/app/phases/markmaster:go_default_library",
"//cmd/kubeadm/app/phases/selfhosting:go_default_library",
@ -45,6 +51,8 @@ go_test(
name = "go_default_test",
srcs = [
"certs_test.go",
"controlplane_test.go",
"etcd_test.go",
"kubeconfig_test.go",
"phase_test.go",
],

View File

@ -0,0 +1,107 @@
/*
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 phases
import (
"github.com/spf13/cobra"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
controlplanephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane"
"k8s.io/kubernetes/pkg/api"
)
// NewCmdControlplane return main command for Controlplane phase
func NewCmdControlplane() *cobra.Command {
cmd := &cobra.Command{
Use: "controlplane",
Short: "Generate all static pod manifest files necessary to establish the control plane.",
RunE: subCmdRunE("controlplane"),
}
manifestPath := kubeadmconstants.GetStaticPodDirectory()
cmd.AddCommand(getControlPlaneSubCommands(manifestPath)...)
return cmd
}
// getControlPlaneSubCommands returns sub commands for Controlplane phase
func getControlPlaneSubCommands(outDir string) []*cobra.Command {
cfg := &kubeadmapiext.MasterConfiguration{}
// Default values for the cobra help text
api.Scheme.Default(cfg)
var cfgPath string
var subCmds []*cobra.Command
subCmdProperties := []struct {
use string
short string
cmdFunc func(outDir string, cfg *kubeadmapi.MasterConfiguration) error
}{
{
use: "all",
short: "Generate all static pod manifest files necessary to establish the control plane.",
cmdFunc: controlplanephase.CreateInitStaticPodManifestFiles,
},
{
use: "apiserver",
short: "Generate apiserver static pod manifest.",
cmdFunc: controlplanephase.CreateAPIServerStaticPodManifestFile,
},
{
use: "controller-manager",
short: "Generate controller-manager static pod manifest.",
cmdFunc: controlplanephase.CreateControllerManagerStaticPodManifestFile,
},
{
use: "scheduler",
short: "Generate scheduler static pod manifest.",
cmdFunc: controlplanephase.CreateSchedulerStaticPodManifestFile,
},
}
for _, properties := range subCmdProperties {
// Creates the UX Command
cmd := &cobra.Command{
Use: properties.use,
Short: properties.short,
Run: runCmdPhase(properties.cmdFunc, &outDir, &cfgPath, cfg),
}
// Add flags to the command
cmd.Flags().StringVar(&cfg.CertificatesDir, "cert-dir", cfg.CertificatesDir, `The path where certificates are stored`)
cmd.Flags().StringVar(&cfg.KubernetesVersion, "kubernetes-version", cfg.KubernetesVersion, `Choose a specific Kubernetes version for the control plane`)
if properties.use == "all" || properties.use == "apiserver" {
cmd.Flags().StringVar(&cfg.API.AdvertiseAddress, "apiserver-advertise-address", cfg.API.AdvertiseAddress, "The IP address the API Server will advertise it's listening on. 0.0.0.0 means the default network interface's address.")
cmd.Flags().Int32Var(&cfg.API.BindPort, "apiserver-bind-port", cfg.API.BindPort, "Port for the API Server to bind to")
cmd.Flags().StringVar(&cfg.Networking.ServiceSubnet, "service-cidr", cfg.Networking.ServiceSubnet, "Use alternative range of IP address for service VIPs")
}
if properties.use == "all" || properties.use == "controller-manager" {
cmd.Flags().StringVar(&cfg.Networking.PodSubnet, "pod-network-cidr", cfg.Networking.PodSubnet, "Specify range of IP addresses for the pod network; if set, the control plane will automatically allocate CIDRs for every node")
}
cmd.Flags().StringVar(&cfgPath, "config", cfgPath, "Path to kubeadm config file (WARNING: Usage of a configuration file is experimental)")
subCmds = append(subCmds, cmd)
}
return subCmds
}

View File

@ -0,0 +1,150 @@
/*
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 phases
import (
"fmt"
"os"
"testing"
// required for triggering api machinery startup when running unit tests
_ "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/install"
testutil "k8s.io/kubernetes/cmd/kubeadm/test"
cmdtestutil "k8s.io/kubernetes/cmd/kubeadm/test/cmd"
)
func TestControlPlaneSubCommandsHasFlags(t *testing.T) {
subCmds := getControlPlaneSubCommands("")
commonFlags := []string{
"cert-dir",
"config",
}
var tests = []struct {
command string
additionalFlags []string
}{
{
command: "all",
additionalFlags: []string{
"kubernetes-version",
"apiserver-advertise-address",
"apiserver-bind-port",
"service-cidr",
"pod-network-cidr",
},
},
{
command: "apiserver",
additionalFlags: []string{
"kubernetes-version",
"apiserver-advertise-address",
"apiserver-bind-port",
"service-cidr",
},
},
{
command: "controller-manager",
additionalFlags: []string{
"kubernetes-version",
"pod-network-cidr",
},
},
{
command: "scheduler",
additionalFlags: []string{
"kubernetes-version",
},
},
}
for _, test := range tests {
expectedFlags := append(commonFlags, test.additionalFlags...)
cmdtestutil.AssertSubCommandHasFlags(t, subCmds, test.command, expectedFlags...)
}
}
func TestControlPlaneCreateFilesWithFlags(t *testing.T) {
var tests = []struct {
command string
additionalFlags []string
expectedFiles []string
}{
{
command: "all",
additionalFlags: []string{
"--kubernetes-version=v1.7.0",
"--apiserver-advertise-address=1.2.3.4",
"--apiserver-bind-port=6443",
"--service-cidr=1.2.3.4/16",
"--pod-network-cidr=1.2.3.4/16",
},
expectedFiles: []string{
"kube-apiserver.yaml",
"kube-controller-manager.yaml",
"kube-scheduler.yaml",
},
},
{
command: "apiserver",
additionalFlags: []string{
"--kubernetes-version=v1.7.0",
"--apiserver-advertise-address=1.2.3.4",
"--apiserver-bind-port=6443",
"--service-cidr=1.2.3.4/16",
},
expectedFiles: []string{"kube-apiserver.yaml"},
},
{
command: "controller-manager",
additionalFlags: []string{
"--kubernetes-version=v1.7.0",
"--pod-network-cidr=1.2.3.4/16",
},
expectedFiles: []string{"kube-controller-manager.yaml"},
},
{
command: "scheduler",
additionalFlags: []string{
"--kubernetes-version=v1.7.0",
},
expectedFiles: []string{"kube-scheduler.yaml"},
},
}
for _, test := range tests {
// Create temp folder for the test case
tmpdir := testutil.SetupTempDir(t)
defer os.RemoveAll(tmpdir)
// Get subcommands working in the temporary directory
subCmds := getControlPlaneSubCommands(tmpdir)
// Execute the subcommand
certDirFlag := fmt.Sprintf("--cert-dir=%s", tmpdir)
allFlags := append(test.additionalFlags, certDirFlag)
cmdtestutil.RunSubCommand(t, subCmds, test.command, allFlags...)
// Checks that requested files are there
testutil.AssertFileExists(t, tmpdir, test.expectedFiles...)
}
}

View File

@ -0,0 +1,76 @@
/*
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 phases
import (
"github.com/spf13/cobra"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
etcdphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/etcd"
"k8s.io/kubernetes/pkg/api"
)
// NewCmdEtcd return main command for Etcd phase
func NewCmdEtcd() *cobra.Command {
cmd := &cobra.Command{
Use: "etcd",
Short: "Generate static pod manifest file for etcd.",
RunE: subCmdRunE("etcd"),
}
manifestPath := kubeadmconstants.GetStaticPodDirectory()
cmd.AddCommand(getEtcdSubCommands(manifestPath)...)
return cmd
}
// getEtcdSubCommands returns sub commands for etcd phase
func getEtcdSubCommands(outDir string) []*cobra.Command {
cfg := &kubeadmapiext.MasterConfiguration{}
// Default values for the cobra help text
api.Scheme.Default(cfg)
var cfgPath string
var subCmds []*cobra.Command
properties := struct {
use string
short string
cmdFunc func(outDir string, cfg *kubeadmapi.MasterConfiguration) error
}{
use: "local",
short: "Generate static pod manifest file for a local, single-node etcd instance.",
cmdFunc: etcdphase.CreateLocalEtcdStaticPodManifestFile,
}
// Creates the UX Command
cmd := &cobra.Command{
Use: properties.use,
Short: properties.short,
Run: runCmdPhase(properties.cmdFunc, &outDir, &cfgPath, cfg),
}
// Add flags to the command
cmd.Flags().StringVar(&cfg.CertificatesDir, "cert-dir", cfg.CertificatesDir, `The path where certificates are stored`)
cmd.Flags().StringVar(&cfgPath, "config", cfgPath, "Path to kubeadm config file (WARNING: Usage of a configuration file is experimental)")
subCmds = append(subCmds, cmd)
return subCmds
}

View File

@ -0,0 +1,86 @@
/*
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 phases
import (
"fmt"
"os"
"testing"
// required for triggering api machinery startup when running unit tests
_ "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/install"
testutil "k8s.io/kubernetes/cmd/kubeadm/test"
cmdtestutil "k8s.io/kubernetes/cmd/kubeadm/test/cmd"
)
func TestEtcdSubCommandsHasFlags(t *testing.T) {
subCmds := getEtcdSubCommands("")
commonFlags := []string{
"cert-dir",
"config",
}
var tests = []struct {
command string
additionalFlags []string
}{
{
command: "local",
},
}
for _, test := range tests {
expectedFlags := append(commonFlags, test.additionalFlags...)
cmdtestutil.AssertSubCommandHasFlags(t, subCmds, test.command, expectedFlags...)
}
}
func TestEtcdCreateFilesWithFlags(t *testing.T) {
var tests = []struct {
command string
additionalFlags []string
expectedFiles []string
}{
{
command: "local",
expectedFiles: []string{"etcd.yaml"},
additionalFlags: []string{},
},
}
for _, test := range tests {
// Create temp folder for the test case
tmpdir := testutil.SetupTempDir(t)
defer os.RemoveAll(tmpdir)
// Get subcommands working in the temporary directory
subCmds := getEtcdSubCommands(tmpdir)
// Execute the subcommand
certDirFlag := fmt.Sprintf("--cert-dir=%s", tmpdir)
allFlags := append(test.additionalFlags, certDirFlag)
cmdtestutil.RunSubCommand(t, subCmds, test.command, allFlags...)
// Checks that requested files are there
testutil.AssertFileExists(t, tmpdir, test.expectedFiles...)
}
}

View File

@ -31,13 +31,15 @@ func NewCmdPhase(out io.Writer) *cobra.Command {
RunE: subCmdRunE("phase"),
}
cmd.AddCommand(NewCmdKubeConfig(out))
cmd.AddCommand(NewCmdBootstrapToken())
cmd.AddCommand(NewCmdCerts())
cmd.AddCommand(NewCmdControlplane())
cmd.AddCommand(NewCmdEtcd())
cmd.AddCommand(NewCmdKubeConfig(out))
cmd.AddCommand(NewCmdMarkMaster())
cmd.AddCommand(NewCmdPreFlight())
cmd.AddCommand(NewCmdSelfhosting())
cmd.AddCommand(NewCmdMarkMaster())
cmd.AddCommand(NewCmdUploadConfig())
cmd.AddCommand(NewCmdBootstrapToken())
return cmd
}

View File

@ -0,0 +1,50 @@
/*
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 phases
import (
"github.com/spf13/cobra"
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"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
)
// runCmdPhase creates a cobra.Command Run function, by composing the call to the given cmdFunc with necessary additional steps (e.g preparation of input parameters)
func runCmdPhase(cmdFunc func(outDir string, cfg *kubeadmapi.MasterConfiguration) error, outDir, cfgPath *string, cfg *kubeadmapiext.MasterConfiguration) func(cmd *cobra.Command, args []string) {
// the following statement build a clousure that wraps a call to a cmdFunc, binding
// the function itself with the specific parameters of each sub command.
// Please note that specific parameter should be passed as value, while other parameters - passed as reference -
// are shared between sub commands and gets access to current value e.g. flags value.
return func(cmd *cobra.Command, args []string) {
if err := validation.ValidateMixedArguments(cmd.Flags()); err != nil {
kubeadmutil.CheckErr(err)
}
// This call returns the ready-to-use configuration based on the configuration file that might or might not exist and the default cfg populated by flags
internalcfg, err := configutil.ConfigFileAndDefaultsToInternalConfig(*cfgPath, cfg)
kubeadmutil.CheckErr(err)
// Execute the cmdFunc
err = cmdFunc(*outDir, internalcfg)
kubeadmutil.CheckErr(err)
}
}

View File

@ -16,11 +16,9 @@ go_test(
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/test:go_default_library",
"//pkg/util/version:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
],
)
@ -35,15 +33,10 @@ go_library(
"//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/images:go_default_library",
"//cmd/kubeadm/app/util/staticpod:go_default_library",
"//pkg/kubeapiserver/authorizer/modes:go_default_library",
"//pkg/kubectl/cmd/util:go_default_library",
"//pkg/kubelet/types:go_default_library",
"//pkg/util/version:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
],
)

View File

@ -17,26 +17,19 @@ limitations under the License.
package controlplane
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/ghodss/yaml"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/sets"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/images"
staticpodutil "k8s.io/kubernetes/cmd/kubeadm/app/util/staticpod"
authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
"k8s.io/kubernetes/pkg/util/version"
)
@ -47,125 +40,96 @@ const (
defaultv17AdmissionControl = "Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,ResourceQuota"
)
// WriteStaticPodManifests builds manifest objects based on user provided configuration and then dumps it to disk
// where kubelet will pick and schedule them.
func WriteStaticPodManifests(cfg *kubeadmapi.MasterConfiguration, k8sVersion *version.Version, manifestsDir string) error {
// CreateInitStaticPodManifestFiles will write all static pod manifest files needed to bring up the control plane.
func CreateInitStaticPodManifestFiles(manifestDir string, cfg *kubeadmapi.MasterConfiguration) error {
return createStaticPodFiles(manifestDir, cfg, kubeadmconstants.KubeAPIServer, kubeadmconstants.KubeControllerManager, kubeadmconstants.KubeScheduler)
}
// CreateAPIServerStaticPodManifestFile will write APIserver static pod manifest file.
func CreateAPIServerStaticPodManifestFile(manifestDir string, cfg *kubeadmapi.MasterConfiguration) error {
return createStaticPodFiles(manifestDir, cfg, kubeadmconstants.KubeAPIServer)
}
// CreateControllerManagerStaticPodManifestFile will write controller manager static pod manifest file.
func CreateControllerManagerStaticPodManifestFile(manifestDir string, cfg *kubeadmapi.MasterConfiguration) error {
return createStaticPodFiles(manifestDir, cfg, kubeadmconstants.KubeControllerManager)
}
// CreateSchedulerStaticPodManifestFile will write scheduler static pod manifest file.
func CreateSchedulerStaticPodManifestFile(manifestDir string, cfg *kubeadmapi.MasterConfiguration) error {
return createStaticPodFiles(manifestDir, cfg, kubeadmconstants.KubeScheduler)
}
// GetStaticPodSpecs returns all staticPodSpecs actualized to the context of the current MasterConfiguration
// NB. this methods holds the information about how kubeadm creates static pod mainfests.
func GetStaticPodSpecs(cfg *kubeadmapi.MasterConfiguration, k8sVersion *version.Version) map[string]v1.Pod {
// Get the required hostpath mounts
mounts := getHostPathVolumesForTheControlPlane(cfg)
// Prepare static pod specs
staticPodSpecs := map[string]v1.Pod{
kubeadmconstants.KubeAPIServer: componentPod(v1.Container{
kubeadmconstants.KubeAPIServer: staticpodutil.ComponentPod(v1.Container{
Name: kubeadmconstants.KubeAPIServer,
Image: images.GetCoreImage(kubeadmconstants.KubeAPIServer, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage),
Command: getAPIServerCommand(cfg, k8sVersion),
VolumeMounts: mounts.GetVolumeMounts(kubeadmconstants.KubeAPIServer),
LivenessProbe: componentProbe(int(cfg.API.BindPort), "/healthz", v1.URISchemeHTTPS),
Resources: componentResources("250m"),
LivenessProbe: staticpodutil.ComponentProbe(int(cfg.API.BindPort), "/healthz", v1.URISchemeHTTPS),
Resources: staticpodutil.ComponentResources("250m"),
Env: getProxyEnvVars(),
}, mounts.GetVolumes(kubeadmconstants.KubeAPIServer)),
kubeadmconstants.KubeControllerManager: componentPod(v1.Container{
kubeadmconstants.KubeControllerManager: staticpodutil.ComponentPod(v1.Container{
Name: kubeadmconstants.KubeControllerManager,
Image: images.GetCoreImage(kubeadmconstants.KubeControllerManager, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage),
Command: getControllerManagerCommand(cfg, k8sVersion),
VolumeMounts: mounts.GetVolumeMounts(kubeadmconstants.KubeControllerManager),
LivenessProbe: componentProbe(10252, "/healthz", v1.URISchemeHTTP),
Resources: componentResources("200m"),
LivenessProbe: staticpodutil.ComponentProbe(10252, "/healthz", v1.URISchemeHTTP),
Resources: staticpodutil.ComponentResources("200m"),
Env: getProxyEnvVars(),
}, mounts.GetVolumes(kubeadmconstants.KubeControllerManager)),
kubeadmconstants.KubeScheduler: componentPod(v1.Container{
kubeadmconstants.KubeScheduler: staticpodutil.ComponentPod(v1.Container{
Name: kubeadmconstants.KubeScheduler,
Image: images.GetCoreImage(kubeadmconstants.KubeScheduler, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage),
Command: getSchedulerCommand(cfg),
VolumeMounts: mounts.GetVolumeMounts(kubeadmconstants.KubeScheduler),
LivenessProbe: componentProbe(10251, "/healthz", v1.URISchemeHTTP),
Resources: componentResources("100m"),
LivenessProbe: staticpodutil.ComponentProbe(10251, "/healthz", v1.URISchemeHTTP),
Resources: staticpodutil.ComponentResources("100m"),
Env: getProxyEnvVars(),
}, mounts.GetVolumes(kubeadmconstants.KubeScheduler)),
}
// Add etcd static pod spec only if external etcd is not configured
if len(cfg.Etcd.Endpoints) == 0 {
return staticPodSpecs
}
etcdPod := componentPod(v1.Container{
Name: kubeadmconstants.Etcd,
Command: getEtcdCommand(cfg),
Image: images.GetCoreImage(kubeadmconstants.Etcd, cfg.ImageRepository, "", cfg.Etcd.Image),
// Mount the etcd datadir path read-write so etcd can store data in a more persistent manner
VolumeMounts: []v1.VolumeMount{newVolumeMount(etcdVolumeName, cfg.Etcd.DataDir, false)},
LivenessProbe: componentProbe(2379, "/health", v1.URISchemeHTTP),
}, []v1.Volume{newVolume(etcdVolumeName, cfg.Etcd.DataDir)})
// createStaticPodFiles creates all the requested static pod files.
func createStaticPodFiles(manifestDir string, cfg *kubeadmapi.MasterConfiguration, componentNames ...string) error {
staticPodSpecs[kubeadmconstants.Etcd] = etcdPod
// TODO: Move the "pkg/util/version".Version object into the internal API instead of always parsing the string
k8sVersion, err := version.ParseSemantic(cfg.KubernetesVersion)
if err != nil {
return err
}
if err := os.MkdirAll(manifestsDir, 0700); err != nil {
return fmt.Errorf("failed to create directory %q [%v]", manifestsDir, err)
}
for name, spec := range staticPodSpecs {
filename := kubeadmconstants.GetStaticPodFilepath(name, manifestsDir)
serialized, err := yaml.Marshal(spec)
if err != nil {
return fmt.Errorf("failed to marshal manifest for %q to YAML [%v]", name, err)
// gets the StaticPodSpecs, actualized for the current MasterConfiguration
specs := GetStaticPodSpecs(cfg, k8sVersion)
// creates required static pod specs
for _, componentName := range componentNames {
// retrives the StaticPodSpec for given component
spec, exists := specs[componentName]
if !exists {
return fmt.Errorf("couldn't retrive StaticPodSpec for %s", componentName)
}
if err := cmdutil.DumpReaderToFile(bytes.NewReader(serialized), filename); err != nil {
return fmt.Errorf("failed to create static pod manifest file for %q (%q) [%v]", name, filename, err)
// writes the StaticPodSpec to disk
if err := staticpodutil.WriteStaticPodToDisk(componentName, manifestDir, spec); err != nil {
return fmt.Errorf("failed to create static pod manifest file for %q: %v", componentName, err)
}
}
return nil
}
// componentResources returns the v1.ResourceRequirements object needed for allocating a specified amount of the CPU
func componentResources(cpu string) v1.ResourceRequirements {
return v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceName(v1.ResourceCPU): resource.MustParse(cpu),
},
}
}
// componentProbe is a helper function building a ready v1.Probe object from some simple parameters
func componentProbe(port int, path string, scheme v1.URIScheme) *v1.Probe {
return &v1.Probe{
Handler: v1.Handler{
HTTPGet: &v1.HTTPGetAction{
// Host has to be set to "127.0.0.1" here due to that our static Pods are on the host's network
Host: "127.0.0.1",
Path: path,
Port: intstr.FromInt(port),
Scheme: scheme,
},
},
InitialDelaySeconds: 15,
TimeoutSeconds: 15,
FailureThreshold: 8,
}
}
// componentPod returns a Pod object from the container and volume specifications
func componentPod(container v1.Container, volumes []v1.Volume) v1.Pod {
return v1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Name: container.Name,
Namespace: metav1.NamespaceSystem,
Annotations: map[string]string{kubetypes.CriticalPodAnnotationKey: ""},
// The component and tier labels are useful for quickly identifying the control plane Pods when doing a .List()
// against Pods in the kube-system namespace. Can for example be used together with the WaitForPodsWithLabel function
Labels: map[string]string{"component": container.Name, "tier": "control-plane"},
},
Spec: v1.PodSpec{
Containers: []v1.Container{container},
HostNetwork: true,
Volumes: volumes,
},
}
}
// getAPIServerCommand builds the right API server command from the given config object and version
func getAPIServerCommand(cfg *kubeadmapi.MasterConfiguration, k8sVersion *version.Version) []string {
defaultArguments := map[string]string{
@ -195,7 +159,7 @@ func getAPIServerCommand(cfg *kubeadmapi.MasterConfiguration, k8sVersion *versio
}
command := []string{"kube-apiserver"}
command = append(command, getExtraParameters(cfg.APIServerExtraArgs, defaultArguments)...)
command = append(command, staticpodutil.GetExtraParameters(cfg.APIServerExtraArgs, defaultArguments)...)
command = append(command, getAuthzParameters(cfg.AuthorizationModes)...)
// Check if the user decided to use an external etcd cluster
@ -227,19 +191,6 @@ func getAPIServerCommand(cfg *kubeadmapi.MasterConfiguration, k8sVersion *versio
return command
}
// getEtcdCommand builds the right etcd command from the given config object
func getEtcdCommand(cfg *kubeadmapi.MasterConfiguration) []string {
defaultArguments := map[string]string{
"listen-client-urls": "http://127.0.0.1:2379",
"advertise-client-urls": "http://127.0.0.1:2379",
"data-dir": cfg.Etcd.DataDir,
}
command := []string{"etcd"}
command = append(command, getExtraParameters(cfg.Etcd.ExtraArgs, defaultArguments)...)
return command
}
// getControllerManagerCommand builds the right controller manager command from the given config object and version
func getControllerManagerCommand(cfg *kubeadmapi.MasterConfiguration, k8sVersion *version.Version) []string {
defaultArguments := map[string]string{
@ -255,7 +206,7 @@ func getControllerManagerCommand(cfg *kubeadmapi.MasterConfiguration, k8sVersion
}
command := []string{"kube-controller-manager"}
command = append(command, getExtraParameters(cfg.ControllerManagerExtraArgs, defaultArguments)...)
command = append(command, staticpodutil.GetExtraParameters(cfg.ControllerManagerExtraArgs, defaultArguments)...)
if cfg.CloudProvider != "" {
command = append(command, "--cloud-provider="+cfg.CloudProvider)
@ -283,7 +234,7 @@ func getSchedulerCommand(cfg *kubeadmapi.MasterConfiguration) []string {
}
command := []string{"kube-scheduler"}
command = append(command, getExtraParameters(cfg.SchedulerExtraArgs, defaultArguments)...)
command = append(command, staticpodutil.GetExtraParameters(cfg.SchedulerExtraArgs, defaultArguments)...)
return command
}
@ -327,19 +278,3 @@ func getAuthzParameters(modes []string) []string {
command = append(command, "--authorization-mode="+strings.Join(modes, ","))
return command
}
// getExtraParameters builds a list of flag arguments two string-string maps, one with default, base commands and one with overrides
func getExtraParameters(overrides map[string]string, defaults map[string]string) []string {
var command []string
for k, v := range overrides {
if len(v) > 0 {
command = append(command, fmt.Sprintf("--%s=%s", k, v))
}
}
for k, v := range defaults {
if _, overrideExists := overrides[k]; !overrideExists {
command = append(command, fmt.Sprintf("--%s=%s", k, v))
}
}
return command
}

View File

@ -18,20 +18,17 @@ package controlplane
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
"testing"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/yaml"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/pkg/util/version"
testutil "k8s.io/kubernetes/cmd/kubeadm/test"
)
const (
@ -39,189 +36,98 @@ const (
etcdDataDir = "/var/lib/etcd"
)
func TestWriteStaticPodManifests(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("Couldn't create tmpdir")
func TestGetStaticPodSpecs(t *testing.T) {
// Creates a Master Configuration
cfg := &kubeadmapi.MasterConfiguration{
KubernetesVersion: "v1.7.0",
}
defer os.RemoveAll(tmpdir)
// set up tmp KubernetesDir for testing
kubeadmconstants.KubernetesDir = fmt.Sprintf("%s/etc/kubernetes", tmpdir)
defer func() { kubeadmconstants.KubernetesDir = "/etc/kubernetes" }()
// Executes GetStaticPodSpecs
var tests = []struct {
cfg *kubeadmapi.MasterConfiguration
expectErr bool
expectedAPIProbePort int32
// TODO: Move the "pkg/util/version".Version object into the internal API instead of always parsing the string
k8sVersion, _ := version.ParseSemantic(cfg.KubernetesVersion)
specs := GetStaticPodSpecs(cfg, k8sVersion)
var assertions = []struct {
staticPodName string
}{
{
cfg: &kubeadmapi.MasterConfiguration{
KubernetesVersion: "v1.7.0",
},
expectErr: false,
staticPodName: kubeadmconstants.KubeAPIServer,
},
{
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
BindPort: 443,
},
KubernetesVersion: "v1.7.0",
},
expectErr: false,
expectedAPIProbePort: 443,
staticPodName: kubeadmconstants.KubeControllerManager,
},
{
staticPodName: kubeadmconstants.KubeScheduler,
},
}
for _, rt := range tests {
actual := WriteStaticPodManifests(rt.cfg, version.MustParseSemantic(rt.cfg.KubernetesVersion), fmt.Sprintf("%s/etc/kubernetes/manifests", tmpdir))
if (actual == nil) && rt.expectErr {
t.Error("expected an error from WriteStaticPodManifests but got none")
continue
}
if (actual != nil) && !rt.expectErr {
t.Errorf("didn't expect an error from WriteStaticPodManifests but got: %v", err)
continue
}
if rt.expectErr {
continue
}
for _, assertion := range assertions {
// Below is dead code.
if rt.expectedAPIProbePort != 0 {
manifest, err := os.Open(filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.ManifestsSubDirName, "kube-apiserver.yaml"))
if err != nil {
t.Errorf("WriteStaticPodManifests: %v", err)
continue
}
defer manifest.Close()
// assert the spec for the staticPodName exists
if spec, ok := specs[assertion.staticPodName]; ok {
var pod v1.Pod
d := yaml.NewYAMLOrJSONDecoder(manifest, 4096)
if err := d.Decode(&pod); err != nil {
t.Error("WriteStaticPodManifests: error decoding manifests/kube-apiserver.yaml into Pod")
continue
// Assert each specs refers to the right pod
if spec.Spec.Containers[0].Name != assertion.staticPodName {
t.Errorf("getKubeConfigSpecs spec for %s contains pod %s, expectes %s", assertion.staticPodName, spec.Spec.Containers[0].Name, assertion.staticPodName)
}
// Lots of individual checks as we traverse pointers so we don't panic dereferencing a nil on failure
containers := pod.Spec.Containers
if containers == nil || len(containers) == 0 {
t.Error("WriteStaticPodManifests: wrote an apiserver manifest without any containers")
continue
}
probe := containers[0].LivenessProbe
if probe == nil {
t.Error("WriteStaticPodManifests: wrote an apiserver manifest without a liveness probe")
continue
}
httpGET := probe.Handler.HTTPGet
if httpGET == nil {
t.Error("WriteStaticPodManifests: wrote an apiserver manifest without an HTTP liveness probe")
continue
}
port := httpGET.Port.IntVal
if rt.expectedAPIProbePort != port {
t.Errorf("WriteStaticPodManifests: apiserver pod liveness probe port was: %v, wanted %v", port, rt.expectedAPIProbePort)
}
} else {
t.Errorf("getStaticPodSpecs didn't create spec for %s ", assertion.staticPodName)
}
}
}
func TestComponentResources(t *testing.T) {
a := componentResources("250m")
if a.Requests == nil {
t.Errorf(
"failed componentResources, return value was nil",
)
}
}
func TestCreateStaticPodFilesAndWrappers(t *testing.T) {
func TestComponentProbe(t *testing.T) {
var tests = []struct {
port int
path string
scheme v1.URIScheme
createStaticPodFunction func(outDir string, cfg *kubeadmapi.MasterConfiguration) error
expectedFiles []string
}{
{
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
{ // CreateInitStaticPodManifestFiles
createStaticPodFunction: CreateInitStaticPodManifestFiles,
expectedFiles: []string{kubeadmconstants.KubeAPIServer, kubeadmconstants.KubeControllerManager, kubeadmconstants.KubeScheduler},
},
{
port: 2,
path: "bar",
scheme: v1.URISchemeHTTPS,
{ // CreateAPIServerStaticPodManifestFile
createStaticPodFunction: CreateAPIServerStaticPodManifestFile,
expectedFiles: []string{kubeadmconstants.KubeAPIServer},
},
}
for _, rt := range tests {
actual := componentProbe(rt.port, rt.path, rt.scheme)
if actual.Handler.HTTPGet.Port != intstr.FromInt(rt.port) {
t.Errorf(
"failed componentProbe:\n\texpected: %v\n\t actual: %v",
rt.port,
actual.Handler.HTTPGet.Port,
)
}
if actual.Handler.HTTPGet.Path != rt.path {
t.Errorf(
"failed componentProbe:\n\texpected: %s\n\t actual: %s",
rt.path,
actual.Handler.HTTPGet.Path,
)
}
if actual.Handler.HTTPGet.Scheme != rt.scheme {
t.Errorf(
"failed componentProbe:\n\texpected: %v\n\t actual: %v",
rt.scheme,
actual.Handler.HTTPGet.Scheme,
)
}
}
}
func TestComponentPod(t *testing.T) {
var tests = []struct {
name string
expected v1.Pod
}{
{
name: "foo",
expected: v1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "kube-system",
Annotations: map[string]string{"scheduler.alpha.kubernetes.io/critical-pod": ""},
Labels: map[string]string{"component": "foo", "tier": "control-plane"},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "foo",
},
},
HostNetwork: true,
Volumes: []v1.Volume{},
},
},
{ // CreateControllerManagerStaticPodManifestFile
createStaticPodFunction: CreateControllerManagerStaticPodManifestFile,
expectedFiles: []string{kubeadmconstants.KubeControllerManager},
},
{ // CreateSchedulerStaticPodManifestFile
createStaticPodFunction: CreateSchedulerStaticPodManifestFile,
expectedFiles: []string{kubeadmconstants.KubeScheduler},
},
}
for _, rt := range tests {
c := v1.Container{Name: rt.name}
actual := componentPod(c, []v1.Volume{})
if !reflect.DeepEqual(rt.expected, actual) {
t.Errorf(
"failed componentPod:\n\texpected: %v\n\t actual: %v",
rt.expected,
actual,
)
for _, test := range tests {
// Create temp folder for the test case
tmpdir := testutil.SetupTempDir(t)
defer os.RemoveAll(tmpdir)
// Creates a Master Configuration
cfg := &kubeadmapi.MasterConfiguration{
KubernetesVersion: "v1.7.0",
}
// Execute createStaticPodFunction
manifestPath := filepath.Join(tmpdir, kubeadmconstants.ManifestsSubDirName)
err := test.createStaticPodFunction(manifestPath, cfg)
if err != nil {
t.Errorf("Error executing createStaticPodFunction: %v", err)
continue
}
// Assert expected files are there
testutil.AssertFilesCount(t, manifestPath, len(test.expectedFiles))
for _, fileName := range test.expectedFiles {
testutil.AssertFileExists(t, manifestPath, fileName+".yaml")
}
}
}
@ -498,62 +404,6 @@ func TestGetControllerManagerCommand(t *testing.T) {
}
}
func TestGetEtcdCommand(t *testing.T) {
var tests = []struct {
cfg *kubeadmapi.MasterConfiguration
expected []string
}{
{
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{DataDir: "/var/lib/etcd"},
},
expected: []string{
"etcd",
"--listen-client-urls=http://127.0.0.1:2379",
"--advertise-client-urls=http://127.0.0.1:2379",
"--data-dir=/var/lib/etcd",
},
},
{
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{
DataDir: "/var/lib/etcd",
ExtraArgs: map[string]string{
"listen-client-urls": "http://10.0.1.10:2379",
"advertise-client-urls": "http://10.0.1.10:2379",
},
},
},
expected: []string{
"etcd",
"--listen-client-urls=http://10.0.1.10:2379",
"--advertise-client-urls=http://10.0.1.10:2379",
"--data-dir=/var/lib/etcd",
},
},
{
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{DataDir: "/etc/foo"},
},
expected: []string{
"etcd",
"--listen-client-urls=http://127.0.0.1:2379",
"--advertise-client-urls=http://127.0.0.1:2379",
"--data-dir=/etc/foo",
},
},
}
for _, rt := range tests {
actual := getEtcdCommand(rt.cfg)
sort.Strings(actual)
sort.Strings(rt.expected)
if !reflect.DeepEqual(actual, rt.expected) {
t.Errorf("failed getEtcdCommand:\nexpected:\n%v\nsaw:\n%v", rt.expected, actual)
}
}
}
func TestGetSchedulerCommand(t *testing.T) {
var tests = []struct {
cfg *kubeadmapi.MasterConfiguration
@ -651,50 +501,3 @@ func TestGetAuthzParameters(t *testing.T) {
}
}
}
func TestGetExtraParameters(t *testing.T) {
var tests = []struct {
overrides map[string]string
defaults map[string]string
expected []string
}{
{
overrides: map[string]string{
"admission-control": "NamespaceLifecycle,LimitRanger",
},
defaults: map[string]string{
"admission-control": "NamespaceLifecycle",
"insecure-bind-address": "127.0.0.1",
"allow-privileged": "true",
},
expected: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
},
},
{
overrides: map[string]string{
"admission-control": "NamespaceLifecycle,LimitRanger",
},
defaults: map[string]string{
"insecure-bind-address": "127.0.0.1",
"allow-privileged": "true",
},
expected: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
},
},
}
for _, rt := range tests {
actual := getExtraParameters(rt.overrides, rt.defaults)
sort.Strings(actual)
sort.Strings(rt.expected)
if !reflect.DeepEqual(actual, rt.expected) {
t.Errorf("failed getExtraParameters:\nexpected:\n%v\nsaw:\n%v", rt.expected, actual)
}
}
}

View File

@ -26,11 +26,11 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
staticpodutil "k8s.io/kubernetes/cmd/kubeadm/app/util/staticpod"
)
const (
k8sCertsVolumeName = "k8s-certs"
etcdVolumeName = "etcd"
caCertsVolumeName = "ca-certs"
caCertsVolumePath = "/etc/ssl/certs"
caCertsPkiVolumeName = "ca-certs-etc-pki"
@ -98,8 +98,8 @@ func newControlPlaneHostPathMounts() controlPlaneHostPathMounts {
}
func (c *controlPlaneHostPathMounts) NewHostPathMount(component, mountName, hostPath, containerPath string, readOnly bool) {
c.volumes[component] = append(c.volumes[component], newVolume(mountName, hostPath))
c.volumeMounts[component] = append(c.volumeMounts[component], newVolumeMount(mountName, containerPath, readOnly))
c.volumes[component] = append(c.volumes[component], staticpodutil.NewVolume(mountName, hostPath))
c.volumeMounts[component] = append(c.volumeMounts[component], staticpodutil.NewVolumeMount(mountName, containerPath, readOnly))
}
func (c *controlPlaneHostPathMounts) AddHostPathMounts(component string, vols []v1.Volume, volMounts []v1.VolumeMount) {
@ -115,25 +115,6 @@ func (c *controlPlaneHostPathMounts) GetVolumeMounts(component string) []v1.Volu
return c.volumeMounts[component]
}
// newVolume creates a v1.Volume with a hostPath mount to the specified location
func newVolume(name, path string) v1.Volume {
return v1.Volume{
Name: name,
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: path},
},
}
}
// newVolumeMount creates a v1.VolumeMount to the specified location
func newVolumeMount(name, path string, readOnly bool) v1.VolumeMount {
return v1.VolumeMount{
Name: name,
MountPath: path,
ReadOnly: readOnly,
}
}
// getEtcdCertVolumes returns the volumes/volumemounts needed for talking to an external etcd cluster
func getEtcdCertVolumes(etcdCfg kubeadmapi.Etcd) ([]v1.Volume, []v1.VolumeMount) {
certPaths := []string{etcdCfg.CAFile, etcdCfg.CertFile, etcdCfg.KeyFile}
@ -166,8 +147,8 @@ func getEtcdCertVolumes(etcdCfg kubeadmapi.Etcd) ([]v1.Volume, []v1.VolumeMount)
volumeMounts := []v1.VolumeMount{}
for i, certDir := range certDirs.List() {
name := fmt.Sprintf("etcd-certs-%d", i)
volumes = append(volumes, newVolume(name, certDir))
volumeMounts = append(volumeMounts, newVolumeMount(name, certDir, true))
volumes = append(volumes, staticpodutil.NewVolume(name, certDir))
volumeMounts = append(volumeMounts, staticpodutil.NewVolumeMount(name, certDir, true))
}
return volumes, volumeMounts
}

View File

@ -28,77 +28,6 @@ import (
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
)
func TestNewVolume(t *testing.T) {
var tests = []struct {
name string
path string
expected v1.Volume
}{
{
name: "foo",
path: "/etc/foo",
expected: v1.Volume{
Name: "foo",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "/etc/foo"},
},
},
},
}
for _, rt := range tests {
actual := newVolume(rt.name, rt.path)
if !reflect.DeepEqual(actual, rt.expected) {
t.Errorf(
"failed newVolume:\n\texpected: %v\n\t actual: %v",
rt.expected,
actual,
)
}
}
}
func TestNewVolumeMount(t *testing.T) {
var tests = []struct {
name string
path string
ro bool
expected v1.VolumeMount
}{
{
name: "foo",
path: "/etc/foo",
ro: false,
expected: v1.VolumeMount{
Name: "foo",
MountPath: "/etc/foo",
ReadOnly: false,
},
},
{
name: "bar",
path: "/etc/foo/bar",
ro: true,
expected: v1.VolumeMount{
Name: "bar",
MountPath: "/etc/foo/bar",
ReadOnly: true,
},
},
}
for _, rt := range tests {
actual := newVolumeMount(rt.name, rt.path, rt.ro)
if !reflect.DeepEqual(actual, rt.expected) {
t.Errorf(
"failed newVolumeMount:\n\texpected: %v\n\t actual: %v",
rt.expected,
actual,
)
}
}
}
func TestGetEtcdCertVolumes(t *testing.T) {
var tests = []struct {
ca, cert, key string

View File

@ -0,0 +1,47 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["local_test.go"],
library = ":go_default_library",
tags = ["automanaged"],
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/test:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["local.go"],
tags = ["automanaged"],
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/images:go_default_library",
"//cmd/kubeadm/app/util/staticpod:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,65 @@
/*
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 etcd
import (
"k8s.io/api/core/v1"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/images"
staticpodutil "k8s.io/kubernetes/cmd/kubeadm/app/util/staticpod"
)
const (
etcdVolumeName = "etcd"
)
// CreateLocalEtcdStaticPodManifestFile will write local etcd static pod manifest file.
func CreateLocalEtcdStaticPodManifestFile(manifestDir string, cfg *kubeadmapi.MasterConfiguration) error {
// gets etcd StaticPodSpec, actualized for the current MasterConfiguration
spec := GetEtcdPodSpec(cfg)
// writes etcd StaticPod to disk
return staticpodutil.WriteStaticPodToDisk(kubeadmconstants.Etcd, manifestDir, spec)
}
// GetEtcdPodSpec returns the etcd static Pod actualized to the context of the current MasterConfiguration
// NB. GetEtcdPodSpec methods holds the information about how kubeadm creates etcd static pod mainfests.
func GetEtcdPodSpec(cfg *kubeadmapi.MasterConfiguration) v1.Pod {
return staticpodutil.ComponentPod(v1.Container{
Name: kubeadmconstants.Etcd,
Command: getEtcdCommand(cfg),
Image: images.GetCoreImage(kubeadmconstants.Etcd, cfg.ImageRepository, "", cfg.Etcd.Image),
// Mount the etcd datadir path read-write so etcd can store data in a more persistent manner
VolumeMounts: []v1.VolumeMount{staticpodutil.NewVolumeMount(etcdVolumeName, cfg.Etcd.DataDir, false)},
LivenessProbe: staticpodutil.ComponentProbe(2379, "/health", v1.URISchemeHTTP),
}, []v1.Volume{staticpodutil.NewVolume(etcdVolumeName, cfg.Etcd.DataDir)})
}
// getEtcdCommand builds the right etcd command from the given config object
func getEtcdCommand(cfg *kubeadmapi.MasterConfiguration) []string {
defaultArguments := map[string]string{
"listen-client-urls": "http://127.0.0.1:2379",
"advertise-client-urls": "http://127.0.0.1:2379",
"data-dir": cfg.Etcd.DataDir,
}
command := []string{"etcd"}
command = append(command, staticpodutil.GetExtraParameters(cfg.Etcd.ExtraArgs, defaultArguments)...)
return command
}

View File

@ -0,0 +1,125 @@
/*
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 etcd
import (
"os"
"path/filepath"
"reflect"
"sort"
"testing"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
testutil "k8s.io/kubernetes/cmd/kubeadm/test"
)
func TestGetEtcdPodSpec(t *testing.T) {
// Creates a Master Configuration
cfg := &kubeadmapi.MasterConfiguration{
KubernetesVersion: "v1.7.0",
}
// Executes GetEtcdPodSpec
spec := GetEtcdPodSpec(cfg)
// Assert each specs refers to the right pod
if spec.Spec.Containers[0].Name != kubeadmconstants.Etcd {
t.Errorf("getKubeConfigSpecs spec for etcd contains pod %s, expectes %s", spec.Spec.Containers[0].Name, kubeadmconstants.Etcd)
}
}
func TestCreateLocalEtcdStaticPodManifestFile(t *testing.T) {
// Create temp folder for the test case
tmpdir := testutil.SetupTempDir(t)
defer os.RemoveAll(tmpdir)
// Creates a Master Configuration
cfg := &kubeadmapi.MasterConfiguration{
KubernetesVersion: "v1.7.0",
}
// Execute createStaticPodFunction
manifestPath := filepath.Join(tmpdir, kubeadmconstants.ManifestsSubDirName)
err := CreateLocalEtcdStaticPodManifestFile(manifestPath, cfg)
if err != nil {
t.Errorf("Error executing CreateEtcdStaticPodManifestFile: %v", err)
}
// Assert expected files are there
testutil.AssertFilesCount(t, manifestPath, 1)
testutil.AssertFileExists(t, manifestPath, kubeadmconstants.Etcd+".yaml")
}
func TestGetEtcdCommand(t *testing.T) {
var tests = []struct {
cfg *kubeadmapi.MasterConfiguration
expected []string
}{
{
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{DataDir: "/var/lib/etcd"},
},
expected: []string{
"etcd",
"--listen-client-urls=http://127.0.0.1:2379",
"--advertise-client-urls=http://127.0.0.1:2379",
"--data-dir=/var/lib/etcd",
},
},
{
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{
DataDir: "/var/lib/etcd",
ExtraArgs: map[string]string{
"listen-client-urls": "http://10.0.1.10:2379",
"advertise-client-urls": "http://10.0.1.10:2379",
},
},
},
expected: []string{
"etcd",
"--listen-client-urls=http://10.0.1.10:2379",
"--advertise-client-urls=http://10.0.1.10:2379",
"--data-dir=/var/lib/etcd",
},
},
{
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{DataDir: "/etc/foo"},
},
expected: []string{
"etcd",
"--listen-client-urls=http://127.0.0.1:2379",
"--advertise-client-urls=http://127.0.0.1:2379",
"--data-dir=/etc/foo",
},
},
}
for _, rt := range tests {
actual := getEtcdCommand(rt.cfg)
sort.Strings(actual)
sort.Strings(rt.expected)
if !reflect.DeepEqual(actual, rt.expected) {
t.Errorf("failed getEtcdCommand:\nexpected:\n%v\nsaw:\n%v", rt.expected, actual)
}
}
}

View File

@ -52,6 +52,7 @@ filegroup(
"//cmd/kubeadm/app/util/config:all-srcs",
"//cmd/kubeadm/app/util/kubeconfig:all-srcs",
"//cmd/kubeadm/app/util/pubkeypin:all-srcs",
"//cmd/kubeadm/app/util/staticpod:all-srcs",
"//cmd/kubeadm/app/util/token:all-srcs",
],
tags = ["automanaged"],

View File

@ -0,0 +1,48 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["utils_test.go"],
library = ":go_default_library",
tags = ["automanaged"],
deps = [
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["utils.go"],
tags = ["automanaged"],
deps = [
"//cmd/kubeadm/app/constants:go_default_library",
"//pkg/kubelet/types:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,137 @@
/*
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 staticpod
import (
"fmt"
"io/ioutil"
"os"
"github.com/ghodss/yaml"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
)
// ComponentPod returns a Pod object from the container and volume specifications
func ComponentPod(container v1.Container, volumes []v1.Volume) v1.Pod {
return v1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Name: container.Name,
Namespace: metav1.NamespaceSystem,
Annotations: map[string]string{kubetypes.CriticalPodAnnotationKey: ""},
},
Spec: v1.PodSpec{
Containers: []v1.Container{container},
HostNetwork: true,
Volumes: volumes,
},
}
}
// ComponentResources returns the v1.ResourceRequirements object needed for allocating a specified amount of the CPU
func ComponentResources(cpu string) v1.ResourceRequirements {
return v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceName(v1.ResourceCPU): resource.MustParse(cpu),
},
}
}
// ComponentProbe is a helper function building a ready v1.Probe object from some simple parameters
func ComponentProbe(port int, path string, scheme v1.URIScheme) *v1.Probe {
return &v1.Probe{
Handler: v1.Handler{
HTTPGet: &v1.HTTPGetAction{
// Host has to be set to "127.0.0.1" here due to that our static Pods are on the host's network
Host: "127.0.0.1",
Path: path,
Port: intstr.FromInt(port),
Scheme: scheme,
},
},
InitialDelaySeconds: 15,
TimeoutSeconds: 15,
FailureThreshold: 8,
}
}
// NewVolume creates a v1.Volume with a hostPath mount to the specified location
func NewVolume(name, path string) v1.Volume {
return v1.Volume{
Name: name,
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: path},
},
}
}
// NewVolumeMount creates a v1.VolumeMount to the specified location
func NewVolumeMount(name, path string, readOnly bool) v1.VolumeMount {
return v1.VolumeMount{
Name: name,
MountPath: path,
ReadOnly: readOnly,
}
}
// GetExtraParameters builds a list of flag arguments two string-string maps, one with default, base commands and one with overrides
func GetExtraParameters(overrides map[string]string, defaults map[string]string) []string {
var command []string
for k, v := range overrides {
if len(v) > 0 {
command = append(command, fmt.Sprintf("--%s=%s", k, v))
}
}
for k, v := range defaults {
if _, overrideExists := overrides[k]; !overrideExists {
command = append(command, fmt.Sprintf("--%s=%s", k, v))
}
}
return command
}
// WriteStaticPodToDisk writes a static pod file to disk
func WriteStaticPodToDisk(componentName, manifestDir string, pod v1.Pod) error {
// creates target folder if not already exists
if err := os.MkdirAll(manifestDir, 0700); err != nil {
return fmt.Errorf("failed to create directory %q: %v", manifestDir, err)
}
// writes the pod to disk
serialized, err := yaml.Marshal(pod)
if err != nil {
return fmt.Errorf("failed to marshal manifest for %q to YAML: %v", componentName, err)
}
filename := kubeadmconstants.GetStaticPodFilepath(componentName, manifestDir)
if err := ioutil.WriteFile(filename, serialized, 0700); err != nil {
return fmt.Errorf("failed to write static pod manifest file for %q (%q): %v", componentName, filename, err)
}
return nil
}

View File

@ -0,0 +1,219 @@
/*
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 staticpod
import (
"reflect"
"sort"
"testing"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
func TestComponentResources(t *testing.T) {
a := ComponentResources("250m")
if a.Requests == nil {
t.Errorf(
"failed componentResources, return value was nil",
)
}
}
func TestComponentProbe(t *testing.T) {
var tests = []struct {
port int
path string
scheme v1.URIScheme
}{
{
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
},
{
port: 2,
path: "bar",
scheme: v1.URISchemeHTTPS,
},
}
for _, rt := range tests {
actual := ComponentProbe(rt.port, rt.path, rt.scheme)
if actual.Handler.HTTPGet.Port != intstr.FromInt(rt.port) {
t.Errorf(
"failed componentProbe:\n\texpected: %v\n\t actual: %v",
rt.port,
actual.Handler.HTTPGet.Port,
)
}
if actual.Handler.HTTPGet.Path != rt.path {
t.Errorf(
"failed componentProbe:\n\texpected: %s\n\t actual: %s",
rt.path,
actual.Handler.HTTPGet.Path,
)
}
if actual.Handler.HTTPGet.Scheme != rt.scheme {
t.Errorf(
"failed componentProbe:\n\texpected: %v\n\t actual: %v",
rt.scheme,
actual.Handler.HTTPGet.Scheme,
)
}
}
}
func TestComponentPod(t *testing.T) {
var tests = []struct {
n string
}{
{
n: "foo",
},
}
for _, rt := range tests {
c := v1.Container{Name: rt.n}
v := []v1.Volume{}
actual := ComponentPod(c, v)
if actual.ObjectMeta.Name != rt.n {
t.Errorf(
"failed componentPod:\n\texpected: %s\n\t actual: %s",
rt.n,
actual.ObjectMeta.Name,
)
}
}
}
func TestNewVolume(t *testing.T) {
var tests = []struct {
name string
path string
expected v1.Volume
}{
{
name: "foo",
path: "/etc/foo",
expected: v1.Volume{
Name: "foo",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "/etc/foo"},
},
},
},
}
for _, rt := range tests {
actual := NewVolume(rt.name, rt.path)
if !reflect.DeepEqual(actual, rt.expected) {
t.Errorf(
"failed newVolume:\n\texpected: %v\n\t actual: %v",
rt.expected,
actual,
)
}
}
}
func TestNewVolumeMount(t *testing.T) {
var tests = []struct {
name string
path string
ro bool
expected v1.VolumeMount
}{
{
name: "foo",
path: "/etc/foo",
ro: false,
expected: v1.VolumeMount{
Name: "foo",
MountPath: "/etc/foo",
ReadOnly: false,
},
},
{
name: "bar",
path: "/etc/foo/bar",
ro: true,
expected: v1.VolumeMount{
Name: "bar",
MountPath: "/etc/foo/bar",
ReadOnly: true,
},
},
}
for _, rt := range tests {
actual := NewVolumeMount(rt.name, rt.path, rt.ro)
if !reflect.DeepEqual(actual, rt.expected) {
t.Errorf(
"failed newVolumeMount:\n\texpected: %v\n\t actual: %v",
rt.expected,
actual,
)
}
}
}
func TestGetExtraParameters(t *testing.T) {
var tests = []struct {
overrides map[string]string
defaults map[string]string
expected []string
}{
{
overrides: map[string]string{
"admission-control": "NamespaceLifecycle,LimitRanger",
},
defaults: map[string]string{
"admission-control": "NamespaceLifecycle",
"insecure-bind-address": "127.0.0.1",
"allow-privileged": "true",
},
expected: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
},
},
{
overrides: map[string]string{
"admission-control": "NamespaceLifecycle,LimitRanger",
},
defaults: map[string]string{
"insecure-bind-address": "127.0.0.1",
"allow-privileged": "true",
},
expected: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
},
},
}
for _, rt := range tests {
actual := GetExtraParameters(rt.overrides, rt.defaults)
sort.Strings(actual)
sort.Strings(rt.expected)
if !reflect.DeepEqual(actual, rt.expected) {
t.Errorf("failed getExtraParameters:\nexpected:\n%v\nsaw:\n%v", rt.expected, actual)
}
}
}