Skip to content

Commit

Permalink
feat(cdk): dev-mode command (#957)
Browse files Browse the repository at this point in the history
Wanna develop a new CDK component? Run the command:
```
lacework component dev <component_name>
```

Signed-off-by: Salim Afiune Maya <[email protected]>
  • Loading branch information
afiune committed Oct 18, 2022
1 parent 4ab28fa commit faff0ae
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 17 deletions.
88 changes: 87 additions & 1 deletion cli/cmd/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 <component>",
Hidden: true,
Short: "Enter development mode of a new or existing component",
Args: cobra.ExactArgs(1),
RunE: runComponentsDevMode,
}
)

func init() {
Expand All @@ -87,6 +98,7 @@ func init() {
componentsCmd.AddCommand(componentsInstallCmd)
componentsCmd.AddCommand(componentsUpdateCmd)
componentsCmd.AddCommand(componentsUninstallCmd)
componentsCmd.AddCommand(componentsDevModeCmd)

// load components dynamically
cli.LoadComponents()
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -404,3 +419,74 @@ func runComponentsDelete(_ *cobra.Command, args []string) (err error) {
cli.OutputHuman("Reach out to us at %s\n", color.HiCyanString("[email protected]"))
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 <COMPONENT_NAME>')"+
"\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
}
107 changes: 91 additions & 16 deletions lwcomponent/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 + "'"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}

0 comments on commit faff0ae

Please sign in to comment.