Unverified Commit 13a662b6 authored by bryanl's avatar bryanl
Browse files

create validate action


Signed-off-by: default avatarbryanl <bryanliles@gmail.com>
parent a89f456d
// Copyright 2018 The ksonnet authors
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package actions
import (
"fmt"
"io"
"os"
"github.com/ksonnet/ksonnet/client"
"github.com/ksonnet/ksonnet/metadata/app"
"github.com/ksonnet/ksonnet/pkg/pipeline"
"github.com/ksonnet/ksonnet/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/discovery"
)
// RunValidate runs `ns list`
func RunValidate(ksApp app.App, envName, nsName string,
componentNames []string, clientConfig *client.Config) error {
v, err := NewValidate(ksApp, envName, nsName, componentNames, clientConfig)
if err != nil {
return err
}
return v.Run()
}
type discoveryFn func(a app.App, clientConfig *client.Config, envName string) (discovery.DiscoveryInterface, error)
type validateObjectFn func(d discovery.DiscoveryInterface,
obj *unstructured.Unstructured) []error
type findObjectsFn func(a app.App, envName string,
componentNames []string) ([]*unstructured.Unstructured, error)
// Validate lists namespaces.
type Validate struct {
app app.App
envName string
nsName string
componentNames []string
clientConfig *client.Config
out io.Writer
discoveryFn discoveryFn
validateObjectFn validateObjectFn
findObjectsFn findObjectsFn
}
// NewValidate creates an instance of Validate.
func NewValidate(ksApp app.App, envName, nsName string,
componentNames []string, clientConfig *client.Config) (*Validate, error) {
v := &Validate{
app: ksApp,
envName: envName,
nsName: nsName,
componentNames: componentNames,
clientConfig: clientConfig,
out: os.Stdout,
discoveryFn: loadDiscovery,
validateObjectFn: validateObject,
findObjectsFn: findObjects,
}
return v, nil
}
// Run lists namespaces.
func (v *Validate) Run() error {
objects, err := v.findObjectsFn(v.app, v.envName, v.componentNames)
if err != nil {
return err
}
disc, err := v.discoveryFn(v.app, v.clientConfig, v.envName)
if err != nil {
return err
}
var hasError bool
for _, obj := range objects {
desc := fmt.Sprintf("%s %s", utils.ResourceNameFor(disc, obj), utils.FqName(obj))
log.Info("Validating ", desc)
errs := v.validateObjectFn(disc, obj)
for _, err := range errs {
log.Errorf("Error in %s: %v", desc, err)
hasError = true
}
}
if hasError {
return errors.Errorf("validation failed")
}
return nil
}
func loadDiscovery(a app.App, clientConfig *client.Config, envName string) (discovery.DiscoveryInterface, error) {
_, d, _, err := clientConfig.RestClient(a, &envName)
return d, err
}
func validateObject(d discovery.DiscoveryInterface, obj *unstructured.Unstructured) []error {
schema, err := utils.NewSwaggerSchemaFor(d, obj.GroupVersionKind().GroupVersion())
if err != nil {
return []error{errors.Wrap(err, "unable to retrieve schema")}
}
return schema.Validate(obj)
}
func findObjects(a app.App, envName string, componentNames []string) ([]*unstructured.Unstructured, error) {
p := pipeline.New(a, envName)
return p.Objects(componentNames)
}
// Copyright 2018 The ksonnet authors
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package actions
import (
"testing"
swagger "github.com/emicklei/go-restful-swagger12"
"github.com/googleapis/gnostic/OpenAPIv2"
"github.com/ksonnet/ksonnet/client"
"github.com/ksonnet/ksonnet/metadata/app"
amocks "github.com/ksonnet/ksonnet/metadata/app/mocks"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/discovery"
restclient "k8s.io/client-go/rest"
)
func TestValidate(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
aEnvName := "default"
aComponentNames := make([]string, 0)
aModuleName := "module"
aClientConfig := &client.Config{}
env := &app.EnvironmentSpec{}
appMock.On("Environment", aEnvName).Return(env, nil)
a, err := NewValidate(appMock, aEnvName, aModuleName, aComponentNames, aClientConfig)
require.NoError(t, err)
a.discoveryFn = func(a app.App, clientConfig *client.Config, envName string) (discovery.DiscoveryInterface, error) {
assert.Equal(t, aEnvName, envName)
return &stubDiscovery{}, nil
}
objects := []*unstructured.Unstructured{
{},
}
a.findObjectsFn = func(a app.App, envName string, componentNames []string) ([]*unstructured.Unstructured, error) {
assert.Equal(t, "default", envName)
assert.Equal(t, aComponentNames, componentNames)
return objects, nil
}
a.validateObjectFn = func(d discovery.DiscoveryInterface, obj *unstructured.Unstructured) []error {
return make([]error, 0)
}
err = a.Run()
require.NoError(t, err)
})
}
type stubDiscovery struct{}
func (d *stubDiscovery) RESTClient() restclient.Interface {
return nil
}
func (d *stubDiscovery) ServerGroups() (*metav1.APIGroupList, error) {
return nil, errors.New("not implemented")
}
func (d *stubDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
return nil, errors.New("not implemented")
}
func (d *stubDiscovery) ServerResources() ([]*metav1.APIResourceList, error) {
return nil, errors.New("not implemented")
}
func (d *stubDiscovery) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
return nil, errors.New("not implemented")
}
func (d *stubDiscovery) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
return nil, errors.New("not implemented")
}
func (d *stubDiscovery) ServerVersion() (*version.Info, error) {
return nil, errors.New("not implemented")
}
func (d *stubDiscovery) SwaggerSchema(version schema.GroupVersion) (*swagger.ApiDeclaration, error) {
return nil, errors.New("not implemented")
}
func (d *stubDiscovery) OpenAPISchema() (*openapi_v2.Document, error) {
return nil, errors.New("not implemented")
}
......@@ -28,7 +28,7 @@ import (
"reflect"
"time"
"github.com/ksonnet/ksonnet/metadata"
"github.com/ksonnet/ksonnet/metadata/app"
str "github.com/ksonnet/ksonnet/strings"
"github.com/ksonnet/ksonnet/utils"
"github.com/pkg/errors"
......@@ -52,9 +52,13 @@ type Config struct {
}
// NewClientConfig initializes a new client.Config with the provided loading rules and overrides.
func NewClientConfig(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)
return &Config{Overrides: &overrides, LoadingRules: &loadingRules, Config: config}
return &Config{
Overrides: &overrides,
LoadingRules: &loadingRules,
Config: config,
}
}
// NewDefaultClientConfig initializes a new ClientConfig with default loading rules and no overrides.
......@@ -64,14 +68,18 @@ func NewDefaultClientConfig() *Config {
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
config := clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin)
return &Config{Overrides: &overrides, LoadingRules: &loadingRules, Config: config}
return &Config{
Overrides: &overrides,
LoadingRules: &loadingRules,
Config: config,
}
}
// InitClient initializes a new ClientConfig given the specified environment
// spec and returns the ClientPool, DiscoveryInterface, and namespace.
func InitClient(env string) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) {
func InitClient(a app.App, env string) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) {
clientConfig := NewDefaultClientConfig()
return clientConfig.RestClient(&env)
return clientConfig.RestClient(a, &env)
}
// GetAPISpec reads the kubernetes API version from this client's swagger.json.
......@@ -166,9 +174,9 @@ func (c *Config) Namespace() (string, error) {
}
// RestClient returns the ClientPool, DiscoveryInterface, and Namespace based on the environment spec.
func (c *Config) RestClient(envName *string) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) {
func (c *Config) RestClient(a app.App, envName *string) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) {
if envName != nil {
err := c.overrideCluster(*envName)
err := c.overrideCluster(a, *envName)
if err != nil {
return nil, nil, "", err
}
......@@ -245,17 +253,7 @@ func (c *Config) ResolveContext(context string) (server, namespace string, err e
// If the environment server the user is attempting to deploy to is not the current
// kubeconfig context, we must manually override the client-go --cluster flag
// to ensure we are deploying to the correct cluster.
func (c *Config) overrideCluster(envName string) error {
cwd, err := os.Getwd()
if err != nil {
return err
}
metadataManager, err := metadata.Find(cwd)
if err != nil {
return err
}
func (c *Config) overrideCluster(a app.App, envName string) error {
rawConfig, err := c.Config.RawConfig()
if err != nil {
return err
......@@ -277,12 +275,14 @@ func (c *Config) overrideCluster(envName string) error {
//
log.Debugf("Validating deployment at '%s' with server '%v'", envName, reflect.ValueOf(servers).MapKeys())
destination, err := metadataManager.GetDestination(envName)
env, err := a.Environment(envName)
if err != nil {
return err
}
server, err := str.NormalizeURL(destination.Server())
destination := env.Destination
server, err := str.NormalizeURL(destination.Server)
if err != nil {
return err
}
......@@ -295,17 +295,17 @@ func (c *Config) overrideCluster(envName string) error {
c.Overrides.Context.Cluster = clusterName
}
if c.Overrides.Context.Namespace == "" {
log.Debugf("Overwriting --namespace flag with '%s'", destination.Namespace())
c.Overrides.Context.Namespace = destination.Namespace()
log.Debugf("Overwriting --namespace flag with '%s'", destination.Namespace)
c.Overrides.Context.Namespace = destination.Namespace
}
return nil
}
return fmt.Errorf("Attempting to deploy to environment '%s' at '%s', but cannot locate a server at that address",
envName, destination.Server())
envName, destination.Server)
}
c.Overrides.Context.Namespace = destination.Namespace()
c.Overrides.Context.Namespace = destination.Namespace
c.Overrides.ClusterInfo.Server = server
// NOTE: ignore TLS verify since we don't have a CA cert to verify with.
c.Overrides.ClusterInfo.InsecureSkipTLSVerify = true
......
......@@ -21,10 +21,12 @@ type initName int
const (
actionInit initName = iota
actionValidate
)
var (
actionMap = map[initName]interface{}{
actionInit: actions.RunInit,
actionInit: actions.RunInit,
actionValidate: actions.RunValidate,
}
)
......@@ -78,7 +78,7 @@ var applyCmd = &cobra.Command{
flags := cmd.Flags()
var err error
c := kubecfg.ApplyCmd{}
c := kubecfg.ApplyCmd{App: ka}
c.Create, err = flags.GetBool(flagCreate)
if err != nil {
......@@ -103,7 +103,7 @@ var applyCmd = &cobra.Command{
c.ClientConfig = applyClientConfig
c.Env = env
componentNames, err := flags.GetStringArray(flagComponent)
componentNames, err := flags.GetStringSlice(flagComponent)
if err != nil {
return err
}
......
......@@ -55,14 +55,14 @@ var deleteCmd = &cobra.Command{
flags := cmd.Flags()
var err error
c := kubecfg.DeleteCmd{}
c := kubecfg.DeleteCmd{App: ka}
c.GracePeriod, err = flags.GetInt64(flagGracePeriod)
if err != nil {
return err
}
componentNames, err := flags.GetStringArray(flagComponent)
componentNames, err := flags.GetStringSlice(flagComponent)
if err != nil {
return err
}
......
......@@ -59,7 +59,7 @@ var diffCmd = &cobra.Command{
return err
}
componentNames, err := flags.GetStringArray(flagComponent)
componentNames, err := flags.GetStringSlice(flagComponent)
if err != nil {
return err
}
......@@ -203,7 +203,7 @@ func initDiffSingleEnv(fs afero.Fs, env, diffStrategy string, files []string, cm
return nil, err
}
c.Client.ClientPool, c.Client.Discovery, c.Client.Namespace, err = client.InitClient(env)
c.Client.ClientPool, c.Client.Discovery, c.Client.Namespace, err = client.InitClient(ka, env)
if err != nil {
return nil, err
}
......@@ -255,12 +255,12 @@ func initDiffRemotesCmd(fs afero.Fs, env1, env2, diffStrategy string, cmd *cobra
return nil, err
}
c.ClientA.ClientPool, c.ClientA.Discovery, c.ClientA.Namespace, err = client.InitClient(c.ClientA.Name)
c.ClientA.ClientPool, c.ClientA.Discovery, c.ClientA.Namespace, err = client.InitClient(ka, c.ClientA.Name)
if err != nil {
return nil, err
}
c.ClientB.ClientPool, c.ClientB.Discovery, c.ClientB.Namespace, err = client.InitClient(c.ClientB.Name)
c.ClientB.ClientPool, c.ClientB.Discovery, c.ClientB.Namespace, err = client.InitClient(ka, c.ClientB.Name)
if err != nil {
return nil, err
}
......@@ -280,7 +280,7 @@ func initDiffRemoteCmd(fs afero.Fs, localEnv, remoteEnv, diffStrategy string, cm
return nil, err
}
c.Client.ClientPool, c.Client.Discovery, c.Client.Namespace, err = client.InitClient(remoteEnv)
c.Client.ClientPool, c.Client.Discovery, c.Client.Namespace, err = client.InitClient(ka, remoteEnv)
if err != nil {
return nil, err
}
......
......@@ -16,6 +16,9 @@
package cmd
const (
// For use in the commands (e.g., diff, apply, delete) that require either an
// environment or the -f flag.
flagComponent = "component"
flagEnv = "env"
flagFilename = "filename"
flagIndex = "index"
......@@ -24,8 +27,9 @@ const (
flagOverride = "override"
flagVersion = "version"
shortFilename = "f"
shortIndex = "i"
shortOutput = "o"
shortOverride = "o"
shortComponent = "c"
shortFilename = "f"
shortIndex = "i"
shortOutput = "o"
shortOverride = "o"
)
// Copyright 2018 The ksonnet authors
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"os"
"testing"
"github.com/spf13/cobra"
)
func withCmd(t *testing.T, cmd *cobra.Command, id initName, override interface{}, fn func()) {
ogAction := actionMap[id]
actionMap[id] = override
envConfig := os.Getenv("KUBECONFIG")
defer func() {
actionMap[id] = ogAction
os.Setenv("KUBECONFIG", envConfig)
}()
fn()
}
......@@ -21,25 +21,10 @@ import (
"testing"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func withCmd(t *testing.T, cmd *cobra.Command, id initName, override interface{}, fn func()) {
ogAction := actionMap[id]
actionMap[id] = override
envConfig := os.Getenv("KUBECONFIG")
defer func() {
actionMap[id] = ogAction
os.Setenv("KUBECONFIG", envConfig)
}()
fn()
}
func Test_initCmd(t *testing.T) {
override := func(fs afero.Fs, name, root, specFlag, server, namespace string) error {
wd, err := os.Getwd()
......
......@@ -55,21 +55,14 @@ const (
flagResolver = "resolve-images"
flagResolvFail = "resolve-images-error"
flagAPISpec = "api-spec"
// For use in the commands (e.g., diff, apply, delete) that require either an
// environment or the -f flag.
flagComponent = "component"
flagComponentShort = "c"
)
var (
appFs afero.Fs
appFs = afero.NewOsFs()
ka app.App
)
func init() {
appFs = afero.NewOsFs()
RootCmd.PersistentFlags().CountP(flagVerbose, "v", "Increase verbosity. May be given multiple times.")
RootCmd.PersistentFlags().Set("logtostderr", "true")
}
......@@ -300,7 +293,7 @@ func dumpJSON(v interface{}) string {
// addEnvCmdFlags adds the flags that are common to the family of commands
// whose form is `[<env>|-f <file-name>]`, e.g., `apply` and `delete`.
func addEnvCmdFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringArrayP(flagComponent, flagComponentShort, nil, "Name of a specific component (multiple -c flags accepted, allows YAML, JSON, and Jsonnet)")
cmd.PersistentFlags().StringSliceP(flagComponent, shortComponent, nil, "Name of a specific component (multiple -c flags accepted, allows YAML, JSON, and Jsonnet)")
}
type cmdObjExpanderConfig struct {
......
......@@ -80,7 +80,7 @@ ks show dev -c redis -c nginx-server
flags := cmd.Flags()
var err error
componentNames, err := flags.GetStringArray(flagComponent)
componentNames, err := flags.GetStringSlice(flagComponent)
if err != nil {
return err
}
......
......@@ -16,17 +16,18 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/viper"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/ksonnet/ksonnet/client"
"github.com/ksonnet/ksonnet/pkg/kubecfg"
"github.com/ksonnet/ksonnet/metadata/app"
)
const (
valShortDesc = "Check generated component manifests against the server's API"
vValidateComponent = "validate-component"
valShortDesc = "Check generated component manifests against the server's API"
)
var (
......@@ -39,6 +40,8 @@ func init() {
bindJsonnetFlags(validateCmd)
validateClientConfig = client.NewDefaultClientConfig()
validateClientConfig.BindClientGoFlags(validateCmd)
viper.BindPFlag(vValidateComponent, validateCmd.Flag(flagComponent))
}
var validateCmd = &cobra.Command{
......@@ -46,40 +49,23 @@ var validateCmd = &cobra.Command{
Short: valShortDesc,
RunE: func(cmd *cobra.Command, args []string) error {