Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add json list output #23

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 80 additions & 22 deletions cmd/discover_test.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,49 @@
package cmd

import "testing"
import (
"testing"

"github.com/cronitorio/cronitor-cli/lib"
"github.com/stretchr/testify/assert"
)

func TestCreateDefaultNameHasAddCandidateSideEffect(t *testing.T) {
allNameCandidates := map[string]bool{"something": true}
createDefaultName("/var/some/command arg1 arg2", "", 11, false, "localhost", nil, allNameCandidates)

if len(allNameCandidates) == 0 || allNameCandidates["[localhost] /var/some/command arg1 arg2"] != true {
t.Error("Name candidate not added to allNameCandidates")
// createDefaultName("/var/some/command arg1 arg2", "", 11, false, "localhost", nil, allNameCandidates)
line := &lib.Line{
Name: "",
FullLine: "",
LineNumber: 11,
CronExpression: "",
CommandToRun: "/var/some/command arg1 arg2",
Code: "",
RunAs: "",
Mon: lib.Monitor{},
}
crontab := &lib.Crontab{
User: "",
IsUserCrontab: false,
IsSaved: false,
Filename: "",
Lines: []*lib.Line{},
TimezoneLocationName: &lib.TimezoneLocationName{},
UsesSixFieldExpressions: false,
}
}

func TestCreateDefaultName(t *testing.T) {

crontabPath = "/discover/test"
_ = createDefaultName(
line,
crontab,
"localhost",
nil,
allNameCandidates)

allNameCandidates := map[string]bool{
"[localhost] /var/some/command arg1 arg2": true,
"[localhost] cd /var/some/deeply/nested/custom/app/directory/containing/command ; FOO=BAR run-command-here arg1 arg2": true,
}
assert.NotZero(t, allNameCandidates)
assert.True(t, allNameCandidates["[localhost] /var/some/command arg1 arg2"])
}

tables := []struct {
func TestCreateDefaultName(t *testing.T) {
tests := []struct {
caseName string
command string
runAs string
Expand Down Expand Up @@ -48,7 +71,10 @@ func TestCreateDefaultName(t *testing.T) {
false,
"localhost",
nil,
allNameCandidates,
map[string]bool{
"[localhost] /var/some/command arg1 arg2": true,
"[localhost] cd /var/some/deeply/nested/custom/app/directory/containing/command ; FOO=BAR run-command-here arg1 arg2": true,
},
"[localhost] /var/some/command arg1 arg2 L11"},

{"short command with runAs name",
Expand All @@ -68,7 +94,10 @@ func TestCreateDefaultName(t *testing.T) {
false,
"localhost",
nil,
allNameCandidates,
map[string]bool{
"[localhost] /var/some/command arg1 arg2": true,
"[localhost] cd /var/some/deeply/nested/custom/app/directory/containing/command ; FOO=BAR run-command-here arg1 arg2": true,
},
"[localhost] rando /var/some/command arg1 arg2"},

{"stdout redirection is trimmed from name",
Expand Down Expand Up @@ -132,7 +161,7 @@ func TestCreateDefaultName(t *testing.T) {
"/var/some/command arg1 arg2"},

{"auto discover name is created",
"cronitor d3x0c1 cronitor discover /discover/test",
"cronitor d3x0c1 cronitor discover --auto /discover/test",
"",
11,
true,
Expand Down Expand Up @@ -168,7 +197,10 @@ func TestCreateDefaultName(t *testing.T) {
false,
"localhost",
nil,
allNameCandidates,
map[string]bool{
"[localhost] /var/some/command arg1 arg2": true,
"[localhost] cd /var/some/deeply/nested/custom/app/directory/containing/command ; FOO=BAR run-command-here arg1 arg2": true,
},
"[localhost] cd /var/some/deeply/...ctory/containing/command ; FOO=BAR run-command-here arg1 arg2 L11"},

{"long command with runAs",
Expand All @@ -182,10 +214,36 @@ func TestCreateDefaultName(t *testing.T) {
"[localhost] rando cd /var/some/deeply/...ory/containing/command ; FOO=BAR run-command-here arg1 arg2"},
}

for _, table := range tables {
defaultName := createDefaultName(table.command, table.runAs, table.lineNumber, table.isAutoDiscoverCommand, table.hostname, table.excludeFromName, table.allNameCandidates)
if defaultName != table.expected {
t.Errorf("Test case '%s' failed, got: %s, expected: %s.", table.caseName, defaultName, table.expected)
}
for _, tt := range tests {
t.Run(tt.caseName, func(t *testing.T) {
line := &lib.Line{
Name: "",
FullLine: "",
LineNumber: tt.lineNumber,
CronExpression: "",
CommandToRun: tt.command,
Code: "",
RunAs: tt.runAs,
Mon: lib.Monitor{},
}
crontab := &lib.Crontab{
User: tt.runAs,
IsUserCrontab: false,
IsSaved: false,
Filename: "/discover/test",
Lines: []*lib.Line{},
TimezoneLocationName: &lib.TimezoneLocationName{},
UsesSixFieldExpressions: false,
}

got := createDefaultName(
line,
crontab,
tt.hostname,
tt.excludeFromName,
tt.allNameCandidates)

assert.Equal(t, tt.expected, got)
})
}
}
173 changes: 122 additions & 51 deletions cmd/list.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package cmd

import (
"github.com/cronitorio/cronitor-cli/lib"
"bytes"
"encoding/json"
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"os"
"os/user"

"github.com/cronitorio/cronitor-cli/lib"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)

var listCmd = &cobra.Command{
Use: "list <optional path>",
Short: "Search for and list all cron jobs",
Long: `
const listLongDescription = `
Cronitor list scans for cron jobs and displays them in an easy to read table

Example:
Expand All @@ -21,69 +21,140 @@ Example:

$ cronitor list /path/to/crontab
> Instead of the user crontab, list the jobs in a provided a crontab file (or directory of crontabs)
`,
Args: func(cmd *cobra.Command, args []string) error {
`

var printJson bool

var listCmd = &cobra.Command{
Use: "list <optional path>",
Short: "Search for and list all cron jobs",
Long: listLongDescription,
Args: func(cmd *cobra.Command, args []string) error {
return nil
},

Run: func(cmd *cobra.Command, args []string) {
var username string
if u, err := user.Current(); err == nil {
username = u.Username
}
Run: runListCmd,
}

crontabs := []*lib.Crontab{}
commands := []string{}
func init() {
RootCmd.AddCommand(listCmd)

if len(args) > 0 {
// A supplied argument can be a specific file or a directory
if isPathToDirectory(args[0]) {
crontabs = lib.ReadCrontabsInDirectory(username, args[0], crontabs)
} else {
crontabs = lib.ReadCrontabFromFile(username, args[0], crontabs)
}
listCmd.Flags().BoolVarP(&printJson, "json", "j", false, "Print output in json format")
}

func runListCmd(cmd *cobra.Command, args []string) {
var username string
if u, err := user.Current(); err == nil {
username = u.Username
}

crontabs := []*lib.Crontab{}

if len(args) > 0 {
// A supplied argument can be a specific file or a directory
if isPathToDirectory(args[0]) {
crontabs = lib.ReadCrontabsInDirectory(username, args[0], crontabs)
} else {
// Without a supplied argument look at the user crontab, system crontab, and the system drop-in directory
crontabs = lib.ReadCrontabFromFile(username, "", crontabs)
crontabs = lib.ReadCrontabFromFile(username, lib.SYSTEM_CRONTAB, crontabs)
crontabs = lib.ReadCrontabsInDirectory(username, lib.DROP_IN_DIRECTORY, crontabs)
crontabs = lib.ReadCrontabFromFile(username, args[0], crontabs)
}
} else {
// Without a supplied argument look at the user crontab, system crontab, and the system drop-in directory
crontabs = lib.ReadCrontabFromFile(username, "", crontabs)
crontabs = lib.ReadCrontabFromFile(username, lib.SYSTEM_CRONTAB, crontabs)
crontabs = lib.ReadCrontabsInDirectory(username, lib.DROP_IN_DIRECTORY, crontabs)
}

cts := filterEmptyCrontabs(crontabs)

// using a switch here in case we want to add more output formats in the future
switch {
case len(crontabs) == 0:
printWarningText("No crontab files found", false)
case printJson:
printToJson(cts)
default:
printToTable(cts)
}
}

// filterEmptyCrontabs removes any empty crontabs and lines
func filterEmptyCrontabs(crontabs []*lib.Crontab) []*lib.Crontab {
cts := []*lib.Crontab{}

if len(crontabs) == 0 {
printWarningText("No crontab files found", false)
return
for _, crontab := range crontabs {
if len(crontab.Lines) == 0 {
continue
}

fmt.Println()
for _, crontab := range crontabs {
if len(crontab.Lines) == 0 {
ct := &lib.Crontab{
Lines: []*lib.Line{},
IsUserCrontab: crontab.IsUserCrontab,
Filename: crontab.Filename,
User: crontab.User}

for _, line := range crontab.Lines {
if len(line.CommandToRun) == 0 {
continue
}

table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Schedule", "Command"})
table.SetAutoWrapText(true)
table.SetHeaderAlignment(3)
table.SetColMinWidth(0, 17)
table.SetColMinWidth(1, 100)
ct.Lines = append(ct.Lines, line)
}

if len(ct.Lines) > 0 {
ct.Filename = crontab.Filename
ct.User = crontab.User

cts = append(cts, ct)
}
}

return cts
}

for _, line := range crontab.Lines {
if len(line.CommandToRun) == 0 {
continue
}
func printToTable(crontabs []*lib.Crontab) {
for _, crontab := range crontabs {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Schedule", "Command"})
table.SetAutoWrapText(true)
table.SetHeaderAlignment(3)
table.SetColMinWidth(0, 17)
table.SetColMinWidth(1, 100)

table.Append([]string{line.CronExpression, line.CommandToRun})
commands = append(commands, line.CommandToRun)
for _, line := range crontab.Lines {
if len(line.CommandToRun) == 0 {
continue
}

printSuccessText(fmt.Sprintf("Checking %s", crontab.DisplayName()), false)
table.Render()
fmt.Println()
table.Append([]string{line.CronExpression, line.CommandToRun})
}
},

fmt.Println()
printSuccessText(fmt.Sprintf("Checking %s", crontab.DisplayName()), false)
table.Render()
fmt.Println()
}
}

func init() {
RootCmd.AddCommand(listCmd)
func printToJson(crontabs []*lib.Crontab) {
jd, err := MarshalWithoutEscaping(crontabs)
if err != nil {
printErrorText(fmt.Sprintf("Error converting to json: %s", err.Error()), false)
}

fmt.Println(string(jd))
}

// MarshalWithoutEscaping returns the JSON encoding of an interface.
// We use this instead of json.Marshal because we don't want to escape characters for HTML.
func MarshalWithoutEscaping(i interface{}) ([]byte, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
err := encoder.Encode(i)
if err != nil {
return nil, err
}

// Encode adds a newline to the end of the buffer, so we trim it off.
return bytes.TrimRight(buffer.Bytes(), "\n"), nil
}
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (

require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
Expand All @@ -26,17 +27,20 @@ require (
github.com/mattn/go-runewidth v0.0.7 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
gopkg.in/ini.v1 v1.63.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

require (
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.8.4
golang.org/x/text v0.3.8 // indirect
)
Loading