Unverified Commit c904092f authored by Bryan Liles's avatar Bryan Liles Committed by GitHub
Browse files

Merge pull request #520 from bryanl/client-go-server-version

Use client-go to retrieve cluster version
parents 9bd5b3a2 8dfed428
...@@ -43,6 +43,7 @@ ks param set guestbook replicas 2 --env=dev ...@@ -43,6 +43,7 @@ ks param set guestbook replicas 2 --env=dev
### Options ### Options
``` ```
--as-string Force value to be interpreted as string
--env string Specify environment to set parameters for --env string Specify environment to set parameters for
-h, --help help for set -h, --help help for set
``` ```
......
...@@ -42,7 +42,7 @@ const ( ...@@ -42,7 +42,7 @@ const (
func init() { func init() {
RootCmd.AddCommand(applyCmd) RootCmd.AddCommand(applyCmd)
applyClientConfig = client.NewDefaultClientConfig() applyClientConfig = client.NewDefaultClientConfig(ka)
applyClientConfig.BindClientGoFlags(applyCmd) applyClientConfig.BindClientGoFlags(applyCmd)
bindJsonnetFlags(applyCmd, "apply") bindJsonnetFlags(applyCmd, "apply")
......
...@@ -39,7 +39,7 @@ var ( ...@@ -39,7 +39,7 @@ var (
func init() { func init() {
RootCmd.AddCommand(deleteCmd) RootCmd.AddCommand(deleteCmd)
deleteClientConfig = client.NewDefaultClientConfig() deleteClientConfig = client.NewDefaultClientConfig(ka)
deleteClientConfig.BindClientGoFlags(deleteCmd) deleteClientConfig.BindClientGoFlags(deleteCmd)
bindJsonnetFlags(deleteCmd, "delete") bindJsonnetFlags(deleteCmd, "delete")
......
...@@ -47,7 +47,7 @@ var ( ...@@ -47,7 +47,7 @@ var (
func init() { func init() {
RootCmd.AddCommand(envCmd) RootCmd.AddCommand(envCmd)
envClientConfig = client.NewDefaultClientConfig() envClientConfig = client.NewDefaultClientConfig(ka)
envClientConfig.BindClientGoFlags(envCmd) envClientConfig.BindClientGoFlags(envCmd)
envCmd.AddCommand(envAddCmd) envCmd.AddCommand(envAddCmd)
......
...@@ -51,7 +51,7 @@ var envAddCmd = &cobra.Command{ ...@@ -51,7 +51,7 @@ var envAddCmd = &cobra.Command{
return err return err
} }
if specFlag == "" { if specFlag == "" {
specFlag = envClientConfig.GetAPISpec(server) specFlag = envClientConfig.GetAPISpec()
} }
isOverride := viper.GetBool(vEnvAddOverride) isOverride := viper.GetBool(vEnvAddOverride)
......
...@@ -43,7 +43,7 @@ var ( ...@@ -43,7 +43,7 @@ var (
func init() { func init() {
RootCmd.AddCommand(initCmd) RootCmd.AddCommand(initCmd)
initClientConfig = client.NewDefaultClientConfig() initClientConfig = client.NewDefaultClientConfig(ka)
initClientConfig.BindClientGoFlags(initCmd) initClientConfig.BindClientGoFlags(initCmd)
initCmd.Flags().String(flagDir, "", "Ksonnet application directory") initCmd.Flags().String(flagDir, "", "Ksonnet application directory")
...@@ -90,7 +90,7 @@ var initCmd = &cobra.Command{ ...@@ -90,7 +90,7 @@ var initCmd = &cobra.Command{
specFlag := viper.GetString(vInitAPISpec) specFlag := viper.GetString(vInitAPISpec)
if specFlag == "" { if specFlag == "" {
specFlag = initClientConfig.GetAPISpec(server) specFlag = initClientConfig.GetAPISpec()
} }
m := map[string]interface{}{ m := map[string]interface{}{
......
...@@ -45,7 +45,7 @@ func Test_initCmd(t *testing.T) { ...@@ -45,7 +45,7 @@ func Test_initCmd(t *testing.T) {
actions.OptionEnvName: "env-name", actions.OptionEnvName: "env-name",
actions.OptionRootPath: root, actions.OptionRootPath: root,
actions.OptionServer: "http://127.0.0.1", actions.OptionServer: "http://127.0.0.1",
actions.OptionSpecFlag: "version:v1.7.0", actions.OptionSpecFlag: "version:v1.8.0",
actions.OptionNamespace: "new-namespace", actions.OptionNamespace: "new-namespace",
actions.OptionSkipDefaultRegistries: false, actions.OptionSkipDefaultRegistries: false,
}, },
......
...@@ -38,7 +38,7 @@ func init() { ...@@ -38,7 +38,7 @@ func init() {
RootCmd.AddCommand(validateCmd) RootCmd.AddCommand(validateCmd)
addEnvCmdFlags(validateCmd) addEnvCmdFlags(validateCmd)
bindJsonnetFlags(validateCmd, "validate") bindJsonnetFlags(validateCmd, "validate")
validateClientConfig = client.NewDefaultClientConfig() validateClientConfig = client.NewDefaultClientConfig(ka)
validateClientConfig.BindClientGoFlags(validateCmd) validateClientConfig.BindClientGoFlags(validateCmd)
viper.BindPFlag(vValidateComponent, validateCmd.Flag(flagComponent)) viper.BindPFlag(vValidateComponent, validateCmd.Flag(flagComponent))
......
...@@ -16,17 +16,9 @@ ...@@ -16,17 +16,9 @@
package client package client
import ( import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http"
"net/url"
"os" "os"
"path"
"reflect" "reflect"
"time"
"github.com/ksonnet/ksonnet/pkg/app" "github.com/ksonnet/ksonnet/pkg/app"
str "github.com/ksonnet/ksonnet/pkg/util/strings" str "github.com/ksonnet/ksonnet/pkg/util/strings"
...@@ -40,7 +32,7 @@ import ( ...@@ -40,7 +32,7 @@ import (
) )
const ( const (
defaultVersion = "version:v1.7.0" defaultVersion = "version:v1.8.0"
) )
// Config is a wrapper around client-go's ClientConfig // Config is a wrapper around client-go's ClientConfig
...@@ -49,122 +41,65 @@ type Config struct { ...@@ -49,122 +41,65 @@ type Config struct {
LoadingRules *clientcmd.ClientConfigLoadingRules LoadingRules *clientcmd.ClientConfigLoadingRules
Config clientcmd.ClientConfig Config clientcmd.ClientConfig
discoveryClient func() (discovery.DiscoveryInterface, error)
}
func defaultDiscoveryClient(config clientcmd.ClientConfig) func() (discovery.DiscoveryInterface, error) {
return func() (discovery.DiscoveryInterface, error) {
c, err := config.ClientConfig()
if err != nil {
return nil, errors.Wrap(err, "retrive client config")
}
return discovery.NewDiscoveryClientForConfig(c)
}
} }
// NewClientConfig initializes a new client.Config with the provided loading rules and overrides. // NewClientConfig initializes a new client.Config with the provided loading rules and overrides.
func NewClientConfig(a app.App, overrides clientcmd.ConfigOverrides, loadingRules clientcmd.ClientConfigLoadingRules) *Config { func NewClientConfig(a app.App, overrides clientcmd.ConfigOverrides, loadingRules clientcmd.ClientConfigLoadingRules) *Config {
config := clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin) config := clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin)
return &Config{ return &Config{
Overrides: &overrides, Overrides: &overrides,
LoadingRules: &loadingRules, LoadingRules: &loadingRules,
Config: config, Config: config,
discoveryClient: defaultDiscoveryClient(config),
} }
} }
// NewDefaultClientConfig initializes a new ClientConfig with default loading rules and no overrides. // NewDefaultClientConfig initializes a new ClientConfig with default loading rules and no overrides.
func NewDefaultClientConfig() *Config { func NewDefaultClientConfig(a app.App) *Config {
overrides := clientcmd.ConfigOverrides{} overrides := clientcmd.ConfigOverrides{}
loadingRules := *clientcmd.NewDefaultClientConfigLoadingRules() loadingRules := *clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
config := clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin)
return &Config{ return NewClientConfig(a, overrides, loadingRules)
Overrides: &overrides,
LoadingRules: &loadingRules,
Config: config,
}
} }
// InitClient initializes a new ClientConfig given the specified environment // InitClient initializes a new ClientConfig given the specified environment
// spec and returns the ClientPool, DiscoveryInterface, and namespace. // spec and returns the ClientPool, DiscoveryInterface, and namespace.
func InitClient(a app.App, env string) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) { func InitClient(a app.App, env string) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) {
clientConfig := NewDefaultClientConfig() clientConfig := NewDefaultClientConfig(a)
return clientConfig.RestClient(a, &env) return clientConfig.RestClient(a, &env)
} }
// GetAPISpec reads the kubernetes API version from this client's swagger.json. // GetAPISpec reads the kubernetes API version from this client's Open API schema.
// We anticipate the swagger.json to be located at <server>/swagger.json. // If there is an error retrieving the schema, return the default version.
// If no swagger is found, or we are unable to authenticate to the server, we func (c *Config) GetAPISpec() string {
// will default to version:v1.7.0. dc, err := c.discoveryClient()
func (c *Config) GetAPISpec(server string) string {
type Info struct {
Version string `json:"version"`
}
type Spec struct {
Info Info `json:"info"`
}
u, err := url.Parse(server)
u.Path = path.Join(u.Path, "swagger.json")
url := u.String()
client := http.Client{
Timeout: time.Second * 2,
}
restConfig, err := c.Config.ClientConfig()
if err != nil { if err != nil {
log.Debugf("Failed to retrieve REST config:\n%v", err) log.WithError(err).Debug("Failed to create discovery client")
}
if len(restConfig.TLSClientConfig.CAData) > 0 {
log.Info("Configuring TLS (from data) for retrieving cluster swagger.json")
client.Transport = buildTransportFromData(restConfig.TLSClientConfig.CAData)
}
if restConfig.TLSClientConfig.CAFile != "" {
log.Info("Configuring TLS (from file) for retrieving cluster swagger.json")
transport, err := buildTransportFromFile(restConfig.TLSClientConfig.CAFile)
if err != nil {
log.Debugf("Failed to read CA file: %v", err)
return defaultVersion
}
client.Transport = transport
}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
log.Debugf("Failed to create request at %s\n%s", url, err.Error())
return defaultVersion return defaultVersion
} }
res, err := client.Do(req) openAPIDoc, err := dc.OpenAPISchema()
if err != nil { if err != nil {
log.Debugf("Failed to open swagger at %s\n%s", url, err.Error()) log.WithError(err).Debug("Failed to retrieve OpenAPI schema")
return defaultVersion return defaultVersion
} }
body, err := ioutil.ReadAll(res.Body) return fmt.Sprintf("version:%s", openAPIDoc.Info.Version)
if err != nil {
log.Debugf("Failed to read swagger at %s\n%s", url, err.Error())
return defaultVersion
}
spec := Spec{}
err = json.Unmarshal(body, &spec)
if err != nil {
log.Debugf("Failed to parse swagger at %s\n%s", url, err.Error())
return defaultVersion
}
return fmt.Sprintf("version:%s", spec.Info.Version)
}
func buildTransportFromData(data []byte) *http.Transport {
tlsConfig := &tls.Config{RootCAs: x509.NewCertPool()}
tlsConfig.RootCAs.AppendCertsFromPEM(data)
return &http.Transport{TLSClientConfig: tlsConfig}
}
func buildTransportFromFile(file string) (*http.Transport, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, errors.Wrap(err, "unable to ready CA file")
}
return buildTransportFromData(data), nil
} }
// Namespace returns the namespace for the provided ClientConfig. // Namespace returns the namespace for the provided ClientConfig.
......
...@@ -16,101 +16,61 @@ ...@@ -16,101 +16,61 @@
package client package client
import ( import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing" "testing"
swagger "github.com/emicklei/go-restful-swagger12"
"github.com/googleapis/gnostic/OpenAPIv2"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/discovery"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api" clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
) )
func TestConfig_GetAPISpec(t *testing.T) { func TestConfig_GetAPISpec(t *testing.T) {
b, err := ioutil.ReadFile("testdata/swagger.json")
require.NoError(t, err)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, string(b))
})
ts := httptest.NewServer(handler)
defer ts.Close()
tsTLS := httptest.NewTLSServer(handler)
defer tsTLS.Close()
tmpfile, err := ioutil.TempFile("", "")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
certPEM := buildPEM(tsTLS.Certificate())
_, err = tmpfile.Write(certPEM)
require.NoError(t, err)
err = tmpfile.Close()
require.NoError(t, err)
cases := []struct { cases := []struct {
name string name string
serverURL string version string
expected string disc discovery.DiscoveryInterface
caData []byte createErr error
caFile string
}{ }{
{ {
name: "invalid server URL", name: "in general",
serverURL: "http://+++", version: "version:v1.9.3",
expected: defaultVersion, disc: &fakeDiscovery{},
},
{
name: "with a server",
serverURL: ts.URL,
expected: "version:v1.9.3",
}, },
{ {
name: "TLS with file cert", name: "unable to create discovery client",
serverURL: tsTLS.URL, version: "version:v1.8.0",
expected: "version:v1.9.3", createErr: errors.New("failed"),
caFile: tmpfile.Name(),
}, },
{ {
name: "TLS with data cert", name: "retrieve open api schema error",
serverURL: tsTLS.URL, version: "version:v1.8.0",
expected: "version:v1.9.3", disc: &fakeDiscovery{withOpenAPISchemaError: true},
caData: certPEM,
}, },
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
c := Config{ c := Config{
Config: &clientConfig{ Config: &clientConfig{},
caFile: tc.caFile, discoveryClient: func() (discovery.DiscoveryInterface, error) {
caData: tc.caData, return tc.disc, tc.createErr
}, },
} }
got := c.GetAPISpec(tc.serverURL) got := c.GetAPISpec()
require.Equal(t, tc.expected, got) require.Equal(t, tc.version, got)
}) })
} }
}
func buildPEM(cert *x509.Certificate) []byte {
b := &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
return pem.EncodeToMemory(b)
} }
type clientConfig struct { type clientConfig struct {
caFile string
caData []byte
} }
var _ clientcmd.ClientConfig = (*clientConfig)(nil) var _ clientcmd.ClientConfig = (*clientConfig)(nil)
...@@ -120,12 +80,7 @@ func (c *clientConfig) RawConfig() (clientcmdapi.Config, error) { ...@@ -120,12 +80,7 @@ func (c *clientConfig) RawConfig() (clientcmdapi.Config, error) {
} }
func (c *clientConfig) ClientConfig() (*restclient.Config, error) { func (c *clientConfig) ClientConfig() (*restclient.Config, error) {
rc := &restclient.Config{ rc := &restclient.Config{}
TLSClientConfig: restclient.TLSClientConfig{
CAData: c.caData,
CAFile: c.caFile,
},
}
return rc, nil return rc, nil
} }
...@@ -139,3 +94,53 @@ func (c *clientConfig) ConfigAccess() clientcmd.ConfigAccess { ...@@ -139,3 +94,53 @@ func (c *clientConfig) ConfigAccess() clientcmd.ConfigAccess {
return ca return ca
} }
type fakeDiscovery struct {
withOpenAPISchemaError bool
}
var _ discovery.DiscoveryInterface = (*fakeDiscovery)(nil)
func (c *fakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
return nil, errors.New("not implemented")
}
func (c *fakeDiscovery) ServerResources() ([]*metav1.APIResourceList, error) {
return nil, errors.New("not implemented")
}
func (c *fakeDiscovery) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
return nil, nil
}
func (c *fakeDiscovery) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
return nil, nil
}
func (c *fakeDiscovery) ServerGroups() (*metav1.APIGroupList, error) {
return nil, errors.New("not implemented")
}
func (c *fakeDiscovery) ServerVersion() (*version.Info, error) {
return nil, errors.New("not implemented")
}
func (c *fakeDiscovery) OpenAPISchema() (*openapi_v2.Document, error) {
if c.withOpenAPISchemaError {
return nil, errors.New("schema error")
}
return &openapi_v2.Document{
Info: &openapi_v2.Info{
Version: "v1.9.3",
},
}, nil
}
func (c *fakeDiscovery) SwaggerSchema(version schema.GroupVersion) (*swagger.ApiDeclaration, error) {
return nil, errors.New("not implemented")
}
func (c *fakeDiscovery) RESTClient() restclient.Interface {
return nil
}
{
"info": {
"version": "v1.9.3"
}
}
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment