Unverified Commit e0e68f8f authored by bryanl's avatar bryanl
Browse files

Update `show` to ksonnet action


Signed-off-by: default avatarbryanl <bryanliles@gmail.com>
parent 352bbaa3
......@@ -41,6 +41,8 @@ const (
OptionDryRun = "dry-run"
// OptionEnvName is envName option.
OptionEnvName = "env-name"
// OptionFormat is format option.
OptionFormat = "format"
// OptionFs is fs option.
OptionFs = "fs"
// OptionGcTag is gcTag option.
......
......@@ -50,7 +50,7 @@ type Delete struct {
func newDelete(m map[string]interface{}, opts ...deleteOpt) (*Delete, error) {
ol := newOptionLoader(m)
a := &Delete{
d := &Delete{
app: ol.loadApp(),
clientConfig: ol.loadClientConfig(),
componentNames: ol.loadStringSlice(OptionComponentNames),
......@@ -65,20 +65,20 @@ func newDelete(m map[string]interface{}, opts ...deleteOpt) (*Delete, error) {
}
for _, opt := range opts {
opt(a)
opt(d)
}
return a, nil
return d, nil
}
func (a *Delete) run() error {
func (d *Delete) run() error {
config := cluster.DeleteConfig{
App: a.app,
ClientConfig: a.clientConfig,
ComponentNames: a.componentNames,
EnvName: a.envName,
GracePeriod: a.gracePeriod,
App: d.app,
ClientConfig: d.clientConfig,
ComponentNames: d.componentNames,
EnvName: d.envName,
GracePeriod: d.gracePeriod,
}
return a.runDeleteFn(config)
return d.runDeleteFn(config)
}
// Copyright 2017 The kubecfg authors
// Copyright 2018 The ksonnet authors
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
......@@ -13,72 +13,76 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package kubecfg
package actions
import (
"fmt"
"sort"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"io"
"os"
"github.com/ksonnet/ksonnet/client"
"github.com/ksonnet/ksonnet/metadata/app"
"github.com/ksonnet/ksonnet/utils"
"github.com/ksonnet/ksonnet/pkg/cluster"
)
// DeleteCmd represents the delete subcommand
type DeleteCmd struct {
App app.App
ClientConfig *client.Config
Env string
GracePeriod int64
}
type runShowFn func(cluster.ShowConfig, ...cluster.ShowOpts) error
func (c *DeleteCmd) Run(apiObjects []*unstructured.Unstructured) error {
clientPool, discovery, namespace, err := c.ClientConfig.RestClient(c.App, &c.Env)
// RunShow runs `show`.
func RunShow(m map[string]interface{}) error {
a, err := newShow(m)
if err != nil {
return err
}
version, err := utils.FetchVersion(discovery)
if err != nil {
return err
}
sort.Sort(sort.Reverse(utils.DependencyOrder(apiObjects)))
deleteOpts := metav1.DeleteOptions{}
if version.Compare(1, 6) < 0 {
// 1.5.x option
boolFalse := false
deleteOpts.OrphanDependents = &boolFalse
} else {
// 1.6.x option (NB: Background is broken)
fg := metav1.DeletePropagationForeground
deleteOpts.PropagationPolicy = &fg
}
if c.GracePeriod >= 0 {
deleteOpts.GracePeriodSeconds = &c.GracePeriod
return a.run()
}
type showOpt func(*Show)
// Show shows objects.
type Show struct {
app app.App
clientConfig *client.Config
componentNames []string
envName string
format string
out io.Writer
runShowFn runShowFn
}
// RunShow runs `show`
func newShow(m map[string]interface{}, opts ...showOpt) (*Show, error) {
ol := newOptionLoader(m)
s := &Show{
app: ol.loadApp(),
componentNames: ol.loadStringSlice(OptionComponentNames),
envName: ol.loadString(OptionEnvName),
format: ol.loadString(OptionFormat),
out: os.Stdout,
runShowFn: cluster.RunShow,
}
for _, obj := range apiObjects {
desc := fmt.Sprintf("%s %s", utils.ResourceNameFor(discovery, obj), utils.FqName(obj))
log.Info("Deleting ", desc)
if ol.err != nil {
return nil, ol.err
}
client, err := utils.ClientForResource(clientPool, discovery, obj, namespace)
if err != nil {
return err
}
for _, opt := range opts {
opt(s)
}
err = client.Delete(obj.GetName(), &deleteOpts)
if err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("Error deleting %s: %s", desc, err)
}
return s, nil
}
log.Debugf("Deleted object: ", obj)
func (s *Show) run() error {
config := cluster.ShowConfig{
App: s.app,
ComponentNames: s.componentNames,
EnvName: s.envName,
Format: s.format,
Out: s.out,
}
return nil
return s.runShowFn(config)
}
// Copyright 2017 The kubecfg authors
// Copyright 2018 The ksonnet authors
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
......@@ -13,59 +13,57 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package kubecfg
package actions
import (
"encoding/json"
"fmt"
"io"
"os"
"testing"
yaml "gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
amocks "github.com/ksonnet/ksonnet/metadata/app/mocks"
"github.com/ksonnet/ksonnet/pkg/cluster"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ShowCmd represents the show subcommand
type ShowCmd struct {
Format string
}
func TestShow(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
in := map[string]interface{}{
OptionApp: appMock,
OptionComponentNames: []string{},
OptionEnvName: "default",
OptionFormat: "yaml",
}
func (c ShowCmd) Run(apiObjects []*unstructured.Unstructured, out io.Writer) error {
switch c.Format {
case "yaml":
for _, obj := range apiObjects {
fmt.Fprintln(out, "---")
// Urgh. Go via json because we need
// to trigger the custom scheme
// encoding.
buf, err := json.Marshal(obj)
if err != nil {
return err
}
o := map[string]interface{}{}
if err := json.Unmarshal(buf, &o); err != nil {
return err
}
buf, err = yaml.Marshal(o)
if err != nil {
return err
}
out.Write(buf)
expected := cluster.ShowConfig{
App: appMock,
ComponentNames: []string{},
EnvName: "default",
Format: "yaml",
Out: os.Stdout,
}
case "json":
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
for _, obj := range apiObjects {
// TODO: this is not valid framing for JSON
if len(apiObjects) > 1 {
fmt.Fprintln(out, "---")
}
if err := enc.Encode(obj); err != nil {
return err
runShowOpt := func(a *Show) {
a.runShowFn = func(config cluster.ShowConfig, opts ...cluster.ShowOpts) error {
assert.Equal(t, expected, config)
return nil
}
}
default:
return fmt.Errorf("Unknown --format: %s", c.Format)
}
return nil
a, err := newShow(in, runShowOpt)
require.NoError(t, err)
err = a.run()
require.NoError(t, err)
})
}
func TestShow_invalid_input(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
in := map[string]interface{}{
OptionClientConfig: "invalid",
}
_, err := newShow(in)
require.Error(t, err)
})
}
......@@ -92,9 +92,9 @@ var (
actionRegistryAdd: actions.RunRegistryAdd,
actionRegistryDescribe: actions.RunRegistryDescribe,
actionRegistryList: actions.RunRegistryList,
// actionShow
actionUpgrade: actions.RunUpgrade,
actionValidate: actions.RunValidate,
actionShow: actions.RunShow,
actionUpgrade: actions.RunUpgrade,
actionValidate: actions.RunValidate,
}
)
......
......@@ -46,6 +46,7 @@ const (
shortComponent = "c"
shortFilename = "f"
shortFormat = "o"
shortIndex = "i"
shortOutput = "o"
shortOverride = "o"
......
......@@ -17,11 +17,10 @@ package cmd
import (
"fmt"
"os"
"github.com/ksonnet/ksonnet/actions"
"github.com/spf13/cobra"
"github.com/ksonnet/ksonnet/pkg/kubecfg"
"github.com/spf13/viper"
)
const (
......@@ -29,11 +28,21 @@ const (
showShortDesc = "Show expanded manifests for a specific environment."
)
const (
vShowComponent = "show-components"
vShowFormat = "show-format"
)
func init() {
RootCmd.AddCommand(showCmd)
addEnvCmdFlags(showCmd)
bindJsonnetFlags(showCmd)
showCmd.PersistentFlags().StringP(flagFormat, "o", "yaml", "Output format. Supported values are: json, yaml")
showCmd.Flags().StringSliceP(flagComponent, shortComponent, nil, "Name of a specific component (multiple -c flags accepted, allows YAML, JSON, and Jsonnet)")
viper.BindPFlag(vShowComponent, showCmd.Flags().Lookup(flagComponent))
showCmd.Flags().StringP(flagFormat, shortFormat, "yaml", "Output format. Supported values are: json, yaml")
viper.BindPFlag(vShowFormat, showCmd.Flags().Lookup(flagFormat))
}
var showCmd = &cobra.Command{
......@@ -75,39 +84,14 @@ ks show dev -c redis -c nginx-server
if len(args) != 1 {
return fmt.Errorf("'show' requires an environment name; use `env list` to see available environments\n\n%s", cmd.UsageString())
}
env := args[0]
flags := cmd.Flags()
var err error
componentNames, err := flags.GetStringSlice(flagComponent)
if err != nil {
return err
}
c := kubecfg.ShowCmd{}
c.Format, err = flags.GetString(flagFormat)
if err != nil {
return err
}
cwd, err := os.Getwd()
if err != nil {
return err
}
te := newCmdObjExpander(cmdObjExpanderConfig{
cmd: cmd,
env: env,
components: componentNames,
cwd: cwd,
})
objs, err := te.Expand()
if err != nil {
return err
m := map[string]interface{}{
actions.OptionApp: ka,
actions.OptionComponentNames: viper.GetStringSlice(vShowComponent),
actions.OptionEnvName: args[0],
actions.OptionFormat: viper.GetString(vShowFormat),
}
return c.Run(objs, cmd.OutOrStdout())
return runAction(actionShow, m)
},
}
......@@ -16,106 +16,31 @@
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
)
func cmdOutput(t *testing.T, args []string) string {
var buf bytes.Buffer
RootCmd.SetOutput(&buf)
defer RootCmd.SetOutput(nil)
t.Log("Running args", args)
RootCmd.SetArgs(args)
if err := RootCmd.Execute(); err != nil {
fmt.Println(buf.String())
t.Fatal("command failed:", err)
}
return buf.String()
}
func OffTestShow(t *testing.T) {
// cd to the test directory we can run the `show` command.
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
err = os.Chdir("../testdata/testapp")
if err != nil {
t.Fatal(err)
}
defer func() {
err = os.Chdir(wd)
if err != nil {
t.Fatal(err)
}
}()
"github.com/ksonnet/ksonnet/actions"
)
// Run the show command.
formats := map[string]func(string) (interface{}, error){
"json": func(text string) (ret interface{}, err error) {
err = json.Unmarshal([]byte(text), &ret)
return
func Test_showCmd(t *testing.T) {
cases := []cmdTestCase{
{
name: "with no options",
args: []string{"show", "default"},
action: actionShow,
expected: map[string]interface{}{
actions.OptionApp: nil,
actions.OptionEnvName: "default",
actions.OptionComponentNames: make([]string, 0),
actions.OptionFormat: "yaml",
},
},
/* Temporarily(!) disabled due to
https://github.com/ksonnet/kubecfg/issues/99
"yaml": func(text string) (ret interface{}, err error) {
err = yaml.Unmarshal([]byte(text), &ret)
return
{
name: "with no env",
args: []string{"show"},
action: actionShow,
isErr: true,
},
*/
}
// Use the fact that JSON is also valid YAML ..
expected := `
{
"apiVersion": "v0alpha1",
"kind": "TestObject",
"nil": null,
"bool": true,
"number": 42,
"string": "bar",
"notAVal": "aVal",
"notAnotherVal": "aVal2",
"filevar": "foo\n",
"array": ["one", 2, [3]],
"object": {"foo": "bar"}
}
`
for format, parser := range formats {
expected, err := parser(expected)
if err != nil {
t.Errorf("error parsing *expected* value: %s", err)
}
os.Setenv("anVar", "aVal2")
defer os.Unsetenv("anVar")
output := cmdOutput(t, []string{
"show",
"default",
"-o", format,
"-c", "test",
"-V", "aVar=aVal",
"-V", "anVar",
"--ext-str-file", "filevar=" + filepath.FromSlash("../extvar.file"),
})
t.Log("output is", output)
actual, err := parser(output)
if err != nil {
t.Errorf("error parsing output of format %s: %s", format, err)
} else if !reflect.DeepEqual(expected, actual) {
t.Errorf("format %s expected != actual: %s != %s", format, expected, actual)
}
}
runTestCmd(t, cases)
}
......@@ -16,6 +16,8 @@
package cmd
import (
"bytes"
"fmt"
"regexp"
"testing"
)
......@@ -28,3 +30,18 @@ func TestVersion(t *testing.T) {
t.Error("Failed to find jsonnet version in:", output)
}
}
func cmdOutput(t *testing.T, args []string) string {
var buf bytes.Buffer
RootCmd.SetOutput(&buf)
defer RootCmd.SetOutput(nil)
t.Log("Running args", args)
RootCmd.SetArgs(args)
if err := RootCmd.Execute(); err != nil {
fmt.Println(buf.String())
t.Fatal("command failed:", err)
}
return buf.String()
}
// 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 cluster
import (
"encoding/json"
"fmt"
"io"
"github.com/ksonnet/ksonnet/metadata/app"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// ShowConfig is configuration for Show.
type ShowConfig struct {
App app.App
ComponentNames []string
EnvName string
Format string
Out io.Writer
}
// ShowOpts is an option for configuring Show.
type ShowOpts func(*Show)
// Show shows objects.
type Show struct {
ShowConfig
// these make it easier to test Show.
findObjectsFn findObjectsFn
}
// RunShow shows objects for a given configuration.
func RunShow(config ShowConfig, opts ...ShowOpts) error {
s := &Show{
ShowConfig: config,
findObjectsFn: findObjects,
}
for _, opt := range opts {
opt(s)
}
return s.Show()
}
// Show shows objects.
func (s *Show) Show() error {
apiObjects, err := s.findObjectsFn(s.App, s.EnvName, s.ComponentNames)
if err != nil {
return errors.Wrap(err, "find objects")
}
switch s.Format {
case "yaml":
return s.showYAML(apiObjects)
case "json":
return s.showJSON(apiObjects)
default:
return fmt.Errorf("Unknown --format: %s", s.Format)