vendor files

This commit is contained in:
Serguei Bezverkhi
2018-01-09 13:57:14 -05:00
parent 558bc6c02a
commit 7b24313bd6
16547 changed files with 4527373 additions and 0 deletions

71
vendor/k8s.io/kubernetes/cmd/kubeadm/app/util/BUILD generated vendored Normal file
View File

@ -0,0 +1,71 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"arguments.go",
"copy.go",
"endpoint.go",
"error.go",
"etcd.go",
"marshal.go",
"template.go",
"version.go",
],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util",
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/preflight:go_default_library",
"//vendor/github.com/coreos/etcd/clientv3:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/scheme:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"arguments_test.go",
"endpoint_test.go",
"error_test.go",
"template_test.go",
"version_test.go",
],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util",
library = ":go_default_library",
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/preflight:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//cmd/kubeadm/app/util/apiclient:all-srcs",
"//cmd/kubeadm/app/util/config:all-srcs",
"//cmd/kubeadm/app/util/dryrun: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,70 @@
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",
],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient",
deps = [
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/util:go_default_library",
"//pkg/registry/core/service/ipallocator:go_default_library",
"//vendor/k8s.io/api/apps/v1beta2:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/rbac/v1: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/runtime/schema: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",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = [
"dryrunclient_test.go",
"init_dryrun_test.go",
],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient",
library = ":go_default_library",
deps = [
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/rbac/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library",
],
)

View File

