Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
caa3ed2
feat: Add jq filtering support for enhanced data querying
nieomylnieja Jul 30, 2025
2aae692
feat: add alias for jq flag and update tests
nieomylnieja Jul 30, 2025
2d6119b
refactor: simplify SLO filtering and counting commands in README.md
nieomylnieja Jul 30, 2025
842df81
feat: improve error handling and JSON scalar formatting
nieomylnieja Jul 30, 2025
303e4fe
feat: add support for integer conversion in JSON scalar printer
nieomylnieja Jul 30, 2025
3a451c8
refactor: remove jq dependency from GetCmd and integrate it into Printer
nieomylnieja Jul 31, 2025
446d3ad
refactor: update error handling and type usage across multiple modules
nieomylnieja Jul 31, 2025
44fd250
feat: update flag description for jq expression filtering
nieomylnieja Jul 31, 2025
ab5dabd
feat: Add support for custom recipes in sloctl CLI
nieomylnieja Jul 31, 2025
9d1e342
feat: add support for managing and executing built-in recipes
nieomylnieja Aug 1, 2025
97d738b
feat: replace YAML encoding with custom encoder for better control
nieomylnieja Aug 5, 2025
f7f1ad0
Merge remote-tracking branch 'origin/main' into add-recipes
nieomylnieja Aug 5, 2025
e66484a
feat: refactor recipes command structure for better encapsulation
nieomylnieja Aug 6, 2025
d6392e3
feat: enhance JSON handling and streamline recipe commands
nieomylnieja Aug 6, 2025
1a8d6a8
Merge branch 'main' into add-recipes
nieomylnieja Aug 6, 2025
cd20900
Merge branch 'main' into add-recipes
nieomylnieja Aug 8, 2025
79e8e09
feat: allow additional arguments in recipes command
nieomylnieja Aug 8, 2025
652ee4a
refactor(printer): simplify YAML printer by removing unused dependenc…
nieomylnieja Aug 12, 2025
e9130e2
Merge remote-tracking branch 'origin/main' into add-recipes
nieomylnieja Jan 8, 2026
2d0d71b
post merge fixes
nieomylnieja Jan 8, 2026
647f63d
Merge branch 'main' into add-recipes
nieomylnieja Jan 8, 2026
387e3d3
Merge remote-tracking branch 'origin/main' into add-recipes
nieomylnieja Jan 28, 2026
fe1dd5d
remove unneeded omitempty
nieomylnieja Jan 29, 2026
424208e
Merge remote-tracking branch 'origin/main' into add-recipes
nieomylnieja Feb 18, 2026
873b323
build(deps): make govy a direct dependency
nieomylnieja Feb 18, 2026
4200df0
feat(recipes): handle unknown recipe commands
nieomylnieja Feb 19, 2026
e1d79cf
refactor(recipes): improve config path handling and tests
nieomylnieja Feb 23, 2026
5df8624
Merge branch 'main' into add-recipes
nieomylnieja Feb 23, 2026
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
10 changes: 8 additions & 2 deletions cmd/sloctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
// example configuration file can be found in this repository samples/config.toml.
package main

import "github.com/nobl9/sloctl/internal"
import (
"os"

"github.com/nobl9/sloctl/internal"
)

func main() {
internal.Execute()
if err := internal.Execute(); err != nil {
os.Exit(1)
}
}
4 changes: 3 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"golangci",
"gosec",
"govulncheck",
"govy",
"ldflags",
"nobl",
"openslo",
Expand All @@ -60,6 +61,7 @@
"unmarshalling",
"vuln",
"vulns",
"wrapf"
"wrapf",
"yamlenc"
]
}
53 changes: 25 additions & 28 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,37 @@ module github.com/nobl9/sloctl
go 1.25.7

