Skip to content

Commit

Permalink
feat(console): add support for multiple input files (#729)
Browse files Browse the repository at this point in the history
* feat(console): add support for multiple input files

* fix: test updates

* fix: doc updates

* fix: file save workflow
  • Loading branch information
meganwolf0 authored Oct 17, 2024
1 parent 86c9376 commit 103ca0d
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 87 deletions.
11 changes: 9 additions & 2 deletions docs/cli-commands/lula_console.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,20 @@ lula console [flags]
To view an OSCAL model in the Console:
lula console -f /path/to/oscal-component.yaml
To view multiple OSCAL models in the Console:
lula console -f /path/to/oscal-component.yaml,/path/to/oscal-assessment-results.yaml
To specify an output file to save any changes made to the component definition:
lula console -f /path/to/oscal-component.yaml -c /path/to/output.yaml
```

### Options

```
-h, --help help for console
-f, --input-file string the path to the target OSCAL model
-c, --component-output string the path to the component definition output file
-h, --help help for console
-f, --input-files strings the path to the target OSCAL models, comma separated
```

### Options inherited from parent commands
Expand Down
10 changes: 9 additions & 1 deletion docs/console/component-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ lula console -f /path/to/oscal-component.yaml
```
The `oscal-component.yaml` will need to be a valid OSCAL model - to use with the Component Definition view, it must contain the `component-definition` top level key.

To include an output file to save any changes made to the component definition, use the `--component-output` or `-c`flag:
```shell
lula console -f /path/to/oscal-component.yaml -c /path/to/output.yaml
```

> [!Note]
> Several component definition models can be passed into the console, via `-f` in a comma-separated list. For multiple component definitions, the output file will default to `component.yaml` unless specified.
## Keys

The Component Definition model responds to the following keys for navigation and interaction (some widgets have additional key response, see respective help views for more information):
Expand All @@ -27,7 +35,7 @@ The Component Definition model responds to the following keys for navigation and
| `/` | Filter list |
| `` | Select item |
| `e` | Edit available fields (remarks and description) |
| `ctrl+s` | Save changes (Note: this will overwrite the original file) |
| `ctrl+s` | Save changes (Note: this may overwrite the original file if an output file unspecified) |
| `esc` | Cancel |

During console viewing, the top-right corner will display the help keys availble in the context of the selected widget. When an overlay is open, the help keys will be displayed in the overlay.
Expand Down
136 changes: 99 additions & 37 deletions src/cmd/console/console.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package console

import (
"fmt"
"os"

oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2"
"github.com/defenseunicorns/lula/src/internal/tui"
"github.com/defenseunicorns/lula/src/pkg/common/oscal"
"github.com/defenseunicorns/lula/src/pkg/message"
Expand All @@ -11,63 +13,123 @@ import (
tea "github.com/charmbracelet/bubbletea"
)

type flags struct {
InputFile string // -f --input-file
}

var opts = &flags{}

var consoleHelp = `
To view an OSCAL model in the Console:
lula console -f /path/to/oscal-component.yaml
To view multiple OSCAL models in the Console:
lula console -f /path/to/oscal-component.yaml,/path/to/oscal-assessment-results.yaml
To specify an output file to save any changes made to the component definition:
lula console -f /path/to/oscal-component.yaml -c /path/to/output.yaml
`

var consoleLong = `
The Lula Console is a text-based terminal user interface that allows users to
interact with the OSCAL documents in a more intuitive and visual way.
`

var consoleCmd = &cobra.Command{
Use: "console",
Aliases: []string{"ui"},
Short: "Console terminal user interface for OSCAL models",
Long: consoleLong,
Example: consoleHelp,
Run: func(cmd *cobra.Command, args []string) {
// Get the OSCAL model from the file
data, err := os.ReadFile(opts.InputFile)
func ConsoleCommand() *cobra.Command {
var inputFiles []string
var componentOutputFile string

consoleCmd := &cobra.Command{
Use: "console",
Aliases: []string{"ui"},
Short: "Console terminal user interface for OSCAL models",
Long: consoleLong,
Example: consoleHelp,
RunE: func(cmd *cobra.Command, args []string) error {
setOutputFiles := make(map[string]string)
// Check if output files are specified - Add more as needed
if componentOutputFile != "" {
setOutputFiles["component"] = componentOutputFile
}

models, modelFiles, err := GetModelsByFiles(inputFiles, setOutputFiles)
if err != nil {
return err
}

// Check validity of all output model files
for _, outputFile := range modelFiles {
_, err = oscal.ValidOSCALModelAtPath(outputFile)
if err != nil {
return fmt.Errorf("invalid OSCAL model at output file: %v", err)
}
}

// TODO: need to integrate with the log file handled by messages
var dumpFile *os.File
if message.GetLogLevel() == message.DebugLevel {
dumpFile, err = os.OpenFile("debug.log", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer dumpFile.Close()
}

p := tea.NewProgram(tui.NewOSCALModel(models, modelFiles, dumpFile), tea.WithAltScreen(), tea.WithMouseCellMotion())

if _, err := p.Run(); err != nil {
return err
}

return nil
},
}

consoleCmd.Flags().StringSliceVarP(&inputFiles, "input-files", "f", []string{}, "the path to the target OSCAL models, comma separated")
err := consoleCmd.MarkFlagRequired("input-files")
if err != nil {
message.Fatal(err, "error initializing console command flags")
}
consoleCmd.Flags().StringVarP(&componentOutputFile, "component-output", "c", "", "the path to the component definition output file")
return consoleCmd
}

func GetModelsByFiles(inputFiles []string, setOutputFiles map[string]string) (map[string]*oscalTypes_1_1_2.OscalModels, map[string]string, error) {
var models = make(map[string]*oscalTypes_1_1_2.OscalModels)
var modelFiles = make(map[string]string)

// Get the OSCAL models from the files
for _, inputFile := range inputFiles {
data, err := os.ReadFile(inputFile)
if err != nil {
message.Fatalf(err, "error reading file: %v", err)
return nil, nil, fmt.Errorf("error reading file: %v", err)
}
oscalModel, err := oscal.NewOscalModel(data)
if err != nil {
message.Fatalf(err, "error creating oscal model from file: %v", err)
return nil, nil, fmt.Errorf("error creating oscal model from file: %v", err)
}

// Add debugging
// TODO: need to integrate with the log file handled by messages
var dumpFile *os.File
if message.GetLogLevel() == message.DebugLevel {
dumpFile, err = os.OpenFile("debug.log", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
message.Fatalf(err, err.Error())
}
defer dumpFile.Close()
// Assign the model type
modelType, err := oscal.GetOscalModel(oscalModel)
if err != nil {
return nil, nil, fmt.Errorf("error getting oscal model type from file %s: %v", inputFile, err)
}

p := tea.NewProgram(tui.NewOSCALModel(oscalModel, opts.InputFile, dumpFile), tea.WithAltScreen(), tea.WithMouseCellMotion())
// Add the model to the map
if _, ok := models[modelType]; ok {
// try to merge the models
newModel, err := oscal.MergeOscalModels(models[modelType], oscalModel, modelType)
if err != nil {
return nil, nil, fmt.Errorf("error merging oscal models: %v", err)
}
models[modelType] = newModel

if _, err := p.Run(); err != nil {
message.Fatalf(err, err.Error())
// get new default output filename
modelFiles[modelType] = fmt.Sprintf("%s.yaml", modelType)
} else {
models[modelType] = oscalModel
modelFiles[modelType] = inputFile
}
},
}
}

func ConsoleCommand() *cobra.Command {
consoleCmd.Flags().StringVarP(&opts.InputFile, "input-file", "f", "", "the path to the target OSCAL model")
err := consoleCmd.MarkFlagRequired("input-file")
if err != nil {
message.Fatal(err, "error initializing console command flags")
// If any output file name is specified, overwrite the modelFiles field
for k, v := range setOutputFiles {
modelFiles[k] = v
}
return consoleCmd

return models, modelFiles, nil
}
39 changes: 39 additions & 0 deletions src/cmd/console/console_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package console_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/defenseunicorns/lula/src/cmd/console"
)

func TestGetModelsByFiles(t *testing.T) {
t.Run("Get output models by files", func(t *testing.T) {
setOutputFiles := make(map[string]string)
inputFiles := []string{"../../test/unit/common/oscal/valid-component.yaml", "../../test/unit/common/oscal/valid-generated-component.yaml", "../../test/unit/common/oscal/valid-assessment-results.yaml"}
models, modelFiles, err := console.GetModelsByFiles(inputFiles, setOutputFiles)
require.NoError(t, err)

require.Len(t, models, 2)
require.Len(t, modelFiles, 2)

require.NotNil(t, models["component"].ComponentDefinition)
require.NotNil(t, models["assessment-results"].AssessmentResults)

require.Equal(t, modelFiles["component"], "component.yaml")
require.Equal(t, modelFiles["assessment-results"], "../../test/unit/common/oscal/valid-assessment-results.yaml")
})

t.Run("Override output model files", func(t *testing.T) {
setOutputFiles := make(map[string]string)
setOutputFiles["component"] = "component-override.yaml"

inputFiles := []string{"../../test/unit/common/oscal/valid-component.yaml", "../../test/unit/common/oscal/valid-generated-component.yaml", "../../test/unit/common/oscal/valid-assessment-results.yaml"}
_, modelFiles, err := console.GetModelsByFiles(inputFiles, setOutputFiles)
require.NoError(t, err)

require.Equal(t, modelFiles["component"], "component-override.yaml")
require.Equal(t, modelFiles["assessment-results"], "../../test/unit/common/oscal/valid-assessment-results.yaml")
})
}
Loading

0 comments on commit 103ca0d

Please sign in to comment.