diff --git a/cmd/component.go b/cmd/component.go index 9fae44c9bc01f5e9960d12f1438e271c04a9857c..fb9103053baf30be98b70baf491fc307aee2352b 100644 --- a/cmd/component.go +++ b/cmd/component.go @@ -28,6 +28,9 @@ func init() { RootCmd.AddCommand(componentCmd) componentCmd.AddCommand(componentListCmd) + componentCmd.AddCommand(componentRmCmd) + + componentRmCmd.PersistentFlags().String(flagComponent, "", "The component to be removed from components/") } var componentCmd = &cobra.Command{ @@ -62,3 +65,25 @@ The ` + "`list`" + ` command displays all known components. # List all components ks component list`, } + +var componentRmCmd = &cobra.Command{ + Use: "rm <component-name>", + Short: "Delete a component from the ksonnet application", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("'component rm' takes a single argument, that is the name of the component") + } + + component := args[0] + + c := kubecfg.NewComponentRmCmd(component) + return c.Run() + }, + Long: `Delete a component from the ksonnet application. This is equivalent to deleting the +component file in the components directory and cleaning up all component +references throughout the project.`, + Example: `# Remove the component 'guestbook'. This is equivalent to deleting guestbook.jsonnet +# in the components directory, and cleaning up references to the component +# throughout the ksonnet application. +ks component rm guestbook`, +} diff --git a/docs/cli-reference/ks_component.md b/docs/cli-reference/ks_component.md index 2a860d9dd9fa8b7ab7e7f745dd6960ef54624ce5..1ccdd345fa30debe7cf28a978a0a31598549a954 100644 --- a/docs/cli-reference/ks_component.md +++ b/docs/cli-reference/ks_component.md @@ -20,4 +20,5 @@ ks component ### SEE ALSO * [ks](ks.md) - Configure your application to deploy to a Kubernetes cluster * [ks component list](ks_component_list.md) - List known components +* [ks component rm](ks_component_rm.md) - Delete a component from the ksonnet application diff --git a/docs/cli-reference/ks_component_rm.md b/docs/cli-reference/ks_component_rm.md new file mode 100644 index 0000000000000000000000000000000000000000..f5664af2fb925702e7eb8df25ebde9824e0d2972 --- /dev/null +++ b/docs/cli-reference/ks_component_rm.md @@ -0,0 +1,39 @@ +## ks component rm + +Delete a component from the ksonnet application + +### Synopsis + + +Delete a component from the ksonnet application. This is equivalent to deleting the +component file in the components directory and cleaning up all component +references throughout the project. + +``` +ks component rm <component-name> +``` + +### Examples + +``` +# Remove the component 'guestbook'. This is equivalent to deleting guestbook.jsonnet +# in the components directory, and cleaning up references to the component +# throughout the ksonnet application. +ks component rm guestbook +``` + +### Options + +``` + --component string The component to be removed from components/ +``` + +### Options inherited from parent commands + +``` + -v, --verbose count[=-1] Increase verbosity. May be given multiple times. +``` + +### SEE ALSO +* [ks component](ks_component.md) - Manage ksonnet components + diff --git a/metadata/component.go b/metadata/component.go index e8a6f6611d65e87afbc5949cbc42428d658f3455..7cbe06ebf768608ca6afe3775195770b9c47b94a 100644 --- a/metadata/component.go +++ b/metadata/component.go @@ -94,3 +94,107 @@ func (m *manager) CreateComponent(name string, text string, params param.Params, log.Debugf("Writing component parameters at '%s/%s", componentsDir, name) return m.writeComponentParams(name, params) } + +// DeleteComponent removes the component file and all references. +// Write operations will happen at the end to minimalize failures that leave +// the directory structure in a half-finished state. +func (m *manager) DeleteComponent(name string) error { + componentPath, err := m.findComponentPath(name) + if err != nil { + return err + } + + // Build the new component/params.libsonnet file. + componentParamsFile, err := afero.ReadFile(m.appFS, string(m.componentParamsPath)) + if err != nil { + return err + } + componentJsonnet, err := param.DeleteComponent(name, string(componentParamsFile)) + if err != nil { + return err + } + + // Build the new environment/<env>/params.libsonnet files. + // environment name -> jsonnet + envJsonnets := make(map[string]string) + envs, err := m.GetEnvironments() + if err != nil { + return err + } + for _, env := range envs { + path := appendToAbsPath(m.environmentsPath, env.Name, paramsFileName) + envParamsFile, err := afero.ReadFile(m.appFS, string(path)) + if err != nil { + return err + } + jsonnet, err := param.DeleteEnvironmentComponent(name, string(envParamsFile)) + if err != nil { + return err + } + envJsonnets[env.Name] = jsonnet + } + + // + // Delete the component references. + // + log.Infof("Removing component parameter references ...") + + // Remove the references in component/params.libsonnet. + log.Debugf("... deleting references in %s", m.componentParamsPath) + err = afero.WriteFile(m.appFS, string(m.componentParamsPath), []byte(componentJsonnet), defaultFilePermissions) + if err != nil { + return err + } + // Remove the component references in each environment's + // environment/<env>/params.libsonnet. + for _, env := range envs { + path := appendToAbsPath(m.environmentsPath, env.Name, paramsFileName) + log.Debugf("... deleting references in %s", path) + err = afero.WriteFile(m.appFS, string(path), []byte(envJsonnets[env.Name]), defaultFilePermissions) + if err != nil { + return err + } + } + + // + // Delete the component file in components/. + // + log.Infof("Deleting component '%s' at path '%s'", name, componentPath) + if err := m.appFS.Remove(componentPath); err != nil { + return err + } + + // TODO: Remove, + // references in main.jsonnet. + // component references in other component files (feature does not yet exist). + log.Infof("Succesfully deleted component '%s'", name) + return nil +} + +func (m *manager) findComponentPath(name string) (string, error) { + componentPaths, err := m.ComponentPaths() + if err != nil { + log.Debugf("Failed to retrieve component paths") + return "", err + } + + var componentPath string + for _, p := range componentPaths { + fileName := path.Base(p) + component := strings.TrimSuffix(fileName, path.Ext(fileName)) + + if component == name { + // need to make sure we don't have multiple files with the same component name + if componentPath != "" { + return "", fmt.Errorf("Found multiple component files with component name '%s'", name) + } + componentPath = p + } + } + + if componentPath == "" { + return "", fmt.Errorf("No component with name '%s' found", name) + } + + return componentPath, nil +} diff --git a/metadata/component_test.go b/metadata/component_test.go index c4a244a507b125f97e2c57454c6128323c5eef13..00a5ddb299fdaa46b4f68f31111714216f3e8dd7 100644 --- a/metadata/component_test.go +++ b/metadata/component_test.go @@ -17,6 +17,7 @@ package metadata import ( "fmt" "os" + "path" "sort" "strings" "testing" @@ -121,3 +122,19 @@ func TestGetAllComponents(t *testing.T) { t.Fatalf("Expected component %s, got %s", expected2, components) } } + +func TestFindComponentPath(t *testing.T) { + m := populateComponentPaths(t) + defer cleanComponentPaths(t) + + component := strings.TrimSuffix(componentFile1, path.Ext(componentFile1)) + expected := fmt.Sprintf("%s/components/%s", componentsPath, componentFile1) + path, err := m.findComponentPath(component) + if err != nil { + t.Fatalf("Failed to find component path, %v", err) + } + + if path != expected { + t.Fatalf("m.findComponentPath failed; expected '%s', got '%s'", expected, path) + } +} diff --git a/metadata/interface.go b/metadata/interface.go index 90c1b7461da28c417dfb1452c3f60b37d3881cc2..f0ae872226113bdd400f7b9d7945fb3a4713b14e 100644 --- a/metadata/interface.go +++ b/metadata/interface.go @@ -52,6 +52,7 @@ type Manager interface { ComponentPaths() (AbsPaths, error) GetAllComponents() ([]string, error) CreateComponent(name string, text string, params param.Params, templateType prototype.TemplateType) error + DeleteComponent(name string) error // Params API. SetComponentParams(component string, params param.Params) error diff --git a/metadata/params/interface.go b/metadata/params/interface.go index f33fd1535e4538397d6fbf122684e9deef4a9c0e..c773b5fd441f777c45bfb7125d0cff2dbbd583be 100644 --- a/metadata/params/interface.go +++ b/metadata/params/interface.go @@ -28,6 +28,16 @@ func AppendComponent(component, snippet string, params Params) (string, error) { return appendComponent(component, snippet, params) } +// DeleteComponent takes +// +// component: the name of the component to be deleted. +// snippet: a jsonnet snippet resembling the current component parameters. +// +// and returns the jsonnet snippet with the removed component. +func DeleteComponent(component, snippet string) (string, error) { + return deleteComponent(component, snippet) +} + // GetComponentParams takes // // component: the name of the component to retrieve params for. @@ -82,3 +92,16 @@ func GetAllEnvironmentParams(snippet string) (map[string]Params, error) { func SetEnvironmentParams(component, snippet string, params Params) (string, error) { return setEnvironmentParams(component, snippet, params) } + +// DeleteEnvironmentComponent takes +// +// component: the name of the component to be deleted. +// snippet: a jsonnet snippet resembling the current environment parameters (not expanded). +// +// and returns the jsonnet snippet with the removed component. +func DeleteEnvironmentComponent(component, snippet string) (string, error) { + // The implementation happens to be the same as DeleteComponent, but we're + // keeping the two interfaces separate since we're fundamentally operating + // on two different jsonnet schemas. + return deleteComponent(component, snippet) +} diff --git a/metadata/params/params.go b/metadata/params/params.go index 8b2d948c28c6615c7cae96cbc8d99c1cb23e74aa..20a10ab105d90587396cbfd062cf08120e8f9570 100644 --- a/metadata/params/params.go +++ b/metadata/params/params.go @@ -194,6 +194,33 @@ func writeParams(indent int, params Params) string { return buffer.String() } +func deleteComponent(component, snippet string) (string, error) { + componentsNode, err := componentsObj(component, snippet) + if err != nil { + return "", err + } + + for _, field := range componentsNode.Fields { + hasComponent, err := hasComponent(component, field) + if err != nil { + return "", err + } + if hasComponent { + lines := strings.Split(snippet, "\n") + + removeLineBegin := field.Expr2.Loc().Begin.Line - 1 + removeLineEnd := field.Expr2.Loc().End.Line + + lines = append(lines[:removeLineBegin], lines[removeLineEnd:]...) + + return strings.Join(lines, "\n"), nil + } + } + + // No component references, just return the original snippet. + return snippet, nil +} + // --------------------------------------------------------------------------- // Component Parameter-specific functionality diff --git a/metadata/params/params_test.go b/metadata/params/params_test.go index f7feeca848e1e39ada5a64179be272a6f9c5124f..81c4cc5c08a09834888910c8a70955a0b7e5c9d3 100644 --- a/metadata/params/params_test.go +++ b/metadata/params/params_test.go @@ -242,6 +242,209 @@ local bar = import "bar"; } } +func TestDeleteComponent(t *testing.T) { + tests := []struct { + componentName string + jsonnet string + expected string + }{ + // Test case with existing component + { + "bar", + ` +{ + components: { + foo: { + name: "foo", + replicas: 1, + }, + bar: { + name: "bar", + }, + }, +}`, + ` +{ + components: { + foo: { + name: "foo", + replicas: 1, + }, + }, +}`, + }, + // Test another case with existing component + { + "bar", + ` +{ + components: { + bar: { + name: "bar", + }, + }, +}`, + ` +{ + components: { + }, +}`, + }, + // Test case where component doesn't exist + { + "bar", + ` +{ + components: { + foo: { + name: "foo", + replicas: 1, + }, + }, +}`, + ` +{ + components: { + foo: { + name: "foo", + replicas: 1, + }, + }, +}`, + }, + } + + for _, s := range tests { + parsed, err := DeleteComponent(s.componentName, s.jsonnet) + if err != nil { + t.Errorf("Unexpected error\n input: %v\n error: %v", s.jsonnet, err) + } + + if parsed != s.expected { + t.Errorf("Wrong conversion\n expected: %v\n got: %v", s.expected, parsed) + } + } +} + +func TestDeleteEnvironmentComponent(t *testing.T) { + tests := []struct { + componentName string + jsonnet string + expected string + }{ + // Test case with existing component + { + "bar", + ` +local params = import "/fake/path"; +params + { + components +: { + bar +: { + name: "bar", + "replica-count": 1, + }, + foo +: { + name: "foo", + }, + }, +}`, + ` +local params = import "/fake/path"; +params + { + components +: { + foo +: { + name: "foo", + }, + }, +}`, + }, + // Test another case with existing component + { + "foo", + ` +local params = import "/fake/path"; +params + { + components +: { + bar +: { + name: "bar", + "replica-count": 1, + }, + foo +: { + name: "foo", + }, + }, +}`, + ` +local params = import "/fake/path"; +params + { + components +: { + bar +: { + name: "bar", + "replica-count": 1, + }, + }, +}`, + }, + // Test case where component doesn't exist + { + "baz", + ` +local params = import "/fake/path"; +params + { + components +: { + bar +: { + name: "bar", + "replica-count": 1, + }, + foo +: { + name: "foo", + }, + }, +}`, + ` +local params = import "/fake/path"; +params + { + components +: { + bar +: { + name: "bar", + "replica-count": 1, + }, + foo +: { + name: "foo", + }, + }, +}`, + }, + // Test case where there are no components + { + "baz", + ` +local params = import "/fake/path"; +params + { + components +: { + }, +}`, + ` +local params = import "/fake/path"; +params + { + components +: { + }, +}`, + }, + } + + for _, s := range tests { + parsed, err := DeleteEnvironmentComponent(s.componentName, s.jsonnet) + if err != nil { + t.Errorf("Unexpected error\n input: %v\n error: %v", s.jsonnet, err) + } + + if parsed != s.expected { + t.Errorf("Wrong conversion\n expected: %v\n got: %v", s.expected, parsed) + } + } +} + func TestGetComponentParams(t *testing.T) { tests := []struct { componentName string diff --git a/pkg/kubecfg/component.go b/pkg/kubecfg/component.go index cdb16d93c7e9b8ae533fb9a45c7b5f00c3370a35..0c4fb9e994f8c0664f3bc114c39917c741504043 100644 --- a/pkg/kubecfg/component.go +++ b/pkg/kubecfg/component.go @@ -53,6 +53,27 @@ func (c *ComponentListCmd) Run(out io.Writer) error { return err } +// ComponentRmCmd stores the information necessary to remove a component from +// the ksonnet application. +type ComponentRmCmd struct { + component string +} + +// NewComponentRmCmd acts as a constructor for ComponentRmCmd. +func NewComponentRmCmd(component string) *ComponentRmCmd { + return &ComponentRmCmd{component: component} +} + +// Run executes the removing of the component. +func (c *ComponentRmCmd) Run() error { + manager, err := manager() + if err != nil { + return err + } + + return manager.DeleteComponent(c.component) +} + func printComponents(out io.Writer, components []string) (string, error) { rows := [][]string{ []string{componentNameHeader},