Unverified Commit 8dfed428 authored by bryanl's avatar bryanl
Browse files

Use client-go to retrieve cluster version



Use functionality in client-go to retrieve version from Open API
schema instead of trying to construct the call manually.

Fixes #503
Signed-off-by: default avatarbryanl <bryanliles@gmail.com>
parent 9bd5b3a2
......@@ -43,6 +43,7 @@ ks param set guestbook replicas 2 --env=dev
### Options
```
--as-string Force value to be interpreted as string
--env string Specify environment to set parameters for
-h, --help help for set
```
......
......@@ -42,7 +42,7 @@ const (
func init() {
RootCmd.AddCommand(applyCmd)
applyClientConfig = client.NewDefaultClientConfig()
applyClientConfig = client.NewDefaultClientConfig(ka)
applyClientConfig.BindClientGoFlags(applyCmd)
bindJsonnetFlags(applyCmd, "apply")
......
......@@ -39,7 +39,7 @@ var (
func init() {
RootCmd.AddCommand(deleteCmd)
deleteClientConfig = client.NewDefaultClientConfig()
deleteClientConfig = client.NewDefaultClientConfig(ka)
deleteClientConfig.BindClientGoFlags(deleteCmd)
bindJsonnetFlags(deleteCmd, "delete")
......
......@@ -47,7 +47,7 @@ var (
func init() {
RootCmd.AddCommand(envCmd)
envClientConfig = client.NewDefaultClientConfig()
envClientConfig = client.NewDefaultClientConfig(ka)
envClientConfig.BindClientGoFlags(envCmd)
envCmd.AddCommand(envAddCmd)
......
......@@ -51,7 +51,7 @@ var envAddCmd = &cobra.Command{
return err
}
if specFlag == "" {
specFlag = envClientConfig.GetAPISpec(server)
specFlag = envClientConfig.GetAPISpec()
}
isOverride := viper.GetBool(vEnvAddOverride)
......
......@@ -43,7 +43,7 @@ var (
func init() {
RootCmd.AddCommand(initCmd)
initClientConfig = client.NewDefaultClientConfig()
initClientConfig = client.NewDefaultClientConfig(ka)
initClientConfig.BindClientGoFlags(initCmd)
initCmd.Flags().String(flagDir, "", "Ksonnet application directory")
......@@ -90,7 +90,7 @@ var initCmd = &cobra.Command{
specFlag := viper.GetString(vInitAPISpec)
if specFlag == "" {
specFlag = initClientConfig.GetAPISpec(server)
specFlag = initClientConfig.GetAPISpec()
}
m := map[string]interface{}{
......
......@@ -45,7 +45,7 @@ func Test_initCmd(t *testing.T) {
actions.OptionEnvName: "env-name",
actions.OptionRootPath: root,
actions.OptionServer: "http://127.0.0.1",
actions.OptionSpecFlag: "version:v1.7.0",
actions.OptionSpecFlag: "version:v1.8.0",
actions.OptionNamespace: "new-namespace",
actions.OptionSkipDefaultRegistries: false,
},
......
......@@ -38,7 +38,7 @@ func init() {
RootCmd.AddCommand(validateCmd)
addEnvCmdFlags(validateCmd)
bindJsonnetFlags(validateCmd, "validate")
validateClientConfig = client.NewDefaultClientConfig()
validateClientConfig = client.NewDefaultClientConfig(ka)
validateClientConfig.BindClientGoFlags(validateCmd)
viper.BindPFlag(vValidateComponent, validateCmd.Flag(flagComponent))
......
......@@ -16,17 +16,9 @@
package client
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"reflect"
"time"
"github.com/ksonnet/ksonnet/pkg/app"
str "github.com/ksonnet/ksonnet/pkg/util/strings"
......@@ -40,7 +32,7 @@ import (
)
const (
defaultVersion = "version:v1.7.0"
defaultVersion = "version:v1.8.0"
)
// Config is a wrapper around client-go's ClientConfig
......@@ -49,122 +41,65 @@ type Config struct {
LoadingRules *clientcmd.ClientConfigLoadingRules
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.
func NewClientConfig(a app.App, overrides clientcmd.ConfigOverrides, loadingRules clientcmd.ClientConfigLoadingRules) *Config {
config := clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin)
return &Config{
Overrides: &overrides,
LoadingRules: &loadingRules,
Config: config,
Overrides: &overrides,
LoadingRules: &loadingRules,
Config: config,
discoveryClient: defaultDiscoveryClient(config),
}
}
// NewDefaultClientConfig initializes a new ClientConfig with default loading rules and no overrides.
func NewDefaultClientConfig() *Config {
func NewDefaultClientConfig(a app.App) *Config {
overrides := clientcmd.ConfigOverrides{}
loadingRules := *clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
config := clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin)
return &Config{
Overrides: &overrides,
LoadingRules: &loadingRules,
Config: config,
}
return NewClientConfig(a, overrides, loadingRules)
}
// InitClient initializes a new ClientConfig given the specified environment
// spec and returns the ClientPool, DiscoveryInterface, and namespace.
func InitClient(a app.App, env string) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) {
clientConfig := NewDefaultClientConfig()
clientConfig := NewDefaultClientConfig(a)
return clientConfig.RestClient(a, &env)
}
// GetAPISpec reads the kubernetes API version from this client's swagger.json.
// We anticipate the swagger.json to be located at <server>/swagger.json.
// If no swagger is found, or we are unable to authenticate to the server, we
// will default to version:v1.7.0.
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()
// GetAPISpec reads the kubernetes API version from this client's Open API schema.
// If there is an error retrieving the schema, return the default version.
func (c *Config) GetAPISpec() string {
dc, err := c.discoveryClient()
if err != nil {
log.Debugf("Failed to retrieve REST config:\n%v", err)
}
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())
log.WithError(err).Debug("Failed to create discovery client")
return defaultVersion
}
res, err := client.Do(req)
openAPIDoc, err := dc.OpenAPISchema()
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
}
body, err := ioutil.ReadAll(res.Body)
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
return fmt.Sprintf("version:%s", openAPIDoc.Info.Version)
}
// Namespace returns the namespace for the provided ClientConfig.
......
......@@ -16,101 +16,61 @@
package client
import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
swagger "github.com/emicklei/go-restful-swagger12"
"github.com/googleapis/gnostic/OpenAPIv2"
"github.com/pkg/errors"
"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"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
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 {
name string
serverURL string
expected string
caData []byte
caFile string
version string
disc discovery.DiscoveryInterface
createErr error
}{
{
name: "invalid server URL",
serverURL: "http://+++",
expected: defaultVersion,
},
{
name: "with a server",
serverURL: ts.URL,
expected: "version:v1.9.3",
name: "in general",
version: "version:v1.9.3",
disc: &fakeDiscovery{},
},
{
name: "TLS with file cert",
serverURL: tsTLS.URL,
expected: "version:v1.9.3",
caFile: tmpfile.Name(),
name: "unable to create discovery client",
version: "version:v1.8.0",
createErr: errors.New("failed"),
},
{
name: "TLS with data cert",
serverURL: tsTLS.URL,
expected: "version:v1.9.3",
caData: certPEM,
name: "retrieve open api schema error",
version: "version:v1.8.0",
disc: &fakeDiscovery{withOpenAPISchemaError: true},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := Config{
Config: &clientConfig{
caFile: tc.caFile,
caData: tc.caData,
Config: &clientConfig{},
discoveryClient: func() (discovery.DiscoveryInterface, error) {
return tc.disc, tc.createErr
},
}
got := c.GetAPISpec(tc.serverURL)
require.Equal(t, tc.expected, got)
got := c.GetAPISpec()
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 {
caFile string
caData []byte
}
var _ clientcmd.ClientConfig = (*clientConfig)(nil)
......@@ -120,12 +80,7 @@ func (c *clientConfig) RawConfig() (clientcmdapi.Config, error) {
}
func (c *clientConfig) ClientConfig() (*restclient.Config, error) {
rc := &restclient.Config{
TLSClientConfig: restclient.TLSClientConfig{
CAData: c.caData,
CAFile: c.caFile,
},
}
rc := &restclient.Config{}
return rc, nil
}
......@@ -139,3 +94,53 @@ func (c *clientConfig) ConfigAccess() clientcmd.ConfigAccess {
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