@ -0,0 +1,156 @@
/*
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"
clientset "k8s.io/client-go/kubernetes"
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 {
client clientset.Interface
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, error) {
client, err := clientset.NewForConfig(config)
if err != nil {
return nil, err
}
return &ClientBackedDryRunGetter{
client: client,
dynClientPool: dynamic.NewDynamicClientPool(config),
}, nil
}
// 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)
}
// 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 err != nil {
return true, nil, err
}
// 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 err != nil {
return true, nil, err
}
// 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
}
// Client gets the backing clientset.Interface
func (clg *ClientBackedDryRunGetter) Client() clientset.Interface {
return clg.client
}
// actionToResourceClient returns the ResourceInterface for the given action
// First; the function gets the right API group interface from the resource type. The API group struct behind the interface
// returned may be cached in the dynamic client pool. Then, an APIResource object is constructed so that it can be passed to
// dynamic.Interface's Resource() function, which will give us the final ResourceInterface to query
func (clg *ClientBackedDryRunGetter) actionToResourceClient(action core.Action) (dynamic.ResourceInterface, error) {
dynIface, err := clg.dynClientPool.ClientForGroupVersionResource(action.GetResource())
if err != nil {
return nil, err
}
apiResource := &metav1.APIResource{
Name: action.GetResource().Resource,
Namespaced: action.GetNamespace() != "",
}
return dynIface.Resource(apiResource, action.GetNamespace()), nil
}
// decodeUnversionedIntoAPIObject converts the *unversioned.Unversioned object returned from the dynamic client
// to bytes; and then decodes it back _to an external api version (k8s.io/api vs k8s.io/kubernetes/pkg/api*)_ using the normal API machinery
func decodeUnversionedIntoAPIObject(action core.Action, unversionedObj runtime.Object) (runtime.Object, error) {
objBytes, err := json.Marshal(unversionedObj)
if err != nil {
return nil, err
}
newObj, err := kuberuntime.Decode(clientsetscheme.Codecs.UniversalDecoder(action.GetResource().GroupVersion()), objBytes)
if err != nil {
return nil, err
}
return newObj, nil
}

View File

@ -0,0 +1,237 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apiclient
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
clientset "k8s.io/client-go/kubernetes"
fakeclientset "k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
)
// 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, schema.GroupVersion) ([]byte, error)
// DefaultMarshalFunc is the default MarshalFunc used; uses YAML to print objects to the user
func DefaultMarshalFunc(obj runtime.Object, gv schema.GroupVersion) ([]byte, error) {
return kubeadmutil.MarshalToYaml(obj, gv)
}
// 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, action.GetResource().GroupVersion())
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, action.GetResource().GroupVersion())
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(), action.GetResource().GroupVersion())
if err == nil {
fmt.Println("[dryrun] Attached object:")
PrintBytesWithLinePrefix(w, objBytes, "\t")
}
}
patchAction, ok := action.(core.PatchAction)
if ok {
// Replace all occurences of \" with a simple " when printing
fmt.Fprintf(w, "[dryrun] Attached patch:\n\t%s\n", strings.Replace(string(patchAction.GetPatch()), `\"`, `"`, -1))
}
}
// PrintBytesWithLinePrefix prints objBytes to writer w with linePrefix in the beginning of every line
func PrintBytesWithLinePrefix(w io.Writer, objBytes []byte, linePrefix string) {
scanner := bufio.NewScanner(bytes.NewReader(objBytes))
for scanner.Scan() {
fmt.Fprintf(w, "%s%s\n", linePrefix, scanner.Text())
}
}

View File

@ -0,0 +1,102 @@
/*
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/v1"
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/v1"
[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"
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
name: foo
spec:
clusterIP: 1.1.1.1
status:
loadBalancer: {}
`),
},
{
action: core.NewPatchAction(schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "default", "my-node", []byte(`{"spec":{"taints":[{"key": "foo", "value": "bar"}]}}`)),
expectedBytes: []byte(`[dryrun] Would perform action PATCH on resource "nodes" in API group "core/v1"
[dryrun] Resource name: "my-node"
[dryrun] Attached patch:
{"spec":{"taints":[{"key": "foo", "value": "bar"}]}}
`),
},
{
action: core.NewDeleteAction(schema.GroupVersionResource{Version: "v1", Resource: "pods"}, "default", "my-pod"),
expectedBytes: []byte(`[dryrun] Would perform action DELETE on resource "pods" in API group "core/v1"
[dryrun] Resource name: "my-pod"
`),
},
}
for _, rt := range tests {
rt.buf = bytes.NewBufferString("")
logDryRunAction(rt.action, rt.buf, DefaultMarshalFunc)
actualBytes := rt.buf.Bytes()
if !bytes.Equal(actualBytes, rt.expectedBytes) {
t.Errorf(
"failed LogDryRunAction:\n\texpected bytes: %q\n\t actual: %q",
rt.expectedBytes,
actualBytes,
)
}
}
}

View File

@ -0,0 +1,173 @@
/*
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"
apps "k8s.io/api/apps/v1beta2"
"k8s.io/api/core/v1"
rbac "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientset "k8s.io/client-go/kubernetes"
)
// TODO: We should invent a dynamic mechanism for this using the dynamic client instead of hard-coding these functions per-type
// TODO: We may want to retry if .Update() fails on 409 Conflict
// CreateOrUpdateConfigMap creates a ConfigMap if the target resource doesn't exist. If the resource exists already, this function will update the resource instead.
func CreateOrUpdateConfigMap(client clientset.Interface, cm *v1.ConfigMap) error {
if _, err := client.CoreV1().ConfigMaps(cm.ObjectMeta.Namespace).Create(cm); err != nil {
if !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unable to create configmap: %v", err)
}
if _, err := client.CoreV1().ConfigMaps(cm.ObjectMeta.Namespace).Update(cm); err != nil {
return fmt.Errorf("unable to update configmap: %v", err)
}
}
return nil
}
// CreateOrUpdateSecret creates a Secret if the target resource doesn't exist. If the resource exists already, this function will update the resource instead.
func CreateOrUpdateSecret(client clientset.Interface, secret *v1.Secret) error {
if _, err := client.CoreV1().Secrets(secret.ObjectMeta.Namespace).Create(secret); err != nil {
if !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unable to create secret: %v", err)
}
if _, err := client.CoreV1().Secrets(secret.ObjectMeta.Namespace).Update(secret); err != nil {
return fmt.Errorf("unable to update secret: %v", err)
}
}
return nil
}
// CreateOrUpdateServiceAccount creates a ServiceAccount if the target resource doesn't exist. If the resource exists already, this function will update the resource instead.
func CreateOrUpdateServiceAccount(client clientset.Interface, sa *v1.ServiceAccount) error {
if _, err := client.CoreV1().ServiceAccounts(sa.ObjectMeta.Namespace).Create(sa); err != nil {
// Note: We don't run .Update here afterwards as that's probably not required
// Only thing that could be updated is annotations/labels in .metadata, but we don't use that currently
if !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unable to create serviceaccount: %v", err)
}
}
return nil
}
// CreateOrUpdateDeployment creates a Deployment if the target resource doesn't exist. If the resource exists already, this function will update the resource instead.
func CreateOrUpdateDeployment(client clientset.Interface, deploy *apps.Deployment) error {
if _, err := client.AppsV1beta2().Deployments(deploy.ObjectMeta.Namespace).Create(deploy); err != nil {
if !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unable to create deployment: %v", err)
}
if _, err := client.AppsV1beta2().Deployments(deploy.ObjectMeta.Namespace).Update(deploy); err != nil {
return fmt.Errorf("unable to update deployment: %v", err)
}
}
return nil
}
// CreateOrUpdateDaemonSet creates a DaemonSet if the target resource doesn't exist. If the resource exists already, this function will update the resource instead.
func CreateOrUpdateDaemonSet(client clientset.Interface, ds *apps.DaemonSet) error {
if _, err := client.AppsV1beta2().DaemonSets(ds.ObjectMeta.Namespace).Create(ds); err != nil {
if !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unable to create daemonset: %v", err)
}
if _, err := client.AppsV1beta2().DaemonSets(ds.ObjectMeta.Namespace).Update(ds); err != nil {
return fmt.Errorf("unable to update daemonset: %v", err)
}
}
return nil
}
// DeleteDaemonSetForeground deletes the specified DaemonSet in foreground mode; i.e. it blocks until/makes sure all the managed Pods are deleted
func DeleteDaemonSetForeground(client clientset.Interface, namespace, name string) error {
foregroundDelete := metav1.DeletePropagationForeground
deleteOptions := &metav1.DeleteOptions{
PropagationPolicy: &foregroundDelete,
}
return client.AppsV1beta2().DaemonSets(namespace).Delete(name, deleteOptions)
}
// DeleteDeploymentForeground deletes the specified Deployment in foreground mode; i.e. it blocks until/makes sure all the managed Pods are deleted
func DeleteDeploymentForeground(client clientset.Interface, namespace, name string) error {
foregroundDelete := metav1.DeletePropagationForeground
deleteOptions := &metav1.DeleteOptions{
PropagationPolicy: &foregroundDelete,
}
return client.AppsV1beta2().Deployments(namespace).Delete(name, deleteOptions)
}
// CreateOrUpdateRole creates a Role if the target resource doesn't exist. If the resource exists already, this function will update the resource instead.
func CreateOrUpdateRole(client clientset.Interface, role *rbac.Role) error {
if _, err := client.RbacV1().Roles(role.ObjectMeta.Namespace).Create(role); err != nil {
if !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unable to create RBAC role: %v", err)
}
if _, err := client.RbacV1().Roles(role.ObjectMeta.Namespace).Update(role); err != nil {
return fmt.Errorf("unable to update RBAC role: %v", err)
}
}
return nil
}
// CreateOrUpdateRoleBinding creates a RoleBinding if the target resource doesn't exist. If the resource exists already, this function will update the resource instead.
func CreateOrUpdateRoleBinding(client clientset.Interface, roleBinding *rbac.RoleBinding) error {
if _, err := client.RbacV1().RoleBindings(roleBinding.ObjectMeta.Namespace).Create(roleBinding); err != nil {
if !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unable to create RBAC rolebinding: %v", err)
}
if _, err := client.RbacV1().RoleBindings(roleBinding.ObjectMeta.Namespace).Update(roleBinding); err != nil {
return fmt.Errorf("unable to update RBAC rolebinding: %v", err)
}
}
return nil
}
// CreateOrUpdateClusterRole creates a ClusterRole if the target resource doesn't exist. If the resource exists already, this function will update the resource instead.
func CreateOrUpdateClusterRole(client clientset.Interface, clusterRole *rbac.ClusterRole) error {
if _, err := client.RbacV1().ClusterRoles().Create(clusterRole); err != nil {
if !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unable to create RBAC clusterrole: %v", err)
}
if _, err := client.RbacV1().ClusterRoles().Update(clusterRole); err != nil {
return fmt.Errorf("unable to update RBAC clusterrole: %v", err)
}
}
return nil
}
// CreateOrUpdateClusterRoleBinding creates a ClusterRoleBinding if the target resource doesn't exist. If the resource exists already, this function will update the resource instead.
func CreateOrUpdateClusterRoleBinding(client clientset.Interface, clusterRoleBinding *rbac.ClusterRoleBinding) error {
if _, err := client.RbacV1().ClusterRoleBindings().Create(clusterRoleBinding); err != nil {
if !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unable to create RBAC clusterrolebinding: %v", err)
}
if _, err := client.RbacV1().ClusterRoleBindings().Update(clusterRoleBinding); err != nil {
return fmt.Errorf("unable to update RBAC clusterrolebinding: %v", err)
}
}
return nil
}

View File

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

View File

@ -0,0 +1,126 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apiclient
import (
"bytes"
"encoding/json"
"testing"
rbac "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
core "k8s.io/client-go/testing"
)
func TestHandleGetAction(t *testing.T) {
masterName := "master-foo"
serviceSubnet := "10.96.0.1/12"
idr := NewInitDryRunGetter(masterName, serviceSubnet)
var tests = []struct {
action core.GetActionImpl
expectedHandled bool
expectedObjectJSON []byte
expectedErr bool
}{
{
action: core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "services"}, "default", "kubernetes"),
expectedHandled: true,
expectedObjectJSON: []byte(`{"metadata":{"name":"kubernetes","namespace":"default","creationTimestamp":null,"labels":{"component":"apiserver","provider":"kubernetes"}},"spec":{"ports":[{"name":"https","port":443,"targetPort":6443}],"clusterIP":"10.96.0.1"},"status":{"loadBalancer":{}}}`),
expectedErr: false,
},
{
action: core.NewRootGetAction(schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, masterName),
expectedHandled: true,
expectedObjectJSON: []byte(`{"metadata":{"name":"master-foo","creationTimestamp":null,"labels":{"kubernetes.io/hostname":"master-foo"}},"spec":{"externalID":"master-foo"},"status":{"daemonEndpoints":{"kubeletEndpoint":{"Port":0}},"nodeInfo":{"machineID":"","systemUUID":"","bootID":"","kernelVersion":"","osImage":"","containerRuntimeVersion":"","kubeletVersion":"","kubeProxyVersion":"","operatingSystem":"","architecture":""}}}`),
expectedErr: false,
},
{
action: core.NewRootGetAction(schema.GroupVersionResource{Group: rbac.GroupName, Version: rbac.SchemeGroupVersion.Version, Resource: "clusterrolebindings"}, "system:node"),
expectedHandled: true,
expectedObjectJSON: []byte(``),
expectedErr: true, // we expect a NotFound error here
},
{
action: core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, "kube-system", "bootstrap-token-abcdef"),
expectedHandled: true,
expectedObjectJSON: []byte(``),
expectedErr: true, // we expect a NotFound error here
},
{ // an ask for a kubernetes service in the _kube-system_ ns should not be answered
action: core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "services"}, "kube-system", "kubernetes"),
expectedHandled: false,
expectedObjectJSON: []byte(``),
expectedErr: false,
},
{ // an ask for an other service than kubernetes should not be answered
action: core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "services"}, "default", "my-other-service"),
expectedHandled: false,
expectedObjectJSON: []byte(``),
expectedErr: false,
},
{ // an ask for an other node than the master should not be answered
action: core.NewRootGetAction(schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "other-node"),
expectedHandled: false,
expectedObjectJSON: []byte(``),
expectedErr: false,
},
{ // an ask for a secret in any other ns than kube-system should not be answered
action: core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, "default", "bootstrap-token-abcdef"),
expectedHandled: false,
expectedObjectJSON: []byte(``),
expectedErr: false,
},
}
for _, rt := range tests {
handled, obj, actualErr := idr.HandleGetAction(rt.action)
objBytes := []byte(``)
if obj != nil {
var err error
objBytes, err = json.Marshal(obj)
if err != nil {
t.Fatalf("couldn't marshal returned object")
}
}
if handled != rt.expectedHandled {
t.Errorf(
"failed HandleGetAction:\n\texpected handled: %t\n\t actual: %t %v",
rt.expectedHandled,
handled,
rt.action,
)
}
if !bytes.Equal(objBytes, rt.expectedObjectJSON) {
t.Errorf(
"failed HandleGetAction:\n\texpected object: %q\n\t actual: %q",
rt.expectedObjectJSON,
objBytes,
)
}
if (actualErr != nil) != rt.expectedErr {
t.Errorf(
"failed HandleGetAction:\n\texpected error: %t\n\t actual: %t %v",
rt.expectedErr,
(actualErr != nil),
rt.action,
)
}
}
}

View File

@ -0,0 +1,260 @@
/*
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 (
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
)
// Waiter is an interface for waiting for criterias in Kubernetes to happen
type Waiter interface {
// WaitForAPI waits for the API Server's /healthz endpoint to become "ok"
WaitForAPI() error
// WaitForPodsWithLabel waits for Pods in the kube-system namespace to become Ready
WaitForPodsWithLabel(kvLabel string) error
// WaitForPodToDisappear waits for the given Pod in the kube-system namespace to be deleted
WaitForPodToDisappear(staticPodName string) error
// WaitForStaticPodSingleHash fetches sha256 hash for the control plane static pod
WaitForStaticPodSingleHash(nodeName string, component string) (string, error)
// WaitForStaticPodControlPlaneHashes fetches sha256 hashes for the control plane static pods
WaitForStaticPodControlPlaneHashes(nodeName string) (map[string]string, error)
// WaitForStaticPodControlPlaneHashChange waits for the given static pod component's static pod hash to get updated.
// By doing that we can be sure that the kubelet has restarted the given Static Pod
WaitForStaticPodControlPlaneHashChange(nodeName, component, previousHash string) error
// WaitForHealthyKubelet blocks until the kubelet /healthz endpoint returns 'ok'
WaitForHealthyKubelet(initalTimeout time.Duration, healthzEndpoint string) error
// SetTimeout adjusts the timeout to the specified duration
SetTimeout(timeout time.Duration)
}
// KubeWaiter is an implementation of Waiter that is backed by a Kubernetes client
type KubeWaiter struct {
client clientset.Interface
timeout time.Duration
writer io.Writer
}
// NewKubeWaiter returns a new Waiter object that talks to the given Kubernetes cluster
func NewKubeWaiter(client clientset.Interface, timeout time.Duration, writer io.Writer) Waiter {
return &KubeWaiter{
client: client,
timeout: timeout,
writer: writer,
}
}
// WaitForAPI waits for the API Server's /healthz endpoint to report "ok"
func (w *KubeWaiter) WaitForAPI() error {
start := time.Now()
return wait.PollImmediate(constants.APICallRetryInterval, w.timeout, func() (bool, error) {
healthStatus := 0
w.client.Discovery().RESTClient().Get().AbsPath("/healthz").Do().StatusCode(&healthStatus)
if healthStatus != http.StatusOK {
return false, nil
}
fmt.Printf("[apiclient] All control plane components are healthy after %f seconds\n", time.Since(start).Seconds())
return true, nil
})
}
// WaitForPodsWithLabel will lookup pods with the given label and wait until they are all
// reporting status as running.
func (w *KubeWaiter) WaitForPodsWithLabel(kvLabel string) error {
lastKnownPodNumber := -1
return wait.PollImmediate(constants.APICallRetryInterval, w.timeout, func() (bool, error) {
listOpts := metav1.ListOptions{LabelSelector: kvLabel}
pods, err := w.client.CoreV1().Pods(metav1.NamespaceSystem).List(listOpts)
if err != nil {
fmt.Fprintf(w.writer, "[apiclient] Error getting Pods with label selector %q [%v]\n", kvLabel, err)
return false, nil
}
if lastKnownPodNumber != len(pods.Items) {
fmt.Fprintf(w.writer, "[apiclient] Found %d Pods for label selector %s\n", len(pods.Items), kvLabel)
lastKnownPodNumber = len(pods.Items)
}
if len(pods.Items) == 0 {
return false, nil
}
for _, pod := range pods.Items {
if pod.Status.Phase != v1.PodRunning {
return false, nil
}
}
return true, nil
})
}
// WaitForPodToDisappear blocks until it timeouts or gets a "NotFound" response from the API Server when getting the Static Pod in question
func (w *KubeWaiter) WaitForPodToDisappear(podName string) error {
return wait.PollImmediate(constants.APICallRetryInterval, w.timeout, func() (bool, error) {
_, err := w.client.CoreV1().Pods(metav1.NamespaceSystem).Get(podName, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
fmt.Printf("[apiclient] The old Pod %q is now removed (which is desired)\n", podName)
return true, nil
}
return false, nil
})
}
// WaitForHealthyKubelet blocks until the kubelet /healthz endpoint returns 'ok'
func (w *KubeWaiter) WaitForHealthyKubelet(initalTimeout time.Duration, healthzEndpoint string) error {
time.Sleep(initalTimeout)
return TryRunCommand(func() error {
resp, err := http.Get(healthzEndpoint)
if err != nil {
fmt.Printf("[kubelet-check] It seems like the kubelet isn't running or healthy.\n")
fmt.Printf("[kubelet-check] The HTTP call equal to 'curl -sSL %s' failed with error: %v.\n", healthzEndpoint, err)
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("[kubelet-check] It seems like the kubelet isn't running or healthy.")
fmt.Printf("[kubelet-check] The HTTP call equal to 'curl -sSL %s' returned HTTP code %d\n", healthzEndpoint, resp.StatusCode)
return fmt.Errorf("the kubelet healthz endpoint is unhealthy")
}
return nil
}, 5) // a failureThreshold of five means waiting for a total of 155 seconds
}
// SetTimeout adjusts the timeout to the specified duration
func (w *KubeWaiter) SetTimeout(timeout time.Duration) {
w.timeout = timeout
}
// WaitForStaticPodControlPlaneHashes blocks until it timeouts or gets a hash map for all components and their Static Pods
func (w *KubeWaiter) WaitForStaticPodControlPlaneHashes(nodeName string) (map[string]string, error) {
componentHash := ""
var err error
mirrorPodHashes := map[string]string{}
for _, component := range constants.MasterComponents {
err = wait.PollImmediate(constants.APICallRetryInterval, w.timeout, func() (bool, error) {
componentHash, err = getStaticPodSingleHash(w.client, nodeName, component)
if err != nil {
return false, nil
}
return true, nil
})
if err != nil {
return nil, err
}
mirrorPodHashes[component] = componentHash
}
return mirrorPodHashes, nil
}
// WaitForStaticPodSingleHash blocks until it timeouts or gets a hash for a single component and its Static Pod
func (w *KubeWaiter) WaitForStaticPodSingleHash(nodeName string, component string) (string, error) {
componentPodHash := ""
var err error
err = wait.PollImmediate(constants.APICallRetryInterval, w.timeout, func() (bool, error) {
componentPodHash, err = getStaticPodSingleHash(w.client, nodeName, component)
if err != nil {
return false, nil
}
return true, nil
})
return componentPodHash, err
}
// WaitForStaticPodControlPlaneHashChange blocks until it timeouts or notices that the Mirror Pod (for the Static Pod, respectively) has changed
// This implicitely means this function blocks until the kubelet has restarted the Static Pod in question
func (w *KubeWaiter) WaitForStaticPodControlPlaneHashChange(nodeName, component, previousHash string) error {
return wait.PollImmediate(constants.APICallRetryInterval, w.timeout, func() (bool, error) {
hashes, err := getStaticPodControlPlaneHashes(w.client, nodeName)
if err != nil {
return false, nil
}
// We should continue polling until the UID changes
if hashes[component] == previousHash {
return false, nil
}
return true, nil
})
}
// getStaticPodControlPlaneHashes computes hashes for all the control plane's Static Pod resources
func getStaticPodControlPlaneHashes(client clientset.Interface, nodeName string) (map[string]string, error) {
mirrorPodHashes := map[string]string{}
for _, component := range constants.MasterComponents {
hash, err := getStaticPodSingleHash(client, nodeName, component)
if err != nil {
return nil, err
}
mirrorPodHashes[component] = hash
}
return mirrorPodHashes, nil
}
// getStaticSinglePodHash computes hashes for a single Static Pod resource
func getStaticPodSingleHash(client clientset.Interface, nodeName string, component string) (string, error) {
staticPodName := fmt.Sprintf("%s-%s", component, nodeName)
staticPod, err := client.CoreV1().Pods(metav1.NamespaceSystem).Get(staticPodName, metav1.GetOptions{})
if err != nil {
return "", err
}
podBytes, err := json.Marshal(staticPod)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", sha256.Sum256(podBytes)), nil
}
// TryRunCommand runs a function a maximum of failureThreshold times, and retries on error. If failureThreshold is hit; the last error is returned
func TryRunCommand(f func() error, failureThreshold int) error {
backoff := wait.Backoff{
Duration: 5 * time.Second,
Factor: 2, // double the timeout for every failure
Steps: failureThreshold,
}
return wait.ExponentialBackoff(backoff, func() (bool, error) {
err := f()
if err != nil {
// Retry until the timeout
return false, nil
}
// The last f() call was a success, return cleanly
return true, nil
})
}

View File

@ -0,0 +1,96 @@
/*
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 util
import (
"fmt"
"strings"
)
// BuildArgumentListFromMap takes two string-string maps, one with the base arguments and one with optional override arguments
func BuildArgumentListFromMap(baseArguments map[string]string, overrideArguments map[string]string) []string {
var command []string
for k, v := range overrideArguments {
// values of "" are allowed as well
command = append(command, fmt.Sprintf("--%s=%s", k, v))
}
for k, v := range baseArguments {
if _, overrideExists := overrideArguments[k]; !overrideExists {
command = append(command, fmt.Sprintf("--%s=%s", k, v))
}
}
return command
}
// ParseArgumentListToMap parses a CLI argument list in the form "--foo=bar" to a string-string map
func ParseArgumentListToMap(arguments []string) map[string]string {
resultingMap := map[string]string{}
for i, arg := range arguments {
key, val, err := parseArgument(arg)
// Ignore if the first argument doesn't satisfy the criteria, it's most often the binary name
// Warn in all other cases, but don't error out. This can happen only if the user has edited the argument list by hand, so they might know what they are doing
if err != nil {
if i != 0 {
fmt.Printf("[kubeadm] WARNING: The component argument %q could not be parsed correctly. The argument must be of the form %q. Skipping...", arg, "--")
}
continue
}
resultingMap[key] = val
}
return resultingMap
}
// ReplaceArgument gets a command list; converts it to a map for easier modification, runs the provided function that
// returns a new modified map, and then converts the map back to a command string slice
func ReplaceArgument(command []string, argMutateFunc func(map[string]string) map[string]string) []string {
argMap := ParseArgumentListToMap(command)
// Save the first command (the executable) if we're sure it's not an argument (i.e. no --)
var newCommand []string
if len(command) > 0 && !strings.HasPrefix(command[0], "--") {
newCommand = append(newCommand, command[0])
}
newArgMap := argMutateFunc(argMap)
newCommand = append(newCommand, BuildArgumentListFromMap(newArgMap, map[string]string{})...)
return newCommand
}
// parseArgument parses the argument "--foo=bar" to "foo" and "bar"
func parseArgument(arg string) (string, string, error) {
if !strings.HasPrefix(arg, "--") {
return "", "", fmt.Errorf("the argument should start with '--'")
}
if !strings.Contains(arg, "=") {
return "", "", fmt.Errorf("the argument should have a '=' between the flag and the value")
}
// Remove the starting --
arg = strings.TrimPrefix(arg, "--")
// Split the string on =. Return only two substrings, since we want only key/value, but the value can include '=' as well
keyvalSlice := strings.SplitN(arg, "=", 2)
// Make sure both a key and value is present
if len(keyvalSlice) != 2 {
return "", "", fmt.Errorf("the argument must have both a key and a value")
}
if len(keyvalSlice[0]) == 0 {
return "", "", fmt.Errorf("the argument must have a key")
}
return keyvalSlice[0], keyvalSlice[1], nil
}

View File

@ -0,0 +1,341 @@
/*
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 util
import (
"reflect"
"sort"
"testing"
)
func TestBuildArgumentListFromMap(t *testing.T) {
var tests = []struct {
base map[string]string
overrides map[string]string
expected []string
}{
{ // override an argument from the base
base: map[string]string{
"admission-control": "NamespaceLifecycle",
"insecure-bind-address": "127.0.0.1",
"allow-privileged": "true",
},
overrides: map[string]string{
"admission-control": "NamespaceLifecycle,LimitRanger",
},
expected: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
},
},
{ // add an argument that is not in base
base: map[string]string{
"insecure-bind-address": "127.0.0.1",
"allow-privileged": "true",
},
overrides: map[string]string{
"admission-control": "NamespaceLifecycle,LimitRanger",
},
expected: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
},
},
{ // allow empty strings in base
base: map[string]string{
"insecure-bind-address": "127.0.0.1",
"allow-privileged": "true",
"something-that-allows-empty-string": "",
},
overrides: map[string]string{
"admission-control": "NamespaceLifecycle,LimitRanger",
},
expected: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
"--something-that-allows-empty-string=",
},
},
{ // allow empty strings in overrides
base: map[string]string{
"insecure-bind-address": "127.0.0.1",
"allow-privileged": "true",
"something-that-allows-empty-string": "foo",
},
overrides: map[string]string{
"admission-control": "NamespaceLifecycle,LimitRanger",
"something-that-allows-empty-string": "",
},
expected: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
"--something-that-allows-empty-string=",
},
},
}
for _, rt := range tests {
actual := BuildArgumentListFromMap(rt.base, rt.overrides)
sort.Strings(actual)
sort.Strings(rt.expected)
if !reflect.DeepEqual(actual, rt.expected) {
t.Errorf("failed BuildArgumentListFromMap:\nexpected:\n%v\nsaw:\n%v", rt.expected, actual)
}
}
}
func TestParseArgumentListToMap(t *testing.T) {
var tests = []struct {
args []string
expectedMap map[string]string
}{
{
// normal case
args: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
},
expectedMap: map[string]string{
"admission-control": "NamespaceLifecycle,LimitRanger",
"insecure-bind-address": "127.0.0.1",
"allow-privileged": "true",
},
},
{
// test that feature-gates is working
args: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
"--feature-gates=EnableFoo=true,EnableBar=false",
},
expectedMap: map[string]string{
"admission-control": "NamespaceLifecycle,LimitRanger",
"insecure-bind-address": "127.0.0.1",
"allow-privileged": "true",
"feature-gates": "EnableFoo=true,EnableBar=false",
},
},
{
// test that a binary can be the first arg
args: []string{
"kube-apiserver",
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
"--feature-gates=EnableFoo=true,EnableBar=false",
},
expectedMap: map[string]string{
"admission-control": "NamespaceLifecycle,LimitRanger",
"insecure-bind-address": "127.0.0.1",
"allow-privileged": "true",
"feature-gates": "EnableFoo=true,EnableBar=false",
},
},
}
for _, rt := range tests {
actualMap := ParseArgumentListToMap(rt.args)
if !reflect.DeepEqual(actualMap, rt.expectedMap) {
t.Errorf("failed ParseArgumentListToMap:\nexpected:\n%v\nsaw:\n%v", rt.expectedMap, actualMap)
}
}
}
func TestReplaceArgument(t *testing.T) {
var tests = []struct {
args []string
mutateFunc func(map[string]string) map[string]string
expectedArgs []string
}{
{
// normal case
args: []string{
"kube-apiserver",
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
},
mutateFunc: func(argMap map[string]string) map[string]string {
argMap["admission-control"] = "NamespaceLifecycle,LimitRanger,ResourceQuota"
return argMap
},
expectedArgs: []string{
"kube-apiserver",
"--admission-control=NamespaceLifecycle,LimitRanger,ResourceQuota",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
},
},
{
// normal case
args: []string{
"kube-apiserver",
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
},
mutateFunc: func(argMap map[string]string) map[string]string {
argMap["new-arg-here"] = "foo"
return argMap
},
expectedArgs: []string{
"kube-apiserver",
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
"--new-arg-here=foo",
},
},
}
for _, rt := range tests {
actualArgs := ReplaceArgument(rt.args, rt.mutateFunc)
sort.Strings(actualArgs)
sort.Strings(rt.expectedArgs)
if !reflect.DeepEqual(actualArgs, rt.expectedArgs) {
t.Errorf("failed ReplaceArgument:\nexpected:\n%v\nsaw:\n%v", rt.expectedArgs, actualArgs)
}
}
}
func TestRoundtrip(t *testing.T) {
var tests = []struct {
args []string
}{
{
// normal case
args: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
},
},
{
// test that feature-gates is working
args: []string{
"--admission-control=NamespaceLifecycle,LimitRanger",
"--insecure-bind-address=127.0.0.1",
"--allow-privileged=true",
"--feature-gates=EnableFoo=true,EnableBar=false",
},
},
}
for _, rt := range tests {
// These two methods should be each other's opposite functions, test that by chaining the methods and see if you get the same result back
actual := BuildArgumentListFromMap(ParseArgumentListToMap(rt.args), map[string]string{})
sort.Strings(actual)
sort.Strings(rt.args)
if !reflect.DeepEqual(actual, rt.args) {
t.Errorf("failed TestRoundtrip:\nexpected:\n%v\nsaw:\n%v", rt.args, actual)
}
}
}
func TestParseArgument(t *testing.T) {
var tests = []struct {
arg string
expectedKey string
expectedVal string
expectedErr bool
}{
{
// cannot be empty
arg: "",
expectedErr: true,
},
{
// must contain -- and =
arg: "a",
expectedErr: true,
},
{
// must contain -- and =
arg: "a-z",
expectedErr: true,
},
{
// must contain --
arg: "a=b",
expectedErr: true,
},
{
// must contain a key
arg: "--=b",
expectedErr: true,
},
{
// can contain key but no value
arg: "--a=",
expectedKey: "a",
expectedVal: "",
expectedErr: false,
},
{
// simple case
arg: "--a=b",
expectedKey: "a",
expectedVal: "b",
expectedErr: false,
},
{
// keys/values with '-' should be supported
arg: "--very-long-flag-name=some-value",
expectedKey: "very-long-flag-name",
expectedVal: "some-value",
expectedErr: false,
},
{
// numbers should be handled correctly
arg: "--some-number=0.2",
expectedKey: "some-number",
expectedVal: "0.2",
expectedErr: false,
},
{
// lists should be handled correctly
arg: "--admission-control=foo,bar,baz",
expectedKey: "admission-control",
expectedVal: "foo,bar,baz",
expectedErr: false,
},
{
// more than one '=' should be allowed
arg: "--feature-gates=EnableFoo=true,EnableBar=false",
expectedKey: "feature-gates",
expectedVal: "EnableFoo=true,EnableBar=false",
expectedErr: false,
},
}
for _, rt := range tests {
key, val, actual := parseArgument(rt.arg)
if (actual != nil) != rt.expectedErr {
t.Errorf("failed parseArgument:\nexpected error:\n%t\nsaw error:\n%v", rt.expectedErr, actual)
}
if (key != rt.expectedKey) || (val != rt.expectedVal) {
t.Errorf("failed parseArgument:\nexpected key: %s\nsaw key: %s\nexpected value: %s\nsaw value: %s", rt.expectedKey, key, rt.expectedVal, val)
}
}
}

View File

@ -0,0 +1,46 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["masterconfig.go"],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/config",
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/util:go_default_library",
"//cmd/kubeadm/app/util/token:go_default_library",
"//pkg/api/legacyscheme:go_default_library",
"//pkg/util/node:go_default_library",
"//pkg/util/version:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["masterconfig_test.go"],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/config",
library = ":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,143 @@
/*
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 cmd
import (
"fmt"
"io/ioutil"
"net"
"k8s.io/apimachinery/pkg/runtime"
netutil "k8s.io/apimachinery/pkg/util/net"
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"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/util/node"
"k8s.io/kubernetes/pkg/util/version"
)
// SetInitDynamicDefaults checks and sets conifugration values for Master node
func SetInitDynamicDefaults(cfg *kubeadmapi.MasterConfiguration) error {
// Choose the right address for the API Server to advertise. If the advertise address is localhost or 0.0.0.0, the default interface's IP address is used
// This is the same logic as the API Server uses
ip, err := netutil.ChooseBindAddress(net.ParseIP(cfg.API.AdvertiseAddress))
if err != nil {
return err
}
cfg.API.AdvertiseAddress = ip.String()
ip = net.ParseIP(cfg.API.AdvertiseAddress)
if ip.To4() != nil {
cfg.KubeProxy.Config.BindAddress = kubeadmapiext.DefaultProxyBindAddressv4
} else {
cfg.KubeProxy.Config.BindAddress = kubeadmapiext.DefaultProxyBindAddressv6
}
// Resolve possible version labels and validate version string
err = NormalizeKubernetesVersion(cfg)
if err != nil {
return err
}
if cfg.Token == "" {
var err error
cfg.Token, err = tokenutil.GenerateToken()
if err != nil {
return fmt.Errorf("couldn't generate random token: %v", err)
}
}
cfg.NodeName = node.GetHostname(cfg.NodeName)
return nil
}
// TryLoadMasterConfiguration tries to loads a Master configuration from the given file (if defined)
func TryLoadMasterConfiguration(cfgPath string, cfg *kubeadmapiext.MasterConfiguration) error {
if cfgPath != "" {
b, err := ioutil.ReadFile(cfgPath)
if err != nil {
return fmt.Errorf("unable to read config from %q [%v]", cfgPath, err)
}
if err := runtime.DecodeInto(legacyscheme.Codecs.UniversalDecoder(), b, cfg); err != nil {
return fmt.Errorf("unable to decode config from %q [%v]", cfgPath, err)
}
}
return nil
}
// ConfigFileAndDefaultsToInternalConfig takes a path to a config file and a versioned configuration that can serve as the default config
// If cfgPath is specified, defaultversionedcfg will always get overridden. Otherwise, the default config (often populated by flags) will be used.
// Then the external, versioned configuration is defaulted and converted to the internal type.
// Right thereafter, the configuration is defaulted again with dynamic values (like IP addresses of a machine, etc)
// Lastly, the internal config is validated and returned.
func ConfigFileAndDefaultsToInternalConfig(cfgPath string, defaultversionedcfg *kubeadmapiext.MasterConfiguration) (*kubeadmapi.MasterConfiguration, error) {
internalcfg := &kubeadmapi.MasterConfiguration{}
// Loads configuration from config file, if provided
// Nb. --config overrides command line flags
if err := TryLoadMasterConfiguration(cfgPath, defaultversionedcfg); err != nil {
return nil, err
}
// Takes passed flags into account; the defaulting is executed once again enforcing assignement of
// static default values to cfg only for values not provided with flags
legacyscheme.Scheme.Default(defaultversionedcfg)
legacyscheme.Scheme.Convert(defaultversionedcfg, internalcfg, nil)
// Applies dynamic defaults to settings not provided with flags
if err := SetInitDynamicDefaults(internalcfg); err != nil {
return nil, err
}
// Validates cfg (flags/configs + defaults + dynamic defaults)
if err := validation.ValidateMasterConfiguration(internalcfg).ToAggregate(); err != nil {
return nil, err
}
return internalcfg, nil
}
// NormalizeKubernetesVersion resolves version labels, sets alternative
// image registry if requested for CI builds, and validates minimal
// version that kubeadm supports.
func NormalizeKubernetesVersion(cfg *kubeadmapi.MasterConfiguration) error {
// Requested version is automatic CI build, thus use KubernetesCI Image Repository for core images
if kubeadmutil.KubernetesIsCIVersion(cfg.KubernetesVersion) {
cfg.CIImageRepository = kubeadmconstants.DefaultCIImageRepository
}
// Parse and validate the version argument and resolve possible CI version labels
ver, err := kubeadmutil.KubernetesReleaseVersion(cfg.KubernetesVersion)
if err != nil {
return err
}
cfg.KubernetesVersion = ver
// Parse the given kubernetes version and make sure it's higher than the lowest supported
k8sVersion, err := version.ParseSemantic(cfg.KubernetesVersion)
if err != nil {
return fmt.Errorf("couldn't parse kubernetes version %q: %v", cfg.KubernetesVersion, err)
}
if k8sVersion.LessThan(kubeadmconstants.MinimumControlPlaneVersion) {
return fmt.Errorf("this version of kubeadm only supports deploying clusters with the control plane version >= %s. Current version: %s", kubeadmconstants.MinimumControlPlaneVersion.String(), cfg.KubernetesVersion)
}
return nil
}

View File

@ -0,0 +1,17 @@
/*
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 cmd

31
vendor/k8s.io/kubernetes/cmd/kubeadm/app/util/copy.go generated vendored Normal file
View File

@ -0,0 +1,31 @@
/*
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 util
import (
"os/exec"
)
// CopyDir copies the content of a folder
func CopyDir(src string, dst string) error {
cmd := exec.Command("cp", "-r", src, dst)
err := cmd.Run()
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,28 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["dryrun.go"],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun",
visibility = ["//visibility:public"],
deps = [
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/util/apiclient:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,128 @@
/*
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 dryrun
import (
"fmt"
"io"
"io/ioutil"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
)
// FileToPrint represents a temporary file on disk that might want to be aliased when printing
// Useful for things like loading a file from /tmp/ but saying to the user "Would write file foo to /etc/kubernetes/..."
type FileToPrint struct {
RealPath string
PrintPath string
}
// NewFileToPrint makes a new instance of FileToPrint with the specified arguments
func NewFileToPrint(realPath, printPath string) FileToPrint {
return FileToPrint{
RealPath: realPath,
PrintPath: printPath,
}
}
// PrintDryRunFiles prints the contents of the FileToPrints given to it to the writer w
func PrintDryRunFiles(files []FileToPrint, w io.Writer) error {
errs := []error{}
for _, file := range files {
if len(file.RealPath) == 0 {
continue
}
fileBytes, err := ioutil.ReadFile(file.RealPath)
if err != nil {
errs = append(errs, err)
continue
}
// Make it possible to fake the path of the file; i.e. you may want to tell the user
// "Here is what would be written to /etc/kubernetes/admin.conf", although you wrote it to /tmp/kubeadm-dryrun/admin.conf and are loading it from there
// Fall back to the "real" path if PrintPath is not set
outputFilePath := file.PrintPath
if len(outputFilePath) == 0 {
outputFilePath = file.RealPath
}
fmt.Fprintf(w, "[dryrun] Would write file %q with content:\n", outputFilePath)
apiclient.PrintBytesWithLinePrefix(w, fileBytes, "\t")
}
return errors.NewAggregate(errs)
}
// Waiter is an implementation of apiclient.Waiter that should be used for dry-running
type Waiter struct{}
// NewWaiter returns a new Waiter object that talks to the given Kubernetes cluster
func NewWaiter() apiclient.Waiter {
return &Waiter{}
}
// WaitForAPI just returns a dummy nil, to indicate that the program should just proceed
func (w *Waiter) WaitForAPI() error {
fmt.Println("[dryrun] Would wait for the API Server's /healthz endpoint to return 'ok'")
return nil
}
// WaitForPodsWithLabel just returns a dummy nil, to indicate that the program should just proceed
func (w *Waiter) WaitForPodsWithLabel(kvLabel string) error {
fmt.Printf("[dryrun] Would wait for the Pods with the label %q in the %s namespace to become Running\n", kvLabel, metav1.NamespaceSystem)
return nil
}
// WaitForPodToDisappear just returns a dummy nil, to indicate that the program should just proceed
func (w *Waiter) WaitForPodToDisappear(podName string) error {
fmt.Printf("[dryrun] Would wait for the %q Pod in the %s namespace to be deleted\n", podName, metav1.NamespaceSystem)
return nil
}
// WaitForHealthyKubelet blocks until the kubelet /healthz endpoint returns 'ok'
func (w *Waiter) WaitForHealthyKubelet(_ time.Duration, healthzEndpoint string) error {
fmt.Printf("[dryrun] Would make sure the kubelet %q endpoint is healthy\n", healthzEndpoint)
return nil
}
// SetTimeout is a no-op; we don't wait in this implementation
func (w *Waiter) SetTimeout(_ time.Duration) {}
// WaitForStaticPodControlPlaneHashes returns an empty hash for all control plane images; WaitForStaticPodControlPlaneHashChange won't block in any case
// but the empty strings there are needed
func (w *Waiter) WaitForStaticPodControlPlaneHashes(_ string) (map[string]string, error) {
return map[string]string{
constants.KubeAPIServer: "",
constants.KubeControllerManager: "",
constants.KubeScheduler: "",
}, nil
}
// WaitForStaticPodSingleHash returns an empty hash
// but the empty strings there are needed
func (w *Waiter) WaitForStaticPodSingleHash(_ string, _ string) (string, error) {
return "", nil
}
// WaitForStaticPodControlPlaneHashChange returns a dummy nil error in order for the flow to just continue as we're dryrunning
func (w *Waiter) WaitForStaticPodControlPlaneHashChange(_, _, _ string) error {
return nil
}

View File

@ -0,0 +1,51 @@
/*
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 util
import (
"fmt"
"net"
"strconv"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
)
// GetMasterEndpoint returns a properly formatted Master Endpoint
// or passes the error from GetMasterHostPort.
func GetMasterEndpoint(cfg *kubeadmapi.MasterConfiguration) (string, error) {
hostPort, err := GetMasterHostPort(cfg)
if err != nil {
return "", err
}
return fmt.Sprintf("https://%s", hostPort), nil
}
// GetMasterHostPort returns a properly formatted Master IP/port pair or error
// if the IP address can not be parsed or port is outside the valid TCP range.
func GetMasterHostPort(cfg *kubeadmapi.MasterConfiguration) (string, error) {
masterIP := net.ParseIP(cfg.API.AdvertiseAddress)
if masterIP == nil {
return "", fmt.Errorf("error parsing address %s", cfg.API.AdvertiseAddress)
}
if cfg.API.BindPort < 0 || cfg.API.BindPort > 65535 {
return "", fmt.Errorf("api server port must be between 0 and 65535")
}
hostPort := net.JoinHostPort(masterIP.String(), strconv.Itoa(int(cfg.API.BindPort)))
return hostPort, nil
}

View File

@ -0,0 +1,223 @@
/*
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 util
import (
"testing"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
)
func TestGetMasterEndpoint(t *testing.T) {
var tests = []struct {
name string
cfg *kubeadmapi.MasterConfiguration
endpoint string
expected bool
}{
{
name: "valid IPv4 endpoint",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 1234,
},
},
endpoint: "https://1.2.3.4:1234",
expected: true,
},
{
name: "valid IPv6 endpoint",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "2001:db8::1",
BindPort: 4321,
},
},
endpoint: "https://[2001:db8::1]:4321",
expected: true,
},
{
name: "invalid IPv4 endpoint",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 1234,
},
},
endpoint: "https://[1.2.3.4]:1234",
expected: false,
},
{
name: "invalid IPv6 endpoint",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "2001:db8::1",
BindPort: 4321,
},
},
endpoint: "https://2001:db8::1:4321",
expected: false,
},
{
name: "invalid IPv4 AdvertiseAddress",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.34",
BindPort: 1234,
},
},
endpoint: "https://1.2.3.4:1234",
expected: false,
},
{
name: "invalid IPv6 AdvertiseAddress",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "2001::db8::1",
BindPort: 4321,
},
},
endpoint: "https://[2001:db8::1]:4321",
expected: false,
},
}
for _, rt := range tests {
actual, err := GetMasterEndpoint(rt.cfg)
if err != nil && rt.expected {
t.Error(err)
}
if actual != rt.endpoint && rt.expected {
t.Errorf(
"%s test case failed:\n\texpected: %s\n\t actual: %s",
rt.name,
rt.endpoint,
(actual),
)
}
}
}
func TestGetMasterHostPort(t *testing.T) {
var tests = []struct {
name string
cfg *kubeadmapi.MasterConfiguration
hostPort string
expected bool
}{
{
name: "valid IPv4 master host and port",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 1234,
},
},
hostPort: "1.2.3.4:1234",
expected: true,
},
{
name: "valid IPv6 master host port",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "2001:db8::1",
BindPort: 4321,
},
},
hostPort: "[2001:db8::1]:4321",
expected: true,
},
{
name: "invalid IPv4 address",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.34",
BindPort: 1234,
},
},
hostPort: "1.2.3.4:1234",
expected: false,
},
{
name: "invalid IPv6 address",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "2001::db8::1",
BindPort: 4321,
},
},
hostPort: "[2001:db8::1]:4321",
expected: false,
},
{
name: "invalid TCP port number",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 987654321,
},
},
hostPort: "1.2.3.4:987654321",
expected: false,
},
{
name: "invalid negative TCP port number",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
BindPort: -987654321,
},
},
hostPort: "1.2.3.4:-987654321",
expected: false,
},
{
name: "unspecified IPv4 TCP port",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
},
},
hostPort: "1.2.3.4:0",
expected: true,
},
{
name: "unspecified IPv6 TCP port",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1:2:3::4",
},
},
hostPort: "[1:2:3::4]:0",
expected: true,
},
}
for _, rt := range tests {
actual, err := GetMasterHostPort(rt.cfg)
if err != nil && rt.expected {
t.Error(err)
}
if actual != rt.hostPort && rt.expected {
t.Errorf(
"%s test case failed:\n\texpected: %s\n\t actual: %s",
rt.name,
rt.hostPort,
(actual),
)
}
}
}

86
vendor/k8s.io/kubernetes/cmd/kubeadm/app/util/error.go generated vendored Normal file
View File

@ -0,0 +1,86 @@
/*
Copyright 2016 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 util
import (
"fmt"
"os"
"strings"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/kubernetes/cmd/kubeadm/app/preflight"
)
const (
// DefaultErrorExitCode defines exit the code for failed action generally
DefaultErrorExitCode = 1
// PreFlightExitCode defines exit the code for preflight checks
PreFlightExitCode = 2
// ValidationExitCode defines the exit code validation checks
ValidationExitCode = 3
)
type debugError interface {
DebugError() (msg string, args []interface{})
}
// fatal prints the message if set and then exits.
func fatal(msg string, code int) {
if len(msg) > 0 {
// add newline if needed
if !strings.HasSuffix(msg, "\n") {
msg += "\n"
}
fmt.Fprint(os.Stderr, msg)
}
os.Exit(code)
}
// CheckErr prints a user friendly error to STDERR and exits with a non-zero
// exit code. Unrecognized errors will be printed with an "error: " prefix.
//
// This method is generic to the command in use and may be used by non-Kubectl
// commands.
func CheckErr(err error) {
checkErr("", err, fatal)
}
// checkErr formats a given error as a string and calls the passed handleErr
// func with that string and an kubectl exit code.
func checkErr(prefix string, err error, handleErr func(string, int)) {
switch err.(type) {
case nil:
return
case *preflight.Error:
handleErr(err.Error(), PreFlightExitCode)
case utilerrors.Aggregate:
handleErr(err.Error(), ValidationExitCode)
default:
handleErr(err.Error(), DefaultErrorExitCode)
}
}
// FormatErrMsg returns a human-readable string describing the slice of errors passed to the function
func FormatErrMsg(errs []error) string {
var errMsg string
for _, err := range errs {
errMsg = fmt.Sprintf("%s\t- %s\n", errMsg, err.Error())
}
return errMsg
}

View File

@ -0,0 +1,83 @@
/*
Copyright 2016 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 util
import (
"fmt"
"testing"
"k8s.io/kubernetes/cmd/kubeadm/app/preflight"
)
func TestCheckErr(t *testing.T) {
var codeReturned int
errHandle := func(err string, code int) {
codeReturned = code
}
var tokenTest = []struct {
e error
expected int
}{
{nil, 0},
{fmt.Errorf(""), DefaultErrorExitCode},
{&preflight.Error{}, PreFlightExitCode},
}
for _, rt := range tokenTest {
codeReturned = 0
checkErr("", rt.e, errHandle)
if codeReturned != rt.expected {
t.Errorf(
"failed checkErr:\n\texpected: %d\n\t actual: %d",
rt.expected,
codeReturned,
)
}
}
}
func TestFormatErrMsg(t *testing.T) {
errMsg1 := "specified version to upgrade to v1.9.0-alpha.3 is equal to or lower than the cluster version v1.10.0-alpha.0.69+638add6ddfb6d2. Downgrades are not supported yet"
errMsg2 := "specified version to upgrade to v1.9.0-alpha.3 is higher than the kubeadm version v1.9.0-alpha.1.3121+84178212527295-dirty. Upgrade kubeadm first using the tool you used to install kubeadm"
testCases := []struct {
errs []error
expect string
}{
{
errs: []error{
fmt.Errorf(errMsg1),
fmt.Errorf(errMsg2),
},
expect: "\t- " + errMsg1 + "\n" + "\t- " + errMsg2 + "\n",
},
{
errs: []error{
fmt.Errorf(errMsg1),
},
expect: "\t- " + errMsg1 + "\n",
},
}
for _, testCase := range testCases {
got := FormatErrMsg(testCase.errs)
if got != testCase.expect {
t.Errorf("FormatErrMsg error, expect: %v, got: %v", testCase.expect, got)
}
}
}

51
vendor/k8s.io/kubernetes/cmd/kubeadm/app/util/etcd.go generated vendored Normal file
View File

@ -0,0 +1,51 @@
/*
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 util
import (
"context"
"github.com/coreos/etcd/clientv3"
"time"
)
// EtcdCluster is an interface to get etcd cluster related information
type EtcdCluster interface {
GetEtcdClusterStatus() (*clientv3.StatusResponse, error)
}
// LocalEtcdCluster represents an instance of a local etcd cluster
type LocalEtcdCluster struct{}
// GetEtcdClusterStatus returns nil for status Up or error for status Down
func (cluster LocalEtcdCluster) GetEtcdClusterStatus() (*clientv3.StatusResponse, error) {
ep := []string{"localhost:2379"}
cli, err := clientv3.New(clientv3.Config{
Endpoints: ep,
DialTimeout: 5 * time.Second,
})
if err != nil {
return nil, err
}
defer cli.Close()
resp, err := cli.Status(context.Background(), ep[0])
if err != nil {
return nil, err
}
return resp, nil
}

View File

@ -0,0 +1,38 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["kubeconfig_test.go"],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig",
library = ":go_default_library",
)
go_library(
name = "go_default_library",
srcs = ["kubeconfig.go"],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig",
deps = [
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd/api: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,112 @@
/*
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 kubeconfig
import (
"fmt"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
// CreateBasic creates a basic, general KubeConfig object that then can be extended
func CreateBasic(serverURL string, clusterName string, userName string, caCert []byte) *clientcmdapi.Config {
// Use the cluster and the username as the context name
contextName := fmt.Sprintf("%s@%s", userName, clusterName)
return &clientcmdapi.Config{
Clusters: map[string]*clientcmdapi.Cluster{
clusterName: {
Server: serverURL,
CertificateAuthorityData: caCert,
},
},
Contexts: map[string]*clientcmdapi.Context{
contextName: {
Cluster: clusterName,
AuthInfo: userName,
},
},
AuthInfos: map[string]*clientcmdapi.AuthInfo{},
CurrentContext: contextName,
}
}
// CreateWithCerts creates a KubeConfig object with access to the API server with client certificates
func CreateWithCerts(serverURL, clusterName, userName string, caCert []byte, clientKey []byte, clientCert []byte) *clientcmdapi.Config {
config := CreateBasic(serverURL, clusterName, userName, caCert)
config.AuthInfos[userName] = &clientcmdapi.AuthInfo{
ClientKeyData: clientKey,
ClientCertificateData: clientCert,
}
return config
}
// CreateWithToken creates a KubeConfig object with access to the API server with a token
func CreateWithToken(serverURL, clusterName, userName string, caCert []byte, token string) *clientcmdapi.Config {
config := CreateBasic(serverURL, clusterName, userName, caCert)
config.AuthInfos[userName] = &clientcmdapi.AuthInfo{
Token: token,
}
return config
}
// ClientSetFromFile returns a ready-to-use client from a KubeConfig file
func ClientSetFromFile(path string) (*clientset.Clientset, error) {
config, err := clientcmd.LoadFromFile(path)
if err != nil {
return nil, fmt.Errorf("failed to load admin kubeconfig [%v]", err)
}
return ToClientSet(config)
}
// ToClientSet converts a KubeConfig object to a client
func ToClientSet(config *clientcmdapi.Config) (*clientset.Clientset, error) {
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)
}
client, err := clientset.NewForConfig(clientConfig)
if err != nil {
return nil, fmt.Errorf("failed to create API client: %v", err)
}
return client, nil
}
// WriteToDisk writes a KubeConfig object down to disk with mode 0600
func WriteToDisk(filename string, kubeconfig *clientcmdapi.Config) error {
err := clientcmd.WriteToFile(*kubeconfig, filename)
if err != nil {
return err
}
return nil
}
// GetClusterFromKubeConfig returns the default Cluster of the specified KubeConfig
func GetClusterFromKubeConfig(config *clientcmdapi.Config) *clientcmdapi.Cluster {
// If there is an unnamed cluster object, use it
if config.Clusters[""] != nil {
return config.Clusters[""]
}
if config.Contexts[config.CurrentContext] != nil {
return config.Clusters[config.Contexts[config.CurrentContext].Cluster]
}
return nil
}

View File

@ -0,0 +1,180 @@
/*
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 kubeconfig
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"testing"
)
const (
configOut1 = `apiVersion: v1
clusters:
- cluster:
server: ""
name: k8s
contexts:
- context:
cluster: k8s
user: user1
name: user1@k8s
current-context: user1@k8s
kind: Config
preferences: {}
users:
- name: user1
user:
token: abc
`
configOut2 = `apiVersion: v1
clusters:
- cluster:
server: localhost:8080
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: user2
name: user2@kubernetes
current-context: user2@kubernetes
kind: Config
preferences: {}
users:
- name: user2
user:
token: cba
`
)
type configClient struct {
clusterName string
userName string
serverURL string
caCert []byte
}
type configClientWithCerts struct {
clientKey []byte
clientCert []byte
}
type configClientWithToken struct {
token string
}
func TestCreateWithCerts(t *testing.T) {
var createBasicTest = []struct {
cc configClient
ccWithCerts configClientWithCerts
expected string
}{
{configClient{}, configClientWithCerts{}, ""},
{configClient{clusterName: "kubernetes"}, configClientWithCerts{}, ""},
}
for _, rt := range createBasicTest {
cwc := CreateWithCerts(
rt.cc.serverURL,
rt.cc.clusterName,
rt.cc.userName,
rt.cc.caCert,
rt.ccWithCerts.clientKey,
rt.ccWithCerts.clientCert,
)
if cwc.Kind != rt.expected {
t.Errorf(
"failed CreateWithCerts:\n\texpected: %s\n\t actual: %s",
rt.expected,
cwc.Kind,
)
}
}
}
func TestCreateWithToken(t *testing.T) {
var createBasicTest = []struct {
cc configClient
ccWithToken configClientWithToken
expected string
}{
{configClient{}, configClientWithToken{}, ""},
{configClient{clusterName: "kubernetes"}, configClientWithToken{}, ""},
}
for _, rt := range createBasicTest {
cwc := CreateWithToken(
rt.cc.serverURL,
rt.cc.clusterName,
rt.cc.userName,
rt.cc.caCert,
rt.ccWithToken.token,
)
if cwc.Kind != rt.expected {
t.Errorf(
"failed CreateWithToken:\n\texpected: %s\n\t actual: %s",
rt.expected,
cwc.Kind,
)
}
}
}
func TestWriteKubeconfigToDisk(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("Couldn't create tmpdir")
}
defer os.RemoveAll(tmpdir)
var writeConfig = []struct {
name string
cc configClient
ccWithToken configClientWithToken
expected error
file []byte
}{
{"test1", configClient{clusterName: "k8s", userName: "user1"}, configClientWithToken{token: "abc"}, nil, []byte(configOut1)},
{"test2", configClient{clusterName: "kubernetes", userName: "user2", serverURL: "localhost:8080"}, configClientWithToken{token: "cba"}, nil, []byte(configOut2)},
}
for _, rt := range writeConfig {
c := CreateWithToken(
rt.cc.serverURL,
rt.cc.clusterName,
rt.cc.userName,
rt.cc.caCert,
rt.ccWithToken.token,
)
configPath := fmt.Sprintf("%s/etc/kubernetes/%s.conf", tmpdir, rt.name)
err := WriteToDisk(configPath, c)
if err != rt.expected {
t.Errorf(
"failed WriteToDisk with an error:\n\texpected: %s\n\t actual: %s",
rt.expected,
err,
)
}
newFile, _ := ioutil.ReadFile(configPath)
if !bytes.Equal(newFile, rt.file) {
t.Errorf(
"failed WriteToDisk config write:\n\texpected: %s\n\t actual: %s",
rt.file,
newFile,
)
}
}
}

View File

@ -0,0 +1,59 @@
/*
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 util
import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
clientsetscheme "k8s.io/client-go/kubernetes/scheme"
)
// MarshalToYaml marshals an object into yaml.
func MarshalToYaml(obj runtime.Object, gv schema.GroupVersion) ([]byte, error) {
return MarshalToYamlForCodecs(obj, gv, clientsetscheme.Codecs)
}
// MarshalToYamlForCodecs marshals an object into yaml using the specified codec
func MarshalToYamlForCodecs(obj runtime.Object, gv schema.GroupVersion, codecs serializer.CodecFactory) ([]byte, error) {
mediaType := "application/yaml"
info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType)
if !ok {
return []byte{}, fmt.Errorf("unsupported media type %q", mediaType)
}
encoder := codecs.EncoderForVersion(info.Serializer, gv)
return runtime.Encode(encoder, obj)
}
// MarshalToYamlForCodecsWithShift adds spaces in front of each line so the indents line up
// correctly in the manifest
func MarshalToYamlForCodecsWithShift(obj runtime.Object, gv schema.GroupVersion, codecs serializer.CodecFactory) (string, error) {
serial, err := MarshalToYamlForCodecs(obj, gv, codecs)
if err != nil {
return "", err
}
lines := strings.Split(string(serial), "\n")
var newSerial string
for _, line := range lines {
newSerial = newSerial + " " + line + "\n"
}
return newSerial, err
}

View File

@ -0,0 +1,33 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["pubkeypin_test.go"],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/pubkeypin",
library = ":go_default_library",
)
go_library(
name = "go_default_library",
srcs = ["pubkeypin.go"],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/pubkeypin",
)
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,108 @@
/*
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 pubkeypin provides primitives for x509 public key pinning in the
// style of RFC7469.
package pubkeypin
import (
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"fmt"
"strings"
)
const (
// formatSHA256 is the prefix for pins that are full-length SHA-256 hashes encoded in base 16 (hex)
formatSHA256 = "sha256"
)
// Set is a set of pinned x509 public keys.
type Set struct {
sha256Hashes map[string]bool
}
// NewSet returns a new, empty PubKeyPinSet
func NewSet() *Set {
return &Set{make(map[string]bool)}
}
// Allow adds an allowed public key hash to the Set
func (s *Set) Allow(pubKeyHashes ...string) error {
for _, pubKeyHash := range pubKeyHashes {
parts := strings.Split(pubKeyHash, ":")
if len(parts) != 2 {
return fmt.Errorf("invalid public key hash, expected \"format:value\"")
}
format, value := parts[0], parts[1]
switch strings.ToLower(format) {
case "sha256":
return s.allowSHA256(value)
default:
return fmt.Errorf("unknown hash format %q", format)
}
}
return nil
}
// Check if a certificate matches one of the public keys in the set
func (s *Set) Check(certificate *x509.Certificate) error {
if s.checkSHA256(certificate) {
return nil
}
return fmt.Errorf("public key %s not pinned", Hash(certificate))
}
// Empty returns true if the Set contains no pinned public keys.
func (s *Set) Empty() bool {
return len(s.sha256Hashes) == 0
}
// Hash calculates the SHA-256 hash of the Subject Public Key Information (SPKI)
// object in an x509 certificate (in DER encoding). It returns the full hash as a
// hex encoded string (suitable for passing to Set.Allow).
func Hash(certificate *x509.Certificate) string {
spkiHash := sha256.Sum256(certificate.RawSubjectPublicKeyInfo)
return formatSHA256 + ":" + strings.ToLower(hex.EncodeToString(spkiHash[:]))
}
// allowSHA256 validates a "sha256" format hash and adds a canonical version of it into the Set
func (s *Set) allowSHA256(hash string) error {
// validate that the hash is the right length to be a full SHA-256 hash
hashLength := hex.DecodedLen(len(hash))
if hashLength != sha256.Size {
return fmt.Errorf("expected a %d byte SHA-256 hash, found %d bytes", sha256.Size, hashLength)
}
// validate that the hash is valid hex
_, err := hex.DecodeString(hash)
if err != nil {
return err
}
// in the end, just store the original hex string in memory (in lowercase)
s.sha256Hashes[strings.ToLower(hash)] = true
return nil
}
// checkSHA256 returns true if the certificate's "sha256" hash is pinned in the Set
func (s *Set) checkSHA256(certificate *x509.Certificate) bool {
actualHash := sha256.Sum256(certificate.RawSubjectPublicKeyInfo)
actualHashHex := strings.ToLower(hex.EncodeToString(actualHash[:]))
return s.sha256Hashes[actualHashHex]
}

View File

@ -0,0 +1,158 @@
/*
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 pubkeypin
import (
"crypto/x509"
"encoding/pem"
"strings"
"testing"
)
// testCertPEM is a simple self-signed test certificate issued with the openssl CLI:
// openssl req -new -newkey rsa:2048 -days 36500 -nodes -x509 -keyout /dev/null -out test.crt
const testCertPEM = `
-----BEGIN CERTIFICATE-----
MIIDRDCCAiygAwIBAgIJAJgVaCXvC6HkMA0GCSqGSIb3DQEBBQUAMB8xHTAbBgNV
BAMTFGt1YmVhZG0ta2V5cGlucy10ZXN0MCAXDTE3MDcwNTE3NDMxMFoYDzIxMTcw
NjExMTc0MzEwWjAfMR0wGwYDVQQDExRrdWJlYWRtLWtleXBpbnMtdGVzdDCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK0ba8mHU9UtYlzM1Own2Fk/XGjR
J4uJQvSeGLtz1hID1IA0dLwruvgLCPadXEOw/f/IWIWcmT+ZmvIHZKa/woq2iHi5
+HLhXs7aG4tjKGLYhag1hLjBI7icqV7ovkjdGAt9pWkxEzhIYClFMXDjKpMSynu+
YX6nZ9tic1cOkHmx2yiZdMkuriRQnpTOa7bb03OC1VfGl7gHlOAIYaj4539WCOr8
+ACTUMJUFEHcRZ2o8a/v6F9GMK+7SC8SJUI+GuroXqlMAdhEv4lX5Co52enYaClN
+D9FJLRpBv2YfiCQdJRaiTvCBSxEFz6BN+PtP5l2Hs703ZWEkOqCByM6HV8CAwEA
AaOBgDB+MB0GA1UdDgQWBBRQgUX8MhK2rWBWQiPHWcKzoWDH5DBPBgNVHSMESDBG
gBRQgUX8MhK2rWBWQiPHWcKzoWDH5KEjpCEwHzEdMBsGA1UEAxMUa3ViZWFkbS1r
ZXlwaW5zLXRlc3SCCQCYFWgl7wuh5DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB
BQUAA4IBAQCaAUif7Pfx3X0F08cxhx8/Hdx4jcJw6MCq6iq6rsXM32ge43t8OHKC
pJW08dk58a3O1YQSMMvD6GJDAiAfXzfwcwY6j258b1ZlI9Ag0VokvhMl/XfdCsdh
AWImnL1t4hvU5jLaImUUMlYxMcSfHBGAm7WJIZ2LdEfg6YWfZh+WGbg1W7uxLxk6
y4h5rWdNnzBHWAGf7zJ0oEDV6W6RSwNXtC0JNnLaeIUm/6xdSddJlQPwUv8YH4jX
c1vuFqTnJBPcb7W//R/GI2Paicm1cmns9NLnPR35exHxFTy+D1yxmGokpoPMdife
aH+sfuxT8xeTPb3kjzF9eJTlnEquUDLM
-----END CERTIFICATE-----`
// expectedHash can be verified using the openssl CLI:
// openssl x509 -pubkey -in test.crt openssl rsa -pubin -outform der 2>&/dev/null | openssl dgst -sha256 -hex
const expectedHash = `sha256:345959acb2c3b2feb87d281961c893f62a314207ef02599f1cc4a5fb255480b3`
// testCert2PEM is a second test cert generated the same way as testCertPEM
const testCert2PEM = `
-----BEGIN CERTIFICATE-----
MIID9jCCAt6gAwIBAgIJAN5MXZDic7qYMA0GCSqGSIb3DQEBBQUAMFkxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCXRlc3RDZXJ0MjAgFw0xNzA3MjQxNjA0
MDFaGA8yMTE3MDYzMDE2MDQwMVowWTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNv
bWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAG
A1UEAxMJdGVzdENlcnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
0brwpJYN2ytPWzRBtZSVc3dhkQlA59AzxzqeLLkano0Pxo9NIc3T/y58nnRI8uaS
I1P7BzUfJTiUEvmAtX8NggqKK4ld/gPrU+IRww1CUYS4KCkA/0d0ctPy0JwBCjD+
b57G3rmNE8c+0jns6J96ZzNtqmv6N+ZlFBAXm1p4S+k0kGi5+hoQ6H7SYXjk2lG+
r/8jPQEjy/NSdw1dcCA0Nc6o+hPr32927dS6J9KOhBeXNYUNdbuDDmroM9/gN2e/
YMSA1olLeDPQ7Xvhk0PIyEDnHh83AffPCx5yM3htVRGddjIsPAVUJEL3z5leJtxe
fzyPghOhHJY0PXqznDQTcwIDAQABo4G+MIG7MB0GA1UdDgQWBBRP0IJqv/5rQ4Uf
SByl77dJeEapRDCBiwYDVR0jBIGDMIGAgBRP0IJqv/5rQ4UfSByl77dJeEapRKFd
pFswWTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoT
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAxMJdGVzdENlcnQyggkA
3kxdkOJzupgwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA0RIMHc10
wHHPMh9UflqBgDMF7gfbOL0juJfGloAOcohWWfMZBBJ0CQKMy3xRyoK3HmbW1eeb
iATjesw7t4VEAwf7mgKAd+eTfWYB952uq5qYJ2TI28mSofEq1Wz3RmrNkC1KCBs1
u+YMFGwyl6necV9zKCeiju4jeovI1GA38TvH7MgYln6vMJ+FbgOXj7XCpek7dQiY
KGaeSSH218mGNQaWRQw2Sm3W6cFdANoCJUph4w18s7gjtFpfV63s80hXRps+vEyv
jEQMEQpG8Ss7HGJLGLBw/xAmG0e//XS/o2dDonbGbvzToFByz8OGxjMhk6yV6hdd
+iyvsLAw/MYMSA==
-----END CERTIFICATE-----
`
// testCert is a small helper to get a test x509.Certificate from the PEM constants
func testCert(t *testing.T, pemString string) *x509.Certificate {
// Decode the example certificate from a PEM file into a PEM block
pemBlock, _ := pem.Decode([]byte(pemString))
if pemBlock == nil {
t.Fatal("failed to parse test certificate PEM")
return nil
}
// Parse the PEM block into an x509.Certificate
result, err := x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
t.Fatalf("failed to parse test certificate: %v", err)
return nil
}
return result
}
func TestSet(t *testing.T) {
s := NewSet()
if !s.Empty() {
t.Error("expected a new set to be empty")
return
}
err := s.Allow("xyz")
if err == nil || !s.Empty() {
t.Error("expected allowing junk to fail")
return
}
err = s.Allow("0011223344")
if err == nil || !s.Empty() {
t.Error("expected allowing something too short to fail")
return
}
err = s.Allow(expectedHash + expectedHash)
if err == nil || !s.Empty() {
t.Error("expected allowing something too long to fail")
return
}
err = s.Check(testCert(t, testCertPEM))
if err == nil {
t.Error("expected test cert to not be allowed (yet)")
return
}
err = s.Allow(strings.ToUpper(expectedHash))
if err != nil || s.Empty() {
t.Error("expected allowing uppercase expectedHash to succeed")
return
}
err = s.Check(testCert(t, testCertPEM))
if err != nil {
t.Errorf("expected test cert to be allowed, but got back: %v", err)
return
}
err = s.Check(testCert(t, testCert2PEM))
if err == nil {
t.Error("expected the second test cert to be disallowed")
return
}
}
func TestHash(t *testing.T) {
actualHash := Hash(testCert(t, testCertPEM))
if actualHash != expectedHash {
t.Errorf(
"failed to Hash() to the expected value\n\texpected: %q\n\t actual: %q",
expectedHash,
actualHash,
)
}
}

View File

@ -0,0 +1,50 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["utils_test.go"],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/staticpod",
library = ":go_default_library",
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/constants: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",
],
)
go_library(
name = "go_default_library",
srcs = ["utils.go"],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/staticpod",
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/util:go_default_library",
"//pkg/kubelet/types: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,236 @@
/*
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"
"net"
"net/url"
"os"
"strings"
"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"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/util"
kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
)
const (
// kubeControllerManagerAddressArg represents the address argument of the kube-controller-manager configuration.
kubeControllerManagerAddressArg = "address"
// kubeSchedulerAddressArg represents the address argument of the kube-scheduler configuration.
kubeSchedulerAddressArg = "address"
// etcdListenClientURLsArg represents the listen-client-urls argument of the etcd configuration.
etcdListenClientURLsArg = "listen-client-urls"
)
// ComponentPod returns a Pod object from the container and volume specifications
func ComponentPod(container v1.Container, volumes map[string]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: VolumeMapToSlice(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(cfg *kubeadmapi.MasterConfiguration, componentName string, port int, path string, scheme v1.URIScheme) *v1.Probe {
return &v1.Probe{
Handler: v1.Handler{
HTTPGet: &v1.HTTPGetAction{
Host: GetProbeAddress(cfg, componentName),
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, pathType *v1.HostPathType) v1.Volume {
return v1.Volume{
Name: name,
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{
Path: path,
Type: pathType,
},
},
}
}
// 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,
}
}
// VolumeMapToSlice returns a slice of volumes from a map's values
func VolumeMapToSlice(volumes map[string]v1.Volume) []v1.Volume {
v := make([]v1.Volume, 0, len(volumes))
for _, vol := range volumes {
v = append(v, vol)
}
return v
}
// VolumeMountMapToSlice returns a slice of volumes from a map's values
func VolumeMountMapToSlice(volumeMounts map[string]v1.VolumeMount) []v1.VolumeMount {
v := make([]v1.VolumeMount, 0, len(volumeMounts))
for _, volMount := range volumeMounts {
v = append(v, volMount)
}
return v
}
// 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 := util.MarshalToYaml(&pod, v1.SchemeGroupVersion)
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
}
// GetProbeAddress returns an IP address or 127.0.0.1 to use for liveness probes
// in static pod manifests.
func GetProbeAddress(cfg *kubeadmapi.MasterConfiguration, componentName string) string {
switch {
case componentName == kubeadmconstants.KubeAPIServer:
if cfg.API.AdvertiseAddress != "" {
return cfg.API.AdvertiseAddress
}
case componentName == kubeadmconstants.KubeControllerManager:
if addr, exists := cfg.ControllerManagerExtraArgs[kubeControllerManagerAddressArg]; exists {
return addr
}
case componentName == kubeadmconstants.KubeScheduler:
if addr, exists := cfg.SchedulerExtraArgs[kubeSchedulerAddressArg]; exists {
return addr
}
case componentName == kubeadmconstants.Etcd:
if cfg.Etcd.ExtraArgs != nil {
if arg, exists := cfg.Etcd.ExtraArgs[etcdListenClientURLsArg]; exists {
// Use the first url in the listen-client-urls if multiple url's are specified.
if strings.ContainsAny(arg, ",") {
arg = strings.Split(arg, ",")[0]
}
parsedURL, err := url.Parse(arg)
if err != nil || parsedURL.Hostname() == "" {
break
}
// Return the IP if the URL contains an address instead of a name.
if ip := net.ParseIP(parsedURL.Hostname()); ip != nil {
return ip.String()
}
// Use the local resolver to try resolving the name within the URL.
// If the name can not be resolved, return an IPv4 loopback address.
// Otherwise, select the first valid IPv4 address.
// If the name does not resolve to an IPv4 address, select the first valid IPv6 address.
addrs, err := net.LookupIP(parsedURL.Hostname())
if err != nil {
break
}
var ip net.IP
for _, addr := range addrs {
if addr.To4() != nil {
ip = addr
break
}
if addr.To16() != nil && ip == nil {
ip = addr
}
}
return ip.String()
}
}
}
return "127.0.0.1"
}

View File

@ -0,0 +1,410 @@
/*
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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
)
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 {
name string
cfg *kubeadmapi.MasterConfiguration
component string
port int
path string
scheme v1.URIScheme
expected string
}{
{
name: "default apiserver advertise address with http",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "",
},
},
component: kubeadmconstants.KubeAPIServer,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "127.0.0.1",
},
{
name: "default apiserver advertise address with https",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "",
},
},
component: kubeadmconstants.KubeAPIServer,
port: 2,
path: "bar",
scheme: v1.URISchemeHTTPS,
expected: "127.0.0.1",
},
{
name: "valid ipv4 apiserver advertise address with http",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
},
},
component: kubeadmconstants.KubeAPIServer,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "1.2.3.4",
},
{
name: "valid ipv6 apiserver advertise address with http",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "2001:db8::1",
},
},
component: kubeadmconstants.KubeAPIServer,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "2001:db8::1",
},
{
name: "valid IPv4 controller-manager probe",
cfg: &kubeadmapi.MasterConfiguration{
ControllerManagerExtraArgs: map[string]string{"address": "1.2.3.4"},
},
component: kubeadmconstants.KubeControllerManager,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "1.2.3.4",
},
{
name: "valid IPv6 controller-manager probe",
cfg: &kubeadmapi.MasterConfiguration{
ControllerManagerExtraArgs: map[string]string{"address": "2001:db8::1"},
},
component: kubeadmconstants.KubeControllerManager,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "2001:db8::1",
},
{
name: "valid IPv4 scheduler probe",
cfg: &kubeadmapi.MasterConfiguration{
SchedulerExtraArgs: map[string]string{"address": "1.2.3.4"},
},
component: kubeadmconstants.KubeScheduler,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "1.2.3.4",
},
{
name: "valid IPv6 scheduler probe",
cfg: &kubeadmapi.MasterConfiguration{
SchedulerExtraArgs: map[string]string{"address": "2001:db8::1"},
},
component: kubeadmconstants.KubeScheduler,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "2001:db8::1",
},
{
name: "valid etcd probe using listen-client-urls IPv4 addresses",
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{
ExtraArgs: map[string]string{
"listen-client-urls": "http://1.2.3.4:2379,http://4.3.2.1:2379"},
},
},
component: kubeadmconstants.Etcd,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "1.2.3.4",
},
{
name: "valid etcd probe using listen-client-urls IPv6 addresses",
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{
ExtraArgs: map[string]string{
"listen-client-urls": "http://[2001:db8::1]:2379,http://[2001:db8::2]:2379"},
},
},
component: kubeadmconstants.Etcd,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "2001:db8::1",
},
{
name: "valid IPv4 etcd probe using hostname for listen-client-urls",
cfg: &kubeadmapi.MasterConfiguration{
Etcd: kubeadmapi.Etcd{
ExtraArgs: map[string]string{
"listen-client-urls": "http://localhost:2379"},
},
},
component: kubeadmconstants.Etcd,
port: 1,
path: "foo",
scheme: v1.URISchemeHTTP,
expected: "127.0.0.1",
},
}
for _, rt := range tests {
actual := ComponentProbe(rt.cfg, rt.component, rt.port, rt.path, rt.scheme)
if actual.Handler.HTTPGet.Host != rt.expected {
t.Errorf("%s test case failed:\n\texpected: %s\n\t actual: %s",
rt.name, rt.expected,
actual.Handler.HTTPGet.Host)
}
if actual.Handler.HTTPGet.Port != intstr.FromInt(rt.port) {
t.Errorf("%s test case failed:\n\texpected: %v\n\t actual: %v",
rt.name, rt.port,
actual.Handler.HTTPGet.Port)
}
if actual.Handler.HTTPGet.Path != rt.path {
t.Errorf("%s test case failed:\n\texpected: %s\n\t actual: %s",
rt.name, rt.path,
actual.Handler.HTTPGet.Path)
}
if actual.Handler.HTTPGet.Scheme != rt.scheme {
t.Errorf("%s test case failed:\n\texpected: %v\n\t actual: %v",
rt.name, 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{},
},
},
},
}
for _, rt := range tests {
c := v1.Container{Name: rt.name}
actual := ComponentPod(c, map[string]v1.Volume{})
if !reflect.DeepEqual(rt.expected, actual) {
t.Errorf(
"failed componentPod:\n\texpected: %v\n\t actual: %v",
rt.expected,
actual,
)
}
}
}
func TestNewVolume(t *testing.T) {
hostPathDirectoryOrCreate := v1.HostPathDirectoryOrCreate
var tests = []struct {
name string
path string
expected v1.Volume
pathType *v1.HostPathType
}{
{
name: "foo",
path: "/etc/foo",
expected: v1.Volume{
Name: "foo",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{
Path: "/etc/foo",
Type: &hostPathDirectoryOrCreate,
},
},
},
pathType: &hostPathDirectoryOrCreate,
},
}
for _, rt := range tests {
actual := NewVolume(rt.name, rt.path, rt.pathType)
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 TestVolumeMapToSlice(t *testing.T) {
testVolumes := map[string]v1.Volume{
"foo": {
Name: "foo",
},
}
volumeSlice := VolumeMapToSlice(testVolumes)
if len(volumeSlice) != 1 {
t.Errorf("Expected slice length of 1, got %d", len(volumeSlice))
}
if volumeSlice[0].Name != "foo" {
t.Errorf("Expected volume name \"foo\", got %s", volumeSlice[0].Name)
}
}
func TestVolumeMountMapToSlice(t *testing.T) {
testVolumeMounts := map[string]v1.VolumeMount{
"foo": {
Name: "foo",
},
}
volumeMountSlice := VolumeMountMapToSlice(testVolumeMounts)
if len(volumeMountSlice) != 1 {
t.Errorf("Expected slice length of 1, got %d", len(volumeMountSlice))
}
if volumeMountSlice[0].Name != "foo" {
t.Errorf("Expected volume mount name \"foo\", got %s", volumeMountSlice[0].Name)
}
}
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

@ -0,0 +1,37 @@
/*
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 util
import (
"bytes"
"fmt"
"text/template"
)
// ParseTemplate validates and parses passed as argument template
func ParseTemplate(strtmpl string, obj interface{}) ([]byte, error) {
var buf bytes.Buffer
tmpl, err := template.New("template").Parse(strtmpl)
if err != nil {
return nil, fmt.Errorf("error when parsing template: %v", err)
}
err = tmpl.Execute(&buf, obj)
if err != nil {
return nil, fmt.Errorf("error when executing template: %v", err)
}
return buf.Bytes(), nil
}

View File

@ -0,0 +1,90 @@
/*
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 util
import (
"testing"
)
const (
validTmpl = "image: {{ .ImageRepository }}/pause-{{ .Arch }}:3.0"
validTmplOut = "image: gcr.io/google_containers/pause-amd64:3.0"
doNothing = "image: gcr.io/google_containers/pause-amd64:3.0"
invalidTmpl1 = "{{ .baz }/d}"
invalidTmpl2 = "{{ !foobar }}"
)
func TestParseTemplate(t *testing.T) {
var tmplTests = []struct {
template string
data interface{}
output string
errExpected bool
}{
// should parse a valid template and set the right values
{
template: validTmpl,
data: struct{ ImageRepository, Arch string }{
ImageRepository: "gcr.io/google_containers",
Arch: "amd64",
},
output: validTmplOut,
errExpected: false,
},
// should noop if there aren't any {{ .foo }} present
{
template: doNothing,
data: struct{ ImageRepository, Arch string }{
ImageRepository: "gcr.io/google_containers",
Arch: "amd64",
},
output: doNothing,
errExpected: false,
},
// invalid syntax, passing nil
{
template: invalidTmpl1,
data: nil,
output: "",
errExpected: true,
},
// invalid syntax
{
template: invalidTmpl2,
data: struct{}{},
output: "",
errExpected: true,
},
}
for _, tt := range tmplTests {
outbytes, err := ParseTemplate(tt.template, tt.data)
if tt.errExpected != (err != nil) {
t.Errorf(
"failed TestParseTemplate:\n\texpected err: %t\n\t actual: %s",
tt.errExpected,
err,
)
}
if tt.output != string(outbytes) {
t.Errorf(
"failed TestParseTemplate:\n\texpected bytes: %s\n\t actual: %s",
tt.output,
outbytes,
)
}
}
}

View File

@ -0,0 +1,35 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["tokens_test.go"],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/token",
library = ":go_default_library",
deps = ["//cmd/kubeadm/app/apis/kubeadm:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = ["tokens.go"],
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/token",
deps = ["//cmd/kubeadm/app/apis/kubeadm: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,103 @@
/*
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 token
import (
"crypto/rand"
"encoding/hex"
"fmt"
"regexp"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
)
const (
// TokenIDBytes defines a number of bytes used for a token id
TokenIDBytes = 3
// TokenSecretBytes defines a number of bytes used for a secret
TokenSecretBytes = 8
)
var (
// TokenIDRegexpString defines token's id regular expression pattern
TokenIDRegexpString = "^([a-z0-9]{6})$"
// TokenIDRegexp is a compiled regular expression of TokenIDRegexpString
TokenIDRegexp = regexp.MustCompile(TokenIDRegexpString)
// TokenRegexpString defines id.secret regular expression pattern
TokenRegexpString = "^([a-z0-9]{6})\\.([a-z0-9]{16})$"
// TokenRegexp is a compiled regular expression of TokenRegexpString
TokenRegexp = regexp.MustCompile(TokenRegexpString)
)
func randBytes(length int) (string, error) {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
// GenerateToken generates a new token with a token ID that is valid as a
// Kubernetes DNS label.
// For more info, see kubernetes/pkg/util/validation/validation.go.
func GenerateToken() (string, error) {
tokenID, err := randBytes(TokenIDBytes)
if err != nil {
return "", err
}
tokenSecret, err := randBytes(TokenSecretBytes)
if err != nil {
return "", err
}
return fmt.Sprintf("%s.%s", tokenID, tokenSecret), nil
}
// ParseTokenID tries and parse a valid token ID from a string.
// An error is returned in case of failure.
func ParseTokenID(s string) error {
if !TokenIDRegexp.MatchString(s) {
return fmt.Errorf("token ID [%q] was not of form [%q]", s, TokenIDRegexpString)
}
return nil
}
// ParseToken tries and parse a valid token from a string.
// A token ID and token secret are returned in case of success, an error otherwise.
func ParseToken(s string) (string, string, error) {
split := TokenRegexp.FindStringSubmatch(s)
if len(split) != 3 {
return "", "", fmt.Errorf("token [%q] was not of form [%q]", s, TokenRegexpString)
}
return split[1], split[2], nil
}
// BearerToken returns a string representation of the passed token.
func BearerToken(d *kubeadmapi.TokenDiscovery) string {
return fmt.Sprintf("%s.%s", d.ID, d.Secret)
}
// ValidateToken validates whether a token is well-formed.
// In case it's not, the corresponding error is returned as well.
func ValidateToken(d *kubeadmapi.TokenDiscovery) (bool, error) {
if _, _, err := ParseToken(d.ID + "." + d.Secret); err != nil {
return false, err
}
return true, nil
}

View File

@ -0,0 +1,173 @@
/*
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 token
import (
"testing"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
)
func TestTokenParse(t *testing.T) {
var tests = []struct {
token string
expected bool
}{
{token: "1234567890123456789012", expected: false}, // invalid parcel size
{token: "12345.1234567890123456", expected: false}, // invalid parcel size
{token: ".1234567890123456", expected: false}, // invalid parcel size
{token: "123456:1234567890.123456", expected: false}, // invalid separation
{token: "abcdef:1234567890123456", expected: false}, // invalid separation
{token: "Abcdef.1234567890123456", expected: false}, // invalid token id
{token: "123456.AABBCCDDEEFFGGHH", expected: false}, // invalid token secret
{token: "abcdef.1234567890123456", expected: true},
{token: "123456.aabbccddeeffgghh", expected: true},
}
for _, rt := range tests {
_, _, actual := ParseToken(rt.token)
if (actual == nil) != rt.expected {
t.Errorf(
"failed ParseToken for this token: [%s]\n\texpected: %t\n\t actual: %t",
rt.token,
rt.expected,
(actual == nil),
)
}
}
}
func TestParseTokenID(t *testing.T) {
var tests = []struct {
tokenID string
expected bool
}{
{tokenID: "", expected: false},
{tokenID: "1234567890123456789012", expected: false},
{tokenID: "12345", expected: false},
{tokenID: "Abcdef", expected: false},
{tokenID: "abcdef", expected: true},
{tokenID: "123456", expected: true},
}
for _, rt := range tests {
actual := ParseTokenID(rt.tokenID)
if (actual == nil) != rt.expected {
t.Errorf(
"failed ParseTokenID for this token ID: [%s]\n\texpected: %t\n\t actual: %t",
rt.tokenID,
rt.expected,
(actual == nil),
)
}
}
}
func TestValidateToken(t *testing.T) {
var tests = []struct {
token *kubeadmapi.TokenDiscovery
expected bool
}{
{token: &kubeadmapi.TokenDiscovery{ID: "", Secret: ""}, expected: false},
{token: &kubeadmapi.TokenDiscovery{ID: "1234567890123456789012", Secret: ""}, expected: false},
{token: &kubeadmapi.TokenDiscovery{ID: "", Secret: "1234567890123456789012"}, expected: false},
{token: &kubeadmapi.TokenDiscovery{ID: "12345", Secret: "1234567890123456"}, expected: false},
{token: &kubeadmapi.TokenDiscovery{ID: "Abcdef", Secret: "1234567890123456"}, expected: false},
{token: &kubeadmapi.TokenDiscovery{ID: "123456", Secret: "AABBCCDDEEFFGGHH"}, expected: false},
{token: &kubeadmapi.TokenDiscovery{ID: "abc*ef", Secret: "1234567890123456"}, expected: false},
{token: &kubeadmapi.TokenDiscovery{ID: "abcdef", Secret: "123456789*123456"}, expected: false},
{token: &kubeadmapi.TokenDiscovery{ID: "abcdef", Secret: "1234567890123456"}, expected: true},
{token: &kubeadmapi.TokenDiscovery{ID: "123456", Secret: "aabbccddeeffgghh"}, expected: true},
{token: &kubeadmapi.TokenDiscovery{ID: "abc456", Secret: "1234567890123456"}, expected: true},
{token: &kubeadmapi.TokenDiscovery{ID: "abcdef", Secret: "123456ddeeffgghh"}, expected: true},
}
for _, rt := range tests {
valid, actual := ValidateToken(rt.token)
if (actual == nil) != rt.expected {
t.Errorf(
"failed ValidateToken for this token ID: [%s]\n\texpected: %t\n\t actual: %t",
rt.token,
rt.expected,
(actual == nil),
)
}
if (valid == true) != rt.expected {
t.Errorf(
"failed ValidateToken for this token ID: [%s]\n\texpected: %t\n\t actual: %t",
rt.token,
rt.expected,
(actual == nil),
)
}
}
}
func TestGenerateToken(t *testing.T) {
token, err := GenerateToken()
if err != nil {
t.Fatalf("GenerateToken returned an unexpected error: %+v", err)
}
tokenID, tokenSecret, err := ParseToken(token)
if err != nil {
t.Fatalf("GenerateToken returned an unexpected error: %+v", err)
}
if len(tokenID) != 6 {
t.Errorf("failed GenerateToken first part length:\n\texpected: 6\n\t actual: %d", len(tokenID))
}
if len(tokenSecret) != 16 {
t.Errorf("failed GenerateToken second part length:\n\texpected: 16\n\t actual: %d", len(tokenSecret))
}
}
func TestRandBytes(t *testing.T) {
var randTest = []int{
0,
1,
2,
3,
100,
}
for _, rt := range randTest {
actual, err := randBytes(rt)
if err != nil {
t.Errorf("failed randBytes: %v", err)
}
if len(actual) != rt*2 {
t.Errorf("failed randBytes:\n\texpected: %d\n\t actual: %d\n", rt*2, len(actual))
}
}
}
func TestBearerToken(t *testing.T) {
var tests = []struct {
token *kubeadmapi.TokenDiscovery
expected string
}{
{token: &kubeadmapi.TokenDiscovery{ID: "foo", Secret: "bar"}, expected: "foo.bar"}, // should use default
}
for _, rt := range tests {
actual := BearerToken(rt.token)
if actual != rt.expected {
t.Errorf(
"failed BearerToken:\n\texpected: %s\n\t actual: %s",
rt.expected,
actual,
)
}
}
}

View File

@ -0,0 +1,147 @@
/*
Copyright 2016 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 util
import (
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strings"
)
var (
kubeReleaseBucketURL = "https://dl.k8s.io"
kubeReleaseRegex = regexp.MustCompile(`^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)([-0-9a-zA-Z_\.+]*)?$`)
kubeReleaseLabelRegex = regexp.MustCompile(`^[[:lower:]]+(-[-\w_\.]+)?$`)
kubeBucketPrefixes = regexp.MustCompile(`^((release|ci|ci-cross)/)?([-\w_\.+]+)$`)
)
// KubernetesReleaseVersion is helper function that can fetch
// available version information from release servers based on
// label names, like "stable" or "latest".
//
// If argument is already semantic version string, it
// will return same string.
//
// In case of labels, it tries to fetch from release
// servers and then return actual semantic version.
//
// Available names on release servers:
// stable (latest stable release)
// stable-1 (latest stable release in 1.x)
// stable-1.0 (and similarly 1.1, 1.2, 1.3, ...)
// latest (latest release, including alpha/beta)
// latest-1 (latest release in 1.x, including alpha/beta)
// latest-1.0 (and similarly 1.1, 1.2, 1.3, ...)
func KubernetesReleaseVersion(version string) (string, error) {
ver := normalizedBuildVersion(version)
if len(ver) != 0 {
return ver, nil
}
bucketURL, versionLabel, err := splitVersion(version)
if err != nil {
return "", err
}
// revalidate, if exact build from e.g. CI bucket requested.
ver = normalizedBuildVersion(versionLabel)
if len(ver) != 0 {
return ver, nil
}
if kubeReleaseLabelRegex.MatchString(versionLabel) {
url := fmt.Sprintf("%s/%s.txt", bucketURL, versionLabel)
body, err := fetchFromURL(url)
if err != nil {
return "", err
}
// Re-validate received version and return.
return KubernetesReleaseVersion(body)
}
return "", fmt.Errorf("version %q doesn't match patterns for neither semantic version nor labels (stable, latest, ...)", version)
}
// KubernetesVersionToImageTag is helper function that replaces all
// non-allowed symbols in tag strings with underscores.
// Image tag can only contain lowercase and uppercase letters, digits,
// underscores, periods and dashes.
// Current usage is for CI images where all of symbols except '+' are valid,
// but function is for generic usage where input can't be always pre-validated.
func KubernetesVersionToImageTag(version string) string {
allowed := regexp.MustCompile(`[^-a-zA-Z0-9_\.]`)
return allowed.ReplaceAllString(version, "_")
}
// KubernetesIsCIVersion checks if user requested CI version
func KubernetesIsCIVersion(version string) bool {
subs := kubeBucketPrefixes.FindAllStringSubmatch(version, 1)
if len(subs) == 1 && len(subs[0]) == 4 && strings.HasPrefix(subs[0][2], "ci") {
return true
}
return false
}
// Internal helper: returns normalized build version (with "v" prefix if needed)
// If input doesn't match known version pattern, returns empty string.
func normalizedBuildVersion(version string) string {
if kubeReleaseRegex.MatchString(version) {
if strings.HasPrefix(version, "v") {
return version
}
return "v" + version
}
return ""
}
// Internal helper: split version parts,
// Return base URL and cleaned-up version
func splitVersion(version string) (string, string, error) {
var urlSuffix string
subs := kubeBucketPrefixes.FindAllStringSubmatch(version, 1)
if len(subs) != 1 || len(subs[0]) != 4 {
return "", "", fmt.Errorf("invalid version %q", version)
}
switch {
case strings.HasPrefix(subs[0][2], "ci"):
// Special case. CI images populated only by ci-cross area
urlSuffix = "ci-cross"
default:
urlSuffix = "release"
}
url := fmt.Sprintf("%s/%s", kubeReleaseBucketURL, urlSuffix)
return url, subs[0][3], nil
}
// Internal helper: return content of URL
func fetchFromURL(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("unable to get URL %q: %s", url, err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unable to fetch file. URL: %q Status: %v", url, resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("unable to read content of URL %q: %s", url, err.Error())
}
return strings.TrimSpace(string(body)), nil
}

View File

@ -0,0 +1,290 @@
/*
Copyright 2016 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 util
import (
"net/http"
"net/http/httptest"
"path"
"strings"
"testing"
)
func TestEmptyVersion(t *testing.T) {
ver, err := KubernetesReleaseVersion("")
if err == nil {
t.Error("KubernetesReleaseVersion returned successfully, but error expected")
}
if ver != "" {
t.Error("KubernetesReleaseVersion returned value, expected only error")
}
}
func TestValidVersion(t *testing.T) {
validVersions := []string{
"v1.3.0",
"v1.4.0-alpha.0",
"v1.4.5",
"v1.4.0-beta.0",
"v2.0.0",
"v1.6.0-alpha.0.536+d60d9f3269288f",
"v1.5.0-alpha.0.1078+1044b6822497da-pull",
"v1.5.0-alpha.1.822+49b9e32fad9f32-pull-gke-gci",
"v1.6.1+coreos.0",
}
for _, s := range validVersions {
ver, err := KubernetesReleaseVersion(s)
t.Log("Valid: ", s, ver, err)
if err != nil {
t.Errorf("KubernetesReleaseVersion unexpected error for version %q: %v", s, err)
}
if ver != s {
t.Errorf("KubernetesReleaseVersion should return same valid version string. %q != %q", s, ver)
}
}
}
func TestInvalidVersion(t *testing.T) {
invalidVersions := []string{
"v1.3",
"1.4",
"b1.4.0",
"c1.4.5+git",
"something1.2",
}
for _, s := range invalidVersions {
ver, err := KubernetesReleaseVersion(s)
t.Log("Invalid: ", s, ver, err)
if err == nil {
t.Errorf("KubernetesReleaseVersion error expected for version %q, but returned successfully", s)
}
if ver != "" {
t.Errorf("KubernetesReleaseVersion should return empty string in case of error. Returned %q for version %q", ver, s)
}
}
}
func TestValidConvenientForUserVersion(t *testing.T) {
validVersions := []string{
"1.4.0",
"1.4.5+git",
"1.6.1_coreos.0",
}
for _, s := range validVersions {
ver, err := KubernetesReleaseVersion(s)
t.Log("Valid: ", s, ver, err)
if err != nil {
t.Errorf("KubernetesReleaseVersion unexpected error for version %q: %v", s, err)
}
if ver != "v"+s {
t.Errorf("KubernetesReleaseVersion should return semantic version string. %q vs. %q", s, ver)
}
}
}
func TestVersionFromNetwork(t *testing.T) {
type T struct {
Content string
Status int
Expected string
ErrorExpected bool
}
cases := map[string]T{
"stable": {"stable-1", http.StatusOK, "v1.4.6", false}, // recursive pointer to stable-1
"stable-1": {"v1.4.6", http.StatusOK, "v1.4.6", false},
"stable-1.3": {"v1.3.10", http.StatusOK, "v1.3.10", false},
"latest": {"v1.6.0-alpha.0", http.StatusOK, "v1.6.0-alpha.0", false},
"latest-1.3": {"v1.3.11-beta.0", http.StatusOK, "v1.3.11-beta.0", false},
"empty": {"", http.StatusOK, "", true},
"garbage": {"<?xml version='1.0'?><Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message></Error>", http.StatusOK, "", true},
"unknown": {"The requested URL was not found on this server.", http.StatusNotFound, "", true},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := strings.TrimSuffix(path.Base(r.URL.Path), ".txt")
res, found := cases[key]
if found {
http.Error(w, res.Content, res.Status)
} else {
http.Error(w, "Unknown test case key!", http.StatusNotFound)
}
}))
defer server.Close()
kubeReleaseBucketURL = server.URL
for k, v := range cases {
ver, err := KubernetesReleaseVersion(k)
t.Logf("Key: %q. Result: %q, Error: %v", k, ver, err)
switch {
case err != nil && !v.ErrorExpected:
t.Errorf("KubernetesReleaseVersion: unexpected error for %q. Error: %v", k, err)
case err == nil && v.ErrorExpected:
t.Errorf("KubernetesReleaseVersion: error expected for key %q, but result is %q", k, ver)
case ver != v.Expected:
t.Errorf("KubernetesReleaseVersion: unexpected result for key %q. Expected: %q Actual: %q", k, v.Expected, ver)
}
}
}
func TestVersionToTag(t *testing.T) {
type T struct {
input string
expected string
}
cases := []T{
// NOP
{"", ""},
// Official releases
{"v1.0.0", "v1.0.0"},
// CI or custom builds
{"v10.1.2-alpha.1.100+0123456789abcdef+SOMETHING", "v10.1.2-alpha.1.100_0123456789abcdef_SOMETHING"},
// random and invalid input: should return safe value
{"v1,0!0+üñµ", "v1_0_0____"},
}
for _, tc := range cases {
tag := KubernetesVersionToImageTag(tc.input)
t.Logf("KubernetesVersionToImageTag: Input: %q. Result: %q. Expected: %q", tc.input, tag, tc.expected)
if tag != tc.expected {
t.Errorf("failed KubernetesVersionToImageTag: Input: %q. Result: %q. Expected: %q", tc.input, tag, tc.expected)
}
}
}
func TestSplitVersion(t *testing.T) {
type T struct {
input string
bucket string
label string
valid bool
}
cases := []T{
// Release area
{"v1.7.0", "https://dl.k8s.io/release", "v1.7.0", true},
{"v1.8.0-alpha.2.1231+afabd012389d53a", "https://dl.k8s.io/release", "v1.8.0-alpha.2.1231+afabd012389d53a", true},
{"release/v1.7.0", "https://dl.k8s.io/release", "v1.7.0", true},
{"release/latest-1.7", "https://dl.k8s.io/release", "latest-1.7", true},
// CI builds area, lookup actual builds at ci-cross/*.txt
{"ci-cross/latest", "https://dl.k8s.io/ci-cross", "latest", true},
{"ci/latest-1.7", "https://dl.k8s.io/ci-cross", "latest-1.7", true},
// unknown label in default (release) area: splitVersion validate only areas.
{"unknown-1", "https://dl.k8s.io/release", "unknown-1", true},
// unknown area, not valid input.
{"unknown/latest-1", "", "", false},
}
// kubeReleaseBucketURL can be overriden during network tests, thus ensure
// it will contain value corresponding to expected outcome for this unit test
kubeReleaseBucketURL = "https://dl.k8s.io"
for _, tc := range cases {
bucket, label, err := splitVersion(tc.input)
switch {
case err != nil && tc.valid:
t.Errorf("splitVersion: unexpected error for %q. Error: %v", tc.input, err)
case err == nil && !tc.valid:
t.Errorf("splitVersion: error expected for key %q, but result is %q, %q", tc.input, bucket, label)
case bucket != tc.bucket:
t.Errorf("splitVersion: unexpected bucket result for key %q. Expected: %q Actual: %q", tc.input, tc.bucket, bucket)
case label != tc.label:
t.Errorf("splitVersion: unexpected label result for key %q. Expected: %q Actual: %q", tc.input, tc.label, label)
}
}
}
func TestKubernetesIsCIVersion(t *testing.T) {
type T struct {
input string
expected bool
}
cases := []T{
{"", false},
// Official releases
{"v1.0.0", false},
{"release/v1.0.0", false},
// CI builds
{"ci/latest-1", true},
{"ci-cross/latest", true},
{"ci/v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
{"ci-cross/v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
}
for _, tc := range cases {
result := KubernetesIsCIVersion(tc.input)
t.Logf("KubernetesIsCIVersion: Input: %q. Result: %v. Expected: %v", tc.input, result, tc.expected)
if result != tc.expected {
t.Errorf("failed KubernetesIsCIVersion: Input: %q. Result: %v. Expected: %v", tc.input, result, tc.expected)
}
}
}
// Validate KubernetesReleaseVersion but with bucket prefixes
func TestCIBuildVersion(t *testing.T) {
type T struct {
input string
expected string
valid bool
}
cases := []T{
// Official releases
{"v1.7.0", "v1.7.0", true},
{"release/v1.8.0", "v1.8.0", true},
{"1.4.0-beta.0", "v1.4.0-beta.0", true},
{"release/0invalid", "", false},
// CI or custom builds
{"ci/v1.9.0-alpha.1.123+acbcbfd53bfa0a", "v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
{"ci-cross/v1.9.0-alpha.1.123+acbcbfd53bfa0a", "v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
{"ci/1.9.0-alpha.1.123+acbcbfd53bfa0a", "v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
{"ci-cross/1.9.0-alpha.1.123+acbcbfd53bfa0a", "v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
{"ci/0invalid", "", false},
}
for _, tc := range cases {
ver, err := KubernetesReleaseVersion(tc.input)
t.Logf("Input: %q. Result: %q, Error: %v", tc.input, ver, err)
switch {
case err != nil && tc.valid:
t.Errorf("KubernetesReleaseVersion: unexpected error for input %q. Error: %v", tc.input, err)
case err == nil && !tc.valid:
t.Errorf("KubernetesReleaseVersion: error expected for input %q, but result is %q", tc.input, ver)
case ver != tc.expected:
t.Errorf("KubernetesReleaseVersion: unexpected result for input %q. Expected: %q Actual: %q", tc.input, tc.expected, ver)
}
}
}
func TestNormalizedBuildVersionVersion(t *testing.T) {
type T struct {
input string
expected string
}
cases := []T{
{"v1.7.0", "v1.7.0"},
{"v1.8.0-alpha.2.1231+afabd012389d53a", "v1.8.0-alpha.2.1231+afabd012389d53a"},
{"1.7.0", "v1.7.0"},
{"unknown-1", ""},
}
for _, tc := range cases {
output := normalizedBuildVersion(tc.input)
if output != tc.expected {
t.Errorf("normalizedBuildVersion: unexpected output %q for input %q. Expected: %q", output, tc.input, tc.expected)
}
}
}