mirror of https://github.com/k3s-io/k3s
parent
3ddde070f3
commit
480635bb72
|
@ -129,6 +129,11 @@
|
|||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "aef70dacbc78771e35beb261bb3a72986adf7906"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/imdario/mergo",
|
||||
"Comment": "0.1.3-8-g6633656",
|
||||
"Rev": "6633656539c1639d9d78127b7d47c622b5d7b6dc"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/miekg/dns",
|
||||
"Rev": "3f504e8dabd5d562e997d19ce0200aa41973e1b2"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
language: go
|
||||
install: go get -t
|
|
@ -0,0 +1,28 @@
|
|||
Copyright (c) 2013 Dario Castañé. All rights reserved.
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,68 @@
|
|||
# Mergo
|
||||
|
||||
A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements.
|
||||
|
||||
Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the Province of Ancona in the Italian region Marche.
|
||||
|
||||
![Mergo dall'alto](http://www.comune.mergo.an.it/Siti/Mergo/Immagini/Foto/mergo_dall_alto.jpg)
|
||||
|
||||
## Status
|
||||
|
||||
It is ready for production use. It works fine although it may use more of testing. Here some projects in the wild using Mergo:
|
||||
|
||||
- [EagerIO/Stout](https://github.com/EagerIO/Stout)
|
||||
- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api)
|
||||
- [russross/canvasassignments](https://github.com/russross/canvasassignments)
|
||||
- [rdegges/cryptly-api](https://github.com/rdegges/cryptly-api)
|
||||
- [casualjim/exeggutor](https://github.com/casualjim/exeggutor)
|
||||
- [divshot/gitling](https://github.com/divshot/gitling)
|
||||
- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl)
|
||||
|
||||
[![Build Status][1]][2]
|
||||
[![GoDoc](https://godoc.org/github.com/imdario/mergo?status.svg)](https://godoc.org/github.com/imdario/mergo)
|
||||
|
||||
[1]: https://travis-ci.org/imdario/mergo.png
|
||||
[2]: https://travis-ci.org/imdario/mergo
|
||||
|
||||
## Installation
|
||||
|
||||
go get github.com/imdario/mergo
|
||||
|
||||
// use in your .go code
|
||||
import (
|
||||
"github.com/imdario/mergo"
|
||||
)
|
||||
|
||||
## Usage
|
||||
|
||||
You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. Also maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection).
|
||||
|
||||
if err := mergo.Merge(&dst, src); err != nil {
|
||||
// ...
|
||||
}
|
||||
|
||||
Additionally, you can map a map[string]interface{} to a struct (and otherwise, from struct to map), following the same restrictions as in Merge(). Keys are capitalized to find each corresponding exported field.
|
||||
|
||||
if err := mergo.Map(&dst, srcMap); err != nil {
|
||||
// ...
|
||||
}
|
||||
|
||||
Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as map[string]interface{}. They will be just assigned as values.
|
||||
|
||||
More information and examples in [godoc documentation](http://godoc.org/github.com/imdario/mergo).
|
||||
|
||||
Note: if test are failing due missing package, please execute:
|
||||
|
||||
go get gopkg.in/yaml.v1
|
||||
|
||||
## Contact me
|
||||
|
||||
If I can help you, you have an idea or you are using Mergo in your projects, don't hesitate to drop me a line (or a pull request): [@im_dario](https://twitter.com/im_dario)
|
||||
|
||||
## About
|
||||
|
||||
Written by [Dario Castañé](http://dario.im).
|
||||
|
||||
## License
|
||||
|
||||
[BSD 3-Clause](http://opensource.org/licenses/BSD-3-Clause) license, as [Go language](http://golang.org/LICENSE).
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2013 Dario Castañé. All rights reserved.
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package mergo merges same-type structs and maps by setting default values in zero-value fields.
|
||||
|
||||
Mergo won't merge unexported (private) fields but will do recursively any exported one. It also won't merge structs inside maps (because they are not addressable using Go reflection).
|
||||
|
||||
Usage
|
||||
|
||||
From my own work-in-progress project:
|
||||
|
||||
type networkConfig struct {
|
||||
Protocol string
|
||||
Address string
|
||||
ServerType string `json: "server_type"`
|
||||
Port uint16
|
||||
}
|
||||
|
||||
type FssnConfig struct {
|
||||
Network networkConfig
|
||||
}
|
||||
|
||||
var fssnDefault = FssnConfig {
|
||||
networkConfig {
|
||||
"tcp",
|
||||
"127.0.0.1",
|
||||
"http",
|
||||
31560,
|
||||
},
|
||||
}
|
||||
|
||||
// Inside a function [...]
|
||||
|
||||
if err := mergo.Merge(&config, fssnDefault); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// More code [...]
|
||||
|
||||
*/
|
||||
package mergo
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright 2014 Dario Castañé. All rights reserved.
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Based on src/pkg/reflect/deepequal.go from official
|
||||
// golang's stdlib.
|
||||
|
||||
package mergo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func changeInitialCase(s string, mapper func(rune) rune) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
r, n := utf8.DecodeRuneInString(s)
|
||||
return string(mapper(r)) + s[n:]
|
||||
}
|
||||
|
||||
func isExported(field reflect.StructField) bool {
|
||||
r, _ := utf8.DecodeRuneInString(field.Name)
|
||||
return r >= 'A' && r <= 'Z'
|
||||
}
|
||||
|
||||
// Traverses recursively both values, assigning src's fields values to dst.
|
||||
// The map argument tracks comparisons that have already been seen, which allows
|
||||
// short circuiting on recursive types.
|
||||
func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) {
|
||||
if dst.CanAddr() {
|
||||
addr := dst.UnsafeAddr()
|
||||
h := 17 * addr
|
||||
seen := visited[h]
|
||||
typ := dst.Type()
|
||||
for p := seen; p != nil; p = p.next {
|
||||
if p.ptr == addr && p.typ == typ {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Remember, remember...
|
||||
visited[h] = &visit{addr, typ, seen}
|
||||
}
|
||||
zeroValue := reflect.Value{}
|
||||
switch dst.Kind() {
|
||||
case reflect.Map:
|
||||
dstMap := dst.Interface().(map[string]interface{})
|
||||
for i, n := 0, src.NumField(); i < n; i++ {
|
||||
srcType := src.Type()
|
||||
field := srcType.Field(i)
|
||||
if !isExported(field) {
|
||||
continue
|
||||
}
|
||||
fieldName := field.Name
|
||||
fieldName = changeInitialCase(fieldName, unicode.ToLower)
|
||||
if v, ok := dstMap[fieldName]; !ok || isEmptyValue(reflect.ValueOf(v)) {
|
||||
dstMap[fieldName] = src.Field(i).Interface()
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
srcMap := src.Interface().(map[string]interface{})
|
||||
for key := range srcMap {
|
||||
srcValue := srcMap[key]
|
||||
fieldName := changeInitialCase(key, unicode.ToUpper)
|
||||
dstElement := dst.FieldByName(fieldName)
|
||||
if dstElement == zeroValue {
|
||||
// We discard it because the field doesn't exist.
|
||||
continue
|
||||
}
|
||||
srcElement := reflect.ValueOf(srcValue)
|
||||
dstKind := dstElement.Kind()
|
||||
srcKind := srcElement.Kind()
|
||||
if srcKind == reflect.Ptr && dstKind != reflect.Ptr {
|
||||
srcElement = srcElement.Elem()
|
||||
srcKind = reflect.TypeOf(srcElement.Interface()).Kind()
|
||||
} else if dstKind == reflect.Ptr {
|
||||
// Can this work? I guess it can't.
|
||||
if srcKind != reflect.Ptr && srcElement.CanAddr() {
|
||||
srcPtr := srcElement.Addr()
|
||||
srcElement = reflect.ValueOf(srcPtr)
|
||||
srcKind = reflect.Ptr
|
||||
}
|
||||
}
|
||||
if !srcElement.IsValid() {
|
||||
continue
|
||||
}
|
||||
if srcKind == dstKind {
|
||||
if err = deepMerge(dstElement, srcElement, visited, depth+1); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if srcKind == reflect.Map {
|
||||
if err = deepMap(dstElement, srcElement, visited, depth+1); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Map sets fields' values in dst from src.
|
||||
// src can be a map with string keys or a struct. dst must be the opposite:
|
||||
// if src is a map, dst must be a valid pointer to struct. If src is a struct,
|
||||
// dst must be map[string]interface{}.
|
||||
// It won't merge unexported (private) fields and will do recursively
|
||||
// any exported field.
|
||||
// If dst is a map, keys will be src fields' names in lower camel case.
|
||||
// Missing key in src that doesn't match a field in dst will be skipped. This
|
||||
// doesn't apply if dst is a map.
|
||||
// This is separated method from Merge because it is cleaner and it keeps sane
|
||||
// semantics: merging equal types, mapping different (restricted) types.
|
||||
func Map(dst, src interface{}) error {
|
||||
var (
|
||||
vDst, vSrc reflect.Value
|
||||
err error
|
||||
)
|
||||
if vDst, vSrc, err = resolveValues(dst, src); err != nil {
|
||||
return err
|
||||
}
|
||||
// To be friction-less, we redirect equal-type arguments
|
||||
// to deepMerge. Only because arguments can be anything.
|
||||
if vSrc.Kind() == vDst.Kind() {
|
||||
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0)
|
||||
}
|
||||
switch vSrc.Kind() {
|
||||
case reflect.Struct:
|
||||
if vDst.Kind() != reflect.Map {
|
||||
return ErrExpectedMapAsDestination
|
||||
}
|
||||
case reflect.Map:
|
||||
if vDst.Kind() != reflect.Struct {
|
||||
return ErrExpectedStructAsDestination
|
||||
}
|
||||
default:
|
||||
return ErrNotSupported
|
||||
}
|
||||
return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0)
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright 2013 Dario Castañé. All rights reserved.
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Based on src/pkg/reflect/deepequal.go from official
|
||||
// golang's stdlib.
|
||||
|
||||
package mergo
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Traverses recursively both values, assigning src's fields values to dst.
|
||||
// The map argument tracks comparisons that have already been seen, which allows
|
||||
// short circuiting on recursive types.
|
||||
func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) {
|
||||
if !src.IsValid() {
|
||||
return
|
||||
}
|
||||
if dst.CanAddr() {
|
||||
addr := dst.UnsafeAddr()
|
||||
h := 17 * addr
|
||||
seen := visited[h]
|
||||
typ := dst.Type()
|
||||
for p := seen; p != nil; p = p.next {
|
||||
if p.ptr == addr && p.typ == typ {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Remember, remember...
|
||||
visited[h] = &visit{addr, typ, seen}
|
||||
}
|
||||
switch dst.Kind() {
|
||||
case reflect.Struct:
|
||||
for i, n := 0, dst.NumField(); i < n; i++ {
|
||||
if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, key := range src.MapKeys() {
|
||||
srcElement := src.MapIndex(key)
|
||||
if !srcElement.IsValid() {
|
||||
continue
|
||||
}
|
||||
dstElement := dst.MapIndex(key)
|
||||
switch reflect.TypeOf(srcElement.Interface()).Kind() {
|
||||
case reflect.Struct:
|
||||
fallthrough
|
||||
case reflect.Map:
|
||||
if err = deepMerge(dstElement, srcElement, visited, depth+1); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !dstElement.IsValid() {
|
||||
dst.SetMapIndex(key, srcElement)
|
||||
}
|
||||
}
|
||||
case reflect.Ptr:
|
||||
fallthrough
|
||||
case reflect.Interface:
|
||||
if src.IsNil() {
|
||||
break
|
||||
} else if dst.IsNil() {
|
||||
if dst.CanSet() && isEmptyValue(dst) {
|
||||
dst.Set(src)
|
||||
}
|
||||
} else if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1); err != nil {
|
||||
return
|
||||
}
|
||||
default:
|
||||
if dst.CanSet() && !isEmptyValue(src) {
|
||||
dst.Set(src)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Merge sets fields' values in dst from src if they have a zero
|
||||
// value of their type.
|
||||
// dst and src must be valid same-type structs and dst must be
|
||||
// a pointer to struct.
|
||||
// It won't merge unexported (private) fields and will do recursively
|
||||
// any exported field.
|
||||
func Merge(dst, src interface{}) error {
|
||||
var (
|
||||
vDst, vSrc reflect.Value
|
||||
err error
|
||||
)
|
||||
if vDst, vSrc, err = resolveValues(dst, src); err != nil {
|
||||
return err
|
||||
}
|
||||
if vDst.Type() != vSrc.Type() {
|
||||
return ErrDifferentArgumentsTypes
|
||||
}
|
||||
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0)
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2013 Dario Castañé. All rights reserved.
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Based on src/pkg/reflect/deepequal.go from official
|
||||
// golang's stdlib.
|
||||
|
||||
package mergo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Errors reported by Mergo when it finds invalid arguments.
|
||||
var (
|
||||
ErrNilArguments = errors.New("src and dst must not be nil")
|
||||
ErrDifferentArgumentsTypes = errors.New("src and dst must be of same type")
|
||||
ErrNotSupported = errors.New("only structs and maps are supported")
|
||||
ErrExpectedMapAsDestination = errors.New("dst was expected to be a map")
|
||||
ErrExpectedStructAsDestination = errors.New("dst was expected to be a struct")
|
||||
)
|
||||
|
||||
// During deepMerge, must keep track of checks that are
|
||||
// in progress. The comparison algorithm assumes that all
|
||||
// checks in progress are true when it reencounters them.
|
||||
// Visited are stored in a map indexed by 17 * a1 + a2;
|
||||
type visit struct {
|
||||
ptr uintptr
|
||||
typ reflect.Type
|
||||
next *visit
|
||||
}
|
||||
|
||||
// From src/pkg/encoding/json.
|
||||
func isEmptyValue(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
return v.Len() == 0
|
||||
case reflect.Bool:
|
||||
return !v.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return v.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() == 0
|
||||
case reflect.Interface, reflect.Ptr:
|
||||
return v.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func resolveValues(dst, src interface{}) (vDst, vSrc reflect.Value, err error) {
|
||||
if dst == nil || src == nil {
|
||||
err = ErrNilArguments
|
||||
return
|
||||
}
|
||||
vDst = reflect.ValueOf(dst).Elem()
|
||||
if vDst.Kind() != reflect.Struct && vDst.Kind() != reflect.Map {
|
||||
err = ErrNotSupported
|
||||
return
|
||||
}
|
||||
vSrc = reflect.ValueOf(src)
|
||||
// We check if vSrc is a pointer to dereference it.
|
||||
if vSrc.Kind() == reflect.Ptr {
|
||||
vSrc = vSrc.Elem()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Traverses recursively both values, assigning src's fields values to dst.
|
||||
// The map argument tracks comparisons that have already been seen, which allows
|
||||
// short circuiting on recursive types.
|
||||
func deeper(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) {
|
||||
if dst.CanAddr() {
|
||||
addr := dst.UnsafeAddr()
|
||||
h := 17 * addr
|
||||
seen := visited[h]
|
||||
typ := dst.Type()
|
||||
for p := seen; p != nil; p = p.next {
|
||||
if p.ptr == addr && p.typ == typ {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Remember, remember...
|
||||
visited[h] = &visit{addr, typ, seen}
|
||||
}
|
||||
return // TODO refactor
|
||||
}
|
|
@ -0,0 +1,288 @@
|
|||
// Copyright 2013 Dario Castañé. All rights reserved.
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mergo
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v1"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type simpleTest struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type complexTest struct {
|
||||
St simpleTest
|
||||
sz int
|
||||
Id string
|
||||
}
|
||||
|
||||
type moreComplextText struct {
|
||||
Ct complexTest
|
||||
St simpleTest
|
||||
Nt simpleTest
|
||||
}
|
||||
|
||||
type pointerTest struct {
|
||||
C *simpleTest
|
||||
}
|
||||
|
||||
type sliceTest struct {
|
||||
S []int
|
||||
}
|
||||
|
||||
func TestNil(t *testing.T) {
|
||||
if err := Merge(nil, nil); err != ErrNilArguments {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestDifferentTypes(t *testing.T) {
|
||||
a := simpleTest{42}
|
||||
b := 42
|
||||
if err := Merge(&a, b); err != ErrDifferentArgumentsTypes {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleStruct(t *testing.T) {
|
||||
a := simpleTest{}
|
||||
b := simpleTest{42}
|
||||
if err := Merge(&a, b); err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if a.Value != 42 {
|
||||
t.Fatalf("b not merged in a properly: a.Value(%d) != b.Value(%d)", a.Value, b.Value)
|
||||
}
|
||||
if !reflect.DeepEqual(a, b) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplexStruct(t *testing.T) {
|
||||
a := complexTest{}
|
||||
a.Id = "athing"
|
||||
b := complexTest{simpleTest{42}, 1, "bthing"}
|
||||
if err := Merge(&a, b); err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if a.St.Value != 42 {
|
||||
t.Fatalf("b not merged in a properly: a.St.Value(%d) != b.St.Value(%d)", a.St.Value, b.St.Value)
|
||||
}
|
||||
if a.sz == 1 {
|
||||
t.Fatalf("a's private field sz not preserved from merge: a.sz(%d) == b.sz(%d)", a.sz, b.sz)
|
||||
}
|
||||
if a.Id != b.Id {
|
||||
t.Fatalf("a's field Id not merged properly: a.Id(%s) != b.Id(%s)", a.Id, b.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPointerStruct(t *testing.T) {
|
||||
s1 := simpleTest{}
|
||||
s2 := simpleTest{19}
|
||||
a := pointerTest{&s1}
|
||||
b := pointerTest{&s2}
|
||||
if err := Merge(&a, b); err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if a.C.Value != b.C.Value {
|
||||
//t.Fatalf("b not merged in a properly: a.C.Value(%d) != b.C.Value(%d)", a.C.Value, b.C.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPointerStructNil(t *testing.T) {
|
||||
a := pointerTest{nil}
|
||||
b := pointerTest{&simpleTest{19}}
|
||||
if err := Merge(&a, b); err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if a.C.Value != b.C.Value {
|
||||
t.Fatalf("b not merged in a properly: a.C.Value(%d) != b.C.Value(%d)", a.C.Value, b.C.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceStruct(t *testing.T) {
|
||||
a := sliceTest{}
|
||||
b := sliceTest{[]int{1, 2, 3}}
|
||||
if err := Merge(&a, b); err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if len(b.S) != 3 {
|
||||
t.FailNow()
|
||||
}
|
||||
if len(a.S) != len(b.S) {
|
||||
t.Fatalf("b not merged in a properly %d != %d", len(a.S), len(b.S))
|
||||
}
|
||||
|
||||
a = sliceTest{[]int{1}}
|
||||
b = sliceTest{[]int{1, 2, 3}}
|
||||
if err := Merge(&a, b); err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if len(b.S) != 3 {
|
||||
t.FailNow()
|
||||
}
|
||||
if len(a.S) != len(b.S) {
|
||||
t.Fatalf("b not merged in a properly %d != %d", len(a.S), len(b.S))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaps(t *testing.T) {
|
||||
m := map[string]simpleTest{
|
||||
"a": simpleTest{},
|
||||
"b": simpleTest{42},
|
||||
}
|
||||
n := map[string]simpleTest{
|
||||
"a": simpleTest{16},
|
||||
"b": simpleTest{},
|
||||
"c": simpleTest{12},
|
||||
}
|
||||
if err := Merge(&m, n); err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
if len(m) != 3 {
|
||||
t.Fatalf(`n not merged in m properly, m must have 3 elements instead of %d`, len(m))
|
||||
}
|
||||
if m["a"].Value != 0 {
|
||||
t.Fatalf(`n merged in m because I solved non-addressable map values TODO: m["a"].Value(%d) != n["a"].Value(%d)`, m["a"].Value, n["a"].Value)
|
||||
}
|
||||
if m["b"].Value != 42 {
|
||||
t.Fatalf(`n wrongly merged in m: m["b"].Value(%d) != n["b"].Value(%d)`, m["b"].Value, n["b"].Value)
|
||||
}
|
||||
if m["c"].Value != 12 {
|
||||
t.Fatalf(`n not merged in m: m["c"].Value(%d) != n["c"].Value(%d)`, m["c"].Value, n["c"].Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestYAMLMaps(t *testing.T) {
|
||||
thing := loadYAML("testdata/thing.yml")
|
||||
license := loadYAML("testdata/license.yml")
|
||||
ft := thing["fields"].(map[interface{}]interface{})
|
||||
fl := license["fields"].(map[interface{}]interface{})
|
||||
expectedLength := len(ft) + len(fl)
|
||||
if err := Merge(&license, thing); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
currentLength := len(license["fields"].(map[interface{}]interface{}))
|
||||
if currentLength != expectedLength {
|
||||
t.Fatalf(`thing not merged in license properly, license must have %d elements instead of %d`, expectedLength, currentLength)
|
||||
}
|
||||
fields := license["fields"].(map[interface{}]interface{})
|
||||
if _, ok := fields["id"]; !ok {
|
||||
t.Fatalf(`thing not merged in license properly, license must have a new id field from thing`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoPointerValues(t *testing.T) {
|
||||
a := &simpleTest{}
|
||||
b := &simpleTest{42}
|
||||
if err := Merge(a, b); err != nil {
|
||||
t.Fatalf(`Boom. You crossed the streams: %s`, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
a := complexTest{}
|
||||
a.Id = "athing"
|
||||
c := moreComplextText{a, simpleTest{}, simpleTest{}}
|
||||
b := map[string]interface{}{
|
||||
"ct": map[string]interface{}{
|
||||
"st": map[string]interface{}{
|
||||
"value": 42,
|
||||
},
|
||||
"sz": 1,
|
||||
"id": "bthing",
|
||||
},
|
||||
"st": &simpleTest{144}, // Mapping a reference
|
||||
"zt": simpleTest{299}, // Mapping a missing field (zt doesn't exist)
|
||||
"nt": simpleTest{3},
|
||||
}
|
||||
if err := Map(&c, b); err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
m := b["ct"].(map[string]interface{})
|
||||
n := m["st"].(map[string]interface{})
|
||||
o := b["st"].(*simpleTest)
|
||||
p := b["nt"].(simpleTest)
|
||||
if c.Ct.St.Value != 42 {
|
||||
t.Fatalf("b not merged in a properly: c.Ct.St.Value(%d) != b.Ct.St.Value(%d)", c.Ct.St.Value, n["value"])
|
||||
}
|
||||
if c.St.Value != 144 {
|
||||
t.Fatalf("b not merged in a properly: c.St.Value(%d) != b.St.Value(%d)", c.St.Value, o.Value)
|
||||
}
|
||||
if c.Nt.Value != 3 {
|
||||
t.Fatalf("b not merged in a properly: c.Nt.Value(%d) != b.Nt.Value(%d)", c.St.Value, p.Value)
|
||||
}
|
||||
if c.Ct.sz == 1 {
|
||||
t.Fatalf("a's private field sz not preserved from merge: c.Ct.sz(%d) == b.Ct.sz(%d)", c.Ct.sz, m["sz"])
|
||||
}
|
||||
if c.Ct.Id != m["id"] {
|
||||
t.Fatalf("a's field Id not merged properly: c.Ct.Id(%s) != b.Ct.Id(%s)", c.Ct.Id, m["id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleMap(t *testing.T) {
|
||||
a := simpleTest{}
|
||||
b := map[string]interface{}{
|
||||
"value": 42,
|
||||
}
|
||||
if err := Map(&a, b); err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
if a.Value != 42 {
|
||||
t.Fatalf("b not merged in a properly: a.Value(%d) != b.Value(%v)", a.Value, b["value"])
|
||||
}
|
||||
}
|
||||
|
||||
type pointerMapTest struct {
|
||||
A int
|
||||
hidden int
|
||||
B *simpleTest
|
||||
}
|
||||
|
||||
func TestBackAndForth(t *testing.T) {
|
||||
pt := pointerMapTest{42, 1, &simpleTest{66}}
|
||||
m := make(map[string]interface{})
|
||||
if err := Map(&m, pt); err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
var (
|
||||
v interface{}
|
||||
ok bool
|
||||
)
|
||||
if v, ok = m["a"]; v.(int) != pt.A || !ok {
|
||||
t.Fatalf("pt not merged properly: m[`a`](%d) != pt.A(%d)", v, pt.A)
|
||||
}
|
||||
if v, ok = m["b"]; !ok {
|
||||
t.Fatalf("pt not merged properly: B is missing in m")
|
||||
}
|
||||
var st *simpleTest
|
||||
if st = v.(*simpleTest); st.Value != 66 {
|
||||
t.Fatalf("something went wrong while mapping pt on m, B wasn't copied")
|
||||
}
|
||||
bpt := pointerMapTest{}
|
||||
if err := Map(&bpt, m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bpt.A != pt.A {
|
||||
t.Fatalf("pt not merged properly: bpt.A(%d) != pt.A(%d)", bpt.A, pt.A)
|
||||
}
|
||||
if bpt.hidden == pt.hidden {
|
||||
t.Fatalf("pt unexpectedly merged: bpt.hidden(%d) == pt.hidden(%d)", bpt.hidden, pt.hidden)
|
||||
}
|
||||
if bpt.B.Value != pt.B.Value {
|
||||
t.Fatalf("pt not merged properly: bpt.B.Value(%d) != pt.B.Value(%d)", bpt.B.Value, pt.B.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func loadYAML(path string) (m map[string]interface{}) {
|
||||
m = make(map[string]interface{})
|
||||
raw, _ := ioutil.ReadFile(path)
|
||||
_ = yaml.Unmarshal(raw, &m)
|
||||
return
|
||||
}
|
3
Godeps/_workspace/src/github.com/imdario/mergo/testdata/license.yml
generated
vendored
Normal file
3
Godeps/_workspace/src/github.com/imdario/mergo/testdata/license.yml
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
import: ../../../../fossene/db/schema/thing.yml
|
||||
fields:
|
||||
site: string
|
|
@ -0,0 +1,5 @@
|
|||
fields:
|
||||
id: int
|
||||
name: string
|
||||
parent: ref "datu:thing"
|
||||
status: enum(draft, public, private)
|
|
@ -24,6 +24,6 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
clientBuilder := clientcmd.NewBuilder(clientcmd.NewPromptingAuthLoader(os.Stdin))
|
||||
clientBuilder := clientcmd.NewInteractiveClientConfig(clientcmd.Config{}, "", &clientcmd.ConfigOverrides{}, os.Stdin)
|
||||
cmd.NewFactory(clientBuilder).Run(os.Stdout)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
# .kubeconfig files
|
||||
In order to easily switch between multiple clusters, a .kubeconfig file was defined. This file contains a series of authentication mechanisms and cluster connection information associated with nicknames. It also introduces the concept of a tuple of authentication information (user) and cluster connection information called a context that is also associated with a nickname.
|
||||
|
||||
Multiple files are .kubeconfig files are allowed. At runtime they are loaded and merged together along with override options specified from the command line (see rules below).
|
||||
|
||||
## Related discussion
|
||||
https://github.com/GoogleCloudPlatform/kubernetes/issues/1755
|
||||
|
||||
## Example .kubeconfig file
|
||||
```
|
||||
preferences:
|
||||
colors: true
|
||||
clusters:
|
||||
cow-cluster:
|
||||
server: http://cow.org:8080
|
||||
api-version: v1beta1
|
||||
horse-cluster:
|
||||
server: https://horse.org:4443
|
||||
certificate-authority: path/to/my/cafile
|
||||
pig-cluster:
|
||||
server: https://pig.org:443
|
||||
insecure-skip-tls-verify: true
|
||||
users:
|
||||
black-user:
|
||||
auth-path: path/to/my/existing/.kubernetes_auth file
|
||||
blue-user:
|
||||
token: blue-token
|
||||
green-user:
|
||||
client-certificate: path/to/my/client/cert
|
||||
client-key: path/to/my/client/key
|
||||
contexts:
|
||||
queen-anne-context:
|
||||
cluster: pig-cluster
|
||||
user: black-user
|
||||
namespace: saw-ns
|
||||
federal-context:
|
||||
cluster: horse-cluster
|
||||
user: green-user
|
||||
namespace: chisel-ns
|
||||
current-context: federal-context
|
||||
```
|
||||
|
||||
## Loading and merging rules
|
||||
The rules for loading and merging the .kubeconfig files are straightforward, but there are a lot of them. The final config is built in this order:
|
||||
1. Merge together the kubeconfig itself. This is done with the following hierarchy and merge rules:
|
||||
|
||||
Empty filenames are ignored. Files with non-deserializable content produced errors.
|
||||
The first file to set a particular value or map key wins and the value or map key is never changed.
|
||||
This means that the first file to set CurrentContext will have its context preserved. It also means that if two files specify a "red-user", only values from the first file's red-user are used. Even non-conflicting entries from the second file's "red-user" are discarded.
|
||||
1. CommandLineLocation - the value of the `kubeconfig` command line option
|
||||
1. EnvVarLocation - the value of $KUBECONFIG
|
||||
1. CurrentDirectoryLocation - ``pwd``/.kubeconfig
|
||||
1. HomeDirectoryLocation = ~/.kube/.kubeconfig
|
||||
1. Determine the context to use based on the first hit in this chain
|
||||
1. command line argument - the value of the `context` command line option
|
||||
1. current-context from the merged kubeconfig file
|
||||
1. Empty is allowed at this stage
|
||||
1. Determine the cluster info and user to use. At this point, we may or may not have a context. They are built based on the first hit in this chain. (run it twice, once for user, once for cluster)
|
||||
1. command line argument - `user` for user name and `cluster` for cluster name
|
||||
1. If context is present, then use the context's value
|
||||
1. Empty is allowed
|
||||
1. Determine the actual cluster info to use. At this point, we may or may not have a cluster info. Build each piece of the cluster info based on the chain (first hit wins):
|
||||
1. command line arguments - `server`, `api-version`, `certificate-authority`, and `insecure-skip-tls-verify`
|
||||
1. If cluster info is present and a value for the attribute is present, use it.
|
||||
1. If you don't have a server location, error.
|
||||
1. User is build using the same rules as cluster info, EXCEPT that you can only have one authentication technique per user.
|
||||
|
||||
The command line flags are: `auth-path`, `client-certificate`, `client-key`, and `token`. If there are two conflicting techniques, fail.
|
||||
1. For any information still missing, use default values and potentially prompt for authentication information
|
|
@ -40,17 +40,16 @@ func (*defaultAuthLoader) LoadAuth(path string) (*clientauth.Info, error) {
|
|||
return clientauth.LoadFromFile(path)
|
||||
}
|
||||
|
||||
type promptingAuthLoader struct {
|
||||
type PromptingAuthLoader struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
// LoadAuth parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist.
|
||||
func (a *promptingAuthLoader) LoadAuth(path string) (*clientauth.Info, error) {
|
||||
func (a *PromptingAuthLoader) LoadAuth(path string) (*clientauth.Info, error) {
|
||||
var auth clientauth.Info
|
||||
// Prompt for user/pass and write a file if none exists.
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
auth.User = promptForString("Username", a.reader)
|
||||
auth.Password = promptForString("Password", a.reader)
|
||||
auth = *a.Prompt()
|
||||
data, err := json.Marshal(auth)
|
||||
if err != nil {
|
||||
return &auth, err
|
||||
|
@ -64,6 +63,16 @@ func (a *promptingAuthLoader) LoadAuth(path string) (*clientauth.Info, error) {
|
|||
}
|
||||
return authPtr, nil
|
||||
}
|
||||
|
||||
// Prompt pulls the user and password from a reader
|
||||
func (a *PromptingAuthLoader) Prompt() *clientauth.Info {
|
||||
auth := &clientauth.Info{}
|
||||
auth.User = promptForString("Username", a.reader)
|
||||
auth.Password = promptForString("Password", a.reader)
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
func promptForString(field string, r io.Reader) string {
|
||||
fmt.Printf("Please enter %s: ", field)
|
||||
var result string
|
||||
|
@ -72,8 +81,8 @@ func promptForString(field string, r io.Reader) string {
|
|||
}
|
||||
|
||||
// NewDefaultAuthLoader is an AuthLoader that parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist.
|
||||
func NewPromptingAuthLoader(reader io.Reader) AuthLoader {
|
||||
return &promptingAuthLoader{reader}
|
||||
func NewPromptingAuthLoader(reader io.Reader) *PromptingAuthLoader {
|
||||
return &PromptingAuthLoader{reader}
|
||||
}
|
||||
|
||||
// NewDefaultAuthLoader returns a default implementation of an AuthLoader that only reads from a config file
|
||||
|
|
|
@ -1,207 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
|
||||
)
|
||||
|
||||
// Builder are used to bind and interpret command line flags to make it easy to get an api server client
|
||||
type Builder interface {
|
||||
// BindFlags must bind and keep track of all the flags required to build a client config object
|
||||
BindFlags(flags *pflag.FlagSet)
|
||||
// Client calls BuildConfig under the covers and uses that config to return a client
|
||||
Client() (*client.Client, error)
|
||||
|
||||
// Config uses the values of the bound flags and builds a complete client config
|
||||
Config() (*client.Config, error)
|
||||
// Override invokes Config(), then passes that to the provided function, and returns a new
|
||||
// builder that will use that config as its default. If Config() returns an error for the default
|
||||
// values the function will not be invoked, and the error will be available when Client() is called.
|
||||
Override(func(*client.Config)) Builder
|
||||
}
|
||||
|
||||
// cmdAuthInfo is used to track whether flags have been set
|
||||
type cmdAuthInfo struct {
|
||||
User StringFlag
|
||||
Password StringFlag
|
||||
CAFile StringFlag
|
||||
CertFile StringFlag
|
||||
KeyFile StringFlag
|
||||
BearerToken StringFlag
|
||||
Insecure BoolFlag
|
||||
}
|
||||
|
||||
// builder is a default implementation of a Builder
|
||||
type builder struct {
|
||||
authLoader AuthLoader
|
||||
cmdAuthInfo cmdAuthInfo
|
||||
authPath string
|
||||
apiserver string
|
||||
apiVersion string
|
||||
matchApiVersion bool
|
||||
|
||||
config *client.Config
|
||||
}
|
||||
|
||||
// NewBuilder returns a valid Builder that uses the passed authLoader. If authLoader is nil, the NewDefaultAuthLoader is used.
|
||||
func NewBuilder(authLoader AuthLoader) Builder {
|
||||
if authLoader == nil {
|
||||
authLoader = NewDefaultAuthLoader()
|
||||
}
|
||||
|
||||
return &builder{
|
||||
authLoader: authLoader,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
FlagApiServer = "server"
|
||||
FlagMatchApiVersion = "match-server-version"
|
||||
FlagApiVersion = "api-version"
|
||||
FlagAuthPath = "auth-path"
|
||||
FlagInsecure = "insecure-skip-tls-verify"
|
||||
FlagCertFile = "client-certificate"
|
||||
FlagKeyFile = "client-key"
|
||||
FlagCAFile = "certificate-authority"
|
||||
FlagBearerToken = "token"
|
||||
)
|
||||
|
||||
// BindFlags implements Builder
|
||||
func (builder *builder) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.StringVarP(&builder.apiserver, FlagApiServer, "s", builder.apiserver, "The address of the Kubernetes API server")
|
||||
flags.BoolVar(&builder.matchApiVersion, FlagMatchApiVersion, false, "Require server version to match client version")
|
||||
flags.StringVar(&builder.apiVersion, FlagApiVersion, latest.Version, "The API version to use when talking to the server")
|
||||
flags.StringVarP(&builder.authPath, FlagAuthPath, "a", os.Getenv("HOME")+"/.kubernetes_auth", "Path to the auth info file. If missing, prompt the user. Only used if using https.")
|
||||
flags.Var(&builder.cmdAuthInfo.Insecure, FlagInsecure, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure.")
|
||||
flags.Var(&builder.cmdAuthInfo.CertFile, FlagCertFile, "Path to a client key file for TLS.")
|
||||
flags.Var(&builder.cmdAuthInfo.KeyFile, FlagKeyFile, "Path to a client key file for TLS.")
|
||||
flags.Var(&builder.cmdAuthInfo.CAFile, FlagCAFile, "Path to a cert. file for the certificate authority.")
|
||||
flags.Var(&builder.cmdAuthInfo.BearerToken, FlagBearerToken, "Bearer token for authentication to the API server.")
|
||||
}
|
||||
|
||||
// Client implements Builder
|
||||
func (builder *builder) Client() (*client.Client, error) {
|
||||
clientConfig, err := builder.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := client.New(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if builder.matchApiVersion {
|
||||
clientVersion := version.Get()
|
||||
serverVersion, err := c.ServerVersion()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't read version from server: %v\n", err)
|
||||
}
|
||||
if s := *serverVersion; !reflect.DeepEqual(clientVersion, s) {
|
||||
return nil, fmt.Errorf("server version (%#v) differs from client version (%#v)!\n", s, clientVersion)
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Config implements Builder
|
||||
func (builder *builder) Config() (*client.Config, error) {
|
||||
if builder.config != nil {
|
||||
return builder.config, nil
|
||||
}
|
||||
return builder.newConfig()
|
||||
}
|
||||
|
||||
// Override implements Builder
|
||||
func (builder *builder) Override(fn func(*client.Config)) Builder {
|
||||
config, err := builder.newConfig()
|
||||
if err != nil {
|
||||
return builder
|
||||
}
|
||||
fn(config)
|
||||
b := *builder
|
||||
b.config = config
|
||||
return &b
|
||||
}
|
||||
|
||||
// newConfig creates a new config object for this builder
|
||||
func (builder *builder) newConfig() (*client.Config, error) {
|
||||
clientConfig := client.Config{}
|
||||
if len(builder.apiserver) > 0 {
|
||||
clientConfig.Host = builder.apiserver
|
||||
} else if len(os.Getenv("KUBERNETES_MASTER")) > 0 {
|
||||
clientConfig.Host = os.Getenv("KUBERNETES_MASTER")
|
||||
} else {
|
||||
// TODO: eventually apiserver should start on 443 and be secure by default
|
||||
clientConfig.Host = "http://localhost:8080"
|
||||
}
|
||||
clientConfig.Version = builder.apiVersion
|
||||
|
||||
// only try to read the auth information if we are secure
|
||||
if client.IsConfigTransportTLS(&clientConfig) {
|
||||
authInfoFileFound := true
|
||||
authInfo, err := builder.authLoader.LoadAuth(builder.authPath)
|
||||
if authInfo == nil && err != nil { // only consider failing if we don't have any auth info
|
||||
if !os.IsNotExist(err) { // if it's just a case of a missing file, simply flag the auth as not found and use the command line arguments
|
||||
return nil, err
|
||||
}
|
||||
authInfoFileFound = false
|
||||
authInfo = &clientauth.Info{}
|
||||
}
|
||||
|
||||
// If provided, the command line options override options from the auth file
|
||||
if !authInfoFileFound || builder.cmdAuthInfo.User.Provided() {
|
||||
authInfo.User = builder.cmdAuthInfo.User.Value
|
||||
}
|
||||
if !authInfoFileFound || builder.cmdAuthInfo.Password.Provided() {
|
||||
authInfo.Password = builder.cmdAuthInfo.Password.Value
|
||||
}
|
||||
if !authInfoFileFound || builder.cmdAuthInfo.CAFile.Provided() {
|
||||
authInfo.CAFile = builder.cmdAuthInfo.CAFile.Value
|
||||
}
|
||||
if !authInfoFileFound || builder.cmdAuthInfo.CertFile.Provided() {
|
||||
authInfo.CertFile = builder.cmdAuthInfo.CertFile.Value
|
||||
}
|
||||
if !authInfoFileFound || builder.cmdAuthInfo.KeyFile.Provided() {
|
||||
authInfo.KeyFile = builder.cmdAuthInfo.KeyFile.Value
|
||||
}
|
||||
if !authInfoFileFound || builder.cmdAuthInfo.BearerToken.Provided() {
|
||||
authInfo.BearerToken = builder.cmdAuthInfo.BearerToken.Value
|
||||
}
|
||||
if !authInfoFileFound || builder.cmdAuthInfo.Insecure.Provided() {
|
||||
authInfo.Insecure = &builder.cmdAuthInfo.Insecure.Value
|
||||
}
|
||||
|
||||
clientConfig, err = authInfo.MergeWithConfig(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &clientConfig, nil
|
||||
}
|
|
@ -1,356 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth"
|
||||
)
|
||||
|
||||
func TestSetAllArgumentsOnly(t *testing.T) {
|
||||
flags := pflag.NewFlagSet("test-flags", pflag.ContinueOnError)
|
||||
clientBuilder := NewBuilder(nil)
|
||||
clientBuilder.BindFlags(flags)
|
||||
|
||||
args := argValues{"https://localhost:8080", "v1beta1", "/auth-path", "cert-file", "key-file", "ca-file", "bearer-token", true, true}
|
||||
flags.Parse(strings.Split(args.toArguments(), " "))
|
||||
|
||||
castBuilder, ok := clientBuilder.(*builder)
|
||||
if !ok {
|
||||
t.Errorf("Got unexpected cast result: %#v", castBuilder)
|
||||
}
|
||||
|
||||
matchStringArg(args.server, castBuilder.apiserver, t)
|
||||
matchStringArg(args.apiVersion, castBuilder.apiVersion, t)
|
||||
matchStringArg(args.authPath, castBuilder.authPath, t)
|
||||
matchStringArg(args.certFile, castBuilder.cmdAuthInfo.CertFile.Value, t)
|
||||
matchStringArg(args.keyFile, castBuilder.cmdAuthInfo.KeyFile.Value, t)
|
||||
matchStringArg(args.caFile, castBuilder.cmdAuthInfo.CAFile.Value, t)
|
||||
matchStringArg(args.bearerToken, castBuilder.cmdAuthInfo.BearerToken.Value, t)
|
||||
matchBoolArg(args.insecure, castBuilder.cmdAuthInfo.Insecure.Value, t)
|
||||
matchBoolArg(args.matchApiVersion, castBuilder.matchApiVersion, t)
|
||||
|
||||
clientConfig, err := clientBuilder.Config()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
matchStringArg(args.server, clientConfig.Host, t)
|
||||
matchStringArg(args.apiVersion, clientConfig.Version, t)
|
||||
matchStringArg(args.certFile, clientConfig.CertFile, t)
|
||||
matchStringArg(args.keyFile, clientConfig.KeyFile, t)
|
||||
matchStringArg(args.caFile, clientConfig.CAFile, t)
|
||||
matchStringArg(args.bearerToken, clientConfig.BearerToken, t)
|
||||
matchBoolArg(args.insecure, clientConfig.Insecure, t)
|
||||
}
|
||||
|
||||
func TestSetInsecureArgumentsOnly(t *testing.T) {
|
||||
flags := pflag.NewFlagSet("test-flags", pflag.ContinueOnError)
|
||||
clientBuilder := NewBuilder(nil)
|
||||
clientBuilder.BindFlags(flags)
|
||||
|
||||
args := argValues{"http://localhost:8080", "v1beta1", "/auth-path", "cert-file", "key-file", "ca-file", "bearer-token", true, true}
|
||||
flags.Parse(strings.Split(args.toArguments(), " "))
|
||||
|
||||
clientConfig, err := clientBuilder.Config()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
matchStringArg(args.server, clientConfig.Host, t)
|
||||
matchStringArg(args.apiVersion, clientConfig.Version, t)
|
||||
|
||||
// all security related params should be empty in the resulting config even though we set them because we're using http transport
|
||||
matchStringArg("", clientConfig.CertFile, t)
|
||||
matchStringArg("", clientConfig.KeyFile, t)
|
||||
matchStringArg("", clientConfig.CAFile, t)
|
||||
matchStringArg("", clientConfig.BearerToken, t)
|
||||
matchBoolArg(false, clientConfig.Insecure, t)
|
||||
}
|
||||
|
||||
func TestReadAuthFile(t *testing.T) {
|
||||
flags := pflag.NewFlagSet("test-flags", pflag.ContinueOnError)
|
||||
clientBuilder := NewBuilder(nil)
|
||||
clientBuilder.BindFlags(flags)
|
||||
authFileContents := fmt.Sprintf(`{"user": "alfa-user", "password": "bravo-password", "cAFile": "charlie", "certFile": "delta", "keyFile": "echo", "bearerToken": "foxtrot"}`)
|
||||
authFile := writeTempAuthFile(authFileContents, t)
|
||||
|
||||
args := argValues{"https://localhost:8080", "v1beta1", authFile, "", "", "", "", true, true}
|
||||
flags.Parse(strings.Split(args.toArguments(), " "))
|
||||
|
||||
castBuilder, ok := clientBuilder.(*builder)
|
||||
if !ok {
|
||||
t.Errorf("Got unexpected cast result: %#v", castBuilder)
|
||||
}
|
||||
|
||||
matchStringArg(args.server, castBuilder.apiserver, t)
|
||||
matchStringArg(args.apiVersion, castBuilder.apiVersion, t)
|
||||
matchStringArg(args.authPath, castBuilder.authPath, t)
|
||||
matchStringArg(args.certFile, castBuilder.cmdAuthInfo.CertFile.Value, t)
|
||||
matchStringArg(args.keyFile, castBuilder.cmdAuthInfo.KeyFile.Value, t)
|
||||
matchStringArg(args.caFile, castBuilder.cmdAuthInfo.CAFile.Value, t)
|
||||
matchStringArg(args.bearerToken, castBuilder.cmdAuthInfo.BearerToken.Value, t)
|
||||
matchBoolArg(args.insecure, castBuilder.cmdAuthInfo.Insecure.Value, t)
|
||||
matchBoolArg(args.matchApiVersion, castBuilder.matchApiVersion, t)
|
||||
|
||||
clientConfig, err := clientBuilder.Config()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
matchStringArg(args.server, clientConfig.Host, t)
|
||||
matchStringArg(args.apiVersion, clientConfig.Version, t)
|
||||
matchStringArg("delta", clientConfig.CertFile, t)
|
||||
matchStringArg("echo", clientConfig.KeyFile, t)
|
||||
matchStringArg("charlie", clientConfig.CAFile, t)
|
||||
matchStringArg("foxtrot", clientConfig.BearerToken, t)
|
||||
matchStringArg("alfa-user", clientConfig.Username, t)
|
||||
matchStringArg("bravo-password", clientConfig.Password, t)
|
||||
matchBoolArg(args.insecure, clientConfig.Insecure, t)
|
||||
}
|
||||
|
||||
func TestAuthFileOverridden(t *testing.T) {
|
||||
flags := pflag.NewFlagSet("test-flags", pflag.ContinueOnError)
|
||||
clientBuilder := NewBuilder(nil)
|
||||
clientBuilder.BindFlags(flags)
|
||||
authFileContents := fmt.Sprintf(`{"user": "alfa-user", "password": "bravo-password", "cAFile": "charlie", "certFile": "delta", "keyFile": "echo", "bearerToken": "foxtrot"}`)
|
||||
authFile := writeTempAuthFile(authFileContents, t)
|
||||
|
||||
args := argValues{"https://localhost:8080", "v1beta1", authFile, "cert-file", "key-file", "ca-file", "bearer-token", true, true}
|
||||
flags.Parse(strings.Split(args.toArguments(), " "))
|
||||
|
||||
castBuilder, ok := clientBuilder.(*builder)
|
||||
if !ok {
|
||||
t.Errorf("Got unexpected cast result: %#v", castBuilder)
|
||||
}
|
||||
|
||||
matchStringArg(args.server, castBuilder.apiserver, t)
|
||||
matchStringArg(args.apiVersion, castBuilder.apiVersion, t)
|
||||
matchStringArg(args.authPath, castBuilder.authPath, t)
|
||||
matchStringArg(args.certFile, castBuilder.cmdAuthInfo.CertFile.Value, t)
|
||||
matchStringArg(args.keyFile, castBuilder.cmdAuthInfo.KeyFile.Value, t)
|
||||
matchStringArg(args.caFile, castBuilder.cmdAuthInfo.CAFile.Value, t)
|
||||
matchStringArg(args.bearerToken, castBuilder.cmdAuthInfo.BearerToken.Value, t)
|
||||
matchBoolArg(args.insecure, castBuilder.cmdAuthInfo.Insecure.Value, t)
|
||||
matchBoolArg(args.matchApiVersion, castBuilder.matchApiVersion, t)
|
||||
|
||||
clientConfig, err := clientBuilder.Config()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
matchStringArg(args.server, clientConfig.Host, t)
|
||||
matchStringArg(args.apiVersion, clientConfig.Version, t)
|
||||
matchStringArg(args.certFile, clientConfig.CertFile, t)
|
||||
matchStringArg(args.keyFile, clientConfig.KeyFile, t)
|
||||
matchStringArg(args.caFile, clientConfig.CAFile, t)
|
||||
matchStringArg(args.bearerToken, clientConfig.BearerToken, t)
|
||||
matchStringArg("alfa-user", clientConfig.Username, t)
|
||||
matchStringArg("bravo-password", clientConfig.Password, t)
|
||||
matchBoolArg(args.insecure, clientConfig.Insecure, t)
|
||||
}
|
||||
|
||||
func TestUseDefaultArgumentsOnly(t *testing.T) {
|
||||
flags := pflag.NewFlagSet("test-flags", pflag.ContinueOnError)
|
||||
clientBuilder := NewBuilder(nil)
|
||||
clientBuilder.BindFlags(flags)
|
||||
|
||||
flags.Parse(strings.Split("", " "))
|
||||
|
||||
castBuilder, ok := clientBuilder.(*builder)
|
||||
if !ok {
|
||||
t.Errorf("Got unexpected cast result: %#v", castBuilder)
|
||||
}
|
||||
|
||||
matchStringArg("", castBuilder.apiserver, t)
|
||||
matchStringArg(latest.Version, castBuilder.apiVersion, t)
|
||||
matchStringArg(os.Getenv("HOME")+"/.kubernetes_auth", castBuilder.authPath, t)
|
||||
matchStringArg("", castBuilder.cmdAuthInfo.CertFile.Value, t)
|
||||
matchStringArg("", castBuilder.cmdAuthInfo.KeyFile.Value, t)
|
||||
matchStringArg("", castBuilder.cmdAuthInfo.CAFile.Value, t)
|
||||
matchStringArg("", castBuilder.cmdAuthInfo.BearerToken.Value, t)
|
||||
matchBoolArg(false, castBuilder.matchApiVersion, t)
|
||||
}
|
||||
|
||||
func TestLoadClientAuthInfoOrPrompt(t *testing.T) {
|
||||
loadAuthInfoTests := []struct {
|
||||
authData string
|
||||
authInfo *clientauth.Info
|
||||
r io.Reader
|
||||
}{
|
||||
{
|
||||
`{"user": "user", "password": "pass"}`,
|
||||
&clientauth.Info{User: "user", Password: "pass"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"", nil, nil,
|
||||
},
|
||||
{
|
||||
"missing",
|
||||
&clientauth.Info{User: "user", Password: "pass"},
|
||||
bytes.NewBufferString("user\npass"),
|
||||
},
|
||||
}
|
||||
for _, loadAuthInfoTest := range loadAuthInfoTests {
|
||||
tt := loadAuthInfoTest
|
||||
aifile, err := ioutil.TempFile("", "testAuthInfo")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if tt.authData != "missing" {
|
||||
defer os.Remove(aifile.Name())
|
||||
defer aifile.Close()
|
||||
_, err = aifile.WriteString(tt.authData)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
} else {
|
||||
aifile.Close()
|
||||
os.Remove(aifile.Name())
|
||||
}
|
||||
prompter := NewPromptingAuthLoader(tt.r)
|
||||
authInfo, err := prompter.LoadAuth(aifile.Name())
|
||||
if len(tt.authData) == 0 && tt.authData != "missing" {
|
||||
if err == nil {
|
||||
t.Error("LoadAuth didn't fail on empty file")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(authInfo, tt.authInfo) {
|
||||
t.Errorf("Expected %#v, got %#v", tt.authInfo, authInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverride(t *testing.T) {
|
||||
b := NewBuilder(nil)
|
||||
cfg, err := b.Config()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if cfg.Version != "" {
|
||||
t.Errorf("unexpected default config version")
|
||||
}
|
||||
|
||||
newCfg, err := b.Override(func(cfg *client.Config) {
|
||||
if cfg.Version != "" {
|
||||
t.Errorf("unexpected default config version")
|
||||
}
|
||||
cfg.Version = "test"
|
||||
}).Config()
|
||||
|
||||
if newCfg.Version != "test" {
|
||||
t.Errorf("unexpected override config version")
|
||||
}
|
||||
|
||||
if cfg.Version != "" {
|
||||
t.Errorf("original object should not change")
|
||||
}
|
||||
|
||||
cfg, err = b.Config()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if cfg.Version != "" {
|
||||
t.Errorf("override should not be persistent")
|
||||
}
|
||||
}
|
||||
|
||||
func matchStringArg(expected, got string, t *testing.T) {
|
||||
if expected != got {
|
||||
t.Errorf("Expected %v, got %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func matchBoolArg(expected, got bool, t *testing.T) {
|
||||
if expected != got {
|
||||
t.Errorf("Expected %v, got %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTempAuthFile(contents string, t *testing.T) string {
|
||||
file, err := ioutil.TempFile("", "testAuthInfo")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to write config file. Test cannot continue due to: %v", err)
|
||||
return ""
|
||||
}
|
||||
_, err = file.WriteString(contents)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return ""
|
||||
}
|
||||
file.Close()
|
||||
|
||||
return file.Name()
|
||||
}
|
||||
|
||||
type argValues struct {
|
||||
server string
|
||||
apiVersion string
|
||||
authPath string
|
||||
certFile string
|
||||
keyFile string
|
||||
caFile string
|
||||
bearerToken string
|
||||
insecure bool
|
||||
matchApiVersion bool
|
||||
}
|
||||
|
||||
func (a *argValues) toArguments() string {
|
||||
args := ""
|
||||
if len(a.server) > 0 {
|
||||
args += "--" + FlagApiServer + "=" + a.server + " "
|
||||
}
|
||||
if len(a.apiVersion) > 0 {
|
||||
args += "--" + FlagApiVersion + "=" + a.apiVersion + " "
|
||||
}
|
||||
if len(a.authPath) > 0 {
|
||||
args += "--" + FlagAuthPath + "=" + a.authPath + " "
|
||||
}
|
||||
if len(a.certFile) > 0 {
|
||||
args += "--" + FlagCertFile + "=" + a.certFile + " "
|
||||
}
|
||||
if len(a.keyFile) > 0 {
|
||||
args += "--" + FlagKeyFile + "=" + a.keyFile + " "
|
||||
}
|
||||
if len(a.caFile) > 0 {
|
||||
args += "--" + FlagCAFile + "=" + a.caFile + " "
|
||||
}
|
||||
if len(a.bearerToken) > 0 {
|
||||
args += "--" + FlagBearerToken + "=" + a.bearerToken + " "
|
||||
}
|
||||
args += "--" + FlagInsecure + "=" + fmt.Sprintf("%v", a.insecure) + " "
|
||||
args += "--" + FlagMatchApiVersion + "=" + fmt.Sprintf("%v", a.matchApiVersion) + " "
|
||||
|
||||
return args
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
var (
|
||||
// TODO: eventually apiserver should start on 443 and be secure by default
|
||||
defaultCluster = Cluster{Server: "http://localhost:8080"}
|
||||
envVarCluster = Cluster{Server: os.Getenv("KUBERNETES_MASTER")}
|
||||
)
|
||||
|
||||
// ClientConfig is used to make it easy to get an api server client
|
||||
type ClientConfig interface {
|
||||
// ClientConfig returns a complete client config
|
||||
ClientConfig() (*client.Config, error)
|
||||
}
|
||||
|
||||
// DirectClientConfig is a ClientConfig interface that is backed by a Config, options overrides, and an optional fallbackReader for auth information
|
||||
type DirectClientConfig struct {
|
||||
config Config
|
||||
contextName string
|
||||
overrides *ConfigOverrides
|
||||
fallbackReader io.Reader
|
||||
}
|
||||
|
||||
// NewDefaultClientConfig creates a DirectClientConfig using the config.CurrentContext as the context name
|
||||
func NewDefaultClientConfig(config Config, overrides *ConfigOverrides) ClientConfig {
|
||||
return DirectClientConfig{config, config.CurrentContext, overrides, nil}
|
||||
}
|
||||
|
||||
// NewNonInteractiveClientConfig creates a DirectClientConfig using the passed context name and does not have a fallback reader for auth information
|
||||
func NewNonInteractiveClientConfig(config Config, contextName string, overrides *ConfigOverrides) ClientConfig {
|
||||
return DirectClientConfig{config, contextName, overrides, nil}
|
||||
}
|
||||
|
||||
// NewInteractiveClientConfig creates a DirectClientConfig using the passed context name and a reader in case auth information is not provided via files or flags
|
||||
func NewInteractiveClientConfig(config Config, contextName string, overrides *ConfigOverrides, fallbackReader io.Reader) ClientConfig {
|
||||
return DirectClientConfig{config, contextName, overrides, fallbackReader}
|
||||
}
|
||||
|
||||
// ClientConfig implements ClientConfig
|
||||
func (config DirectClientConfig) ClientConfig() (*client.Config, error) {
|
||||
if err := config.ConfirmUsable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configAuthInfo := config.getAuthInfo()
|
||||
configClusterInfo := config.getCluster()
|
||||
|
||||
clientConfig := client.Config{}
|
||||
clientConfig.Host = configClusterInfo.Server
|
||||
clientConfig.Version = configClusterInfo.APIVersion
|
||||
|
||||
// only try to read the auth information if we are secure
|
||||
if client.IsConfigTransportTLS(&clientConfig) {
|
||||
var authInfo *clientauth.Info
|
||||
var err error
|
||||
switch {
|
||||
case len(configAuthInfo.AuthPath) > 0:
|
||||
authInfo, err = NewDefaultAuthLoader().LoadAuth(configAuthInfo.AuthPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case len(configAuthInfo.Token) > 0:
|
||||
authInfo = &clientauth.Info{BearerToken: configAuthInfo.Token}
|
||||
|
||||
case len(configAuthInfo.ClientCertificate) > 0:
|
||||
authInfo = &clientauth.Info{
|
||||
CertFile: configAuthInfo.ClientCertificate,
|
||||
KeyFile: configAuthInfo.ClientKey,
|
||||
}
|
||||
|
||||
default:
|
||||
authInfo = &clientauth.Info{}
|
||||
}
|
||||
|
||||
if !authInfo.Complete() && (config.fallbackReader != nil) {
|
||||
prompter := NewPromptingAuthLoader(config.fallbackReader)
|
||||
authInfo = prompter.Prompt()
|
||||
}
|
||||
|
||||
authInfo.Insecure = &configClusterInfo.InsecureSkipTLSVerify
|
||||
|
||||
clientConfig, err = authInfo.MergeWithConfig(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &clientConfig, nil
|
||||
}
|
||||
|
||||
// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config,
|
||||
// but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible.
|
||||
func (config DirectClientConfig) ConfirmUsable() error {
|
||||
validationErrors := make([]error, 0)
|
||||
validationErrors = append(validationErrors, validateAuthInfo(config.getAuthInfoName(), config.getAuthInfo())...)
|
||||
validationErrors = append(validationErrors, validateClusterInfo(config.getClusterName(), config.getCluster())...)
|
||||
|
||||
return util.SliceToError(validationErrors)
|
||||
}
|
||||
|
||||
func (config DirectClientConfig) getContextName() string {
|
||||
if len(config.overrides.CurrentContext) != 0 {
|
||||
return config.overrides.CurrentContext
|
||||
}
|
||||
if len(config.contextName) != 0 {
|
||||
return config.contextName
|
||||
}
|
||||
|
||||
return config.config.CurrentContext
|
||||
}
|
||||
|
||||
func (config DirectClientConfig) getAuthInfoName() string {
|
||||
if len(config.overrides.AuthInfoName) != 0 {
|
||||
return config.overrides.AuthInfoName
|
||||
}
|
||||
return config.getContext().AuthInfo
|
||||
}
|
||||
|
||||
func (config DirectClientConfig) getClusterName() string {
|
||||
if len(config.overrides.ClusterName) != 0 {
|
||||
return config.overrides.ClusterName
|
||||
}
|
||||
return config.getContext().Cluster
|
||||
}
|
||||
|
||||
func (config DirectClientConfig) getContext() Context {
|
||||
return config.config.Contexts[config.getContextName()]
|
||||
}
|
||||
|
||||
func (config DirectClientConfig) getAuthInfo() AuthInfo {
|
||||
authInfos := config.config.AuthInfos
|
||||
authInfoName := config.getAuthInfoName()
|
||||
|
||||
var mergedAuthInfo AuthInfo
|
||||
if configAuthInfo, exists := authInfos[authInfoName]; exists {
|
||||
mergo.Merge(&mergedAuthInfo, configAuthInfo)
|
||||
}
|
||||
mergo.Merge(&mergedAuthInfo, config.overrides.AuthInfo)
|
||||
|
||||
return mergedAuthInfo
|
||||
}
|
||||
|
||||
func (config DirectClientConfig) getCluster() Cluster {
|
||||
clusterInfos := config.config.Clusters
|
||||
clusterInfoName := config.getClusterName()
|
||||
|
||||
var mergedClusterInfo Cluster
|
||||
mergo.Merge(&mergedClusterInfo, defaultCluster)
|
||||
mergo.Merge(&mergedClusterInfo, envVarCluster)
|
||||
if configClusterInfo, exists := clusterInfos[clusterInfoName]; exists {
|
||||
mergo.Merge(&mergedClusterInfo, configClusterInfo)
|
||||
}
|
||||
mergo.Merge(&mergedClusterInfo, config.overrides.ClusterInfo)
|
||||
|
||||
return mergedClusterInfo
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
)
|
||||
|
||||
func createValidTestConfig() *Config {
|
||||
const (
|
||||
server = "https://anything.com:8080"
|
||||
token = "the-token"
|
||||
)
|
||||
|
||||
config := NewConfig()
|
||||
config.Clusters["clean"] = Cluster{
|
||||
Server: server,
|
||||
APIVersion: latest.Version,
|
||||
}
|
||||
config.AuthInfos["clean"] = AuthInfo{
|
||||
Token: token,
|
||||
}
|
||||
config.Contexts["clean"] = Context{
|
||||
Cluster: "clean",
|
||||
AuthInfo: "clean",
|
||||
}
|
||||
config.CurrentContext = "clean"
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func TestCreateClean(t *testing.T) {
|
||||
config := createValidTestConfig()
|
||||
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{})
|
||||
|
||||
clientConfig, err := clientBuilder.ClientConfig()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
matchStringArg(config.Clusters["clean"].Server, clientConfig.Host, t)
|
||||
matchStringArg(config.Clusters["clean"].APIVersion, clientConfig.Version, t)
|
||||
matchBoolArg(config.Clusters["clean"].InsecureSkipTLSVerify, clientConfig.Insecure, t)
|
||||
matchStringArg(config.AuthInfos["clean"].Token, clientConfig.BearerToken, t)
|
||||
}
|
||||
|
||||
func TestCreateCleanDefault(t *testing.T) {
|
||||
config := createValidTestConfig()
|
||||
clientBuilder := NewDefaultClientConfig(*config, &ConfigOverrides{})
|
||||
|
||||
clientConfig, err := clientBuilder.ClientConfig()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
matchStringArg(config.Clusters["clean"].Server, clientConfig.Host, t)
|
||||
matchStringArg(config.Clusters["clean"].APIVersion, clientConfig.Version, t)
|
||||
matchBoolArg(config.Clusters["clean"].InsecureSkipTLSVerify, clientConfig.Insecure, t)
|
||||
matchStringArg(config.AuthInfos["clean"].Token, clientConfig.BearerToken, t)
|
||||
}
|
||||
|
||||
func TestCreateMissingContext(t *testing.T) {
|
||||
const expectedErrorContains = "Context was not found for specified context"
|
||||
config := createValidTestConfig()
|
||||
clientBuilder := NewNonInteractiveClientConfig(*config, "not-present", &ConfigOverrides{})
|
||||
expectedConfig := &client.Config{Host: "http://localhost:8080"}
|
||||
|
||||
clientConfig, err := clientBuilder.ClientConfig()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(expectedConfig, clientConfig) {
|
||||
t.Errorf("Expected %#v, got %#v", expectedConfig, clientConfig)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func matchBoolArg(expected, got bool, t *testing.T) {
|
||||
if expected != got {
|
||||
t.Errorf("Expected %v, got %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func matchStringArg(expected, got string, t *testing.T) {
|
||||
if expected != got {
|
||||
t.Errorf("Expected %v, got %v", expected, got)
|
||||
}
|
||||
}
|
|
@ -15,11 +15,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
/*
|
||||
Package cmd provides one stop shopping for a command line executable to bind the correct flags,
|
||||
build the client config, and create a working client. The code for usage looks like this:
|
||||
Package clientcmd provides one stop shopping for building a working client from a fixed config,
|
||||
from a .kubeconfig file, from command line flags, or from any merged combination.
|
||||
|
||||
clientBuilder := clientcmd.NewBuilder(clientcmd.NewDefaultAuthLoader())
|
||||
clientBuilder.BindFlags(cmds.PersistentFlags())
|
||||
apiClient, err := clientBuilder.Client()
|
||||
Sample usage from merged .kubeconfig files (local directory, home directory)
|
||||
loadingRules := clientcmd.NewKubeConfigLoadingRules()
|
||||
// if you want to change the loading rules (which files in which order), you can do so here
|
||||
|
||||
configOverrides := &clientcmd.ConfigOverrides{}
|
||||
// if you want to change override values or bind them to flags, there are methods to help you
|
||||
|
||||
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingKubeConfig(loadingRules, configOverrides)
|
||||
kubeConfig.Client()
|
||||
*/
|
||||
package clientcmd
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"gopkg.in/v2/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
RecommendedConfigPathFlag = "kubeconfig"
|
||||
RecommendedConfigPathEnvVar = "KUBECONFIG"
|
||||
)
|
||||
|
||||
// ClientConfigLoadingRules is a struct that calls our specific locations that are used for merging together a Config
|
||||
type ClientConfigLoadingRules struct {
|
||||
CommandLinePath string
|
||||
EnvVarPath string
|
||||
CurrentDirectoryPath string
|
||||
HomeDirectoryPath string
|
||||
}
|
||||
|
||||
// NewClientConfigLoadingRules returns a ClientConfigLoadingRules object with default fields filled in. You are not required to
|
||||
// use this constructor
|
||||
func NewClientConfigLoadingRules() *ClientConfigLoadingRules {
|
||||
return &ClientConfigLoadingRules{
|
||||
CurrentDirectoryPath: ".kubeconfig",
|
||||
HomeDirectoryPath: os.Getenv("HOME") + "/.kube/.kubeconfig",
|
||||
}
|
||||
}
|
||||
|
||||
// Load takes the loading rules and merges together a Config object based on following order.
|
||||
// 1. CommandLinePath
|
||||
// 2. EnvVarPath
|
||||
// 3. CurrentDirectoryPath
|
||||
// 4. HomeDirectoryPath
|
||||
// Empty filenames are ignored. Files with non-deserializable content produced errors.
|
||||
// The first file to set a particular value or map key wins and the value or map key is never changed.
|
||||
// This means that the first file to set CurrentContext will have its context preserved. It also means
|
||||
// that if two files specify a "red-user", only values from the first file's red-user are used. Even
|
||||
// non-conflicting entries from the second file's "red-user" are discarded.
|
||||
func (rules *ClientConfigLoadingRules) Load() (*Config, error) {
|
||||
config := NewConfig()
|
||||
|
||||
mergeConfigWithFile(config, rules.CommandLinePath)
|
||||
mergeConfigWithFile(config, rules.EnvVarPath)
|
||||
mergeConfigWithFile(config, rules.CurrentDirectoryPath)
|
||||
mergeConfigWithFile(config, rules.HomeDirectoryPath)
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func mergeConfigWithFile(startingConfig *Config, filename string) error {
|
||||
if len(filename) == 0 {
|
||||
// no work to do
|
||||
return nil
|
||||
}
|
||||
|
||||
config, err := LoadFromFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mergo.Merge(startingConfig, config)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFromFile takes a filename and deserializes the contents into Config object
|
||||
func LoadFromFile(filename string) (*Config, error) {
|
||||
config := &Config{}
|
||||
|
||||
kubeconfigBytes, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(kubeconfigBytes, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// WriteToFile serializes the config to yaml and writes it out to a file. If no present, it creates the file with 0644. If it is present
|
||||
// it stomps the contents
|
||||
func WriteToFile(config Config, filename string) error {
|
||||
content, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filename, content, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"gopkg.in/v2/yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
testConfigAlfa = Config{
|
||||
AuthInfos: map[string]AuthInfo{
|
||||
"red-user": {Token: "red-token"}},
|
||||
Clusters: map[string]Cluster{
|
||||
"cow-cluster": {Server: "http://cow.org:8080"}},
|
||||
Contexts: map[string]Context{
|
||||
"federal-context": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"}},
|
||||
}
|
||||
testConfigBravo = Config{
|
||||
AuthInfos: map[string]AuthInfo{
|
||||
"black-user": {Token: "black-token"}},
|
||||
Clusters: map[string]Cluster{
|
||||
"pig-cluster": {Server: "http://pig.org:8080"}},
|
||||
Contexts: map[string]Context{
|
||||
"queen-anne-context": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"}},
|
||||
}
|
||||
testConfigCharlie = Config{
|
||||
AuthInfos: map[string]AuthInfo{
|
||||
"green-user": {Token: "green-token"}},
|
||||
Clusters: map[string]Cluster{
|
||||
"horse-cluster": {Server: "http://horse.org:8080"}},
|
||||
Contexts: map[string]Context{
|
||||
"shaker-context": {AuthInfo: "green-user", Cluster: "horse-cluster", Namespace: "chisel-ns"}},
|
||||
}
|
||||
testConfigDelta = Config{
|
||||
AuthInfos: map[string]AuthInfo{
|
||||
"blue-user": {Token: "blue-token"}},
|
||||
Clusters: map[string]Cluster{
|
||||
"chicken-cluster": {Server: "http://chicken.org:8080"}},
|
||||
Contexts: map[string]Context{
|
||||
"gothic-context": {AuthInfo: "blue-user", Cluster: "chicken-cluster", Namespace: "plane-ns"}},
|
||||
}
|
||||
testConfigConflictAlfa = Config{
|
||||
AuthInfos: map[string]AuthInfo{
|
||||
"red-user": {Token: "a-different-red-token"},
|
||||
"yellow-user": {Token: "yellow-token"}},
|
||||
Clusters: map[string]Cluster{
|
||||
"cow-cluster": {Server: "http://a-different-cow.org:8080", InsecureSkipTLSVerify: true},
|
||||
"donkey-cluster": {Server: "http://donkey.org:8080", InsecureSkipTLSVerify: true}},
|
||||
CurrentContext: "federal-context",
|
||||
}
|
||||
)
|
||||
|
||||
func ExampleMergingSomeWithConflict() {
|
||||
commandLineFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(commandLineFile.Name())
|
||||
envVarFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(envVarFile.Name())
|
||||
|
||||
WriteToFile(testConfigAlfa, commandLineFile.Name())
|
||||
WriteToFile(testConfigConflictAlfa, envVarFile.Name())
|
||||
|
||||
loadingRules := ClientConfigLoadingRules{
|
||||
CommandLinePath: commandLineFile.Name(),
|
||||
EnvVarPath: envVarFile.Name(),
|
||||
}
|
||||
|
||||
mergedConfig, err := loadingRules.Load()
|
||||
|
||||
output, err := yaml.Marshal(mergedConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%v", string(output))
|
||||
// Output:
|
||||
// preferences: {}
|
||||
// clusters:
|
||||
// cow-cluster:
|
||||
// server: http://cow.org:8080
|
||||
// donkey-cluster:
|
||||
// server: http://donkey.org:8080
|
||||
// insecure-skip-tls-verify: true
|
||||
// users:
|
||||
// red-user:
|
||||
// token: red-token
|
||||
// yellow-user:
|
||||
// token: yellow-token
|
||||
// contexts:
|
||||
// federal-context:
|
||||
// cluster: cow-cluster
|
||||
// user: red-user
|
||||
// namespace: hammer-ns
|
||||
// current-context: federal-context
|
||||
}
|
||||
|
||||
func ExampleMergingEverythingNoConflicts() {
|
||||
commandLineFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(commandLineFile.Name())
|
||||
envVarFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(envVarFile.Name())
|
||||
currentDirFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(currentDirFile.Name())
|
||||
homeDirFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(homeDirFile.Name())
|
||||
|
||||
WriteToFile(testConfigAlfa, commandLineFile.Name())
|
||||
WriteToFile(testConfigBravo, envVarFile.Name())
|
||||
WriteToFile(testConfigCharlie, currentDirFile.Name())
|
||||
WriteToFile(testConfigDelta, homeDirFile.Name())
|
||||
|
||||
loadingRules := ClientConfigLoadingRules{
|
||||
CommandLinePath: commandLineFile.Name(),
|
||||
EnvVarPath: envVarFile.Name(),
|
||||
CurrentDirectoryPath: currentDirFile.Name(),
|
||||
HomeDirectoryPath: homeDirFile.Name(),
|
||||
}
|
||||
|
||||
mergedConfig, err := loadingRules.Load()
|
||||
|
||||
output, err := yaml.Marshal(mergedConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%v", string(output))
|
||||
// Output:
|
||||
// preferences: {}
|
||||
// clusters:
|
||||
// chicken-cluster:
|
||||
// server: http://chicken.org:8080
|
||||
// cow-cluster:
|
||||
// server: http://cow.org:8080
|
||||
// horse-cluster:
|
||||
// server: http://horse.org:8080
|
||||
// pig-cluster:
|
||||
// server: http://pig.org:8080
|
||||
// users:
|
||||
// black-user:
|
||||
// token: black-token
|
||||
// blue-user:
|
||||
// token: blue-token
|
||||
// green-user:
|
||||
// token: green-token
|
||||
// red-user:
|
||||
// token: red-token
|
||||
// contexts:
|
||||
// federal-context:
|
||||
// cluster: cow-cluster
|
||||
// user: red-user
|
||||
// namespace: hammer-ns
|
||||
// gothic-context:
|
||||
// cluster: chicken-cluster
|
||||
// user: blue-user
|
||||
// namespace: plane-ns
|
||||
// queen-anne-context:
|
||||
// cluster: pig-cluster
|
||||
// user: black-user
|
||||
// namespace: saw-ns
|
||||
// shaker-context:
|
||||
// cluster: horse-cluster
|
||||
// user: green-user
|
||||
// namespace: chisel-ns
|
||||
// current-context: ""
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
)
|
||||
|
||||
// DeferredLoadingClientConfig is a ClientConfig interface that is backed by a set of loading rules
|
||||
// It is used in cases where the loading rules may change after you've instantiated them and you want to be sure that
|
||||
// the most recent rules are used. This is useful in cases where you bind flags to loading rule parameters before
|
||||
// the parse happens and you want your calling code to be ignorant of how the values are being mutated to avoid
|
||||
// passing extraneous information down a call stack
|
||||
type DeferredLoadingClientConfig struct {
|
||||
loadingRules *ClientConfigLoadingRules
|
||||
overrides *ConfigOverrides
|
||||
fallbackReader io.Reader
|
||||
}
|
||||
|
||||
// NewNonInteractiveDeferredLoadingClientConfig creates a ConfigClientClientConfig using the passed context name
|
||||
func NewNonInteractiveDeferredLoadingClientConfig(loadingRules *ClientConfigLoadingRules, overrides *ConfigOverrides) ClientConfig {
|
||||
return DeferredLoadingClientConfig{loadingRules, overrides, nil}
|
||||
}
|
||||
|
||||
// NewInteractiveDeferredLoadingClientConfig creates a ConfigClientClientConfig using the passed context name and the fallback auth reader
|
||||
func NewInteractiveDeferredLoadingClientConfig(loadingRules *ClientConfigLoadingRules, overrides *ConfigOverrides, fallbackReader io.Reader) ClientConfig {
|
||||
return DeferredLoadingClientConfig{loadingRules, overrides, fallbackReader}
|
||||
}
|
||||
|
||||
func (config DeferredLoadingClientConfig) createClientConfig() (ClientConfig, error) {
|
||||
mergedConfig, err := config.loadingRules.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var mergedClientConfig ClientConfig
|
||||
if config.fallbackReader != nil {
|
||||
mergedClientConfig = NewInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides, config.fallbackReader)
|
||||
} else {
|
||||
mergedClientConfig = NewNonInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides)
|
||||
}
|
||||
|
||||
return mergedClientConfig, nil
|
||||
}
|
||||
|
||||
// ClientConfig implements ClientConfig
|
||||
func (config DeferredLoadingClientConfig) ClientConfig() (*client.Config, error) {
|
||||
mergedClientConfig, err := config.createClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mergedClientConfig.ClientConfig()
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// ConfigOverrides holds values that should override whatever information is pulled from the actual Config object. You can't
|
||||
// simply use an actual Config object, because Configs hold maps, but overrides are restricted to "at most one"
|
||||
type ConfigOverrides struct {
|
||||
AuthInfo AuthInfo
|
||||
ClusterInfo Cluster
|
||||
Namespace string
|
||||
CurrentContext string
|
||||
ClusterName string
|
||||
AuthInfoName string
|
||||
}
|
||||
|
||||
// ConfigOverrideFlags holds the flag names to be used for binding command line flags. Notice that this structure tightly
|
||||
// corresponds to ConfigOverrides
|
||||
type ConfigOverrideFlags struct {
|
||||
AuthOverrideFlags AuthOverrideFlags
|
||||
ClusterOverrideFlags ClusterOverrideFlags
|
||||
Namespace string
|
||||
CurrentContext string
|
||||
ClusterName string
|
||||
AuthInfoName string
|
||||
}
|
||||
|
||||
// AuthOverrideFlags holds the flag names to be used for binding command line flags for AuthInfo objects
|
||||
type AuthOverrideFlags struct {
|
||||
AuthPath string
|
||||
ClientCertificate string
|
||||
ClientKey string
|
||||
Token string
|
||||
}
|
||||
|
||||
// ClusterOverride holds the flag names to be used for binding command line flags for Cluster objects
|
||||
type ClusterOverrideFlags struct {
|
||||
APIServer string
|
||||
APIVersion string
|
||||
CertificateAuthority string
|
||||
InsecureSkipTLSVerify string
|
||||
}
|
||||
|
||||
const (
|
||||
FlagClusterName = "cluster"
|
||||
FlagAuthInfoName = "user"
|
||||
FlagContext = "context"
|
||||
FlagNamespace = "namespace"
|
||||
FlagAPIServer = "server"
|
||||
FlagAPIVersion = "api-version"
|
||||
FlagAuthPath = "auth-path"
|
||||
FlagInsecure = "insecure-skip-tls-verify"
|
||||
FlagCertFile = "client-certificate"
|
||||
FlagKeyFile = "client-key"
|
||||
FlagCAFile = "certificate-authority"
|
||||
FlagBearerToken = "token"
|
||||
)
|
||||
|
||||
// RecommendedAuthOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
|
||||
func RecommendedAuthOverrideFlags(prefix string) AuthOverrideFlags {
|
||||
return AuthOverrideFlags{
|
||||
AuthPath: prefix + FlagAuthPath,
|
||||
ClientCertificate: prefix + FlagCertFile,
|
||||
ClientKey: prefix + FlagKeyFile,
|
||||
Token: prefix + FlagBearerToken,
|
||||
}
|
||||
}
|
||||
|
||||
// RecommendedClusterOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
|
||||
func RecommendedClusterOverrideFlags(prefix string) ClusterOverrideFlags {
|
||||
return ClusterOverrideFlags{
|
||||
APIServer: prefix + FlagAPIServer,
|
||||
APIVersion: prefix + FlagAPIVersion,
|
||||
CertificateAuthority: prefix + FlagCAFile,
|
||||
InsecureSkipTLSVerify: prefix + FlagInsecure,
|
||||
}
|
||||
}
|
||||
|
||||
// RecommendedConfigOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
|
||||
func RecommendedConfigOverrideFlags(prefix string) ConfigOverrideFlags {
|
||||
return ConfigOverrideFlags{
|
||||
AuthOverrideFlags: RecommendedAuthOverrideFlags(prefix),
|
||||
ClusterOverrideFlags: RecommendedClusterOverrideFlags(prefix),
|
||||
Namespace: prefix + FlagNamespace,
|
||||
CurrentContext: prefix + FlagContext,
|
||||
ClusterName: prefix + FlagClusterName,
|
||||
AuthInfoName: prefix + FlagAuthInfoName,
|
||||
}
|
||||
}
|
||||
|
||||
// BindFlags is a convenience method to bind the specified flags to their associated variables
|
||||
func (authInfo *AuthInfo) BindFlags(flags *pflag.FlagSet, flagNames AuthOverrideFlags) {
|
||||
// TODO short flag names are impossible to prefix, decide whether to keep them or not
|
||||
flags.StringVarP(&authInfo.AuthPath, flagNames.AuthPath, "a", "", "Path to the auth info file. If missing, prompt the user. Only used if using https.")
|
||||
flags.StringVar(&authInfo.ClientCertificate, flagNames.ClientCertificate, "", "Path to a client key file for TLS.")
|
||||
flags.StringVar(&authInfo.ClientKey, flagNames.ClientKey, "", "Path to a client key file for TLS.")
|
||||
flags.StringVar(&authInfo.Token, flagNames.Token, "", "Bearer token for authentication to the API server.")
|
||||
}
|
||||
|
||||
// BindFlags is a convenience method to bind the specified flags to their associated variables
|
||||
func (clusterInfo *Cluster) BindFlags(flags *pflag.FlagSet, flagNames ClusterOverrideFlags) {
|
||||
// TODO short flag names are impossible to prefix, decide whether to keep them or not
|
||||
flags.StringVarP(&clusterInfo.Server, flagNames.APIServer, "s", "", "The address of the Kubernetes API server")
|
||||
flags.StringVar(&clusterInfo.APIVersion, flagNames.APIVersion, "", "The API version to use when talking to the server")
|
||||
flags.StringVar(&clusterInfo.CertificateAuthority, flagNames.CertificateAuthority, "", "Path to a cert. file for the certificate authority.")
|
||||
flags.BoolVar(&clusterInfo.InsecureSkipTLSVerify, flagNames.InsecureSkipTLSVerify, false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure.")
|
||||
}
|
||||
|
||||
// BindFlags is a convenience method to bind the specified flags to their associated variables
|
||||
func (overrides *ConfigOverrides) BindFlags(flags *pflag.FlagSet, flagNames ConfigOverrideFlags) {
|
||||
(&overrides.AuthInfo).BindFlags(flags, flagNames.AuthOverrideFlags)
|
||||
(&overrides.ClusterInfo).BindFlags(flags, flagNames.ClusterOverrideFlags)
|
||||
// TODO not integrated yet
|
||||
// flags.StringVar(&overrides.Namespace, flagNames.Namespace, "", "If present, the namespace scope for this CLI request.")
|
||||
flags.StringVar(&overrides.CurrentContext, flagNames.CurrentContext, "", "The name of the kubeconfig context to use")
|
||||
flags.StringVar(&overrides.ClusterName, flagNames.ClusterName, "", "The name of the kubeconfig cluster to use")
|
||||
flags.StringVar(&overrides.AuthInfoName, flagNames.AuthInfoName, "", "The name of the kubeconfig user to use")
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// FlagProvider adds a check for whether .Set was called on this flag variable
|
||||
type FlagProvider interface {
|
||||
// Provided returns true iff .Set was called on this flag
|
||||
Provided() bool
|
||||
pflag.Value
|
||||
}
|
||||
|
||||
// StringFlag implements FlagProvider
|
||||
type StringFlag struct {
|
||||
Default string
|
||||
Value string
|
||||
WasProvided bool
|
||||
}
|
||||
|
||||
// SetDefault sets a default value for a flag while keeping Provided() false
|
||||
func (flag *StringFlag) SetDefault(value string) {
|
||||
flag.Value = value
|
||||
flag.WasProvided = false
|
||||
}
|
||||
|
||||
func (flag *StringFlag) Set(value string) error {
|
||||
flag.Value = value
|
||||
flag.WasProvided = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (flag *StringFlag) Type() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
func (flag *StringFlag) Provided() bool {
|
||||
return flag.WasProvided
|
||||
}
|
||||
|
||||
func (flag *StringFlag) String() string {
|
||||
return flag.Value
|
||||
}
|
||||
|
||||
// BoolFlag implements FlagProvider
|
||||
type BoolFlag struct {
|
||||
Default bool
|
||||
Value bool
|
||||
WasProvided bool
|
||||
}
|
||||
|
||||
// SetDefault sets a default value for a flag while keeping Provided() false
|
||||
func (flag *BoolFlag) SetDefault(value bool) {
|
||||
flag.Value = value
|
||||
flag.WasProvided = false
|
||||
}
|
||||
|
||||
func (flag *BoolFlag) Set(value string) error {
|
||||
boolValue, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
flag.Value = boolValue
|
||||
flag.WasProvided = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (flag *BoolFlag) Type() string {
|
||||
return "bool"
|
||||
}
|
||||
|
||||
func (flag *BoolFlag) Provided() bool {
|
||||
return flag.WasProvided
|
||||
}
|
||||
|
||||
func (flag *BoolFlag) String() string {
|
||||
return fmt.Sprintf("%t", flag.Value)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import ()
|
||||
|
||||
// Where possible, yaml tags match the cli argument names.
|
||||
// Top level config objects and all values required for proper functioning are not "omitempty". Any truly optional piece of config is allowed to be omitted.
|
||||
|
||||
// Config holds the information needed to build connect to remote kubernetes clusters as a given user
|
||||
type Config struct {
|
||||
// Preferences holds general information to be use for cli interactions
|
||||
Preferences Preferences `yaml:"preferences"`
|
||||
// Clusters is a map of referencable names to cluster configs
|
||||
Clusters map[string]Cluster `yaml:"clusters"`
|
||||
// AuthInfos is a map of referencable names to user configs
|
||||
AuthInfos map[string]AuthInfo `yaml:"users"`
|
||||
// Contexts is a map of referencable names to context configs
|
||||
Contexts map[string]Context `yaml:"contexts"`
|
||||
// CurrentContext is the name of the context that you would like to use by default
|
||||
CurrentContext string `yaml:"current-context"`
|
||||
}
|
||||
|
||||
type Preferences struct {
|
||||
Colors bool `yaml:"colors,omitempty"`
|
||||
}
|
||||
|
||||
// Cluster contains information about how to communicate with a kubernetes cluster
|
||||
type Cluster struct {
|
||||
// Server is the address of the kubernetes cluster (https://hostname:port).
|
||||
Server string `yaml:"server"`
|
||||
// APIVersion is the preferred api version for communicating with the kubernetes cluster (v1beta1, v1beta2, v1beta3, etc).
|
||||
APIVersion string `yaml:"api-version,omitempty"`
|
||||
// InsecureSkipTLSVerify skips the validity check for the server's certificate. This will make your HTTPS connections insecure.
|
||||
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify,omitempty"`
|
||||
// CertificateAuthority is the path to a cert file for the certificate authority.
|
||||
CertificateAuthority string `yaml:"certificate-authority,omitempty"`
|
||||
}
|
||||
|
||||
// AuthInfo contains information that describes identity information. This is use to tell the kubernetes cluster who you are.
|
||||
type AuthInfo struct {
|
||||
// AuthPath is the path to a kubernetes auth file (~/.kubernetes_auth). If you provide an AuthPath, the other options specified are ignored
|
||||
AuthPath string `yaml:"auth-path,omitempty"`
|
||||
// ClientCertificate is the path to a client cert file for TLS.
|
||||
ClientCertificate string `yaml:"client-certificate,omitempty"`
|
||||
// ClientKey is the path to a client key file for TLS.
|
||||
ClientKey string `yaml:"client-key,omitempty"`
|
||||
// Token is the bearer token for authentication to the kubernetes cluster.
|
||||
Token string `yaml:"token,omitempty"`
|
||||
}
|
||||
|
||||
// Context is a tuple of references to a cluster (how do I communicate with a kubernetes cluster), a user (how do I identify myself), and a namespace (what subset of resources do I want to work with)
|
||||
type Context struct {
|
||||
// Cluster is the name of the cluster for this context
|
||||
Cluster string `yaml:"cluster"`
|
||||
// AuthInfo is the name of the authInfo for this context
|
||||
AuthInfo string `yaml:"user"`
|
||||
// Namespace is the default namespace to use on unspecified requests
|
||||
Namespace string `yaml:"namespace,omitempty"`
|
||||
}
|
||||
|
||||
// NewConfig is a convenience function that returns a new Config object with non-nil maps
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
Clusters: make(map[string]Cluster),
|
||||
AuthInfos: make(map[string]AuthInfo),
|
||||
Contexts: make(map[string]Context),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/v2/yaml"
|
||||
)
|
||||
|
||||
func ExampleEmptyConfig() {
|
||||
defaultConfig := NewConfig()
|
||||
|
||||
output, err := yaml.Marshal(defaultConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%v", string(output))
|
||||
// Output:
|
||||
// preferences: {}
|
||||
// clusters: {}
|
||||
// users: {}
|
||||
// contexts: {}
|
||||
// current-context: ""
|
||||
}
|
||||
|
||||
func ExampleOfOptionsConfig() {
|
||||
defaultConfig := NewConfig()
|
||||
defaultConfig.Preferences.Colors = true
|
||||
defaultConfig.Clusters["alfa"] = Cluster{
|
||||
Server: "https://alfa.org:8080",
|
||||
APIVersion: "v1beta2",
|
||||
InsecureSkipTLSVerify: true,
|
||||
CertificateAuthority: "path/to/my/cert-ca-filename",
|
||||
}
|
||||
defaultConfig.Clusters["bravo"] = Cluster{
|
||||
Server: "https://bravo.org:8080",
|
||||
APIVersion: "v1beta1",
|
||||
InsecureSkipTLSVerify: false,
|
||||
}
|
||||
defaultConfig.AuthInfos["black-mage-via-file"] = AuthInfo{
|
||||
AuthPath: "path/to/my/.kubernetes_auth",
|
||||
}
|
||||
defaultConfig.AuthInfos["white-mage-via-cert"] = AuthInfo{
|
||||
ClientCertificate: "path/to/my/client-cert-filename",
|
||||
ClientKey: "path/to/my/client-key-filename",
|
||||
}
|
||||
defaultConfig.AuthInfos["red-mage-via-token"] = AuthInfo{
|
||||
Token: "my-secret-token",
|
||||
}
|
||||
defaultConfig.Contexts["bravo-as-black-mage"] = Context{
|
||||
Cluster: "bravo",
|
||||
AuthInfo: "black-mage-via-file",
|
||||
Namespace: "yankee",
|
||||
}
|
||||
defaultConfig.Contexts["alfa-as-black-mage"] = Context{
|
||||
Cluster: "alfa",
|
||||
AuthInfo: "black-mage-via-file",
|
||||
Namespace: "zulu",
|
||||
}
|
||||
defaultConfig.Contexts["alfa-as-white-mage"] = Context{
|
||||
Cluster: "alfa",
|
||||
AuthInfo: "white-mage-via-cert",
|
||||
}
|
||||
defaultConfig.CurrentContext = "alfa-as-white-mage"
|
||||
|
||||
output, err := yaml.Marshal(defaultConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%v", string(output))
|
||||
// Output:
|
||||
// preferences:
|
||||
// colors: true
|
||||
// clusters:
|
||||
// alfa:
|
||||
// server: https://alfa.org:8080
|
||||
// api-version: v1beta2
|
||||
// insecure-skip-tls-verify: true
|
||||
// certificate-authority: path/to/my/cert-ca-filename
|
||||
// bravo:
|
||||
// server: https://bravo.org:8080
|
||||
// api-version: v1beta1
|
||||
// users:
|
||||
// black-mage-via-file:
|
||||
// auth-path: path/to/my/.kubernetes_auth
|
||||
// red-mage-via-token:
|
||||
// token: my-secret-token
|
||||
// white-mage-via-cert:
|
||||
// client-certificate: path/to/my/client-cert-filename
|
||||
// client-key: path/to/my/client-key-filename
|
||||
// contexts:
|
||||
// alfa-as-black-mage:
|
||||
// cluster: alfa
|
||||
// user: black-mage-via-file
|
||||
// namespace: zulu
|
||||
// alfa-as-white-mage:
|
||||
// cluster: alfa
|
||||
// user: white-mage-via-cert
|
||||
// bravo-as-black-mage:
|
||||
// cluster: bravo
|
||||
// user: black-mage-via-file
|
||||
// namespace: yankee
|
||||
// current-context: alfa-as-white-mage
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
var ErrNoContext = errors.New("no context chosen")
|
||||
|
||||
type errContextNotFound struct {
|
||||
ContextName string
|
||||
}
|
||||
|
||||
func (e *errContextNotFound) Error() string {
|
||||
return fmt.Sprintf("context was not found for specified context: %v", e.ContextName)
|
||||
}
|
||||
|
||||
// IsContextNotFound returns a boolean indicating whether the error is known to
|
||||
// report that a context was not found
|
||||
func IsContextNotFound(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.Contains(err.Error(), "context was not found for specified context")
|
||||
}
|
||||
|
||||
// Validate checks for errors in the Config. It does not return early so that it can find as many errors as possible.
|
||||
func Validate(config Config) error {
|
||||
validationErrors := make([]error, 0)
|
||||
|
||||
if len(config.CurrentContext) != 0 {
|
||||
if _, exists := config.Contexts[config.CurrentContext]; !exists {
|
||||
validationErrors = append(validationErrors, &errContextNotFound{config.CurrentContext})
|
||||
}
|
||||
}
|
||||
|
||||
for contextName, context := range config.Contexts {
|
||||
validationErrors = append(validationErrors, validateContext(contextName, context, config)...)
|
||||
}
|
||||
|
||||
for authInfoName, authInfo := range config.AuthInfos {
|
||||
validationErrors = append(validationErrors, validateAuthInfo(authInfoName, authInfo)...)
|
||||
}
|
||||
|
||||
for clusterName, clusterInfo := range config.Clusters {
|
||||
validationErrors = append(validationErrors, validateClusterInfo(clusterName, clusterInfo)...)
|
||||
}
|
||||
|
||||
return util.SliceToError(validationErrors)
|
||||
}
|
||||
|
||||
// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config,
|
||||
// but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible.
|
||||
func ConfirmUsable(config Config, passedContextName string) error {
|
||||
validationErrors := make([]error, 0)
|
||||
|
||||
var contextName string
|
||||
if len(passedContextName) != 0 {
|
||||
contextName = passedContextName
|
||||
} else {
|
||||
contextName = config.CurrentContext
|
||||
}
|
||||
|
||||
if len(contextName) == 0 {
|
||||
return ErrNoContext
|
||||
}
|
||||
|
||||
context, exists := config.Contexts[contextName]
|
||||
if !exists {
|
||||
validationErrors = append(validationErrors, &errContextNotFound{contextName})
|
||||
}
|
||||
|
||||
if exists {
|
||||
validationErrors = append(validationErrors, validateContext(contextName, context, config)...)
|
||||
validationErrors = append(validationErrors, validateAuthInfo(context.AuthInfo, config.AuthInfos[context.AuthInfo])...)
|
||||
validationErrors = append(validationErrors, validateClusterInfo(context.Cluster, config.Clusters[context.Cluster])...)
|
||||
}
|
||||
|
||||
return util.SliceToError(validationErrors)
|
||||
}
|
||||
|
||||
// validateClusterInfo looks for conflicts and errors in the cluster info
|
||||
func validateClusterInfo(clusterName string, clusterInfo Cluster) []error {
|
||||
validationErrors := make([]error, 0)
|
||||
|
||||
if len(clusterInfo.Server) == 0 {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("no server found for %v", clusterName))
|
||||
}
|
||||
if len(clusterInfo.CertificateAuthority) != 0 {
|
||||
clientCertCA, err := os.Open(clusterInfo.CertificateAuthority)
|
||||
defer clientCertCA.Close()
|
||||
if err != nil {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("unable to read certificate-authority %v for %v due to %v", clusterInfo.CertificateAuthority, clusterName, err))
|
||||
}
|
||||
}
|
||||
|
||||
return validationErrors
|
||||
}
|
||||
|
||||
// validateAuthInfo looks for conflicts and errors in the auth info
|
||||
func validateAuthInfo(authInfoName string, authInfo AuthInfo) []error {
|
||||
validationErrors := make([]error, 0)
|
||||
|
||||
methods := make([]string, 0, 3)
|
||||
if len(authInfo.Token) != 0 {
|
||||
methods = append(methods, "token")
|
||||
}
|
||||
if len(authInfo.AuthPath) != 0 {
|
||||
methods = append(methods, "authFile")
|
||||
|
||||
file, err := os.Open(authInfo.AuthPath)
|
||||
os.IsNotExist(err)
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("unable to read auth-path %v for %v due to %v", authInfo.AuthPath, authInfoName, err))
|
||||
}
|
||||
}
|
||||
if len(authInfo.ClientCertificate) != 0 {
|
||||
methods = append(methods, "clientCert")
|
||||
|
||||
clientCertFile, err := os.Open(authInfo.ClientCertificate)
|
||||
defer clientCertFile.Close()
|
||||
if err != nil {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err))
|
||||
}
|
||||
clientKeyFile, err := os.Open(authInfo.ClientKey)
|
||||
defer clientKeyFile.Close()
|
||||
if err != nil {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err))
|
||||
}
|
||||
}
|
||||
|
||||
if (len(methods)) > 1 {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v. Found %v, only one is allowed", authInfoName, methods))
|
||||
}
|
||||
|
||||
return validationErrors
|
||||
}
|
||||
|
||||
// validateContext looks for errors in the context. It is not transitive, so errors in the reference authInfo or cluster configs are not included in this return
|
||||
func validateContext(contextName string, context Context, config Config) []error {
|
||||
validationErrors := make([]error, 0)
|
||||
|
||||
if len(context.AuthInfo) == 0 {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("user was not specified for Context %v", contextName))
|
||||
} else if _, exists := config.AuthInfos[context.AuthInfo]; !exists {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("user, %v, was not found for Context %v", context.AuthInfo, contextName))
|
||||
}
|
||||
|
||||
if len(context.Cluster) == 0 {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for Context %v", contextName))
|
||||
} else if _, exists := config.Clusters[context.Cluster]; !exists {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("cluster, %v, was not found for Context %v", context.Cluster, contextName))
|
||||
}
|
||||
|
||||
if (len(context.Namespace) != 0) && !util.IsDNS952Label(context.Namespace) {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("namespace, %v, for context %v, does not conform to the kubernetest DNS952 rules", context.Namespace, contextName))
|
||||
}
|
||||
|
||||
return validationErrors
|
||||
}
|
|
@ -0,0 +1,404 @@
|
|||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 clientcmd
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
func TestConfirmUsableBadInfoButOkConfig(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.Clusters["missing ca"] = Cluster{
|
||||
Server: "anything",
|
||||
CertificateAuthority: "missing",
|
||||
}
|
||||
config.AuthInfos["error"] = AuthInfo{
|
||||
AuthPath: "anything",
|
||||
Token: "here",
|
||||
}
|
||||
config.Contexts["dirty"] = Context{
|
||||
Cluster: "missing ca",
|
||||
AuthInfo: "error",
|
||||
}
|
||||
config.Clusters["clean"] = Cluster{
|
||||
Server: "anything",
|
||||
}
|
||||
config.AuthInfos["clean"] = AuthInfo{
|
||||
Token: "here",
|
||||
}
|
||||
config.Contexts["clean"] = Context{
|
||||
Cluster: "clean",
|
||||
AuthInfo: "clean",
|
||||
}
|
||||
|
||||
badValidation := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"unable to read auth-path", "more than one authentication method", "unable to read certificate-authority"},
|
||||
}
|
||||
okTest := configValidationTest{
|
||||
config: config,
|
||||
}
|
||||
|
||||
okTest.testConfirmUsable("clean", t)
|
||||
badValidation.testConfig(t)
|
||||
}
|
||||
func TestConfirmUsableBadInfoConfig(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.Clusters["missing ca"] = Cluster{
|
||||
Server: "anything",
|
||||
CertificateAuthority: "missing",
|
||||
}
|
||||
config.AuthInfos["error"] = AuthInfo{
|
||||
AuthPath: "anything",
|
||||
Token: "here",
|
||||
}
|
||||
config.Contexts["first"] = Context{
|
||||
Cluster: "missing ca",
|
||||
AuthInfo: "error",
|
||||
}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"unable to read auth-path", "more than one authentication method", "unable to read certificate-authority"},
|
||||
}
|
||||
|
||||
test.testConfirmUsable("first", t)
|
||||
}
|
||||
func TestConfirmUsableEmptyConfig(t *testing.T) {
|
||||
config := NewConfig()
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"no context chosen"},
|
||||
}
|
||||
|
||||
test.testConfirmUsable("", t)
|
||||
}
|
||||
func TestConfirmUsableMissingConfig(t *testing.T) {
|
||||
config := NewConfig()
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"context was not found for"},
|
||||
}
|
||||
|
||||
test.testConfirmUsable("not-here", t)
|
||||
}
|
||||
func TestValidateEmptyConfig(t *testing.T) {
|
||||
config := NewConfig()
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
}
|
||||
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestValidateMissingCurrentContextConfig(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.CurrentContext = "anything"
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"context was not found for specified "},
|
||||
}
|
||||
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestIsContextNotFound(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.CurrentContext = "anything"
|
||||
|
||||
err := Validate(*config)
|
||||
if !IsContextNotFound(err) {
|
||||
t.Errorf("Expected context not found, but got %v", err)
|
||||
}
|
||||
}
|
||||
func TestValidateMissingReferencesConfig(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.CurrentContext = "anything"
|
||||
config.Contexts["anything"] = Context{Cluster: "missing", AuthInfo: "missing"}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"user, missing, was not found for Context anything", "cluster, missing, was not found for Context anything"},
|
||||
}
|
||||
|
||||
test.testContext("anything", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestValidateEmptyContext(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.CurrentContext = "anything"
|
||||
config.Contexts["anything"] = Context{}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"user was not specified for Context anything", "cluster was not specified for Context anything"},
|
||||
}
|
||||
|
||||
test.testContext("anything", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
|
||||
func TestValidateEmptyClusterInfo(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.Clusters["empty"] = Cluster{}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"no server found for"},
|
||||
}
|
||||
|
||||
test.testCluster("empty", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestValidateMissingCAFileClusterInfo(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.Clusters["missing ca"] = Cluster{
|
||||
Server: "anything",
|
||||
CertificateAuthority: "missing",
|
||||
}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"unable to read certificate-authority"},
|
||||
}
|
||||
|
||||
test.testCluster("missing ca", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestValidateCleanClusterInfo(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.Clusters["clean"] = Cluster{
|
||||
Server: "anything",
|
||||
}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
}
|
||||
|
||||
test.testCluster("clean", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestValidateCleanWithCAClusterInfo(t *testing.T) {
|
||||
tempFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
config := NewConfig()
|
||||
config.Clusters["clean"] = Cluster{
|
||||
Server: "anything",
|
||||
CertificateAuthority: tempFile.Name(),
|
||||
}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
}
|
||||
|
||||
test.testCluster("clean", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
|
||||
func TestValidateEmptyAuthInfo(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.AuthInfos["error"] = AuthInfo{}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
}
|
||||
|
||||
test.testAuthInfo("error", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestValidateTooMayTechniquesAuthInfo(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.AuthInfos["error"] = AuthInfo{
|
||||
AuthPath: "anything",
|
||||
Token: "here",
|
||||
}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"more than one authentication method found"},
|
||||
}
|
||||
|
||||
test.testAuthInfo("error", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestValidatePathNotFoundAuthInfo(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.AuthInfos["error"] = AuthInfo{
|
||||
AuthPath: "missing",
|
||||
}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"unable to read auth-path"},
|
||||
}
|
||||
|
||||
test.testAuthInfo("error", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestValidateCertFilesNotFoundAuthInfo(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.AuthInfos["error"] = AuthInfo{
|
||||
ClientCertificate: "missing",
|
||||
ClientKey: "missing",
|
||||
}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
expectedErrorSubstring: []string{"unable to read client-cert", "unable to read client-key"},
|
||||
}
|
||||
|
||||
test.testAuthInfo("error", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestValidateCleanCertFilesAuthInfo(t *testing.T) {
|
||||
tempFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
config := NewConfig()
|
||||
config.AuthInfos["clean"] = AuthInfo{
|
||||
ClientCertificate: tempFile.Name(),
|
||||
ClientKey: tempFile.Name(),
|
||||
}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
}
|
||||
|
||||
test.testAuthInfo("clean", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestValidateCleanPathAuthInfo(t *testing.T) {
|
||||
tempFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
config := NewConfig()
|
||||
config.AuthInfos["clean"] = AuthInfo{
|
||||
AuthPath: tempFile.Name(),
|
||||
}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
}
|
||||
|
||||
test.testAuthInfo("clean", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
func TestValidateCleanTokenAuthInfo(t *testing.T) {
|
||||
config := NewConfig()
|
||||
config.AuthInfos["clean"] = AuthInfo{
|
||||
Token: "any-value",
|
||||
}
|
||||
test := configValidationTest{
|
||||
config: config,
|
||||
}
|
||||
|
||||
test.testAuthInfo("clean", t)
|
||||
test.testConfig(t)
|
||||
}
|
||||
|
||||
type configValidationTest struct {
|
||||
config *Config
|
||||
expectedErrorSubstring []string
|
||||
}
|
||||
|
||||
func (c configValidationTest) testContext(contextName string, t *testing.T) {
|
||||
errs := validateContext(contextName, c.config.Contexts[contextName], *c.config)
|
||||
|
||||
if len(c.expectedErrorSubstring) != 0 {
|
||||
if len(errs) == 0 {
|
||||
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
|
||||
}
|
||||
for _, curr := range c.expectedErrorSubstring {
|
||||
if len(errs) != 0 && !strings.Contains(util.SliceToError(errs).Error(), curr) {
|
||||
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, util.SliceToError(errs))
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("Unexpected error: %v", util.SliceToError(errs))
|
||||
}
|
||||
}
|
||||
}
|
||||
func (c configValidationTest) testConfirmUsable(contextName string, t *testing.T) {
|
||||
err := ConfirmUsable(*c.config, contextName)
|
||||
|
||||
if len(c.expectedErrorSubstring) != 0 {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
|
||||
} else {
|
||||
for _, curr := range c.expectedErrorSubstring {
|
||||
if err != nil && !strings.Contains(err.Error(), curr) {
|
||||
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
func (c configValidationTest) testConfig(t *testing.T) {
|
||||
err := Validate(*c.config)
|
||||
|
||||
if len(c.expectedErrorSubstring) != 0 {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
|
||||
} else {
|
||||
for _, curr := range c.expectedErrorSubstring {
|
||||
if err != nil && !strings.Contains(err.Error(), curr) {
|
||||
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
func (c configValidationTest) testCluster(clusterName string, t *testing.T) {
|
||||
errs := validateClusterInfo(clusterName, c.config.Clusters[clusterName])
|
||||
|
||||
if len(c.expectedErrorSubstring) != 0 {
|
||||
if len(errs) == 0 {
|
||||
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
|
||||
}
|
||||
for _, curr := range c.expectedErrorSubstring {
|
||||
if len(errs) != 0 && !strings.Contains(util.SliceToError(errs).Error(), curr) {
|
||||
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, util.SliceToError(errs))
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("Unexpected error: %v", util.SliceToError(errs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c configValidationTest) testAuthInfo(authInfoName string, t *testing.T) {
|
||||
errs := validateAuthInfo(authInfoName, c.config.AuthInfos[authInfoName])
|
||||
|
||||
if len(c.expectedErrorSubstring) != 0 {
|
||||
if len(errs) == 0 {
|
||||
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
|
||||
}
|
||||
for _, curr := range c.expectedErrorSubstring {
|
||||
if len(errs) != 0 && !strings.Contains(util.SliceToError(errs).Error(), curr) {
|
||||
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, util.SliceToError(errs))
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("Unexpected error: %v", util.SliceToError(errs))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,10 +21,12 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
|
||||
)
|
||||
|
||||
// Config holds the common attributes that can be passed to a Kubernetes client on
|
||||
|
@ -101,6 +103,24 @@ func New(c *Config) (*Client, error) {
|
|||
return &Client{client}, nil
|
||||
}
|
||||
|
||||
func MatchesServerVersion(c *Config) error {
|
||||
client, err := New(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientVersion := version.Get()
|
||||
serverVersion, err := client.ServerVersion()
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't read version from server: %v\n", err)
|
||||
}
|
||||
if s := *serverVersion; !reflect.DeepEqual(clientVersion, s) {
|
||||
return fmt.Errorf("server version (%#v) differs from client version (%#v)!\n", s, clientVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewOrDie creates a Kubernetes client and panics if the provided API version is not recognized.
|
||||
func NewOrDie(c *Config) *Client {
|
||||
client, err := New(c)
|
||||
|
|
|
@ -117,3 +117,9 @@ func (info Info) MergeWithConfig(c client.Config) (client.Config, error) {
|
|||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (info Info) Complete() bool {
|
||||
return len(info.User) > 0 ||
|
||||
len(info.CertFile) > 0 ||
|
||||
len(info.BearerToken) > 0
|
||||
}
|
||||
|
|
|
@ -34,10 +34,14 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
FlagMatchBinaryVersion = "match-server-version"
|
||||
)
|
||||
|
||||
// Factory provides abstractions that allow the Kubectl command to be extended across multiple types
|
||||
// of resources and different API sets.
|
||||
type Factory struct {
|
||||
ClientBuilder clientcmd.Builder
|
||||
ClientConfig clientcmd.ClientConfig
|
||||
Mapper meta.RESTMapper
|
||||
Typer runtime.ObjectTyper
|
||||
Client func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error)
|
||||
|
@ -47,14 +51,19 @@ type Factory struct {
|
|||
}
|
||||
|
||||
// NewFactory creates a factory with the default Kubernetes resources defined
|
||||
func NewFactory(clientBuilder clientcmd.Builder) *Factory {
|
||||
return &Factory{
|
||||
ClientBuilder: clientBuilder,
|
||||
func NewFactory(clientConfig clientcmd.ClientConfig) *Factory {
|
||||
ret := &Factory{
|
||||
ClientConfig: clientConfig,
|
||||
Mapper: latest.RESTMapper,
|
||||
Typer: api.Scheme,
|
||||
Validator: func(cmd *cobra.Command) (validation.Schema, error) {
|
||||
Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) {
|
||||
return kubectl.NewHumanReadablePrinter(noHeaders), nil
|
||||
},
|
||||
}
|
||||
|
||||
ret.Validator = func(cmd *cobra.Command) (validation.Schema, error) {
|
||||
if GetFlagBool(cmd, "validate") {
|
||||
client, err := clientBuilder.Client()
|
||||
client, err := getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -62,14 +71,12 @@ func NewFactory(clientBuilder clientcmd.Builder) *Factory {
|
|||
} else {
|
||||
return validation.NullSchema{}, nil
|
||||
}
|
||||
},
|
||||
Client: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) {
|
||||
return clientBuilder.Override(func(c *client.Config) {
|
||||
c.Version = mapping.APIVersion
|
||||
}).Client()
|
||||
},
|
||||
Describer: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) {
|
||||
client, err := clientBuilder.Client()
|
||||
}
|
||||
ret.Client = func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) {
|
||||
return getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion))
|
||||
}
|
||||
ret.Describer = func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) {
|
||||
client, err := getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -78,11 +85,8 @@ func NewFactory(clientBuilder clientcmd.Builder) *Factory {
|
|||
return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind)
|
||||
}
|
||||
return describer, nil
|
||||
},
|
||||
Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) {
|
||||
return kubectl.NewHumanReadablePrinter(noHeaders), nil
|
||||
},
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f *Factory) Run(out io.Writer) {
|
||||
|
@ -96,12 +100,13 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
|
|||
Run: runHelp,
|
||||
}
|
||||
|
||||
f.ClientBuilder.BindFlags(cmds.PersistentFlags())
|
||||
f.ClientConfig = getClientConfig(cmds)
|
||||
|
||||
// Globally persistent flags across all subcommands.
|
||||
// TODO Change flag names to consts to allow safer lookup from subcommands.
|
||||
// TODO Add a verbose flag that turns on glog logging. Probably need a way
|
||||
// to do that automatically for every subcommand.
|
||||
cmds.PersistentFlags().Bool(FlagMatchBinaryVersion, false, "Require server version to match client version")
|
||||
cmds.PersistentFlags().String("ns-path", os.Getenv("HOME")+"/.kubernetes_ns", "Path to the namespace info file that holds the namespace context to use for CLI requests.")
|
||||
cmds.PersistentFlags().StringP("namespace", "n", "", "If present, the namespace scope for this CLI request.")
|
||||
cmds.PersistentFlags().Bool("validate", false, "If true, use a schema to validate the input before sending it")
|
||||
|
@ -125,6 +130,50 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
|
|||
}
|
||||
}
|
||||
|
||||
// getClientBuilder creates a clientcmd.ClientConfig that has a hierarchy like this:
|
||||
// 1. Use the kubeconfig builder. The number of merges and overrides here gets a little crazy. Stay with me.
|
||||
// 1. Merge together the kubeconfig itself. This is done with the following hierarchy and merge rules:
|
||||
// 1. CommandLineLocation - this parsed from the command line, so it must be late bound
|
||||
// 2. EnvVarLocation
|
||||
// 3. CurrentDirectoryLocation
|
||||
// 4. HomeDirectoryLocation
|
||||
// Empty filenames are ignored. Files with non-deserializable content produced errors.
|
||||
// The first file to set a particular value or map key wins and the value or map key is never changed.
|
||||
// This means that the first file to set CurrentContext will have its context preserved. It also means
|
||||
// that if two files specify a "red-user", only values from the first file's red-user are used. Even
|
||||
// non-conflicting entries from the second file's "red-user" are discarded.
|
||||
// 2. Determine the context to use based on the first hit in this chain
|
||||
// 1. command line argument - again, parsed from the command line, so it must be late bound
|
||||
// 2. CurrentContext from the merged kubeconfig file
|
||||
// 3. Empty is allowed at this stage
|
||||
// 3. Determine the cluster info and auth info to use. At this point, we may or may not have a context. They
|
||||
// are built based on the first hit in this chain. (run it twice, once for auth, once for cluster)
|
||||
// 1. command line argument
|
||||
// 2. If context is present, then use the context value
|
||||
// 3. Empty is allowed
|
||||
// 4. Determine the actual cluster info to use. At this point, we may or may not have a cluster info. Build
|
||||
// each piece of the cluster info based on the chain:
|
||||
// 1. command line argument
|
||||
// 2. If cluster info is present and a value for the attribute is present, use it.
|
||||
// 3. If you don't have a server location, bail.
|
||||
// 5. Auth info is build using the same rules as cluster info, EXCEPT that you can only have one authentication
|
||||
// technique per auth info. The following conditions result in an error:
|
||||
// 1. If there are two conflicting techniques specified from the command line, fail.
|
||||
// 2. If the command line does not specify one, and the auth info has conflicting techniques, fail.
|
||||
// 3. If the command line specifies one and the auth info specifies another, honor the command line technique.
|
||||
// 2. Use default values and potentially prompt for auth information
|
||||
func getClientConfig(cmd *cobra.Command) clientcmd.ClientConfig {
|
||||
loadingRules := clientcmd.NewClientConfigLoadingRules()
|
||||
loadingRules.EnvVarPath = os.Getenv(clientcmd.RecommendedConfigPathEnvVar)
|
||||
cmd.PersistentFlags().StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.")
|
||||
|
||||
overrides := &clientcmd.ConfigOverrides{}
|
||||
overrides.BindFlags(cmd.PersistentFlags(), clientcmd.RecommendedConfigOverrideFlags(""))
|
||||
clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin)
|
||||
|
||||
return clientConfig
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
glog.FatalDepth(1, err)
|
||||
|
@ -196,3 +245,25 @@ func (c *clientSwaggerSchema) ValidateBytes(data []byte) error {
|
|||
}
|
||||
return schema.ValidateBytes(data)
|
||||
}
|
||||
|
||||
// TODO Need to only run server version match once per client host creation
|
||||
func getClient(clientConfig clientcmd.ClientConfig, matchServerVersion bool) (*client.Client, error) {
|
||||
config, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if matchServerVersion {
|
||||
err := client.MatchesServerVersion(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
client, err := client.New(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
)
|
||||
|
||||
func (f *Factory) NewCmdLog(out io.Writer) *cobra.Command {
|
||||
|
@ -44,7 +46,9 @@ Examples:
|
|||
}
|
||||
|
||||
namespace := GetKubeNamespace(cmd)
|
||||
client, err := f.ClientBuilder.Client()
|
||||
config, err := f.ClientConfig.ClientConfig()
|
||||
checkErr(err)
|
||||
client, err := client.New(config)
|
||||
checkErr(err)
|
||||
|
||||
podID := args[0]
|
||||
|
|
|
@ -33,7 +33,7 @@ func (f *Factory) NewCmdProxy(out io.Writer) *cobra.Command {
|
|||
port := GetFlagInt(cmd, "port")
|
||||
glog.Infof("Starting to serve on localhost:%d", port)
|
||||
|
||||
clientConfig, err := f.ClientBuilder.Config()
|
||||
clientConfig, err := f.ClientConfig.ClientConfig()
|
||||
checkErr(err)
|
||||
|
||||
server, err := kubectl.NewProxyServer(GetFlagString(cmd, "www"), clientConfig, port)
|
||||
|
|
|
@ -19,8 +19,10 @@ package cmd
|
|||
import (
|
||||
"io"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
|
||||
)
|
||||
|
||||
func (f *Factory) NewCmdVersion(out io.Writer) *cobra.Command {
|
||||
|
@ -31,7 +33,9 @@ func (f *Factory) NewCmdVersion(out io.Writer) *cobra.Command {
|
|||
if GetFlagBool(cmd, "client") {
|
||||
kubectl.GetClientVersion(out)
|
||||
} else {
|
||||
client, err := f.ClientBuilder.Client()
|
||||
config, err := f.ClientConfig.ClientConfig()
|
||||
checkErr(err)
|
||||
client, err := client.New(config)
|
||||
checkErr(err)
|
||||
|
||||
kubectl.GetVersion(out, client)
|
||||
|
|
Loading…
Reference in New Issue