Unverified Commit 9452c3c6 authored by bryanl's avatar bryanl
Browse files

Add `param delete` command



Fixes #325
Signed-off-by: default avatarbryanl <bryanliles@gmail.com>
parent c1710f3f
// 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 (
"strings"
"github.com/ksonnet/ksonnet/component"
"github.com/ksonnet/ksonnet/metadata/app"
"github.com/ksonnet/ksonnet/pkg/env"
"github.com/pkg/errors"
)
type getModuleFn func(ksApp app.App, moduleName string) (component.Module, error)
type deleteEnvFn func(ksApp app.App, envName, componentName, paramName string) error
// RunParamDelete runs `param set`
func RunParamDelete(m map[string]interface{}) error {
pd, err := NewParamDelete(m)
if err != nil {
return err
}
return pd.Run()
}
// ParamDelete sets a parameter for a component.
type ParamDelete struct {
app app.App
name string
rawPath string
index int
global bool
envName string
deleteEnvFn deleteEnvFn
getModuleFn getModuleFn
resolvePathFn func(a app.App, path string) (component.Module, component.Component, error)
}
// NewParamDelete creates an instance of ParamDelete.
func NewParamDelete(m map[string]interface{}) (*ParamDelete, error) {
ol := newOptionLoader(m)
pd := &ParamDelete{
app: ol.loadApp(),
name: ol.loadString(OptionName),
rawPath: ol.loadString(OptionPath),
global: ol.loadOptionalBool(OptionGlobal),
envName: ol.loadOptionalString(OptionEnvName),
index: ol.loadOptionalInt(OptionIndex),
deleteEnvFn: env.DeleteParam,
resolvePathFn: component.ResolvePath,
getModuleFn: component.GetModule,
}
if ol.err != nil {
return nil, ol.err
}
if pd.envName != "" && pd.global {
return nil, errors.New("unable to delete global param for environments")
}
return pd, nil
}
// Run runs the action.
func (pd *ParamDelete) Run() error {
if pd.envName != "" {
return pd.deleteEnvFn(pd.app, pd.envName, pd.name, pd.rawPath)
}
path := strings.Split(pd.rawPath, ".")
if pd.global {
return pd.deleteGlobal(path)
}
return pd.deleteLocal(path)
}
func (pd *ParamDelete) deleteGlobal(path []string) error {
module, err := pd.getModuleFn(pd.app, pd.name)
if err != nil {
return errors.Wrap(err, "retrieve module")
}
if err := module.DeleteParam(path); err != nil {
return errors.Wrap(err, "delete global param")
}
return nil
}
func (pd *ParamDelete) deleteLocal(path []string) error {
_, c, err := pd.resolvePathFn(pd.app, pd.name)
if err != nil {
return errors.Wrap(err, "could not find component")
}
options := component.ParamOptions{
Index: pd.index,
}
if err := c.DeleteParam(path, options); err != nil {
return errors.Wrap(err, "delete param")
}
return nil
}
// 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"
"github.com/ksonnet/ksonnet/component"
cmocks "github.com/ksonnet/ksonnet/component/mocks"
"github.com/ksonnet/ksonnet/metadata/app"
amocks "github.com/ksonnet/ksonnet/metadata/app/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParamDelete(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
componentName := "deployment"
path := "replicas"
c := &cmocks.Component{}
c.On("DeleteParam", []string{"replicas"}, component.ParamOptions{}).Return(nil)
in := map[string]interface{}{
OptionApp: appMock,
OptionName: componentName,
OptionPath: path,
}
a, err := NewParamDelete(in)
require.NoError(t, err)
a.resolvePathFn = func(app.App, string) (component.Module, component.Component, error) {
return nil, c, nil
}
err = a.Run()
require.NoError(t, err)
})
}
func TestParamDelete_index(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
componentName := "deployment"
path := "replicas"
c := &cmocks.Component{}
c.On("DeleteParam", []string{"replicas"}, component.ParamOptions{Index: 1}).Return(nil)
in := map[string]interface{}{
OptionApp: appMock,
OptionName: componentName,
OptionPath: path,
OptionIndex: 1,
}
a, err := NewParamDelete(in)
require.NoError(t, err)
a.resolvePathFn = func(app.App, string) (component.Module, component.Component, error) {
return nil, c, nil
}
err = a.Run()
require.NoError(t, err)
})
}
func TestParamDelete_global(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
module := "/"
path := "replicas"
m := &cmocks.Module{}
m.On("DeleteParam", []string{"replicas"}).Return(nil)
in := map[string]interface{}{
OptionApp: appMock,
OptionName: module,
OptionPath: path,
OptionGlobal: true,
}
a, err := NewParamDelete(in)
require.NoError(t, err)
a.getModuleFn = func(app.App, string) (component.Module, error) {
return m, nil
}
err = a.Run()
require.NoError(t, err)
})
}
func TestParamDelete_env(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
name := "deployment"
path := "replicas"
in := map[string]interface{}{
OptionApp: appMock,
OptionName: name,
OptionPath: path,
OptionEnvName: "default",
}
a, err := NewParamDelete(in)
require.NoError(t, err)
envDelete := func(ksApp app.App, envName, name, pName string) error {
assert.Equal(t, "default", envName)
assert.Equal(t, "deployment", name)
assert.Equal(t, "replicas", pName)
return nil
}
a.deleteEnvFn = envDelete
err = a.Run()
require.NoError(t, err)
})
}
......@@ -47,10 +47,9 @@ type ParamSet struct {
global bool
envName string
// TODO: remove once ksonnet has more robust env param handling.
setEnv func(ksApp app.App, envName, name, pName, value string) error
cm component.Manager
getModuleFn getModuleFn
resolvePathFn func(a app.App, path string) (component.Module, component.Component, error)
setEnvFn func(ksApp app.App, envName, name, pName, value string) error
}
// NewParamSet creates an instance of ParamSet.
......@@ -66,8 +65,9 @@ func NewParamSet(m map[string]interface{}) (*ParamSet, error) {
envName: ol.loadOptionalString(OptionEnvName),
index: ol.loadOptionalInt(OptionIndex),
cm: component.DefaultManager,
setEnv: setEnv,
getModuleFn: component.GetModule,
resolvePathFn: component.ResolvePath,
setEnvFn: setEnv,
}
if ol.err != nil {
......@@ -94,7 +94,7 @@ func (ps *ParamSet) Run() error {
}
if ps.envName != "" {
return ps.setEnv(ps.app, ps.envName, ps.name, ps.rawPath, evaluatedValue)
return ps.setEnvFn(ps.app, ps.envName, ps.name, ps.rawPath, evaluatedValue)
}
path := strings.Split(ps.rawPath, ".")
......@@ -107,7 +107,7 @@ func (ps *ParamSet) Run() error {
}
func (ps *ParamSet) setGlobal(path []string, value interface{}) error {
module, err := ps.cm.Module(ps.app, ps.name)
module, err := ps.getModuleFn(ps.app, ps.name)
if err != nil {
return errors.Wrap(err, "retrieve module")
}
......@@ -120,7 +120,7 @@ func (ps *ParamSet) setGlobal(path []string, value interface{}) error {
}
func (ps *ParamSet) setLocal(path []string, value interface{}) error {
_, c, err := ps.cm.ResolvePath(ps.app, ps.name)
_, c, err := ps.resolvePathFn(ps.app, ps.name)
if err != nil {
return errors.Wrap(err, "could not find component")
}
......@@ -135,6 +135,7 @@ func (ps *ParamSet) setLocal(path []string, value interface{}) error {
return nil
}
// TODO: move this to pkg/env
func setEnv(ksApp app.App, envName, name, pName, value string) error {
spc := env.SetParamsConfig{
App: ksApp,
......
......@@ -32,14 +32,9 @@ func TestParamSet(t *testing.T) {
path := "replicas"
value := "3"
cm := &cmocks.Manager{}
var ns component.Component
c := &cmocks.Component{}
c.On("SetParam", []string{"replicas"}, 3, component.ParamOptions{}).Return(nil)
cm.On("ResolvePath", appMock, "deployment").Return(ns, c, nil)
in := map[string]interface{}{
OptionApp: appMock,
OptionName: componentName,
......@@ -50,7 +45,9 @@ func TestParamSet(t *testing.T) {
a, err := NewParamSet(in)
require.NoError(t, err)
a.cm = cm
a.resolvePathFn = func(app.App, string) (component.Module, component.Component, error) {
return nil, c, nil
}
err = a.Run()
require.NoError(t, err)
......@@ -63,14 +60,9 @@ func TestParamSet_index(t *testing.T) {
path := "replicas"
value := "3"
cm := &cmocks.Manager{}
var ns component.Component
c := &cmocks.Component{}
c.On("SetParam", []string{"replicas"}, 3, component.ParamOptions{Index: 1}).Return(nil)
cm.On("ResolvePath", appMock, "deployment").Return(ns, c, nil)
in := map[string]interface{}{
OptionApp: appMock,
OptionName: componentName,
......@@ -82,7 +74,9 @@ func TestParamSet_index(t *testing.T) {
a, err := NewParamSet(in)
require.NoError(t, err)
a.cm = cm
a.resolvePathFn = func(app.App, string) (component.Module, component.Component, error) {
return nil, c, nil
}
err = a.Run()
require.NoError(t, err)
......@@ -95,12 +89,8 @@ func TestParamSet_global(t *testing.T) {
path := "replicas"
value := "3"
cm := &cmocks.Manager{}
ns := &cmocks.Module{}
ns.On("SetParam", []string{"replicas"}, 3).Return(nil)
cm.On("Module", appMock, "/").Return(ns, nil)
m := &cmocks.Module{}
m.On("SetParam", []string{"replicas"}, 3).Return(nil)
in := map[string]interface{}{
OptionApp: appMock,
......@@ -113,7 +103,9 @@ func TestParamSet_global(t *testing.T) {
a, err := NewParamSet(in)
require.NoError(t, err)
a.cm = cm
a.getModuleFn = func(app.App, string) (component.Module, error) {
return m, nil
}
err = a.Run()
require.NoError(t, err)
......@@ -144,7 +136,7 @@ func TestParamSet_env(t *testing.T) {
assert.Equal(t, "3", value)
return nil
}
a.setEnv = envSetter
a.setEnvFn = envSetter
err = a.Run()
require.NoError(t, err)
......
......@@ -38,6 +38,7 @@ const (
actionInit
actionModuleCreate
actionModuleList
actionParamDelete
actionParamDiff
actionParamList
actionParamSet
......@@ -77,6 +78,7 @@ var (
actionModuleCreate: actions.RunModuleCreate,
actionModuleList: actions.RunModuleList,
// actionParamDiff
actionParamDelete: actions.RunParamDelete,
actionParamList: actions.RunParamList,
actionParamSet: actions.RunParamSet,
actionPkgDescribe: actions.RunPkgDescribe,
......
......@@ -23,9 +23,10 @@ import (
)
var paramShortDesc = map[string]string{
"set": "Change component or environment parameters (e.g. replica count, name)",
"list": "List known component parameters",
"diff": "Display differences between the component parameters of two environments",
"delete": "Delete component or environment parameters",
"set": "Change component or environment parameters (e.g. replica count, name)",
"list": "List known component parameters",
"diff": "Display differences between the component parameters of two environments",
}
func init() {
......
// 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 (
"fmt"
"github.com/ksonnet/ksonnet/actions"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
vParamDeleteEnv = "param-delete-env"
vParamDeleteIndex = "param-delete-index"
)
var paramDeleteCmd = &cobra.Command{
Use: "delete <component-name> <param-key>",
Short: paramShortDesc["delete"],
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf("'param delete' takes exactly two arguments, (1) the name of the component, and the key")
}
m := map[string]interface{}{
actions.OptionApp: ka,
actions.OptionName: args[0],
actions.OptionPath: args[1],
actions.OptionEnvName: viper.GetString(vParamDeleteEnv),
actions.OptionIndex: viper.GetInt(vParamDeleteIndex),
}
return runAction(actionParamDelete, m)
},
Long: `
The ` + "`delete`" + ` command deletes component or environment parameters.
### Related Commands
* ` + "`ks param set` " + `— ` + paramShortDesc["set"] + `
* ` + "`ks param diff` " + `— ` + paramShortDesc["diff"] + `
* ` + "`ks apply` " + `— ` + applyShortDesc + `
### Syntax
`,
Example: `
# Delete 'guestbook' component replica parameter
ks param delete guestbook replicas
# Delete 'guestbook' component replicate in 'dev' environment
ks param delete guestbook replicas --env=dev`,
}
func init() {
paramCmd.AddCommand(paramDeleteCmd)
paramDeleteCmd.Flags().String(flagEnv, "", "Specify environment to delete parameter from")
viper.BindPFlag(vParamDeleteEnv, paramDeleteCmd.Flags().Lookup(flagEnv))
paramDeleteCmd.Flags().IntP(flagIndex, shortIndex, 0, "Index in manifest")
viper.BindPFlag(vParamDeleteIndex, paramDeleteCmd.Flags().Lookup(flagIndex))
}
......@@ -13,24 +13,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package component
package cmd
import (
"path/filepath"
"testing"
"github.com/ksonnet/ksonnet/metadata/app/mocks"
"github.com/stretchr/testify/mock"
"github.com/spf13/afero"
"github.com/ksonnet/ksonnet/actions"
)
func appMock(root string) (*mocks.App, afero.Fs) {
fs := afero.NewMemMapFs()
app := &mocks.App{}
app.On("Fs").Return(fs)
app.On("Root").Return(root)
app.On("LibPath", mock.AnythingOfType("string")).Return(filepath.Join(root, "lib", "v1.8.7"), nil)
return app, fs
func Test_paramDeleteCmd(t *testing.T) {
cases := []cmdTestCase{
{
name: "in general",
args: []string{"param", "delete", "component-name", "param-name"},
action: actionParamDelete,
expected: map[string]interface{}{
actions.OptionApp: ka,
actions.OptionName: "component-name",
actions.OptionPath: "param-name",
actions.OptionEnvName: "",
actions.OptionIndex: 0,
},
},
}
runTestCmd(t, cases)
}
......@@ -18,128 +18,131 @@ package component
import (
"testing"
"github.com/ksonnet/ksonnet/metadata/app/mocks"
"github.com/ksonnet/ksonnet/pkg/util/test"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestJsonnet_Name(t *testing.T) {
app, fs := appMock("/")
files := []string{"guestbook-ui.jsonnet", "params.libsonnet"}
for _, file := range files {
stageFile(t, fs, "guestbook/"+file, "/components/"+file)
stageFile(t, fs, "guestbook/"+file, "/components/nested/"+file)
}
root := NewJsonnet(app, "", "/components/guestbook-ui.jsonnet", "/components/params.libsonnet")
nested := NewJsonnet(app, "nested", "/components/nested/guestbook-ui.jsonnet", "/components/nested/params.libsonnet")
cases := []struct {
name string
isNameSpaced bool
expected string
c *Jsonnet
}{
{
name: "wants namespaced",
isNameSpaced: true,
expected: "guestbook-ui",
c: root,
},
{
name: "no namespace",
isNameSpaced: false,
expected: "guestbook-ui",
c: root,
},
{
name: "nested: wants namespaced",
isNameSpaced: true,
expected: "nested/guestbook-ui",
c: nested,
},
{
name: "nested: no namespace",
isNameSpaced: false,
expected: "guestbook-ui",
c: nested,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {