From faff0ae724206eff05d173ae3516fc5095330cc7 Mon Sep 17 00:00:00 2001 From: Salim Afiune Date: Tue, 18 Oct 2022 09:03:25 -0700 Subject: [PATCH] feat(cdk): dev-mode command (#957) Wanna develop a new CDK component? Run the command: ``` lacework component dev ``` Signed-off-by: Salim Afiune Maya --- cli/cmd/component.go | 88 +++++++++++++++++++++++++++++++- lwcomponent/component.go | 107 +++++++++++++++++++++++++++++++++------ 2 files changed, 178 insertions(+), 17 deletions(-) diff --git a/cli/cmd/component.go b/cli/cmd/component.go index c5f054f21..dde0b501a 100644 --- a/cli/cmd/component.go +++ b/cli/cmd/component.go @@ -21,7 +21,9 @@ package cmd import ( "fmt" "os" + "path/filepath" + "github.com/AlecAivazis/survey/v2" "github.com/fatih/color" "github.com/olekukonko/tablewriter" "github.com/pkg/errors" @@ -76,6 +78,15 @@ var ( Args: cobra.ExactArgs(1), RunE: runComponentsDelete, } + + // componentsDevModeCmd represents the dev sub-command inside the components command + componentsDevModeCmd = &cobra.Command{ + Use: "dev ", + Hidden: true, + Short: "Enter development mode of a new or existing component", + Args: cobra.ExactArgs(1), + RunE: runComponentsDevMode, + } ) func init() { @@ -87,6 +98,7 @@ func init() { componentsCmd.AddCommand(componentsInstallCmd) componentsCmd.AddCommand(componentsUpdateCmd) componentsCmd.AddCommand(componentsUninstallCmd) + componentsCmd.AddCommand(componentsDevModeCmd) // load components dynamically cli.LoadComponents() @@ -360,7 +372,10 @@ func runComponentsDelete(_ *cobra.Command, args []string) (err error) { return } - if !component.IsInstalled() { + if component.UnderDevelopment() { + cli.OutputHuman("Component '%s' in under development. Bypassing checks.\n\n", + color.HiYellowString(component.Name)) + } else if !component.IsInstalled() { err = errors.Errorf( "component not installed. Try running 'lacework component install %s'", args[0], @@ -404,3 +419,74 @@ func runComponentsDelete(_ *cobra.Command, args []string) (err error) { cli.OutputHuman("Reach out to us at %s\n", color.HiCyanString("support@lacework.net")) return } + +func runComponentsDevMode(_ *cobra.Command, args []string) error { + cli.StartProgress("Loading components state...") + var err error + cli.LwComponents, err = lwcomponent.LoadState(cli.LwApi) + cli.StopProgress() + if err != nil { + return errors.Wrap(err, "unable to load components") + } + + component, found := cli.LwComponents.GetComponent(args[0]) + if !found { + component = &lwcomponent.Component{ + Name: args[0], + } + + if component.UnderDevelopment() { + return errors.New("component already under development.") + } + + cli.OutputHuman("Component '%s' not found. Defining a new component.\n", + color.HiYellowString(component.Name)) + + var ( + cType string + helpMsg = fmt.Sprintf("What are these component types ?\n"+ + "\n'%s' - A regular standalone-binary (this component type is not accessible via the CLI)"+ + "\n'%s' - A binary accessible via the Lacework CLI (Users will run 'lacework ')"+ + "\n'%s' - A library that only provides content for the CLI or other components\n", + lwcomponent.BinaryType, lwcomponent.CommandType, lwcomponent.LibraryType) + ) + if err := survey.AskOne(&survey.Select{ + Message: "Select the type of component you are developing:", + Help: helpMsg, + Options: []string{ + lwcomponent.BinaryType, + lwcomponent.CommandType, + lwcomponent.LibraryType, + }, + }, &cType); err != nil { + return err + } + + component.Type = lwcomponent.Type(cType) + + if err := survey.AskOne(&survey.Input{ + Message: "What is this component about? (component description):", + }, &component.Description); err != nil { + return err + } + } + + if err := component.EnterDevelopmentMode(); err != nil { + return errors.Wrap(err, "unable to enter development mode") + } + + rPath, err := component.RootPath() + if err != nil { + return errors.New("unable to detect RootPath") + } + + cli.OutputHuman("Component '%s' in now in development mode.\n\n", + color.HiYellowString(component.Name)) + cli.OutputHuman("Root path: %s\n", rPath) + cli.OutputHuman("Dev specs: %s\n", filepath.Join(rPath, ".dev")) + if component.Type == lwcomponent.CommandType { + cli.OutputHuman("\nDeploy your dev component at: %s\n", + color.HiYellowString(filepath.Join(rPath, component.Name))) + } + return nil +} diff --git a/lwcomponent/component.go b/lwcomponent/component.go index 64d0bcbce..1b7c1a06d 100644 --- a/lwcomponent/component.go +++ b/lwcomponent/component.go @@ -24,6 +24,7 @@ import ( _ "embed" "encoding/base64" "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" @@ -59,32 +60,69 @@ type State struct { func LoadState(client *api.Client) (*State, error) { if client != nil { s := new(State) + + // load remote components err := client.RequestDecoder("GET", "v2/Components", nil, s) if err != nil { return s, err } - s.loadDevComponent() + // load local components + s.loadComponentsFromDisk() + + // load dev components + s.loadDevComponents() return s, s.WriteState() } return nil, errors.New("invalid api client") } -// loadDevComponent will load a component that is under development, -// developers need to export the environment variable 'LW_CDK_DEV_COMPONENT' -func (s *State) loadDevComponent() { - if devComponent := os.Getenv("LW_CDK_DEV_COMPONENT"); devComponent != "" { - for i := range s.Components { - if s.Components[i].Name == devComponent { - // existing component being developed - if err := s.Components[i].loadDevSpecs(); err != nil { - s.Components[i].Description = err.Error() +// loadComponentsFromDisk will load all component from disk (local) +func (s *State) loadComponentsFromDisk() { + if dir, err := Dir(); err == nil { + components, err := os.ReadDir(dir) + if err != nil { + return + } + + // traverse components dir + for _, c := range components { + if !c.IsDir() { + continue + } + + // load components that are not already registered + if _, found := s.GetComponent(c.Name()); !found { + component := Component{Name: c.Name()} + + // verify that the directory is a component, that means that the + // directory contains either a '.dev' file or, both '.version' + // and '.signature' files + // + // TODO @afiune maybe we should deploy a .specs file? + err := component.isVerified() + + if component.UnderDevelopment() || err == nil { + s.Components = append(s.Components, component) } - return } } + } +} +// loadDevComponents will load all components that are under development +func (s *State) loadDevComponents() { + for i := range s.Components { + if s.Components[i].UnderDevelopment() { + // existing component being developed + if err := s.Components[i].loadDevSpecs(); err != nil { + s.Components[i].Description = err.Error() + } + } + } + + if devComponent := os.Getenv("LW_CDK_DEV_COMPONENT"); devComponent != "" { // component is not yet defined, add it to the state dev := Component{Name: devComponent} if err := dev.loadDevSpecs(); err != nil { @@ -176,7 +214,7 @@ func (s State) Install(name string) error { } // verify development mode - if component.underDevelopment() { + if component.UnderDevelopment() { p, _ := component.Path() // @afiune we don't care if the component exists or not msg := "components under development can't be installed.\n\n" + "Deploy the component manually at '" + p + "'" @@ -330,7 +368,7 @@ func (c Component) Path() (string, error) { func (c Component) CurrentVersion() (*semver.Version, error) { // development mode, avoid loading the current version, // return latest which is what's inside the '.dev' specs - if c.underDevelopment() { + if c.UnderDevelopment() { return &c.LatestVersion, nil } @@ -478,10 +516,10 @@ func (c *Component) loadDevSpecs() error { return nil } -// underDevelopment returns true if the component is under development +// UnderDevelopment returns true if the component is under development // that is, if the component root path has the '.dev' specs file or, if // the environment variable 'LW_CDK_DEV_COMPONENT' matches the component name -func (c Component) underDevelopment() bool { +func (c Component) UnderDevelopment() bool { if os.Getenv("LW_CDK_DEV_COMPONENT") == c.Name { return true } @@ -497,7 +535,7 @@ func (c Component) underDevelopment() bool { // isVerified checks if the component has a valid signature func (c Component) isVerified() error { // development mode, avoid verifying - if c.underDevelopment() { + if c.UnderDevelopment() { return nil } @@ -528,3 +566,40 @@ func (c Component) isVerified() error { // validate the signature return verifySignature(rootPublicKey, f, sig) } + +func (c Component) EnterDevelopmentMode() error { + if c.UnderDevelopment() { + return errors.New("component already under development.") + } + + dir, err := c.RootPath() + if err != nil { + return errors.New("unable to detect RootPath") + } + + devSpecs := filepath.Join(dir, ".dev") + if !file.FileExists(devSpecs) { + // remove prod artifacts + c.Artifacts = make([]Artifact, 0) + + // configure dev version + cv, _ := semver.NewVersion("0.0.0-dev") + c.LatestVersion = *cv + + // update description + c.Description = fmt.Sprintf("(dev-mode) %s", c.Description) + + buf := new(bytes.Buffer) + if err := json.NewEncoder(buf).Encode(c); err != nil { + return err + } + + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + + return ioutil.WriteFile(devSpecs, buf.Bytes(), 0644) + } + + return nil +}