diff --git a/actions/param_delete.go b/actions/param_delete.go new file mode 100644 index 0000000000000000000000000000000000000000..ca0821d7842ce0d261da378d30211d78eafc4da5 --- /dev/null +++ b/actions/param_delete.go @@ -0,0 +1,125 @@ +// 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 +} diff --git a/actions/param_delete_test.go b/actions/param_delete_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c2fe01e7222ba304495b727c24d4ad9d40c669bc --- /dev/null +++ b/actions/param_delete_test.go @@ -0,0 +1,135 @@ +// 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) + }) +} diff --git a/actions/param_set.go b/actions/param_set.go index 78b1fa1ccad68e1daaf622c8121254df6674ac70..24d8c9b6ae2935446a8aefb277fc38dcf164d38d 100644 --- a/actions/param_set.go +++ b/actions/param_set.go @@ -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, diff --git a/actions/param_set_test.go b/actions/param_set_test.go index ae2d36a4b0ab8bf98f1468e4fb49bdfb65c1ceee..c32163de97645c7c99631c872975eceab261a4b8 100644 --- a/actions/param_set_test.go +++ b/actions/param_set_test.go @@ -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) diff --git a/cmd/actions.go b/cmd/actions.go index 5527312c17827e736b52fab3f977dd8189101aeb..9bf577c99276a8b9b4a7b17f10af7e049ad3b925 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -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, diff --git a/cmd/param.go b/cmd/param.go index a771d953d556be70c0202b522f7bb297615370d3..1a4534268fd9eff1424ec96a1799757887b0b0f7 100644 --- a/cmd/param.go +++ b/cmd/param.go @@ -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() { diff --git a/cmd/param_delete.go b/cmd/param_delete.go new file mode 100644 index 0000000000000000000000000000000000000000..d09db6d232b2b248f81e5af39c2b5c8020fb7100 --- /dev/null +++ b/cmd/param_delete.go @@ -0,0 +1,75 @@ +// 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)) +} diff --git a/component/helpers_test.go b/cmd/param_delete_test.go similarity index 55% rename from component/helpers_test.go rename to cmd/param_delete_test.go index fb508670b676e8c170b79f5b0b0059c0695f8672..0e95765c6b17e4956e46d02d59577934c00b5613 100644 --- a/component/helpers_test.go +++ b/cmd/param_delete_test.go @@ -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) } diff --git a/component/jsonnet_test.go b/component/jsonnet_test.go index fcd94383c32a8c6f44944cf2aa9526a9cfb25661..b4c0e18853489a0b0a935117517152c1108a3b33 100644 --- a/component/jsonnet_test.go +++ b/component/jsonnet_test.go @@ -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) { - require.Equal(t, tc.expected, tc.c.Name(tc.isNameSpaced)) - }) - } + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + + files := []string{"guestbook-ui.jsonnet", "params.libsonnet"} + for _, file := range files { + test.StageFile(t, fs, "guestbook/"+file, "/app/components/"+file) + test.StageFile(t, fs, "guestbook/"+file, "/app/components/nested/"+file) + } + + root := NewJsonnet(a, "", "/app/components/guestbook-ui.jsonnet", "/app/components/params.libsonnet") + nested := NewJsonnet(a, "nested", "/app/components/nested/guestbook-ui.jsonnet", "/app/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) { + require.Equal(t, tc.expected, tc.c.Name(tc.isNameSpaced)) + }) + } + }) } func TestJsonnet_Objects(t *testing.T) { - app, fs := appMock("/") - - files := []string{"guestbook-ui.jsonnet", "k.libsonnet", "k8s.libsonnet", "params.libsonnet"} - for _, file := range files { - stageFile(t, fs, "guestbook/"+file, "/components/"+file) - } - - files = []string{"k.libsonnet", "k8s.libsonnet"} - for _, file := range files { - stageFile(t, fs, "guestbook/"+file, "/lib/v1.8.7/"+file) - } - - c := NewJsonnet(app, "", "/components/guestbook-ui.jsonnet", "/components/params.libsonnet") - - paramsStr := testdata(t, "guestbook/params.libsonnet") - - list, err := c.Objects(string(paramsStr), "default") - require.NoError(t, err) - - expected := []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Service", - "metadata": map[string]interface{}{ - "name": "guiroot", - }, - "spec": map[string]interface{}{ - "ports": []interface{}{ - map[string]interface{}{ - "port": int64(80), - "targetPort": int64(80), - }, + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + files := []string{"guestbook-ui.jsonnet", "k.libsonnet", "k8s.libsonnet", "params.libsonnet"} + for _, file := range files { + test.StageFile(t, fs, "guestbook/"+file, "/app/components/"+file) + } + + files = []string{"k.libsonnet", "k8s.libsonnet"} + for _, file := range files { + test.StageFile(t, fs, "guestbook/"+file, "/app/lib/v1.8.7/"+file) + } + + c := NewJsonnet(a, "", "/app/components/guestbook-ui.jsonnet", "/app/components/params.libsonnet") + + paramsStr := testdata(t, "guestbook/params.libsonnet") + + list, err := c.Objects(string(paramsStr), "default") + require.NoError(t, err) + + expected := []*unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "guiroot", }, - "selector": map[string]interface{}{ - "app": "guiroot", + "spec": map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "port": int64(80), + "targetPort": int64(80), + }, + }, + "selector": map[string]interface{}{ + "app": "guiroot", + }, + "type": "ClusterIP", }, - "type": "ClusterIP", }, }, - }, - { - Object: map[string]interface{}{ - "apiVersion": "apps/v1beta1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "guiroot", - }, - "spec": map[string]interface{}{ - "replicas": int64(1), - "template": map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "app": "guiroot", + { + Object: map[string]interface{}{ + "apiVersion": "apps/v1beta1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "guiroot", + }, + "spec": map[string]interface{}{ + "replicas": int64(1), + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "guiroot", + }, }, - }, - "spec": map[string]interface{}{ - "containers": []interface{}{ - map[string]interface{}{ - "image": "gcr.io/heptio-images/ks-guestbook-demo:0.1", - "name": "guiroot", - "ports": []interface{}{ - map[string]interface{}{ - "containerPort": int64(80), + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "image": "gcr.io/heptio-images/ks-guestbook-demo:0.1", + "name": "guiroot", + "ports": []interface{}{ + map[string]interface{}{ + "containerPort": int64(80), + }, }, }, }, @@ -148,131 +151,135 @@ func TestJsonnet_Objects(t *testing.T) { }, }, }, - }, - } + } - require.Equal(t, expected, list) + require.Equal(t, expected, list) + }) } func TestJsonnet_Params(t *testing.T) { - app, fs := appMock("/") - - files := []string{"guestbook-ui.jsonnet", "k.libsonnet", "k8s.libsonnet", "params.libsonnet"} - for _, file := range files { - stageFile(t, fs, "guestbook/"+file, "/components/"+file) - } - - c := NewJsonnet(app, "", "/components/guestbook-ui.jsonnet", "/components/params.libsonnet") - - params, err := c.Params("") - require.NoError(t, err) - - expected := []ModuleParameter{ - { - Component: "guestbook-ui", - Index: "0", - Key: "containerPort", - Value: "80", - }, - { - Component: "guestbook-ui", - Index: "0", - Key: "image", - Value: `"gcr.io/heptio-images/ks-guestbook-demo:0.1"`, - }, - { - Component: "guestbook-ui", - Index: "0", - Key: "name", - Value: `"guiroot"`, - }, - { - Component: "guestbook-ui", - Index: "0", - Key: "obj", - Value: `{"a":"b"}`, - }, - { - Component: "guestbook-ui", - Index: "0", - Key: "replicas", - Value: "1", - }, - { - Component: "guestbook-ui", - Index: "0", - Key: "servicePort", - Value: "80", - }, - { - Component: "guestbook-ui", - Index: "0", - Key: "type", - Value: `"ClusterIP"`, - }, - } - - require.Equal(t, expected, params) + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + + files := []string{"guestbook-ui.jsonnet", "k.libsonnet", "k8s.libsonnet", "params.libsonnet"} + for _, file := range files { + test.StageFile(t, fs, "guestbook/"+file, "/app/components/"+file) + } + + c := NewJsonnet(a, "", "/app/components/guestbook-ui.jsonnet", "/app/components/params.libsonnet") + + params, err := c.Params("") + require.NoError(t, err) + + expected := []ModuleParameter{ + { + Component: "guestbook-ui", + Index: "0", + Key: "containerPort", + Value: "80", + }, + { + Component: "guestbook-ui", + Index: "0", + Key: "image", + Value: `"gcr.io/heptio-images/ks-guestbook-demo:0.1"`, + }, + { + Component: "guestbook-ui", + Index: "0", + Key: "name", + Value: `"guiroot"`, + }, + { + Component: "guestbook-ui", + Index: "0", + Key: "obj", + Value: `{"a":"b"}`, + }, + { + Component: "guestbook-ui", + Index: "0", + Key: "replicas", + Value: "1", + }, + { + Component: "guestbook-ui", + Index: "0", + Key: "servicePort", + Value: "80", + }, + { + Component: "guestbook-ui", + Index: "0", + Key: "type", + Value: `"ClusterIP"`, + }, + } + + require.Equal(t, expected, params) + }) } func TestJsonnet_Summarize(t *testing.T) { - app, fs := appMock("/") + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { - files := []string{"guestbook-ui.jsonnet", "k.libsonnet", "k8s.libsonnet", "params.libsonnet"} - for _, file := range files { - stageFile(t, fs, "guestbook/"+file, "/components/"+file) - } + files := []string{"guestbook-ui.jsonnet", "k.libsonnet", "k8s.libsonnet", "params.libsonnet"} + for _, file := range files { + test.StageFile(t, fs, "guestbook/"+file, "/app/components/"+file) + } - c := NewJsonnet(app, "", "/components/guestbook-ui.jsonnet", "/components/params.libsonnet") + c := NewJsonnet(a, "", "/app/components/guestbook-ui.jsonnet", "/app/components/params.libsonnet") - got, err := c.Summarize() - require.NoError(t, err) + got, err := c.Summarize() + require.NoError(t, err) - expected := []Summary{ - {ComponentName: "guestbook-ui", IndexStr: "0", Type: "jsonnet"}, - } + expected := []Summary{ + {ComponentName: "guestbook-ui", IndexStr: "0", Type: "jsonnet"}, + } - require.Equal(t, expected, got) + require.Equal(t, expected, got) + }) } func TestJsonnet_SetParam(t *testing.T) { - app, fs := appMock("/") + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { - files := []string{"guestbook-ui.jsonnet", "k.libsonnet", "k8s.libsonnet", "params.libsonnet"} - for _, file := range files { - stageFile(t, fs, "guestbook/"+file, "/components/"+file) - } + files := []string{"guestbook-ui.jsonnet", "k.libsonnet", "k8s.libsonnet", "params.libsonnet"} + for _, file := range files { + test.StageFile(t, fs, "guestbook/"+file, "/app/components/"+file) + } - c := NewJsonnet(app, "", "/components/guestbook-ui.jsonnet", "/components/params.libsonnet") + c := NewJsonnet(a, "", "/app/components/guestbook-ui.jsonnet", "/app/components/params.libsonnet") - err := c.SetParam([]string{"replicas"}, 4, ParamOptions{}) - require.NoError(t, err) + err := c.SetParam([]string{"replicas"}, 4, ParamOptions{}) + require.NoError(t, err) - b, err := afero.ReadFile(fs, "/components/params.libsonnet") - require.NoError(t, err) + b, err := afero.ReadFile(fs, "/app/components/params.libsonnet") + require.NoError(t, err) - expected := testdata(t, "guestbook/set-params.libsonnet") + expected := testdata(t, "guestbook/set-params.libsonnet") - require.Equal(t, string(expected), string(b)) + require.Equal(t, string(expected), string(b)) + }) } func TestJsonnet_DeleteParam(t *testing.T) { - app, fs := appMock("/") + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { - files := []string{"guestbook-ui.jsonnet", "k.libsonnet", "k8s.libsonnet", "params.libsonnet"} - for _, file := range files { - stageFile(t, fs, "guestbook/"+file, "/components/"+file) - } + files := []string{"guestbook-ui.jsonnet", "k.libsonnet", "k8s.libsonnet", "params.libsonnet"} + for _, file := range files { + test.StageFile(t, fs, "guestbook/"+file, "/app/components/"+file) + } - c := NewJsonnet(app, "", "/components/guestbook-ui.jsonnet", "/components/params.libsonnet") + c := NewJsonnet(a, "", "/app/components/guestbook-ui.jsonnet", "/app/components/params.libsonnet") - err := c.DeleteParam([]string{"replicas"}, ParamOptions{}) - require.NoError(t, err) + err := c.DeleteParam([]string{"replicas"}, ParamOptions{}) + require.NoError(t, err) - b, err := afero.ReadFile(fs, "/components/params.libsonnet") - require.NoError(t, err) + b, err := afero.ReadFile(fs, "/app/components/params.libsonnet") + require.NoError(t, err) - expected := testdata(t, "guestbook/delete-params.libsonnet") + expected := testdata(t, "guestbook/delete-params.libsonnet") - require.Equal(t, string(expected), string(b)) + require.Equal(t, string(expected), string(b)) + }) } diff --git a/component/manager.go b/component/manager.go index 723e924198e9dce8c5c2fc3e7817adab1f6c13b7..f2d12338d3be58980d0db894574153baf39c9ae8 100644 --- a/component/manager.go +++ b/component/manager.go @@ -27,6 +27,39 @@ import ( "github.com/spf13/afero" ) +func ResolvePath(ksApp app.App, path string) (Module, Component, error) { + isDir, err := isComponentDir2(ksApp, path) + if err != nil { + return nil, nil, errors.Wrap(err, "check for namespace directory") + } + + if isDir { + ns, err := GetModule(ksApp, path) + if err != nil { + return nil, nil, err + } + + return ns, nil, nil + } + + module, cName, err := checkComponent(ksApp, path) + if err != nil { + return nil, nil, err + } + + ns, err := GetModule(ksApp, module) + if err != nil { + return nil, nil, err + } + + c, err := LocateComponent(ksApp, module, cName) + if err != nil { + return nil, nil, err + } + + return ns, c, nil +} + var ( // DefaultManager is the default manager for components. DefaultManager = &defaultManager{} @@ -38,10 +71,9 @@ type Manager interface { Component(ksApp app.App, module, componentName string) (Component, error) CreateComponent(ksApp app.App, name, text string, params param.Params, templateType prototype.TemplateType) (string, error) CreateModule(ksApp app.App, name string) error - Module(ksApp app.App, module string) (Module, error) + Module(ksApp app.App, moduleName string) (Module, error) Modules(ksApp app.App, envName string) ([]Module, error) NSResolveParams(ns Module) (string, error) - ResolvePath(ksApp app.App, path string) (Module, Component, error) } type defaultManager struct{} @@ -84,40 +116,7 @@ func (dm *defaultManager) CreateModule(ksApp app.App, name string) error { return afero.WriteFile(ksApp.Fs(), paramsDir, GenParamsContent(), app.DefaultFilePermissions) } -func (dm *defaultManager) ResolvePath(ksApp app.App, path string) (Module, Component, error) { - isDir, err := dm.isComponentDir(ksApp, path) - if err != nil { - return nil, nil, errors.Wrap(err, "check for namespace directory") - } - - if isDir { - ns, err := dm.Module(ksApp, path) - if err != nil { - return nil, nil, err - } - - return ns, nil, nil - } - - module, cName, err := dm.checkComponent(ksApp, path) - if err != nil { - return nil, nil, err - } - - ns, err := dm.Module(ksApp, module) - if err != nil { - return nil, nil, err - } - - c, err := dm.Component(ksApp, module, cName) - if err != nil { - return nil, nil, err - } - - return ns, c, nil -} - -func (dm *defaultManager) isComponentDir(ksApp app.App, path string) (bool, error) { +func isComponentDir2(ksApp app.App, path string) (bool, error) { parts := strings.Split(path, "/") dir := filepath.Join(append([]string{ksApp.Root(), componentsRoot}, parts...)...) dir = filepath.Clean(dir) @@ -125,7 +124,7 @@ func (dm *defaultManager) isComponentDir(ksApp app.App, path string) (bool, erro return afero.DirExists(ksApp.Fs(), dir) } -func (dm *defaultManager) checkComponent(ksApp app.App, name string) (string, string, error) { +func checkComponent(ksApp app.App, name string) (string, string, error) { parts := strings.Split(name, "/") base := filepath.Join(append([]string{ksApp.Root(), componentsRoot}, parts...)...) base = filepath.Clean(base) @@ -148,5 +147,5 @@ func (dm *defaultManager) checkComponent(ksApp app.App, name string) (string, st } } - return "", "", errors.Errorf("%q is not a component or a namespace", name) + return "", "", errors.Errorf("%q is not a component or a module", name) } diff --git a/component/manager_test.go b/component/manager_test.go index 7004fec44a29bf39db14a79cee361530368cc964..22d93f96664ef4db4c6491198c0cbaa6fc99b204 100644 --- a/component/manager_test.go +++ b/component/manager_test.go @@ -18,96 +18,99 @@ 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/assert" "github.com/stretchr/testify/require" ) func Test_defaultManager_Component(t *testing.T) { - app, fs := appMock("/") + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { - stageFile(t, fs, "params-mixed.libsonnet", "/components/params.libsonnet") - stageFile(t, fs, "deployment.yaml", "/components/deployment.yaml") - stageFile(t, fs, "params-mixed.libsonnet", "/components/nested/params.libsonnet") - stageFile(t, fs, "deployment.yaml", "/components/nested/deployment.yaml") + test.StageFile(t, fs, "params-mixed.libsonnet", "/app/components/params.libsonnet") + test.StageFile(t, fs, "deployment.yaml", "/app/components/deployment.yaml") + test.StageFile(t, fs, "params-mixed.libsonnet", "/app/components/nested/params.libsonnet") + test.StageFile(t, fs, "deployment.yaml", "/app/components/nested/deployment.yaml") - dm := defaultManager{} + dm := defaultManager{} - c, err := dm.Component(app, "", "deployment") - require.NoError(t, err) + c, err := dm.Component(a, "", "deployment") + require.NoError(t, err) - expected := "deployment" - require.Equal(t, expected, c.Name(false)) + expected := "deployment" + require.Equal(t, expected, c.Name(false)) + }) } -func Test_default_manager_ResolvePath(t *testing.T) { - app, fs := appMock("/") - - stageFile(t, fs, "params-mixed.libsonnet", "/components/params.libsonnet") - stageFile(t, fs, "deployment.yaml", "/components/deployment.yaml") - stageFile(t, fs, "params-mixed.libsonnet", "/components/nested/params.libsonnet") - stageFile(t, fs, "deployment.yaml", "/components/nested/deployment.yaml") - - dm := &defaultManager{} - - cases := []struct { - name string - cName string - module string - isErr bool - }{ - { - name: "/", - module: "/", - }, - { - name: "deployment", - module: "/", - cName: "deployment", - }, - { - name: "/deployment", - module: "/", - cName: "deployment", - }, - { - name: "/nested/deployment", - module: "/nested", - cName: "deployment", - }, - { - name: "nested/deployment", - module: "/nested", - cName: "deployment", - }, - { - name: "nested/invalid", - isErr: true, - }, - { - name: "invalid", - isErr: true, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - ns, c, err := dm.ResolvePath(app, tc.name) - - if tc.isErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - - if tc.cName == "" { - assert.Nil(t, c) - } else { - require.NotNil(t, c) - assert.Equal(t, tc.cName, c.Name(false)) - } - - assert.Equal(t, tc.module, ns.Name()) - }) - } +func Test_ResolvePath(t *testing.T) { + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + + test.StageFile(t, fs, "params-mixed.libsonnet", "/app/components/params.libsonnet") + test.StageFile(t, fs, "deployment.yaml", "/app/components/deployment.yaml") + test.StageFile(t, fs, "params-mixed.libsonnet", "/app/components/nested/params.libsonnet") + test.StageFile(t, fs, "deployment.yaml", "/app/components/nested/deployment.yaml") + + cases := []struct { + name string + cName string + module string + isErr bool + }{ + { + name: "/", + module: "/", + }, + { + name: "deployment", + module: "/", + cName: "deployment", + }, + { + name: "/deployment", + module: "/", + cName: "deployment", + }, + { + name: "/nested/deployment", + module: "/nested", + cName: "deployment", + }, + { + name: "nested/deployment", + module: "/nested", + cName: "deployment", + }, + { + name: "nested/invalid", + isErr: true, + }, + { + name: "invalid", + isErr: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ns, c, err := ResolvePath(a, tc.name) + + if tc.isErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + + if tc.cName == "" { + assert.Nil(t, c) + } else { + require.NotNil(t, c) + assert.Equal(t, tc.cName, c.Name(false)) + } + + assert.Equal(t, tc.module, ns.Name()) + }) + } + }) } diff --git a/component/mocks/Module.go b/component/mocks/Module.go index ca874f493c4d2cbb3af85c7750ae74727aae44cb..db5671562f3f79d88a414da3dc8a6e81ff6fa802 100644 --- a/component/mocks/Module.go +++ b/component/mocks/Module.go @@ -47,6 +47,20 @@ func (_m *Module) Components() ([]component.Component, error) { return r0, r1 } +// DeleteParam provides a mock function with given fields: path +func (_m *Module) DeleteParam(path []string) error { + ret := _m.Called(path) + + var r0 error + if rf, ok := ret.Get(0).(func([]string) error); ok { + r0 = rf(path) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Dir provides a mock function with given fields: func (_m *Module) Dir() string { ret := _m.Called() diff --git a/component/module.go b/component/module.go index 2ec9bb451b612c8757a62514642b78cabfe1e699..edcaaf5e0945d75e5c5650b7547c09eac670333d 100644 --- a/component/module.go +++ b/component/module.go @@ -29,9 +29,9 @@ import ( ) func nsErrorMsg(format, module string) string { - s := fmt.Sprintf("namespace %q", module) + s := fmt.Sprintf("module %q", module) if module == "" { - s = "root namespace" + s = "root module" } return fmt.Sprintf(format, s) @@ -47,9 +47,10 @@ type Module interface { ParamsPath() string ResolvedParams() (string, error) SetParam(path []string, value interface{}) error + DeleteParam(path []string) error } -// FilesystemModule is a component namespace that uses a filesystem for storage. +// FilesystemModule is a component module that uses a filesystem for storage. type FilesystemModule struct { path string @@ -58,19 +59,19 @@ type FilesystemModule struct { var _ Module = (*FilesystemModule)(nil) -// NewModule creates an instance of . +// NewModule creates an instance of module. func NewModule(ksApp app.App, path string) *FilesystemModule { return &FilesystemModule{app: ksApp, path: path} } -// ExtractModuleComponent extracts a namespace and a component from a path. +// ExtractModuleComponent extracts a module and a component from a path. func ExtractModuleComponent(a app.App, path string) (Module, string) { nsPath, component := filepath.Split(path) ns := &FilesystemModule{path: nsPath, app: a} return ns, component } -// Name returns the namespace name. +// Name returns the module name. func (n *FilesystemModule) Name() string { if n.path == "" { return "/" @@ -78,9 +79,9 @@ func (n *FilesystemModule) Name() string { return n.path } -// GetModule gets a namespace by path. -func GetModule(a app.App, module string) (Module, error) { - parts := strings.Split(module, "/") +// GetModule gets a module by path. +func GetModule(a app.App, moduleName string) (Module, error) { + parts := strings.Split(moduleName, "/") nsDir := filepath.Join(append([]string{a.Root(), componentsRoot}, parts...)...) exists, err := afero.Exists(a.Fs(), nsDir) @@ -89,34 +90,45 @@ func GetModule(a app.App, module string) (Module, error) { } if !exists { - return nil, errors.New(nsErrorMsg("unable to find %s", module)) + return nil, errors.New(nsErrorMsg("unable to find %s", moduleName)) } - return &FilesystemModule{path: module, app: a}, nil + return &FilesystemModule{path: moduleName, app: a}, nil } -// ParamsPath generates the path to params.libsonnet for a namespace. +// ParamsPath generates the path to params.libsonnet for a module. func (n *FilesystemModule) ParamsPath() string { return filepath.Join(n.Dir(), paramsFile) } -// SetParam sets params for a namespace. +// SetParam sets params for a module. func (n *FilesystemModule) SetParam(path []string, value interface{}) error { paramsData, err := n.readParams() if err != nil { return err } - updatedParams, err := params.Set(path, paramsData, "", value, "global") + updated, err := params.Set(path, paramsData, "", value, "global") if err != nil { return err } - if err = n.writeParams(updatedParams); err != nil { + return n.writeParams(updated) +} + +// DeleteParam deletes params for a module. +func (n *FilesystemModule) DeleteParam(path []string) error { + paramsData, err := n.readParams() + if err != nil { + return err + } + + updated, err := params.Delete(path, paramsData, "", "global") + if err != nil { return err } - return nil + return n.writeParams(updated) } func (n *FilesystemModule) writeParams(src string) error { @@ -134,7 +146,7 @@ func (n *FilesystemModule) Dir() string { return filepath.Join(path...) } -// ModuleParameter is a namespaced paramater. +// ModuleParameter is a module parameter. type ModuleParameter struct { Component string Index string @@ -142,7 +154,7 @@ type ModuleParameter struct { Value string } -// ResolvedParams resolves paramaters for a namespace. It returns a JSON encoded +// ResolvedParams resolves paramaters for a module. It returns a JSON encoded // string of component parameters. func (n *FilesystemModule) ResolvedParams() (string, error) { s, err := n.readParams() @@ -153,7 +165,7 @@ func (n *FilesystemModule) ResolvedParams() (string, error) { return applyGlobals(s) } -// Params returns the params for a namespace. +// Params returns the params for a module. func (n *FilesystemModule) Params(envName string) ([]ModuleParameter, error) { components, err := n.Components() if err != nil { @@ -184,7 +196,7 @@ func (n *FilesystemModule) readParams() (string, error) { return string(b), nil } -// ModulesFromEnv returns all namespaces given an environment. +// ModulesFromEnv returns all modules given an environment. func ModulesFromEnv(a app.App, env string) ([]Module, error) { paths, err := MakePaths(a, env) if err != nil { @@ -211,7 +223,7 @@ func ModulesFromEnv(a app.App, env string) ([]Module, error) { return namespaces, nil } -// Modules returns all component namespaces +// Modules returns all component modules func Modules(a app.App) ([]Module, error) { componentRoot := filepath.Join(a.Root(), componentsRoot) @@ -250,7 +262,7 @@ func Modules(a app.App) ([]Module, error) { return namespaces, nil } -// Components returns the components in a namespace. +// Components returns the components in a module. func (n *FilesystemModule) Components() ([]Component, error) { parts := strings.Split(n.path, "/") nsDir := filepath.Join(append([]string{n.app.Root(), componentsRoot}, parts...)...) diff --git a/component/module_test.go b/component/module_test.go index b05aab3a8028d05e254eb70efa27be22b0143219..231245fb1a360d8d9b1effd7885ab270f8a9f170 100644 --- a/component/module_test.go +++ b/component/module_test.go @@ -18,45 +18,60 @@ 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/assert" "github.com/stretchr/testify/require" ) func TestModule_Components(t *testing.T) { - app, fs := appMock("/app") - - stageFile(t, fs, "certificate-crd.yaml", "/app/components/ns1/certificate-crd.yaml") - stageFile(t, fs, "params-with-entry.libsonnet", "/app/components/ns1/params.libsonnet") - stageFile(t, fs, "params-no-entry.libsonnet", "/app/components/params.libsonnet") - - cases := []struct { - name string - module string - count int - }{ - { - name: "no components", - module: "/", - }, - { - name: "with components", - module: "ns1", - count: 1, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - - ns, err := GetModule(app, tc.module) - require.NoError(t, err) - - assert.Equal(t, tc.module, ns.Name()) - components, err := ns.Components() - require.NoError(t, err) - - assert.Len(t, components, tc.count) - }) - } + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + test.StageFile(t, fs, "certificate-crd.yaml", "/app/components/ns1/certificate-crd.yaml") + test.StageFile(t, fs, "params-with-entry.libsonnet", "/app/components/ns1/params.libsonnet") + test.StageFile(t, fs, "params-no-entry.libsonnet", "/app/components/params.libsonnet") + cases := []struct { + name string + module string + count int + }{ + { + name: "no components", + module: "/", + }, + { + name: "with components", + module: "ns1", + count: 1, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + + ns, err := GetModule(a, tc.module) + require.NoError(t, err) + + assert.Equal(t, tc.module, ns.Name()) + components, err := ns.Components() + require.NoError(t, err) + + assert.Len(t, components, tc.count) + }) + } + }) +} + +func TestFilesystemModule_DeleteParam(t *testing.T) { + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + test.StageFile(t, fs, "params-global.libsonnet", "/app/components/params.libsonnet") + + module := NewModule(a, "/") + + err := module.DeleteParam([]string{"metadata"}) + require.NoError(t, err) + + test.AssertContents(t, fs, "params-delete-global.libsonnet", "/app/components/params.libsonnet") + }) } diff --git a/component/testdata/params-delete-global.libsonnet b/component/testdata/params-delete-global.libsonnet new file mode 100644 index 0000000000000000000000000000000000000000..341d89321fdf1e16b7f5b67dc8b1735973b718cc --- /dev/null +++ b/component/testdata/params-delete-global.libsonnet @@ -0,0 +1,14 @@ +{ + global: { + }, + components: { + a: { + other: 1, + metadata: { + labels: { + locala: "local", + }, + }, + }, + }, +} \ No newline at end of file diff --git a/component/yaml_test.go b/component/yaml_test.go index df6d3fb08746bafbf4e06a84628a19650d1d5eb3..1cb7e27b3bbcef27b060fd43585614f8b641c6a1 100644 --- a/component/yaml_test.go +++ b/component/yaml_test.go @@ -17,376 +17,390 @@ package component import ( "io/ioutil" - "path/filepath" "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 TestYAML_Name(t *testing.T) { - app, fs := appMock("/") - - stageFile(t, fs, "params-mixed.libsonnet", "/components/params.libsonnet") - stageFile(t, fs, "deployment.yaml", "/components/deployment.yaml") - stageFile(t, fs, "k8s.libsonnet", "/lib/v1.8.7/k8s.libsonnet") - - y := NewYAML(app, "", "/components/deployment.yaml", "/components/params.libsonnet") - - cases := []struct { - name string - isNameSpaced bool - expected string - }{ - { - name: "wants namespaced", - isNameSpaced: true, - expected: "deployment", - }, - { - name: "no namespace", - isNameSpaced: false, - expected: "deployment", - }, - } + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + + test.StageFile(t, fs, "params-mixed.libsonnet", "/app/components/params.libsonnet") + test.StageFile(t, fs, "deployment.yaml", "/app/components/deployment.yaml") + test.StageFile(t, fs, "k8s.libsonnet", "/app/lib/v1.8.7/k8s.libsonnet") + + y := NewYAML(a, "", "/app/components/deployment.yaml", "/app/components/params.libsonnet") + + cases := []struct { + name string + isNameSpaced bool + expected string + }{ + { + name: "wants namespaced", + isNameSpaced: true, + expected: "deployment", + }, + { + name: "no namespace", + isNameSpaced: false, + expected: "deployment", + }, + } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - require.Equal(t, tc.expected, y.Name(tc.isNameSpaced)) - }) - } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, y.Name(tc.isNameSpaced)) + }) + } + }) } func TestYAML_Params(t *testing.T) { - app, fs := appMock("/") - - stageFile(t, fs, "params-mixed.libsonnet", "/components/params.libsonnet") - stageFile(t, fs, "deployment.yaml", "/components/deployment.yaml") - stageFile(t, fs, "k8s.libsonnet", "/lib/v1.8.7/k8s.libsonnet") - - y := NewYAML(app, "", "/components/deployment.yaml", "/components/params.libsonnet") - params, err := y.Params("") - require.NoError(t, err) - - require.Len(t, params, 1) - - param := params[0] - expected := ModuleParameter{ - Component: "deployment", - Index: "0", - Key: "metadata.labels", - Value: `{"label1":"label1","label2":"label2"}`, - } - require.Equal(t, expected, param) + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + + test.StageFile(t, fs, "params-mixed.libsonnet", "/app/components/params.libsonnet") + test.StageFile(t, fs, "deployment.yaml", "/app/components/deployment.yaml") + test.StageFile(t, fs, "k8s.libsonnet", "/app/lib/v1.8.7/k8s.libsonnet") + + y := NewYAML(a, "", "/app/components/deployment.yaml", "/app/components/params.libsonnet") + params, err := y.Params("") + require.NoError(t, err) + + require.Len(t, params, 1) + + param := params[0] + expected := ModuleParameter{ + Component: "deployment", + Index: "0", + Key: "metadata.labels", + Value: `{"label1":"label1","label2":"label2"}`, + } + require.Equal(t, expected, param) + }) } func TestYAML_Params_literal(t *testing.T) { - app, fs := appMock("/") - - stageFile(t, fs, "params-mixed.libsonnet", "/params.libsonnet") - stageFile(t, fs, "rbac.yaml", "/rbac.yaml") - stageFile(t, fs, "k8s.libsonnet", "/lib/v1.8.7/k8s.libsonnet") - - y := NewYAML(app, "", "/rbac.yaml", "/params.libsonnet") - params, err := y.Params("") - require.NoError(t, err) - - require.Len(t, params, 1) - - param := params[0] - expected := ModuleParameter{ - Component: "rbac", - Index: "1", - Key: "metadata.name", - Value: "cert-manager2", - } - require.Equal(t, expected, param) + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + + test.StageFile(t, fs, "params-mixed.libsonnet", "/params.libsonnet") + test.StageFile(t, fs, "rbac.yaml", "/rbac.yaml") + test.StageFile(t, fs, "k8s.libsonnet", "/app/lib/v1.8.7/k8s.libsonnet") + + y := NewYAML(a, "", "/rbac.yaml", "/params.libsonnet") + params, err := y.Params("") + require.NoError(t, err) + + require.Len(t, params, 1) + + param := params[0] + expected := ModuleParameter{ + Component: "rbac", + Index: "1", + Key: "metadata.name", + Value: "cert-manager2", + } + require.Equal(t, expected, param) + }) } func TestYAML_Objects_no_params(t *testing.T) { - app, fs := appMock("/") - - stageFile(t, fs, "certificate-crd.yaml", "/certificate-crd.yaml") - stageFile(t, fs, "params-no-entry.libsonnet", "/params.libsonnet") - - y := NewYAML(app, "", "/certificate-crd.yaml", "/params.libsonnet") - - list, err := y.Objects("", "") - require.NoError(t, err) - - expected := []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apiextensions.k8s.io/v1beta1", - "kind": "CustomResourceDefinition", - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "app": "cert-manager", - "chart": "cert-manager-0.2.2", - "heritage": "Tiller", - "release": "cert-manager", + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + + test.StageFile(t, fs, "certificate-crd.yaml", "/certificate-crd.yaml") + test.StageFile(t, fs, "params-no-entry.libsonnet", "/params.libsonnet") + + y := NewYAML(a, "", "/certificate-crd.yaml", "/params.libsonnet") + + list, err := y.Objects("", "") + require.NoError(t, err) + + expected := []*unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1beta1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "cert-manager", + "chart": "cert-manager-0.2.2", + "heritage": "Tiller", + "release": "cert-manager", + }, + "name": "certificates.certmanager.k8s.io", }, - "name": "certificates.certmanager.k8s.io", - }, - "spec": map[string]interface{}{ - "version": "v1alpha1", - "group": "certmanager.k8s.io", - "names": map[string]interface{}{ - "kind": "Certificate", - "plural": "certificates", + "spec": map[string]interface{}{ + "version": "v1alpha1", + "group": "certmanager.k8s.io", + "names": map[string]interface{}{ + "kind": "Certificate", + "plural": "certificates", + }, + "scope": "Namespaced", }, - "scope": "Namespaced", }, }, - }, - } + } - require.Equal(t, expected, list) + require.Equal(t, expected, list) + }) } + func TestYAML_Objects_no_params_with_json(t *testing.T) { - app, fs := appMock("/") - - stageFile(t, fs, "certificate-crd.json", "/certificate-crd.json") - stageFile(t, fs, "params-no-entry.libsonnet", "/params.libsonnet") - - y := NewYAML(app, "", "/certificate-crd.json", "/params.libsonnet") - - list, err := y.Objects("", "") - require.NoError(t, err) - - expected := []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apiextensions.k8s.io/v1beta1", - "kind": "CustomResourceDefinition", - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "app": "cert-manager", - "chart": "cert-manager-0.2.2", - "heritage": "Tiller", - "release": "cert-manager", + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + + test.StageFile(t, fs, "certificate-crd.json", "/certificate-crd.json") + test.StageFile(t, fs, "params-no-entry.libsonnet", "/params.libsonnet") + + y := NewYAML(a, "", "/certificate-crd.json", "/params.libsonnet") + + list, err := y.Objects("", "") + require.NoError(t, err) + + expected := []*unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1beta1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "cert-manager", + "chart": "cert-manager-0.2.2", + "heritage": "Tiller", + "release": "cert-manager", + }, + "name": "certificates.certmanager.k8s.io", }, - "name": "certificates.certmanager.k8s.io", - }, - "spec": map[string]interface{}{ - "version": "v1alpha1", - "group": "certmanager.k8s.io", - "names": map[string]interface{}{ - "kind": "Certificate", - "plural": "certificates", + "spec": map[string]interface{}{ + "version": "v1alpha1", + "group": "certmanager.k8s.io", + "names": map[string]interface{}{ + "kind": "Certificate", + "plural": "certificates", + }, + "scope": "Namespaced", }, - "scope": "Namespaced", }, }, - }, - } + } - require.Equal(t, expected, list) + require.Equal(t, expected, list) + }) } func TestYAML_Objects_params_exist_with_no_entry(t *testing.T) { - app, fs := appMock("/") - - stageFile(t, fs, "certificate-crd.yaml", "/certificate-crd.yaml") - stageFile(t, fs, "params-no-entry.libsonnet", "/params.libsonnet") - - y := NewYAML(app, "", "/certificate-crd.yaml", "/params.libsonnet") - - list, err := y.Objects("", "") - require.NoError(t, err) - - expected := []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apiextensions.k8s.io/v1beta1", - "kind": "CustomResourceDefinition", - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "app": "cert-manager", - "chart": "cert-manager-0.2.2", - "heritage": "Tiller", - "release": "cert-manager", + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + + test.StageFile(t, fs, "certificate-crd.yaml", "/certificate-crd.yaml") + test.StageFile(t, fs, "params-no-entry.libsonnet", "/params.libsonnet") + + y := NewYAML(a, "", "/certificate-crd.yaml", "/params.libsonnet") + + list, err := y.Objects("", "") + require.NoError(t, err) + + expected := []*unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1beta1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "cert-manager", + "chart": "cert-manager-0.2.2", + "heritage": "Tiller", + "release": "cert-manager", + }, + "name": "certificates.certmanager.k8s.io", }, - "name": "certificates.certmanager.k8s.io", - }, - "spec": map[string]interface{}{ - "version": "v1alpha1", - "group": "certmanager.k8s.io", - "names": map[string]interface{}{ - "kind": "Certificate", - "plural": "certificates", + "spec": map[string]interface{}{ + "version": "v1alpha1", + "group": "certmanager.k8s.io", + "names": map[string]interface{}{ + "kind": "Certificate", + "plural": "certificates", + }, + "scope": "Namespaced", }, - "scope": "Namespaced", }, }, - }, - } + } - require.Equal(t, expected, list) + require.Equal(t, expected, list) + }) } func TestYAML_Objects_params_exist_with_entry(t *testing.T) { - app, fs := appMock("/") - - stageFile(t, fs, "certificate-crd.yaml", "/certificate-crd.yaml") - stageFile(t, fs, "params-with-entry.libsonnet", "/params.libsonnet") - - y := NewYAML(app, "", "/certificate-crd.yaml", "/params.libsonnet") - - list, err := y.Objects("", "") - require.NoError(t, err) - - expected := []*unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "apiVersion": "apiextensions.k8s.io/v1beta1", - "kind": "CustomResourceDefinition", - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "app": "cert-manager", - "chart": "cert-manager-0.2.2", - "heritage": "Tiller", - "release": "cert-manager", + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { + + test.StageFile(t, fs, "certificate-crd.yaml", "/certificate-crd.yaml") + test.StageFile(t, fs, "params-with-entry.libsonnet", "/params.libsonnet") + + y := NewYAML(a, "", "/certificate-crd.yaml", "/params.libsonnet") + + list, err := y.Objects("", "") + require.NoError(t, err) + + expected := []*unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1beta1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "cert-manager", + "chart": "cert-manager-0.2.2", + "heritage": "Tiller", + "release": "cert-manager", + }, + "name": "certificates.certmanager.k8s.io", }, - "name": "certificates.certmanager.k8s.io", - }, - "spec": map[string]interface{}{ - "version": "v2", - "group": "certmanager.k8s.io", - "names": map[string]interface{}{ - "kind": "Certificate", - "plural": "certificates", + "spec": map[string]interface{}{ + "version": "v2", + "group": "certmanager.k8s.io", + "names": map[string]interface{}{ + "kind": "Certificate", + "plural": "certificates", + }, + "scope": "Namespaced", }, - "scope": "Namespaced", }, }, - }, - } + } - require.Equal(t, expected, list) + require.Equal(t, expected, list) + }) } func TestYAML_SetParam(t *testing.T) { - app, fs := appMock("/") + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { - stageFile(t, fs, "certificate-crd.yaml", "/certificate-crd.yaml") - stageFile(t, fs, "params-no-entry.libsonnet", "/params.libsonnet") + test.StageFile(t, fs, "certificate-crd.yaml", "/certificate-crd.yaml") + test.StageFile(t, fs, "params-no-entry.libsonnet", "/params.libsonnet") - y := NewYAML(app, "", "/certificate-crd.yaml", "/params.libsonnet") + y := NewYAML(a, "", "/certificate-crd.yaml", "/params.libsonnet") - err := y.SetParam([]string{"spec", "version"}, "v2", ParamOptions{}) - require.NoError(t, err) + err := y.SetParam([]string{"spec", "version"}, "v2", ParamOptions{}) + require.NoError(t, err) - b, err := afero.ReadFile(fs, "/params.libsonnet") - require.NoError(t, err) + b, err := afero.ReadFile(fs, "/params.libsonnet") + require.NoError(t, err) - expected := testdata(t, "updated-yaml-params.libsonnet") + expected := testdata(t, "updated-yaml-params.libsonnet") - require.Equal(t, string(expected), string(b)) + require.Equal(t, string(expected), string(b)) + }) } func TestYAML_DeleteParam(t *testing.T) { - app, fs := appMock("/") + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { - stageFile(t, fs, "certificate-crd.yaml", "/certificate-crd.yaml") - stageFile(t, fs, "params-with-entry.libsonnet", "/params.libsonnet") + test.StageFile(t, fs, "certificate-crd.yaml", "/certificate-crd.yaml") + test.StageFile(t, fs, "params-with-entry.libsonnet", "/params.libsonnet") - y := NewYAML(app, "", "/certificate-crd.yaml", "/params.libsonnet") + y := NewYAML(a, "", "/certificate-crd.yaml", "/params.libsonnet") - err := y.DeleteParam([]string{"spec", "version"}, ParamOptions{}) - require.NoError(t, err) + err := y.DeleteParam([]string{"spec", "version"}, ParamOptions{}) + require.NoError(t, err) - b, err := afero.ReadFile(fs, "/params.libsonnet") - require.NoError(t, err) + b, err := afero.ReadFile(fs, "/params.libsonnet") + require.NoError(t, err) - expected := testdata(t, "params-delete-entry.libsonnet") + expected := testdata(t, "params-delete-entry.libsonnet") - require.Equal(t, string(expected), string(b)) + require.Equal(t, string(expected), string(b)) + }) } func TestYAML_Summarize(t *testing.T) { - app, fs := appMock("/") + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { - stageFile(t, fs, "rbac.yaml", "/components/rbac.yaml") - stageFile(t, fs, "params-no-entry.libsonnet", "/components/params.libsonnet") + test.StageFile(t, fs, "rbac.yaml", "/components/rbac.yaml") + test.StageFile(t, fs, "params-no-entry.libsonnet", "/components/params.libsonnet") - y := NewYAML(app, "", "/components/rbac.yaml", "/components/params.libsonnet") + y := NewYAML(a, "", "/components/rbac.yaml", "/components/params.libsonnet") - list, err := y.Summarize() - require.NoError(t, err) + list, err := y.Summarize() + require.NoError(t, err) - expected := []Summary{ - { - ComponentName: "rbac", - IndexStr: "0", - Type: "yaml", - APIVersion: "rbac.authorization.k8s.io/v1beta1", - Kind: "ClusterRole", - Name: "cert-manager", - }, - { - ComponentName: "rbac", - IndexStr: "1", - Type: "yaml", - APIVersion: "rbac.authorization.k8s.io/v1beta1", - Kind: "ClusterRoleBinding", - Name: "cert-manager", - }, - } + expected := []Summary{ + { + ComponentName: "rbac", + IndexStr: "0", + Type: "yaml", + APIVersion: "rbac.authorization.k8s.io/v1beta1", + Kind: "ClusterRole", + Name: "cert-manager", + }, + { + ComponentName: "rbac", + IndexStr: "1", + Type: "yaml", + APIVersion: "rbac.authorization.k8s.io/v1beta1", + Kind: "ClusterRoleBinding", + Name: "cert-manager", + }, + } - require.Equal(t, expected, list) + require.Equal(t, expected, list) + }) } func TestYAML_Summarize_json(t *testing.T) { - app, fs := appMock("/") + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { - stageFile(t, fs, "certificate-crd.json", "/components/certificate-crd.json") - stageFile(t, fs, "params-no-entry.libsonnet", "/components/params.libsonnet") + test.StageFile(t, fs, "certificate-crd.json", "/components/certificate-crd.json") + test.StageFile(t, fs, "params-no-entry.libsonnet", "/components/params.libsonnet") - y := NewYAML(app, "", "/components/certificate-crd.json", "/components/params.libsonnet") + y := NewYAML(a, "", "/components/certificate-crd.json", "/components/params.libsonnet") - list, err := y.Summarize() - require.NoError(t, err) + list, err := y.Summarize() + require.NoError(t, err) - expected := []Summary{ - { - ComponentName: "certificate-crd", - IndexStr: "0", - Type: "json", - APIVersion: "apiextensions.k8s.io/v1beta1", - Kind: "CustomResourceDefinition", - Name: "certificates_certmanager_k8s_io", - }, - } + expected := []Summary{ + { + ComponentName: "certificate-crd", + IndexStr: "0", + Type: "json", + APIVersion: "apiextensions.k8s.io/v1beta1", + Kind: "CustomResourceDefinition", + Name: "certificates_certmanager_k8s_io", + }, + } - require.Equal(t, expected, list) + require.Equal(t, expected, list) + }) } func TestYAML_Summarize_yaml_trailing_dashes(t *testing.T) { - app, fs := appMock("/") + test.WithApp(t, "/app", func(a *mocks.App, fs afero.Fs) { - stageFile(t, fs, "trailing-dash.yaml", "/components/certificate-crd.yaml") - stageFile(t, fs, "params-no-entry.libsonnet", "/components/params.libsonnet") + test.StageFile(t, fs, "trailing-dash.yaml", "/components/certificate-crd.yaml") + test.StageFile(t, fs, "params-no-entry.libsonnet", "/components/params.libsonnet") - y := NewYAML(app, "", "/components/certificate-crd.yaml", "/components/params.libsonnet") + y := NewYAML(a, "", "/components/certificate-crd.yaml", "/components/params.libsonnet") - list, err := y.Summarize() - require.NoError(t, err) + list, err := y.Summarize() + require.NoError(t, err) - expected := []Summary{ - { - ComponentName: "certificate-crd", - IndexStr: "0", - Type: "yaml", - APIVersion: "apiextensions.k8s.io/v1beta1", - Kind: "CustomResourceDefinition", - Name: "certificates_certmanager_k8s_io", - }, - } + expected := []Summary{ + { + ComponentName: "certificate-crd", + IndexStr: "0", + Type: "yaml", + APIVersion: "apiextensions.k8s.io/v1beta1", + Kind: "CustomResourceDefinition", + Name: "certificates_certmanager_k8s_io", + }, + } - require.Equal(t, expected, list) + require.Equal(t, expected, list) + }) } func Test_mapToPaths(t *testing.T) { @@ -412,20 +426,7 @@ func Test_mapToPaths(t *testing.T) { } require.Equal(t, expected, got) -} - -func stageFile(t *testing.T, fs afero.Fs, src, dest string) { - in := filepath.Join("testdata", src) - - b, err := ioutil.ReadFile(in) - require.NoError(t, err) - - dir := filepath.Dir(dest) - err = fs.MkdirAll(dir, 0755) - require.NoError(t, err) - err = afero.WriteFile(fs, dest, b, 0644) - require.NoError(t, err) } func testdata(t *testing.T, name string) []byte { diff --git a/docs/cli-reference/ks_param.md b/docs/cli-reference/ks_param.md index c9d439340f031c2fd2897b392ef38390b187f1d5..4b0bb552572220c4409b2df94c3429f2ff007401 100644 --- a/docs/cli-reference/ks_param.md +++ b/docs/cli-reference/ks_param.md @@ -53,6 +53,7 @@ ks param [flags] ### SEE ALSO * [ks](ks.md) - Configure your application to deploy to a Kubernetes cluster +* [ks param delete](ks_param_delete.md) - Delete component or environment parameters * [ks param diff](ks_param_diff.md) - Display differences between the component parameters of two environments * [ks param list](ks_param_list.md) - List known component parameters * [ks param set](ks_param_set.md) - Change component or environment parameters (e.g. replica count, name) diff --git a/docs/cli-reference/ks_param_delete.md b/docs/cli-reference/ks_param_delete.md new file mode 100644 index 0000000000000000000000000000000000000000..4d7219a1c2c3edcbec899a5fb6d7f5a3af634c1c --- /dev/null +++ b/docs/cli-reference/ks_param_delete.md @@ -0,0 +1,51 @@ +## ks param delete + +Delete component or environment parameters + +### Synopsis + + +The `delete` command deletes component or environment parameters. + +### Related Commands + +* `ks param set` — Change component or environment parameters (e.g. replica count, name) +* `ks param diff` — Display differences between the component parameters of two environments +* `ks apply` — Apply local Kubernetes manifests (components) to remote clusters + +### Syntax + + +``` +ks param delete <component-name> <param-key> [flags] +``` + +### Examples + +``` + +# Delete 'guestbook' component replica parameter +ks param delete guestbook replicas + +# Delete 'guestbook' component replicate in 'dev' environment +ks param delete guestbook replicas --env=dev +``` + +### Options + +``` + --env string Specify environment to delete parameter from + -h, --help help for delete + -i, --index int Index in manifest +``` + +### Options inherited from parent commands + +``` + -v, --verbose count[=-1] Increase verbosity. May be given multiple times. +``` + +### SEE ALSO + +* [ks param](ks_param.md) - Manage ksonnet parameters for components and environments + diff --git a/e2e/param_test.go b/e2e/param_test.go index fa578b4cdb9cf71eb926e6292211fc3e5b066091..ca167c1dbc717dcd65987d3772536a41462bbe94 100644 --- a/e2e/param_test.go +++ b/e2e/param_test.go @@ -27,6 +27,53 @@ var _ = Describe("ks param", func() { BeforeEach(func() { a = e.initApp(nil) a.generateDeployedService() + + }) + + FDescribe("delete", func() { + var ( + component = "guestbook-ui" + envName = "default" + local = "local-value" + localValue = "1" + env = "env-value" + envValue = "2" + ) + + BeforeEach(func() { + a.paramSet(component, local, localValue) + a.paramSet(component, env, envValue, "--env", envName) + + o := a.paramList() + assertOutput("param/delete/pre-local.txt", o.stdout) + + o = a.paramList("--env", envName) + assertOutput("param/delete/pre-env.txt", o.stdout) + }) + + Context("at the component level", func() { + JustBeforeEach(func() { + o := a.runKs("param", "delete", component, local) + assertExitStatus(o, 0) + }) + + It("removes a parameter's value", func() { + o := a.paramList() + assertOutput("param/delete/local.txt", o.stdout) + }) + }) + + Context("at the environment level", func() { + JustBeforeEach(func() { + o := a.runKs("param", "delete", component, env, "--env", envName) + assertExitStatus(o, 0) + }) + + XIt("removes a parameter's environment value", func() { + o := a.paramList("--env=" + envName) + assertOutput("param/delete/env.txt", o.stdout) + }) + }) }) Describe("list", func() { diff --git a/e2e/testdata/output/param/delete/env.txt b/e2e/testdata/output/param/delete/env.txt new file mode 100644 index 0000000000000000000000000000000000000000..42a0df273afa06dc5c6bc0df4559a1d0da1ab51f --- /dev/null +++ b/e2e/testdata/output/param/delete/env.txt @@ -0,0 +1,9 @@ +COMPONENT INDEX PARAM VALUE +========= ===== ===== ===== +guestbook-ui 0 containerPort 80 +guestbook-ui 0 image "gcr.io/heptio-images/ks-guestbook-demo:0.1" +guestbook-ui 0 local-value 1 +guestbook-ui 0 name "guestbook-ui" +guestbook-ui 0 replicas 1 +guestbook-ui 0 servicePort 80 +guestbook-ui 0 type "ClusterIP" diff --git a/e2e/testdata/output/param/delete/local.txt b/e2e/testdata/output/param/delete/local.txt new file mode 100644 index 0000000000000000000000000000000000000000..6ac0662186fabf2f1b9e5cf14e7515a31800f7e1 --- /dev/null +++ b/e2e/testdata/output/param/delete/local.txt @@ -0,0 +1,8 @@ +COMPONENT INDEX PARAM VALUE +========= ===== ===== ===== +guestbook-ui 0 containerPort 80 +guestbook-ui 0 image "gcr.io/heptio-images/ks-guestbook-demo:0.1" +guestbook-ui 0 name "guestbook-ui" +guestbook-ui 0 replicas 1 +guestbook-ui 0 servicePort 80 +guestbook-ui 0 type "ClusterIP" diff --git a/e2e/testdata/output/param/delete/pre-env.txt b/e2e/testdata/output/param/delete/pre-env.txt new file mode 100644 index 0000000000000000000000000000000000000000..d69c4bc53e8c150d77d99f60894009b6ad83ea90 --- /dev/null +++ b/e2e/testdata/output/param/delete/pre-env.txt @@ -0,0 +1,10 @@ +COMPONENT INDEX PARAM VALUE +========= ===== ===== ===== +guestbook-ui 0 containerPort 80 +guestbook-ui 0 env-value 2 +guestbook-ui 0 image "gcr.io/heptio-images/ks-guestbook-demo:0.1" +guestbook-ui 0 local-value 1 +guestbook-ui 0 name "guestbook-ui" +guestbook-ui 0 replicas 1 +guestbook-ui 0 servicePort 80 +guestbook-ui 0 type "ClusterIP" diff --git a/e2e/testdata/output/param/delete/pre-local.txt b/e2e/testdata/output/param/delete/pre-local.txt new file mode 100644 index 0000000000000000000000000000000000000000..42a0df273afa06dc5c6bc0df4559a1d0da1ab51f --- /dev/null +++ b/e2e/testdata/output/param/delete/pre-local.txt @@ -0,0 +1,9 @@ +COMPONENT INDEX PARAM VALUE +========= ===== ===== ===== +guestbook-ui 0 containerPort 80 +guestbook-ui 0 image "gcr.io/heptio-images/ks-guestbook-demo:0.1" +guestbook-ui 0 local-value 1 +guestbook-ui 0 name "guestbook-ui" +guestbook-ui 0 replicas 1 +guestbook-ui 0 servicePort 80 +guestbook-ui 0 type "ClusterIP" diff --git a/metadata/params/interface.go b/metadata/params/interface.go index c773b5fd441f777c45bfb7125d0cff2dbbd583be..51303db02009e76b34a2956cecf7f0bccaef321e 100644 --- a/metadata/params/interface.go +++ b/metadata/params/interface.go @@ -15,6 +15,17 @@ package params +import ( + "bytes" + + "github.com/google/go-jsonnet/ast" + "github.com/ksonnet/ksonnet-lib/ksonnet-gen/astext" + "github.com/ksonnet/ksonnet-lib/ksonnet-gen/printer" + "github.com/ksonnet/ksonnet/pkg/docparser" + "github.com/ksonnet/ksonnet/pkg/util/jsonnet" + "github.com/pkg/errors" +) + type Params map[string]string // AppendComponent takes the following params @@ -93,6 +104,74 @@ func SetEnvironmentParams(component, snippet string, params Params) (string, err return setEnvironmentParams(component, snippet, params) } +// DeleteEnvironmentParam deletes a parameter for an environment param file. It returns +// the updated snippet. +func DeleteEnvironmentParam(componentName, paramName, snippet string) (string, error) { + tokens, err := docparser.Lex("snippet", snippet) + if err != nil { + return "", err + } + + node, err := docparser.Parse(tokens) + if err != nil { + return "", err + } + + l, ok := node.(*ast.Local) + if !ok { + return "", errors.New("unable to parse params") + } + + switch t := l.Body.(type) { + default: + return "", errors.Errorf("unknown body type %T", t) + // params + {} + case *ast.Binary: + components, ok := t.Right.(*astext.Object) + if !ok { + return "", errors.New("unable to find components in params") + } + + return deleteFromEnv(l, components, componentName, paramName) + case *ast.ApplyBrace: + components, ok := t.Right.(*astext.Object) + if !ok { + return "", errors.New("unable to find components in params") + } + + return deleteFromEnv(l, components, componentName, paramName) + } +} + +func deleteFromEnv(l *ast.Local, components *astext.Object, componentName, paramName string) (string, error) { + paramObject, err := jsonnet.FindObject(components, []string{"components", componentName, paramName}) + if err != nil { + return "", err + } + + var fields []astext.ObjectField + + for i := range paramObject.Fields { + fieldID, err := jsonnet.FieldID(paramObject.Fields[i]) + if err != nil { + return "", err + } + + if fieldID != paramName { + fields = append(fields, paramObject.Fields[i]) + } + } + + paramObject.Fields = fields + + var buf bytes.Buffer + if err := printer.Fprint(&buf, l); err != nil { + return "", err + } + + return buf.String(), nil +} + // DeleteEnvironmentComponent takes // // component: the name of the component to be deleted. diff --git a/metadata/params/params_test.go b/metadata/params/params_test.go index 81c4cc5c08a09834888910c8a70955a0b7e5c9d3..db8e1d0c9d8af18bb8dbe9c89d9f81e668794568 100644 --- a/metadata/params/params_test.go +++ b/metadata/params/params_test.go @@ -16,8 +16,13 @@ package params import ( + "io/ioutil" + "path/filepath" "reflect" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAppendComponentParams(t *testing.T) { @@ -217,7 +222,7 @@ local bar = import "bar"; name: "baz", replicas: 5, }, - }, + }, }`, Params{"replicas": "5", "name": `"baz"`}, }, @@ -1186,3 +1191,59 @@ params + { } } } + +func TestDeleteEnvironmentParams(t *testing.T) { + buildPath := func(in ...string) []string { + return append([]string{"delete-env-param"}, in...) + } + + cases := []struct { + name string + componentName string + paramName string + snippetPath []string + expectedPath []string + isErr bool + }{ + { + name: "params binary", + componentName: "foo", + paramName: "name", + snippetPath: buildPath("case1.libsonnet"), + expectedPath: buildPath("expected1.libsonnet"), + }, + { + name: "params apply brace", + componentName: "foo", + paramName: "name", + snippetPath: buildPath("case2.libsonnet"), + expectedPath: buildPath("expected2.libsonnet"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + snippet := readSnippet(t, tc.snippetPath...) + + updated, err := DeleteEnvironmentParam(tc.componentName, tc.paramName, snippet) + if tc.isErr { + require.Error(t, err) + return + } + + expected := readSnippet(t, tc.expectedPath...) + + require.NoError(t, err) + assert.Equal(t, expected, updated) + }) + } +} + +func readSnippet(t *testing.T, path ...string) string { + snippetPath := filepath.Join(append([]string{"testdata"}, path...)...) + + data, err := ioutil.ReadFile(snippetPath) + require.NoError(t, err) + + return string(data) +} diff --git a/metadata/params/testdata/delete-env-param/case1.libsonnet b/metadata/params/testdata/delete-env-param/case1.libsonnet new file mode 100644 index 0000000000000000000000000000000000000000..f5a7b70abc2f14d94968efc9c13822f8e96e6d0a --- /dev/null +++ b/metadata/params/testdata/delete-env-param/case1.libsonnet @@ -0,0 +1,9 @@ +local params = {}; +params + { + components +: { + foo +: { + name: "foo", + replicas: 1, + }, + }, +} \ No newline at end of file diff --git a/metadata/params/testdata/delete-env-param/case2.libsonnet b/metadata/params/testdata/delete-env-param/case2.libsonnet new file mode 100644 index 0000000000000000000000000000000000000000..f71d79ad367f1342c630c58ccc70eb426a0364e4 --- /dev/null +++ b/metadata/params/testdata/delete-env-param/case2.libsonnet @@ -0,0 +1,9 @@ +local params = {}; +params { + components +: { + foo +: { + name: "foo", + replicas: 1, + }, + }, +} \ No newline at end of file diff --git a/metadata/params/testdata/delete-env-param/expected1.libsonnet b/metadata/params/testdata/delete-env-param/expected1.libsonnet new file mode 100644 index 0000000000000000000000000000000000000000..24d08832cd8e3784feee00cb92c987a76209cfc8 --- /dev/null +++ b/metadata/params/testdata/delete-env-param/expected1.libsonnet @@ -0,0 +1,10 @@ +local params = { +}; + +params + { + components+: { + foo+: { + replicas: 1, + }, + }, +} \ No newline at end of file diff --git a/metadata/params/testdata/delete-env-param/expected2.libsonnet b/metadata/params/testdata/delete-env-param/expected2.libsonnet new file mode 100644 index 0000000000000000000000000000000000000000..6f9acbac9d93753b00aa87bb4bae5e6a272149a3 --- /dev/null +++ b/metadata/params/testdata/delete-env-param/expected2.libsonnet @@ -0,0 +1,10 @@ +local params = { +}; + +params { + components+: { + foo+: { + replicas: 1, + }, + }, +} \ No newline at end of file diff --git a/pkg/env/params.go b/pkg/env/params.go index 8903ee6cc2fec5852398f800d0dcafa0b75e7cfd..e01d11e4c305dd3b0e4d8fccace1d3685b2765a0 100644 --- a/pkg/env/params.go +++ b/pkg/env/params.go @@ -19,7 +19,6 @@ import ( "github.com/ksonnet/ksonnet/component" "github.com/ksonnet/ksonnet/metadata/app" param "github.com/ksonnet/ksonnet/metadata/params" - "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/afero" ) @@ -31,13 +30,9 @@ type SetParamsConfig struct { // SetParams sets params for an environment. func SetParams(envName, component string, params param.Params, config SetParamsConfig) error { - exists, err := envExists(config.App, envName) - if err != nil { + if err := ensureEnvExists(config.App, envName); err != nil { return err } - if !exists { - return errors.Errorf("Environment %q does not exist", envName) - } path := envPath(config.App, envName, paramsFileName) @@ -60,6 +55,34 @@ func SetParams(envName, component string, params param.Params, config SetParamsC return nil } +// DeleteParam deletes a param in an environment. +func DeleteParam(a app.App, envName, component, name string) error { + if err := ensureEnvExists(a, envName); err != nil { + return err + } + + path := envPath(a, envName, paramsFileName) + + text, err := afero.ReadFile(a.Fs(), path) + if err != nil { + return err + } + + updated, err := param.DeleteEnvironmentParam(component, name, string(text)) + if err != nil { + return err + } + + err = afero.WriteFile(a.Fs(), path, []byte(updated), app.DefaultFilePermissions) + if err != nil { + return err + } + + log.Debugf("deleted parameter %q for component %q at environment %q", + name, component, envName) + return nil +} + // GetParamsConfig is config items for getting environment params. type GetParamsConfig struct { App app.App @@ -67,13 +90,9 @@ type GetParamsConfig struct { // GetParams gets all parameters for an environment. func GetParams(envName, module string, config GetParamsConfig) (map[string]param.Params, error) { - exists, err := envExists(config.App, envName) - if err != nil { + if err := ensureEnvExists(config.App, envName); err != nil { return nil, err } - if !exists { - return nil, errors.Errorf("Environment %q does not exist", envName) - } // Get the environment specific params envParamsPath := envPath(config.App, envName, paramsFileName) diff --git a/pkg/env/params_test.go b/pkg/env/params_test.go index 75dc22973bcb3693c7f04c2bbdcce859413632d4..e57684149b3d7bf533ab0515e70d0103cfb6b240 100644 --- a/pkg/env/params_test.go +++ b/pkg/env/params_test.go @@ -42,6 +42,15 @@ func TestSetParams(t *testing.T) { }) } +func TestDeleteParams(t *testing.T) { + withEnv(t, func(appMock *mocks.App, fs afero.Fs) { + err := DeleteParam(appMock, "env1", "component1", "foo") + require.NoError(t, err) + + compareOutput(t, fs, "delete-params.libsonnet", "/environments/env1/params.libsonnet") + }) +} + func TestGetParams(t *testing.T) { withEnv(t, func(appMock *mocks.App, fs afero.Fs) { config := GetParamsConfig{ diff --git a/pkg/env/rename.go b/pkg/env/rename.go index e480238876a8ef708253bce8a233fbfa7f52d447..24ca2df620a90b16a2f7f91c1e676ad0c56e39d9 100644 --- a/pkg/env/rename.go +++ b/pkg/env/rename.go @@ -98,6 +98,19 @@ func envExists(ksApp app.App, name string) (bool, error) { return afero.Exists(ksApp.Fs(), path) } +func ensureEnvExists(a app.App, name string) error { + exists, err := envExists(a, name) + if err != nil { + return err + } + + if !exists { + return errors.Errorf("environment %q does not exist", name) + } + + return nil +} + func envPath(ksApp app.App, name string, subPath ...string) string { return filepath.Join(append([]string{ksApp.Root(), envRoot, name}, subPath...)...) } diff --git a/pkg/env/testdata/delete-params.libsonnet b/pkg/env/testdata/delete-params.libsonnet new file mode 100644 index 0000000000000000000000000000000000000000..6e82d7a0b903a70a76872c8a92782530e831c17e --- /dev/null +++ b/pkg/env/testdata/delete-params.libsonnet @@ -0,0 +1,13 @@ +local params = import "../../components/params.libsonnet"; + +params + { + components+: { + // Insert component parameter overrides here. Ex: + // guestbook +: { + // name: "guestbook-dev", + // replicas: params.global.replicas, + // }, + component1+: { + }, + }, +} \ No newline at end of file diff --git a/pkg/params/params.go b/pkg/params/params.go index b6fff262261d8b9dedb097950d32a68d8b246ebd..c52094a40bcaf0790cbba0571e0cebae6e58389b 100644 --- a/pkg/params/params.go +++ b/pkg/params/params.go @@ -67,6 +67,7 @@ func Delete(path []string, paramsData, key, root string) (string, error) { if err != nil { return "", err } + cur := props for i, k := range path { @@ -82,7 +83,12 @@ func Delete(path []string, paramsData, key, root string) (string, error) { } } - return Update([]string{root, key}, paramsData, props) + updatePath := []string{root} + if key != "" { + updatePath = []string{root, key} + } + + return Update(updatePath, paramsData, props) } // Update updates a params file with the params for a component. diff --git a/pkg/util/jsonnet/object.go b/pkg/util/jsonnet/object.go index 3c6158ceedf2ccca178eec13bf96cc2428468676..7059a73644b58334a3f6d06ac3062b3d17755bc3 100644 --- a/pkg/util/jsonnet/object.go +++ b/pkg/util/jsonnet/object.go @@ -17,6 +17,7 @@ package jsonnet import ( "fmt" + "strings" "github.com/google/go-jsonnet/ast" "github.com/ksonnet/ksonnet-lib/ksonnet-gen/astext" @@ -128,7 +129,7 @@ func FindObject(object *astext.Object, path []string) (*astext.Object, error) { } } - return nil, errors.New("path was not found") + return nil, errors.Errorf("path %s was not found", strings.Join(path, ".")) } // FieldID returns the id for an object field. diff --git a/pkg/util/test/test.go b/pkg/util/test/test.go index ab26bf9b273f593ec70e1c497e6fb49fc5774204..b18488a5733b153399378b51e8e8b41e06a24c7e 100644 --- a/pkg/util/test/test.go +++ b/pkg/util/test/test.go @@ -25,6 +25,7 @@ import ( "github.com/ksonnet/ksonnet/metadata/app/mocks" "github.com/spf13/afero" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -91,6 +92,7 @@ func WithApp(t *testing.T, root string, fn func(*mocks.App, afero.Fs)) { a := &mocks.App{} a.On("Fs").Return(fs) a.On("Root").Return(root) + a.On("LibPath", mock.AnythingOfType("string")).Return(filepath.Join(root, "lib", "v1.8.7"), nil) fn(a, fs) }