require (
github.com/BurntSushi/toml v1.6.0
github.com/OpenSLO/go-sdk v0.9.1
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/huh v0.8.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/x/exp/teatest v0.0.0-20250930200525-31788bbe6486
github.com/go-playground/validator/v10 v10.30.1
github.com/goccy/go-yaml v1.19.2
github.com/itchyny/gojq v0.12.18
github.com/mark3labs/mcp-go v0.44.0
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
github.com/nobl9/go-yaml v1.0.1
github.com/nobl9/govy v0.23.0
github.com/nobl9/nobl9-go v0.122.2
github.com/nobl9/nobl9-openslo v0.1.3
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.19.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tidwall/sjson v1.2.5
golang.org/x/sync v0.19.0
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go v1.55.8 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-udiff v0.3.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
Expand All @@ -37,55 +45,43 @@ require (
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/itchyny/timefmt-go v0.1.7 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/teambition/rrule-go v1.8.2 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

require (
github.com/BurntSushi/toml v1.6.0
github.com/OpenSLO/go-sdk v0.9.1
github.com/aws/aws-sdk-go v1.55.8 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-yaml v1.19.2
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/nobl9/govy v0.23.0 // indirect
github.com/nobl9/nobl9-openslo v0.1.3
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.10
github.com/teambition/rrule-go v1.8.2 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sys v0.39.0 // indirect
Expand All @@ -95,4 +91,5 @@ require (
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
32 changes: 32 additions & 0 deletions internal/builtin_recipes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
object-names:
description: Fetch object names
args: [get]
example: |
# Fetch SLO object names
sloctl recipe object-names slo
validators:
atLeastArgs: [kind]
jq: .[].metadata.name
prometheus-slos:
args:
- get
- slo
description: Fetch all prometheus SLOs
jq: |
[ .[] | select(
.spec.objectives[] |
(.rawMetric and .rawMetric.query["prometheus"])
or
(.countMetrics and .countMetrics.total["prometheus"])
)]
unique-integrations:
args:
- get
- slo
- -A
description: Display unique integration types for all SLOs
jq: |
[.[] |
first(.. | (.query? // .total?) |
select(type == "object") |
keys[0])] | unique[]
12 changes: 2 additions & 10 deletions internal/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/nobl9/nobl9-go/sdk"
"github.com/nobl9/nobl9-openslo/pkg/openslotonobl9"

"github.com/nobl9/sloctl/internal/jsonbuffer"
"github.com/nobl9/sloctl/internal/printer"
)

Expand Down Expand Up @@ -123,7 +124,7 @@ func (c ConvertCmd) convertOpenSLODefinitions(cmd *cobra.Command) ([]manifest.Ob

func (c ConvertCmd) readOpenSLODefinitionsFromSource(def *sdk.RawDefinition) ([]openslo.Object, error) {
format := openslosdk.FormatYAML
if isJSONBuffer(def.Definition) {
if jsonbuffer.IsJSON(def.Definition) {
format = openslosdk.FormatJSON
}
objects, err := openslosdk.Decode(bytes.NewReader(def.Definition), format)
Expand Down Expand Up @@ -164,15 +165,6 @@ func (c ConvertCmd) setManifestSourceForOpenSLOObject(object []byte, basePath, s
return object, nil
}

var jsonBufferRegex = regexp.MustCompile(`^\s*\[?\s*{`)

// isJSONBuffer scans the provided buffer, looking for an open brace indicating this is JSON.
// While a simple list like ["a", "b", "c"] is still a valid JSON,
// it does not really concern us when processing complex objects.
func isJSONBuffer(buf []byte) bool {
return jsonBufferRegex.Match(buf)
}

var opensloAPIVersionRegex = regexp.MustCompile(`"?apiVersion"?\s*:\s*"?openslo`)

func (c ConvertCmd) filterOpenSLORawDefinitions(definitions []*sdk.RawDefinition) ([]*sdk.RawDefinition, error) {
Expand Down
8 changes: 4 additions & 4 deletions internal/jq/jq.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
"github.com/itchyny/gojq"
)

func NewExpressionRunner(config Config) *ExpressionRunner {
return &ExpressionRunner{config: config}
func NewExpressionRunner(config Config) ExpressionRunner {
return ExpressionRunner{config: config}
}

type ExpressionRunner struct {
Expand All @@ -23,13 +23,13 @@ type Config struct {
Expression string
}

func (e *ExpressionRunner) ShouldRun() bool {
func (e ExpressionRunner) ShouldRun() bool {
return e.config.Expression != ""
}

// Evaluate parses, compiles and runs jq expressions.
// It returns an iterator which yields jq expression result and error (if any).
func (e *ExpressionRunner) Evaluate(v any) (iter.Seq2[any, error], error) {
func (e ExpressionRunner) Evaluate(v any) (iter.Seq2[any, error], error) {
query, err := gojq.Parse(e.config.Expression)
if err != nil {
var parseErr *gojq.ParseError
Expand Down
14 changes: 14 additions & 0 deletions internal/jsonbuffer/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Package jsonbuffer is a small utility package which helps identify JSON buffers.
package jsonbuffer

import "regexp"

var jsonBufferRegex = regexp.MustCompile(`^\s*\[?\s*{`)

// IsJSON scans the provided buffer, looking for an open brace indicating this is JSON.
//
// While a simple list like ["a", "b", "c"] is still a valid JSON,
// it does not really concern us when processing complex objects.
func IsJSON(buf []byte) bool {
return jsonBufferRegex.Match(buf)
}
136 changes: 136 additions & 0 deletions internal/jsonbuffer/json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package jsonbuffer

import "testing"

func TestIsJSON(t *testing.T) {
tests := []struct {
name string
input []byte
expected bool
}{
{
name: "valid json object",
input: []byte(`{"key": "value"}`),
expected: true,
},
{
name: "valid json object with leading whitespace",
input: []byte(` {"key": "value"}`),
expected: true,
},
{
name: "valid json object with tabs",
input: []byte(` {"key": "value"}`),
expected: true,
},
{
name: "valid json array of objects",
input: []byte(`[{"key": "value"}]`),
expected: true,
},
{
name: "valid json array of objects with whitespace",
input: []byte(` [ {"key": "value"}]`),
expected: true,
},
{
name: "valid json array of objects with mixed whitespace",
input: []byte(` [ {"key": "value"}]`),
expected: true,
},
{
name: "simple json array",
input: []byte(`["a", "b", "c"]`),
expected: false,
},
{
name: "simple json array with whitespace",
input: []byte(` ["a", "b", "c"]`),
expected: false,
},
{
name: "empty json object",
input: []byte(`{}`),
expected: true,
},
{
name: "empty json array",
input: []byte(`[]`),
expected: false,
},
{
name: "plain text",
input: []byte(`this is not json`),
expected: false,
},
{
name: "empty buffer",
input: []byte(``),
expected: false,
},
{
name: "only whitespace",
input: []byte(` `),
expected: false,
},
{
name: "json string",
input: []byte(`"hello world"`),
expected: false,
},
{
name: "json number",
input: []byte(`42`),
expected: false,
},
{
name: "json boolean",
input: []byte(`true`),
expected: false,
},
{
name: "json null",
input: []byte(`null`),
expected: false,
},
{
name: "malformed json starting with brace",
input: []byte(`{invalid json`),
expected: true,
},
{
name: "nested json object",
input: []byte(`{"outer": {"inner": "value"}}`),
expected: true,
},
{
name: "complex json object",
input: []byte(`{"name": "test", "items": [{"id": 1}, {"id": 2}]}`),
expected: true,
},
{
name: "array starting with object",
input: []byte(`[{"first": true}, "second", 3]`),
expected: true,
},
{
name: "newlines and spaces",
input: []byte(" \n \t {\n \"key\": \"value\"\n}"),
expected: true,
},
{
name: "newlines before array with object",
input: []byte(" \n [\n {\"key\": \"value\"}\n]"),
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsJSON(tt.input)
if result != tt.expected {
t.Errorf("IsJSON(%q) = %v, expected %v", string(tt.input), result, tt.expected)
}
})
}
}
Loading
Loading