Skip to content
Snippets Groups Projects
Unverified Commit 4cb12c84 authored by Jess's avatar Jess Committed by GitHub
Browse files

Merge pull request #288 from jessicayuen/component-rm

Add remove component functionality
parents d4aa8826 8f26d9a4
No related branches found
No related tags found
No related merge requests found
...@@ -28,6 +28,9 @@ func init() { ...@@ -28,6 +28,9 @@ func init() {
RootCmd.AddCommand(componentCmd) RootCmd.AddCommand(componentCmd)
componentCmd.AddCommand(componentListCmd) componentCmd.AddCommand(componentListCmd)
componentCmd.AddCommand(componentRmCmd)
componentRmCmd.PersistentFlags().String(flagComponent, "", "The component to be removed from components/")
} }
var componentCmd = &cobra.Command{ var componentCmd = &cobra.Command{
...@@ -62,3 +65,25 @@ The ` + "`list`" + ` command displays all known components. ...@@ -62,3 +65,25 @@ The ` + "`list`" + ` command displays all known components.
# List all components # List all components
ks component list`, 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`,
}
...@@ -20,4 +20,5 @@ ks component ...@@ -20,4 +20,5 @@ ks component
### SEE ALSO ### SEE ALSO
* [ks](ks.md) - Configure your application to deploy to a Kubernetes cluster * [ks](ks.md) - Configure your application to deploy to a Kubernetes cluster
* [ks component list](ks_component_list.md) - List known components * [ks component list](ks_component_list.md) - List known components
* [ks component rm](ks_component_rm.md) - Delete a component from the ksonnet application
## 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
...@@ -94,3 +94,107 @@ func (m *manager) CreateComponent(name string, text string, params param.Params, ...@@ -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) log.Debugf("Writing component parameters at '%s/%s", componentsDir, name)
return m.writeComponentParams(name, params) 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
}
...@@ -17,6 +17,7 @@ package metadata ...@@ -17,6 +17,7 @@ package metadata
import ( import (
"fmt" "fmt"
"os" "os"
"path"
"sort" "sort"
"strings" "strings"
"testing" "testing"
...@@ -121,3 +122,19 @@ func TestGetAllComponents(t *testing.T) { ...@@ -121,3 +122,19 @@ func TestGetAllComponents(t *testing.T) {
t.Fatalf("Expected component %s, got %s", expected2, components) 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)
}
}
...@@ -52,6 +52,7 @@ type Manager interface { ...@@ -52,6 +52,7 @@ type Manager interface {
ComponentPaths() (AbsPaths, error) ComponentPaths() (AbsPaths, error)
GetAllComponents() ([]string, error) GetAllComponents() ([]string, error)
CreateComponent(name string, text string, params param.Params, templateType prototype.TemplateType) error CreateComponent(name string, text string, params param.Params, templateType prototype.TemplateType) error
DeleteComponent(name string) error
// Params API. // Params API.
SetComponentParams(component string, params param.Params) error SetComponentParams(component string, params param.Params) error
......
...@@ -28,6 +28,16 @@ func AppendComponent(component, snippet string, params Params) (string, error) { ...@@ -28,6 +28,16 @@ func AppendComponent(component, snippet string, params Params) (string, error) {
return appendComponent(component, snippet, params) 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 // GetComponentParams takes
// //
// component: the name of the component to retrieve params for. // component: the name of the component to retrieve params for.
...@@ -82,3 +92,16 @@ func GetAllEnvironmentParams(snippet string) (map[string]Params, error) { ...@@ -82,3 +92,16 @@ func GetAllEnvironmentParams(snippet string) (map[string]Params, error) {
func SetEnvironmentParams(component, snippet string, params Params) (string, error) { func SetEnvironmentParams(component, snippet string, params Params) (string, error) {
return setEnvironmentParams(component, snippet, params) 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)
}
...@@ -194,6 +194,33 @@ func writeParams(indent int, params Params) string { ...@@ -194,6 +194,33 @@ func writeParams(indent int, params Params) string {
return buffer.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 // Component Parameter-specific functionality
......
...@@ -242,6 +242,209 @@ local bar = import "bar"; ...@@ -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) { func TestGetComponentParams(t *testing.T) {
tests := []struct { tests := []struct {
componentName string componentName string
......
...@@ -53,6 +53,27 @@ func (c *ComponentListCmd) Run(out io.Writer) error { ...@@ -53,6 +53,27 @@ func (c *ComponentListCmd) Run(out io.Writer) error {
return err 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) { func printComponents(out io.Writer, components []string) (string, error) {
rows := [][]string{ rows := [][]string{
[]string{componentNameHeader}, []string{componentNameHeader},
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment