Skip to content

Commit

Permalink
Exit status management and query functions + unit tests (#12)
Browse files Browse the repository at this point in the history
Signed-off-by: Tullio Sebastiani <[email protected]>
  • Loading branch information
tsebastiani authored Nov 22, 2024
1 parent e29d6aa commit 856affa
Show file tree
Hide file tree
Showing 30 changed files with 1,054 additions and 496 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,13 @@ standard output this command comes into help.

### `clean`:
will remove all the krkn containers from the container runtime, will delete all the kubeconfig files
and logfiles created by the tool in the current folder.
and logfiles created by the tool in the current folder.

### `query-status <container Id or Name> [--graph <graph file path>]`:

Will query the container platform to return container informations of a container name or Id if the `--graph` flag is not
set, else will query the status of all the container names contained in the graph file.
If a single container Id or name is queried the tool will exit with the same exit status of the container.

>[!TIP]
> This function can be integrated into CI/CD pipelines to halt execution if the chaos run encounters any failure.
2 changes: 1 addition & 1 deletion cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func NewListScenariosCommand(factory *providerfactory.ProviderFactory, config co
provider := GetProvider(false, factory)
s := NewSpinnerWithSuffix("fetching scenarios...")
s.Start()
scenarios, err := provider.GetScenarios(dataSource)
scenarios, err := provider.GetRegistryImages(dataSource)
if err != nil {
s.Stop()
log.Fatalf("failed to fetch scenarios: %v", err)
Expand Down
128 changes: 128 additions & 0 deletions cmd/query_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package cmd

import (
"context"
"encoding/json"
"fmt"
"github.com/krkn-chaos/krknctl/internal/config"
provider_models "github.com/krkn-chaos/krknctl/pkg/provider/models"
"github.com/krkn-chaos/krknctl/pkg/scenario_orchestrator"
"github.com/krkn-chaos/krknctl/pkg/scenario_orchestrator/models"
"github.com/spf13/cobra"
"os"
)

func resolveContainerIdOrName(orchestrator scenario_orchestrator.ScenarioOrchestrator, arg string, conn context.Context, conf config.Config) error {
var scenarioContainer *models.ScenarioContainer
var containerId *string

containerId, err := orchestrator.ResolveContainerName(arg, conn)
if err != nil {
return err
}
if containerId == nil {
containerId = &arg
}

scenarioContainer, err = orchestrator.InspectScenario(models.Container{Id: *containerId}, conn)

if err != nil {
return err
}

if scenarioContainer == nil {
return fmt.Errorf("scenarioContainer with id or name %s not found", arg)
}

containerJson, err := json.Marshal(scenarioContainer.Container)
if err != nil {
return err
}
fmt.Println(string(containerJson))
if scenarioContainer.Container.ExitStatus != 0 {
return fmt.Errorf("%s %d", conf.ContainerExitStatusPrefix, scenarioContainer.Container.ExitStatus)
}
return nil
}

func resolveGraphFile(orchestrator scenario_orchestrator.ScenarioOrchestrator, filename string, conn context.Context, conf config.Config) error {
var scenarioFile = make(map[string]provider_models.ScenarioDetail)
var containers = make([]models.Container, 0)

fileData, err := os.ReadFile(filename)
if err != nil {
return err
}
err = json.Unmarshal(fileData, &scenarioFile)
if err != nil {
return err
}
for key, _ := range scenarioFile {
scenario, err := orchestrator.ResolveContainerName(key, conn)
if err != nil {
return err
}
if scenario != nil {
containerScenario, err := orchestrator.InspectScenario(models.Container{Id: *scenario}, conn)
if err != nil {
return err
}
if containerScenario != nil {
if (*containerScenario).Container != nil {
containers = append(containers, *(*containerScenario).Container)
}
}
}
}
containersJson, err := json.Marshal(containers)
if err != nil {
return err
}
fmt.Println(string(containersJson))
return nil
}

func NewQueryStatusCommand(scenarioOrchestrator *scenario_orchestrator.ScenarioOrchestrator, config config.Config) *cobra.Command {
var command = &cobra.Command{
Use: "query-status",
Short: "checks the status of a container or a list of containers",
Long: `checks the status of a container or a list of containers by container name or container Id`,
Args: cobra.MaximumNArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
socket, err := (*scenarioOrchestrator).GetContainerRuntimeSocket(nil)
if err != nil {
return err
}
conn, err := (*scenarioOrchestrator).Connect(*socket)
if err != nil {
return err
}
if len(args) > 0 {
err = resolveContainerIdOrName(*scenarioOrchestrator, args[0], conn, config)
return err
}

graphPath, err := cmd.Flags().GetString("graph")
if err != nil {
return err
}

if graphPath == "" {
return fmt.Errorf("neither container Id or name nor graph plan file specified")
}

if CheckFileExists(graphPath) == false {
return fmt.Errorf("graph file %s not found", graphPath)
}

err = resolveGraphFile(*scenarioOrchestrator, graphPath, conn, config)

if err != nil {
return err
}
return nil
},
}
return command
}
18 changes: 17 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"github.com/krkn-chaos/krknctl/pkg/scenario_orchestrator"
"github.com/spf13/cobra"
"os"
"strconv"
"strings"
)

func Execute(providerFactory *factory.ProviderFactory, scenarioOrchestrator *scenario_orchestrator.ScenarioOrchestrator, config config.Config) {
Expand Down Expand Up @@ -73,9 +75,23 @@ func Execute(providerFactory *factory.ProviderFactory, scenarioOrchestrator *sce

attachCmd := NewAttachCmd(scenarioOrchestrator)
rootCmd.AddCommand(attachCmd)
queryCmd := NewQueryStatusCommand(scenarioOrchestrator, config)
queryCmd.Flags().String("graph", "", "to query the exit status of a previously run graph file")
rootCmd.AddCommand(queryCmd)

if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
// intercept the propagated exit status from the container and exits with the same code
if strings.Contains(err.Error(), config.ContainerExitStatusPrefix) {
exitCodeStr := strings.Split(err.Error(), " ")
if len(exitCodeStr) == 2 {
exitStatus, err := strconv.ParseInt(exitCodeStr[1], 10, 32)
if err != nil {
fmt.Println(fmt.Sprintf("Error converting exit code to int: %s", err))
os.Exit(1)
}
os.Exit(int(exitStatus))
}
}
os.Exit(1)
}
}
2 changes: 1 addition & 1 deletion cmd/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func NewGraphTable(graph [][]string) table.Table {
return tbl
}

func NewRunningScenariosTable(runningScenarios []orchestratormodels.RunningScenario) table.Table {
func NewRunningScenariosTable(runningScenarios []orchestratormodels.ScenarioContainer) table.Table {
tbl := table.New("Scenario ID", "Scenario Name", "Running Since", "Container Name")
tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt)
for i, v := range runningScenarios {
Expand Down
2 changes: 1 addition & 1 deletion cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func GetProvider(offline bool, providerFactory *factory.ProviderFactory) provide
}

func FetchScenarios(provider provider.ScenarioDataProvider, dataSource string) (*[]string, error) {
scenarios, err := provider.GetScenarios(dataSource)
scenarios, err := provider.GetRegistryImages(dataSource)
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Config struct {
KubeconfigPrefix string `json:"kubeconfig_prefix"`
PodmanDarwinSocketTemplate string `json:"podman_darwin_socket_template"`
PodmanLinuxSocketTemplate string `json:"podman_linux_socket_template"`
ContainerExitStatusPrefix string `json:"container_exit_status_prefix"`
PodmanSocketRoot string `json:"podman_socket_root_linux"`
PodmanRunningState string `json:"podman_running_state"`
DockerSocketRoot string `json:"docker_socket_root"`
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"krknctl_logs": "krknct-log",
"podman_darwin_socket_template": "unix://%s/.local/share/containers/podman/machine/podman.sock",
"podman_linux_socket_template": "unix://run/user/%d/podman/podman.sock",
"container_exit_status_prefix": "!#KRKN_EXIT_STATUS",
"podman_socket_root_linux": "unix://run/podman/podman.sock",
"podman_running_state": "running",
"docker_socket_root": "unix:///var/run/docker.sock",
Expand Down
6 changes: 5 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ func main() {
os.Exit(1)
}

scenarioOrchestrator := scenarioOrchestratorFactory.NewInstance(*detectedRuntime, &config)
scenarioOrchestrator := scenarioOrchestratorFactory.NewInstance(*detectedRuntime)
if scenarioOrchestrator == nil {
fmt.Printf("%s\n", color.New(color.FgHiRed).Sprint("failed to build scenario orchestrator instance"))
os.Exit(1)
}
providerFactory := providerfactory.NewProviderFactory(&config)

cmd.Execute(providerFactory, &scenarioOrchestrator, config)
Expand Down
30 changes: 30 additions & 0 deletions pkg/provider/factory/provide_factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package factory

import (
"github.com/krkn-chaos/krknctl/internal/config"
"github.com/krkn-chaos/krknctl/pkg/provider"
"github.com/krkn-chaos/krknctl/pkg/provider/offline"
"github.com/krkn-chaos/krknctl/pkg/provider/quay"
"github.com/stretchr/testify/assert"
"testing"
)

func TestProviderFactory_NewInstance(t *testing.T) {
typeScenarioQuay := &quay.ScenarioProvider{}
typeScenarioOffline := &offline.ScenarioProvider{}
conf, err := config.LoadConfig()
assert.Nil(t, err)
assert.NotNil(t, conf)

factory := NewProviderFactory(&conf)
assert.NotNil(t, factory)

factoryQuay := factory.NewInstance(provider.Online)
assert.NotNil(t, factoryQuay)
assert.IsType(t, factoryQuay, typeScenarioQuay)

factoryOffline := factory.NewInstance(provider.Offline)
assert.NotNil(t, factoryOffline)
assert.IsType(t, factoryOffline, typeScenarioOffline)

}
6 changes: 3 additions & 3 deletions pkg/provider/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ type ScenarioTag struct {

type ScenarioDetail struct {
ScenarioTag
Title string `json:"title"`
Description string `json:"description"`
Fields []typing.InputField
Title string `json:"title"`
Description string `json:"description"`
Fields []typing.InputField `json:"fields"`
}

func (s *ScenarioDetail) GetFieldByName(name string) *typing.InputField {
Expand Down
106 changes: 106 additions & 0 deletions pkg/provider/models/models_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package models

import (
"encoding/json"
"github.com/krkn-chaos/krknctl/pkg/typing"
"github.com/stretchr/testify/assert"
"testing"
)

func getScenarioDetail(t *testing.T) ScenarioDetail {
data := `
{
"title":"title",
"description":"description",
"fields":[
{
"name":"testfield_1",
"type":"string",
"description":"test field 1",
"variable":"TESTFIELD_1"
},
{
"name":"testfield_2",
"type":"number",
"description":"test field 2",
"variable":"TESTFIELD_2"
},
{
"name":"testfield_3",
"description":"test field 3",
"type":"boolean",
"variable":"TESTFIELD_3"
},
{
"name":"testfield_4",
"description":"test field 4",
"type":"file",
"variable":"TESTFIELD_4",
"mount_path":"/mnt/test"
}
]
}
`
scenarioDetail := ScenarioDetail{}
err := json.Unmarshal([]byte(data), &scenarioDetail)
assert.Nil(t, err)
return scenarioDetail
}

func TestScenarioDetail_GetFieldByEnvVar(t *testing.T) {
scenarioDetail := getScenarioDetail(t)
field1 := scenarioDetail.GetFieldByEnvVar("TESTFIELD_1")
assert.NotNil(t, field1)
assert.Equal(t, (*field1).Type, typing.String)
assert.Equal(t, *((*field1).Description), "test field 1")
assert.Equal(t, *((*field1).Variable), "TESTFIELD_1")
field2 := scenarioDetail.GetFieldByEnvVar("TESTFIELD_2")
assert.NotNil(t, field2)
assert.Equal(t, (*field2).Type, typing.Number)
assert.Equal(t, *((*field2).Description), "test field 2")
assert.Equal(t, *((*field2).Variable), "TESTFIELD_2")
field3 := scenarioDetail.GetFieldByEnvVar("TESTFIELD_3")
assert.NotNil(t, field3)
assert.Equal(t, (*field3).Type, typing.Boolean)
assert.Equal(t, *((*field3).Description), "test field 3")
assert.Equal(t, *((*field3).Variable), "TESTFIELD_3")

nofield := scenarioDetail.GetFieldByName("nofield")
assert.Nil(t, nofield)

}

func TestScenarioDetail_GetFieldByName(t *testing.T) {
scenarioDetail := getScenarioDetail(t)
field1 := scenarioDetail.GetFieldByName("testfield_1")
assert.NotNil(t, field1)
assert.Equal(t, (*field1).Type, typing.String)
assert.Equal(t, *((*field1).Description), "test field 1")
assert.Equal(t, *((*field1).Variable), "TESTFIELD_1")
field2 := scenarioDetail.GetFieldByName("testfield_2")
assert.NotNil(t, field2)
assert.Equal(t, (*field2).Type, typing.Number)
assert.Equal(t, *((*field2).Description), "test field 2")
assert.Equal(t, *((*field2).Variable), "TESTFIELD_2")
field3 := scenarioDetail.GetFieldByName("testfield_3")
assert.NotNil(t, field3)
assert.Equal(t, (*field3).Type, typing.Boolean)
assert.Equal(t, *((*field3).Description), "test field 3")
assert.Equal(t, *((*field3).Variable), "TESTFIELD_3")

nofield := scenarioDetail.GetFieldByName("nofield")
assert.Nil(t, nofield)

}

func TestScenarioDetail_GetFileFieldByMountPath(t *testing.T) {
scenarioDetail := getScenarioDetail(t)
field4 := scenarioDetail.GetFileFieldByMountPath("/mnt/test")
assert.NotNil(t, field4)
assert.Equal(t, (*field4).Type, typing.File)
assert.Equal(t, *((*field4).Description), "test field 4")
assert.Equal(t, *((*field4).Variable), "TESTFIELD_4")

nofield := scenarioDetail.GetFieldByName("/mnt/notfound")
assert.Nil(t, nofield)
}
2 changes: 1 addition & 1 deletion pkg/provider/offline/scenario_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
type ScenarioProvider struct {
}

func (p *ScenarioProvider) GetScenarios(dataSource string) (*[]models.ScenarioTag, error) {
func (p *ScenarioProvider) GetRegistryImages(dataSource string) (*[]models.ScenarioTag, error) {
return nil, errors.New("not yet implemented")
}

Expand Down
Loading

0 comments on commit 856affa

Please sign in to comment.