From 5bfa3e6f60d5869317ba1c469653d4eb31500268 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Thu, 21 Nov 2024 15:07:57 +0100 Subject: [PATCH 1/5] switch to openslo sdk --- Makefile | 2 +- examples/definitions/service.yaml | 7 - examples/definitions/slo.yaml | 27 - examples/example.go | 24 - go.mod | 25 +- go.sum | 49 +- internal/cli/convert.go | 89 - internal/cli/convert_test.go | 356 ---- internal/cli/flags.go | 20 + internal/cli/fmt.go | 32 +- internal/cli/root.go | 1 - internal/cli/root_test.go | 1 + internal/cli/validate.go | 15 +- internal/convert/convert.go | 1127 ------------- internal/convert/convert_test.go | 1452 ----------------- .../files/discover.go | 31 +- .../files/discover_test.go | 6 +- internal/files/format.go | 54 + internal/files/format_test.go | 119 ++ internal/files/reader.go | 91 ++ .../files/reader_test.go | 85 +- {pkg/yamlutil => internal/files}/test-input | 0 .../files}/testfiles/a/a1.yml | 0 .../files}/testfiles/a/a2.yml | 0 .../files}/testfiles/a/b/b1.yml | 0 .../files}/testfiles/a/b/b2.yml | 0 .../files}/testfiles/aa/aa1.yml | 0 .../files}/testfiles/x.yml | 0 .../files}/testfiles/y.yaml | 0 internal/fmt/fmt.go | 70 - internal/fmt/fmt_test.go | 94 -- internal/manifest/nobl9/objects.go | 35 - internal/manifest/nobl9/v1alpha/objects.go | 380 ----- internal/pathutil/pathutil.go | 13 - pkg/manifest/models.go | 35 - pkg/manifest/v1/alerts.go | 107 -- pkg/manifest/v1/indicator.go | 137 -- pkg/manifest/v1/indicator_test.go | 117 -- pkg/manifest/v1/objective.go | 70 - pkg/manifest/v1/objects.go | 159 -- pkg/manifest/v1alpha/objects.go | 161 -- pkg/validate/validate.go | 104 -- pkg/validate/validate_test.go | 283 ---- pkg/yamlutil/yamlutil.go | 137 -- test/cli/convert.bats | 61 - test/v1alpha/invalid-service.yaml | 4 +- 46 files changed, 361 insertions(+), 5219 deletions(-) delete mode 100644 examples/definitions/service.yaml delete mode 100644 examples/definitions/slo.yaml delete mode 100644 examples/example.go delete mode 100644 internal/cli/convert.go delete mode 100644 internal/cli/convert_test.go create mode 100644 internal/cli/flags.go delete mode 100644 internal/convert/convert.go delete mode 100644 internal/convert/convert_test.go rename pkg/discoverfiles/discoverfiles.go => internal/files/discover.go (64%) rename pkg/discoverfiles/discoverfiles_test.go => internal/files/discover_test.go (94%) create mode 100644 internal/files/format.go create mode 100644 internal/files/format_test.go create mode 100644 internal/files/reader.go rename pkg/yamlutil/yamlutil_test.go => internal/files/reader_test.go (52%) rename {pkg/yamlutil => internal/files}/test-input (100%) rename {pkg/discoverfiles => internal/files}/testfiles/a/a1.yml (100%) rename {pkg/discoverfiles => internal/files}/testfiles/a/a2.yml (100%) rename {pkg/discoverfiles => internal/files}/testfiles/a/b/b1.yml (100%) rename {pkg/discoverfiles => internal/files}/testfiles/a/b/b2.yml (100%) rename {pkg/discoverfiles => internal/files}/testfiles/aa/aa1.yml (100%) rename {pkg/discoverfiles => internal/files}/testfiles/x.yml (100%) rename {pkg/discoverfiles => internal/files}/testfiles/y.yaml (100%) delete mode 100644 internal/fmt/fmt.go delete mode 100644 internal/fmt/fmt_test.go delete mode 100644 internal/manifest/nobl9/objects.go delete mode 100644 internal/manifest/nobl9/v1alpha/objects.go delete mode 100644 internal/pathutil/pathutil.go delete mode 100644 pkg/manifest/models.go delete mode 100644 pkg/manifest/v1/alerts.go delete mode 100644 pkg/manifest/v1/indicator.go delete mode 100644 pkg/manifest/v1/indicator_test.go delete mode 100644 pkg/manifest/v1/objective.go delete mode 100644 pkg/manifest/v1/objects.go delete mode 100644 pkg/manifest/v1alpha/objects.go delete mode 100644 pkg/validate/validate.go delete mode 100644 pkg/validate/validate_test.go delete mode 100644 pkg/yamlutil/yamlutil.go delete mode 100644 test/cli/convert.bats diff --git a/Makefile b/Makefile index db7837c..df13209 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ VERSION := $(shell git describe --tags) .PHONY: build build: - go build -ldflags="-X 'main.version=${VERSION}'" -o bin/oslo + go build -ldflags="-X 'main.version=${VERSION}'" -o bin/oslo ./cmd/oslo/ .PHONY: install install: run/tests build diff --git a/examples/definitions/service.yaml b/examples/definitions/service.yaml deleted file mode 100644 index 6a884c3..0000000 --- a/examples/definitions/service.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: openslo/v1alpha -kind: Service -metadata: - name: my-rad-service - displayName: My Rad Service -spec: - description: This is a great description of an even better service. diff --git a/examples/definitions/slo.yaml b/examples/definitions/slo.yaml deleted file mode 100644 index f3cfaf9..0000000 --- a/examples/definitions/slo.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: openslo/v1alpha -kind: SLO -metadata: - displayName: Ratio - name: ratio -spec: - budgetingMethod: Timeslices - description: A great description of a ratio based SLO - objectives: - - ratioMetrics: - good: - source: prometheus - queryType: promql - query: latency_west_c7{code="ALL",instance="localhost:3000",job="prometheus",service="globalaccount"} - counter: true - total: - source: prometheus - queryType: promql - query: latency_west_c7{code="ALL",instance="localhost:3000",job="prometheus",service="globalaccount"} - displayName: painful - target: 0.98 - value: 1 - service: my-test-service - timeWindows: - - count: 28 - isRolling: true - unit: Day diff --git a/examples/example.go b/examples/example.go deleted file mode 100644 index 892eb4d..0000000 --- a/examples/example.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "fmt" - "path/filepath" - "runtime" - - "github.com/OpenSLO/oslo/pkg/discoverfiles" - "github.com/OpenSLO/oslo/pkg/validate" -) - -func main() { - _, filename, _, _ := runtime.Caller(0) - - filePaths := []string{filepath.Join(filepath.Dir(filename), "definitions")} - discoveredFilePaths, err := discoverfiles.DiscoverFilePaths(filePaths, true) - if err != nil { - panic(err) - } - if err = validate.Files(discoveredFilePaths); err != nil { - panic(err) - } - fmt.Println("Valid!") -} diff --git a/go.mod b/go.mod index 8f67f33..771cdcc 100644 --- a/go.mod +++ b/go.mod @@ -1,30 +1,23 @@ module github.com/OpenSLO/oslo -go 1.20 +go 1.23 require ( - github.com/fatih/color v1.17.0 - github.com/go-playground/validator/v10 v10.22.1 - github.com/hashicorp/go-multierror v1.1.1 + github.com/OpenSLO/OpenSLO v1.0.1-0.20241121000037-aa3d4d584dce github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 - gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/nobl9/govy v0.11.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/tools v0.24.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 199a576..b5efac8 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,15 @@ +github.com/OpenSLO/OpenSLO v1.0.1-0.20241121000037-aa3d4d584dce h1:kp1jD2ca4uekUdKdUVzvkXc1jzgzZ1nzuMw3LDUuWBA= +github.com/OpenSLO/OpenSLO v1.0.1-0.20241121000037-aa3d4d584dce/go.mod h1:mRxytwzxC8dkmF8LXrRiwuJ/doSpkYyB3xPhJ/DHvXY= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= -github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/nobl9/govy v0.11.0 h1:8Z+tj/eEz9YcFetg53K/jb4FYKB1gk6AiDtUq6NOrEg= +github.com/nobl9/govy v0.11.0/go.mod h1:O+xSiKwZ6gs/orRvH5qLkfkgyT7CkuXprRIq3C5uNXQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -34,17 +19,17 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/cli/convert.go b/internal/cli/convert.go deleted file mode 100644 index a143a3b..0000000 --- a/internal/cli/convert.go +++ /dev/null @@ -1,89 +0,0 @@ -/* -Package convert provides a command to convert from openslo to other formats. - -# Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package cli - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/OpenSLO/oslo/internal/convert" - "github.com/OpenSLO/oslo/pkg/discoverfiles" -) - -// NewConvertCmd returns a new command for formatting a file. -func NewConvertCmd() *cobra.Command { - var passedFilePaths []string - var recursive bool - var format string - var project string - - convertCmd := &cobra.Command{ - Use: "convert", - Short: "Converts from OpenSLO to another format.", - Long: `Converts from OpenSLO to another format. - -Supported output formats are: -- nobl9 - -Multiple files can be converted by specifying them as arguments: - - oslo convert -f file1.yaml -f file2.yaml -o nobl9 - -You can also convert a directory of files: - - oslo convert -d path/to/directory -o nobl9 - -The output is written to standard output. If you want to write to a file, you can redirect the output: - - oslo convert -f file.yaml -o nobl9 > output.yaml -`, - Args: cobra.MinimumNArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - discoveredFilePaths, err := discoverfiles.DiscoverFilePaths(passedFilePaths, recursive) - if err != nil { - return err - } - discoveredFilePaths = convert.RemoveDuplicates(discoveredFilePaths) - switch format { - case "nobl9": - if err := convert.Nobl9(cmd.OutOrStdout(), discoveredFilePaths, project); err != nil { - return err - } - default: - return fmt.Errorf("unsupported format: %s", format) - } - return nil - }, - } - - discoverfiles.RegisterFileRelatedFlags(convertCmd, &passedFilePaths, &recursive) - convertCmd.Flags().StringVarP(&format, "output", "o", "", "The output format to convert to.") - if err := convertCmd.MarkFlagRequired("output"); err != nil { - panic(err) - } - convertCmd.Flags().StringVarP( - &project, - "project", - "p", - "default", - "Used for nobl9 output. What project to assign the resources to.", - ) - - return convertCmd -} diff --git a/internal/cli/convert_test.go b/internal/cli/convert_test.go deleted file mode 100644 index 2ab5898..0000000 --- a/internal/cli/convert_test.go +++ /dev/null @@ -1,356 +0,0 @@ -/* -Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package cli - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewConvertCmd(t *testing.T) { - t.Parallel() - tests := []struct { - name string - args []string - wantOut string - wantErr bool - }{ - { - name: "Single file - Service", - args: []string{ - "-o", "nobl9", - "-f", "../../test/v1/service/service.yaml", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: Service -metadata: - name: my-rad-service - displayName: My Rad Service - project: default -spec: - description: This is a great description of an even better service. -`, - wantErr: false, - }, - { - name: "Single file - Service - labels", - args: []string{ - "-o", "nobl9", - "-f", "../../test/v1/service/service-with-labels.yaml", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: Service -metadata: - name: my-rad-service - displayName: My Rad Service - project: default - labels: - costCentre: - - project1 - serviceTier: - - tier-1 - team: - - identity - userImpacting: - - "true" -spec: - description: This is a great description of an even better service. -`, - wantErr: false, - }, - { - name: "Single file - Service - non-default project", - args: []string{ - "-o", "nobl9", - "-p", "my-project", - "-f", "../../test/v1/service/service.yaml", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: Service -metadata: - name: my-rad-service - displayName: My Rad Service - project: my-project -spec: - description: This is a great description of an even better service. -`, - wantErr: false, - }, - { - name: "Single file - Alert Policy", - args: []string{ - "-o", "nobl9", - "-f", "../../test/v1/alert-policy/alert-policy.yaml", - }, - wantOut: ``, - wantErr: true, - }, - { - name: "Alert Policy - Separate Condition", - args: []string{ - "-o", "nobl9", - "-f", "../../test/v1/alert-policy/alert-policy.yaml", - "-f", "../../test/v1/alert-condition/alert-condition.yaml", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: AlertPolicy -metadata: - name: AlertPolicy - displayName: Alert Policy - project: default -spec: - description: Alert policy for cpu usage breaches, notifies on-call devops via email - severity: Medium - coolDown: 5m - conditions: - - measurement: averageBurnRate - value: 24 - lastsFor: 3m - op: gt - alertMethods: [] -`, - wantErr: false, - }, - { - name: "Alert Policy - Inline Condition", - args: []string{ - "-o", "nobl9", - "-f", "../../test/v1/alert-policy/alert-policy-inline-cond.yaml", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: AlertPolicy -metadata: - name: AlertPolicy - displayName: Alert Policy - project: default -spec: - description: Alert policy for cpu usage breaches, notifies on-call devops via email - severity: Medium - coolDown: 5m - conditions: - - measurement: averageBurnRate - value: 2 - lastsFor: 5m - op: gt - alertMethods: [] -`, - wantErr: false, - }, - { - name: "Alert Policy - Multiple Condition", - args: []string{ - "-o", "nobl9", - "-f", "../../test/v1/alert-policy/alert-policy-many-cond.yaml", - "-f", "../../test/v1/alert-condition/alert-condition.yaml", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: AlertPolicy -metadata: - name: AlertPolicy - displayName: Alert Policy - project: default -spec: - description: Alert policy for cpu usage breaches, notifies on-call devops via email - severity: Medium - coolDown: 5m - conditions: - - measurement: averageBurnRate - value: 24 - lastsFor: 3m - op: gt - - measurement: averageBurnRate - value: 24 - lastsFor: 3m - op: gt - - measurement: averageBurnRate - value: 24 - lastsFor: 3m - op: gt - - measurement: averageBurnRate - value: 24 - lastsFor: 3m - op: gt - - measurement: averageBurnRate - value: 24 - lastsFor: 3m - op: gt - - measurement: averageBurnRate - value: 24 - lastsFor: 3m - op: gt - - measurement: averageBurnRate - value: 24 - lastsFor: 3m - op: gt - - measurement: averageBurnRate - value: 24 - lastsFor: 3m - op: gt - - measurement: averageBurnRate - value: 24 - lastsFor: 3m - op: gt - alertMethods: [] -`, - wantErr: false, - }, - { - name: "Alert Policy - No Matching Condition", - args: []string{ - "-o", "nobl9", - "-f", "../../test/v1/alert-policy/alert-policy.yaml", - "-f", "../../test/v1/alert-condition/alert-condition-invalid-name.yaml", - }, - wantOut: ``, - wantErr: true, - }, - { - name: "Duplicate file", - args: []string{ - "-o", "nobl9", - "-f", "../../test/v1/service/service.yaml", - "-f", "../../test/v1/service/service.yaml", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: Service -metadata: - name: my-rad-service - displayName: My Rad Service - project: default -spec: - description: This is a great description of an even better service. -`, - wantErr: false, - }, - { - name: "Single SLO", - args: []string{ - "-o", "nobl9", - "-f", "../../test/v1/slo/slo-no-indicatorref-rolling-alerts.yaml", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: SLO -metadata: - name: TestSLO - displayName: Test SLO - project: default -spec: - description: This is a great description - indicator: - metricSource: - project: default - name: datadog-datasource - kind: Agent - budgetingMethod: Occurrences - objectives: - - displayName: Foo Total Errors - value: 1 - target: 0.98 - countMetrics: - incremental: true - good: - datadog: - query: sum:trace.http.request.hits.by_http_status{http.status_code:200}.as_count() - total: - datadog: - query: sum:trace.http.request.hits.by_http_status{*}.as_count() - op: lt - service: TheServiceName - timeWindows: - - unit: Month - count: 1 - isRolling: true - alertPolicies: - - FooAlertPolicy -`, - wantErr: false, - }, - { - name: "SLO with labels", - args: []string{ - "-o", "nobl9", - "-f", "../../test/v1/slo/slo-with-labels.yaml", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: SLO -metadata: - name: labeled-slo - displayName: Labeled SLO - project: default - labels: - inline: - - test - multiple: - - one - - two - single: - - green -spec: - description: "" - indicator: - metricSource: - project: default - name: Changeme - kind: Agent - budgetingMethod: Occurrences - objectives: - - displayName: Keeping Up Appearances - value: 10 - target: 0.98 - op: gte - service: tv-show - timeWindows: - - unit: Day - count: 28 - isRolling: true - alertPolicies: [] -`, - wantErr: false, - }, - } - for _, tt := range tests { - tt := tt // Parallel testing - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - actual := new(bytes.Buffer) - root := NewConvertCmd() - root.SetOut(actual) - root.SetErr(actual) - root.SetArgs(tt.args) - - if err := root.Execute(); (err != nil) != tt.wantErr { - t.Errorf("Error executing root command: %s", err) - return - } - - // if tt.wantErr is false assert that the output is correct - if !tt.wantErr { - assert.Equal(t, tt.wantOut, actual.String()) - } - }) - } -} diff --git a/internal/cli/flags.go b/internal/cli/flags.go new file mode 100644 index 0000000..7caf87e --- /dev/null +++ b/internal/cli/flags.go @@ -0,0 +1,20 @@ +package cli + +import "github.com/spf13/cobra" + +// registerFileRelatedFlags registers flags --file | -f and --recursive | -R for command +// passed as the argument and make them required. +func registerFileRelatedFlags(cmd *cobra.Command, filePaths *[]string, recursive *bool) { + const fileFlag = "file" + cmd.Flags().StringArrayVarP( + filePaths, fileFlag, "f", []string{}, + "The file(s) that contain the configurations.", + ) + if err := cmd.MarkFlagRequired(fileFlag); err != nil { + panic(err) + } + cmd.Flags().BoolVarP( + recursive, "recursive", "R", false, + "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.", //nolint:lll + ) +} diff --git a/internal/cli/fmt.go b/internal/cli/fmt.go index 28780eb..fb32883 100644 --- a/internal/cli/fmt.go +++ b/internal/cli/fmt.go @@ -18,29 +18,47 @@ limitations under the License. package cli import ( + "fmt" + "github.com/spf13/cobra" - "github.com/OpenSLO/oslo/internal/fmt" - "github.com/OpenSLO/oslo/pkg/discoverfiles" + "github.com/OpenSLO/OpenSLO/pkg/openslosdk" + "github.com/OpenSLO/oslo/internal/files" ) // NewFmtCmd returns a new command for formatting a file. func NewFmtCmd() *cobra.Command { - var passedFilePaths []string - var recursive bool + var ( + passedFilePaths []string + recursive bool + output string + ) fmtCmd := &cobra.Command{ Use: "fmt", Short: "Formats the provided input into the standard format.", Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { - discoveredFilePaths, err := discoverfiles.DiscoverFilePaths(passedFilePaths, recursive) + discoveredFilePaths, err := files.Discover(passedFilePaths, recursive) if err != nil { return err } - return fmt.Files(cmd.OutOrStdout(), discoveredFilePaths) + var format openslosdk.ObjectFormat + switch output { + case "json": + format = openslosdk.FormatJSON + case "yaml": + format = openslosdk.FormatYAML + default: + return fmt.Errorf("invalid output format: %s", output) + } + return files.Format(cmd.OutOrStdout(), format, discoveredFilePaths) }, } - discoverfiles.RegisterFileRelatedFlags(fmtCmd, &passedFilePaths, &recursive) + registerFileRelatedFlags(fmtCmd, &passedFilePaths, &recursive) + fmtCmd.Flags().StringVarP( + &output, "ouput", "o", "yaml", + "The output format, one of [json, yaml].", + ) return fmtCmd } diff --git a/internal/cli/root.go b/internal/cli/root.go index 36ce5af..5eb24b0 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -36,7 +36,6 @@ func newRootCmd(version string) *cobra.Command { rootCmd.AddCommand(NewValidateCmd()) rootCmd.AddCommand(NewFmtCmd()) - rootCmd.AddCommand(NewConvertCmd()) return rootCmd } diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 4d2cf45..f8ea427 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -63,6 +63,7 @@ Usage: Flags: -f, --file stringArray The file(s) that contain the configurations. -h, --help help for fmt + -o, --ouput string The output format, one of [json, yaml]. (default "yaml") -R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory. `, wantErr: false, diff --git a/internal/cli/validate.go b/internal/cli/validate.go index c76a5c4..42c0d31 100644 --- a/internal/cli/validate.go +++ b/internal/cli/validate.go @@ -20,8 +20,7 @@ import ( "github.com/spf13/cobra" - "github.com/OpenSLO/oslo/pkg/discoverfiles" - "github.com/OpenSLO/oslo/pkg/validate" + "github.com/OpenSLO/oslo/internal/files" ) // NewValidateCmd returns a new cobra.Command for the validate command. @@ -35,17 +34,23 @@ func NewValidateCmd() *cobra.Command { Long: `Validates your yaml file against the OpenSLO spec.`, Args: cobra.MinimumNArgs(0), RunE: func(cmd *cobra.Command, args []string) error { - discoveredFilePaths, err := discoverfiles.DiscoverFilePaths(passedFilePaths, recursive) + discoveredFilePaths, err := files.Discover(passedFilePaths, recursive) if err != nil { return err } - if err := validate.Files(discoveredFilePaths); err != nil { + objects, err := files.ReadObjects(discoveredFilePaths) + if err != nil { return err } + for _, obj := range objects { + if err = obj.Validate(); err != nil { + return err + } + } fmt.Println("Valid!") return nil }, } - discoverfiles.RegisterFileRelatedFlags(validateCmd, &passedFilePaths, &recursive) + registerFileRelatedFlags(validateCmd, &passedFilePaths, &recursive) return validateCmd } diff --git a/internal/convert/convert.go b/internal/convert/convert.go deleted file mode 100644 index 8647efc..0000000 --- a/internal/convert/convert.go +++ /dev/null @@ -1,1127 +0,0 @@ -/* -Package convert provides a command to convert from openslo to other formats. - -# Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package convert - -import ( - "errors" - "fmt" - "io" - "os" - "reflect" - "strconv" - "strings" - - "github.com/fatih/color" - "gopkg.in/yaml.v3" - - nobl9manifest "github.com/OpenSLO/oslo/internal/manifest/nobl9" - nobl9v1alpha "github.com/OpenSLO/oslo/internal/manifest/nobl9/v1alpha" - "github.com/OpenSLO/oslo/pkg/manifest" - v1 "github.com/OpenSLO/oslo/pkg/manifest/v1" - "github.com/OpenSLO/oslo/pkg/yamlutil" -) - -// RemoveDuplicates to remove duplicate string from a slice. -func RemoveDuplicates(s []string) []string { - result := make([]string, 0, len(s)) - m := make(map[string]struct{}) - - for _, v := range s { - if _, exists := m[v]; !exists { - m[v] = struct{}{} - result = append(result, v) - } - } - - return result -} - -func getParsedObjects(filenames []string) (parsed []manifest.OpenSLOKind, err error) { - for _, filename := range filenames { - // Get the file contents. - content, err := yamlutil.ReadConf(filename) - if err != nil { - return nil, fmt.Errorf("issue reading content: %w", err) - } - - // Parse the byte arrays to OpenSLOKind objects. - var p []manifest.OpenSLOKind - p, err = yamlutil.Parse(content, filename) - if err != nil { - return nil, fmt.Errorf("issue parsing content: %w", err) - } - - parsed = append(parsed, p...) - } - return parsed, nil -} - -// getObjectsByKind function that that returns an object by Kind from a list of OpenSLOKinds. -func getObjectByKind(kind string, objects []manifest.OpenSLOKind) []manifest.OpenSLOKind { - var found []manifest.OpenSLOKind - for _, o := range objects { - if o.Kind() == kind { - found = append(found, o) - } - } - return found -} - -//------------------------------------------------------------------------------ -// -// Nobl9 Conversion -// - -const ( - n9KindAnnotation = "nobl9.com/indicator-kind" - - kindAgent string = "Agent" - kindDirect string = "Direct" -) - -/* -Nobl9 converts the provided file to Nobl9 yaml. - -Nobl9 currently supports the following kinds: -- AlertMethod -- AlertPolicy -- Annotation -- DataExport -- Objective -- Project -- RoleBinding -- Service -- SLO - -However OpenSLO doesn't support Annotation, DataExport, Project or RoleBinding so we can only convert to -AlertMethod, AlertPolicy, Objective, Service and SLO. -*/ -func Nobl9(out io.Writer, filenames []string, project string) error { - var rval []interface{} - // These are used to track the names of the objects that - // we have, in order to warn the user if they need to add - // the objects to Nobl9 separately. - var serviceNames []string - var alertPolicyNames []string - - parsed, err := getParsedObjects(filenames) - if err != nil { - return fmt.Errorf("issue parsing content: %w", err) - } - - // Get the service objects. - if err = getN9ServiceObjects(parsed, &rval, &serviceNames, project); err != nil { - return fmt.Errorf("issue getting service objects: %w", err) - } - - // Get the alertPolicy objects. - if err = getN9AlertPolicyObjects(parsed, &rval, &alertPolicyNames, project); err != nil { - return fmt.Errorf("issue getting alertPolicy objects: %w", err) - } - - // Get the SLO objects. - if err = getN9SLObjects(parsed, &rval, serviceNames, alertPolicyNames, project); err != nil { - return fmt.Errorf("issue getting SLO objects: %w", err) - } - - // Print out all of our objects. - for _, s := range rval { - err = printYaml(out, s) - if err != nil { - return fmt.Errorf("issue printing content: %w", err) - } - } - - return nil -} - -// Constructs Nobl9 SLO objects from our list of OpenSLOKinds. -func getN9SLObjects( - parsed []manifest.OpenSLOKind, - rval *[]interface{}, - serviceNames, - alertPolicies []string, - project string, -) error { - objects := getObjectByKind("SLO", parsed) - - if len(objects) == 0 { - return nil - } - - // For each SLO object, create a Nobl9 SLO object. - for _, slo := range objects { - s, ok := slo.(v1.SLO) - if !ok { - return errors.New("the convert command is only supported for apiVersion 'openslo/v1'") - } - - // Check that the service name is in the list of service names, and warn the user if it isn't. - if s.Spec.Service != "" && !stringInSlice(s.Spec.Service, serviceNames) { - _ = printWarning( - fmt.Sprintf( - "Service %s is not in the list of services for SLO %s. "+ - "You will need verify that it is present in Nobl9 before applying.", - s.Spec.Service, - s.Metadata.DisplayName, - ), - ) - } - - // Check that the alert policy name is in the list of alert policies, and warn the user if it isn't. - for _, ap := range s.Spec.AlertPolicies { - if !stringInSlice(ap, alertPolicies) { - _ = printWarning( - fmt.Sprintf( - "Alert policy %s is not in the list of alert policies for SLO %s. "+ - "You will need verify that it is present in Nobl9 before applying.", - ap, - s.Metadata.DisplayName, - ), - ) - } - } - - tw, err := getN9TimeWindow(s.Spec.TimeWindow) - if err != nil { - return fmt.Errorf("issue getting time window: %w", err) - } - - // Get the Objectives, aka Thresholds - indicator := getN9SLISpec(s.Spec, parsed) - thresholds, err := getN9Thresholds(s.Spec.Objectives, indicator) - if err != nil { - return fmt.Errorf("issue getting thresholds: %w", err) - } - - indicatorMetadata := getN9IndicatorMetadata(s.Spec) - n9Indicator := getN9Indicator(indicator, indicatorMetadata, project) - - *rval = append(*rval, nobl9v1alpha.SLO{ - ObjectHeader: getN9ObjectHeader("SLO", s.Metadata.Name, s.Metadata.DisplayName, project, s.Metadata.Labels), - Spec: nobl9v1alpha.SLOSpec{ - Indicator: n9Indicator, - Description: s.Spec.Description, - BudgetingMethod: s.Spec.BudgetingMethod, - Service: s.Spec.Service, - AlertPolicies: s.Spec.AlertPolicies, - TimeWindows: tw, - Thresholds: thresholds, - }, - }) - } - - return nil -} - -func getN9MetricSourceName(msh v1.MetricSourceHolder) (string, bool) { - name := msh.MetricSource.MetricSourceRef - if name != "" { - return name, true - } - - return "", false -} - -func getN9IndicatorMetadata(sloSpec v1.SLOSpec) (metadata v1.Metadata) { - if sloSpec.Indicator != nil { - return sloSpec.Indicator.Metadata - } - return metadata -} - -// returns nobl9 indicator base on discovery and assumptions. -// -//nolint:gocognit,cyclop -func getN9Indicator(sliSpec v1.SLISpec, metadata v1.Metadata, project string) nobl9v1alpha.Indicator { - // check to make sure we have a project, and use default if not - metricSourceProject := "default" - if project != "" { - metricSourceProject = project - } - - // check to make sure that we have an indicator - //nolint:nestif - if !reflect.ValueOf(sliSpec).IsZero() { - var name string - // check to see if we have a ThresholdMetric, and use that to set the MetricSource - if !reflect.ValueOf(sliSpec.ThresholdMetric).IsZero() { - if n, ok := getN9MetricSourceName(*sliSpec.ThresholdMetric); ok { - name = n - } else { - _ = printWarning( - "Threshold MetricSource was set but the MetricSourceRef was not, " + - "so setting the name to Changeme for the MetricSource. Please update accordingly", - ) - name = "Changeme" - } - } - - // check to see if we have a RawMetric and use that to set the MetricSource - if !reflect.ValueOf(sliSpec.RatioMetric).IsZero() { - // try all the possible MetricSourceHolders that a ratio metric might have - if !reflect.ValueOf(sliSpec.RatioMetric.Good).IsZero() { - if n, ok := getN9MetricSourceName(*sliSpec.RatioMetric.Good); ok { - name = n - } - } - if !reflect.ValueOf(sliSpec.RatioMetric.Bad).IsZero() { - if n, ok := getN9MetricSourceName(*sliSpec.RatioMetric.Bad); ok { - name = n - } - } - if !reflect.ValueOf(sliSpec.RatioMetric.Total).IsZero() { - if n, ok := getN9MetricSourceName(sliSpec.RatioMetric.Total); ok { - name = n - } - } - } - if name != "" { - return nobl9v1alpha.Indicator{ - MetricSource: nobl9v1alpha.MetricSourceSpec{ - Project: metricSourceProject, - Name: name, - Kind: getKindFromAnnotations(metadata), - }, - } - } - } - - // Default return. This handles any issues we might have found - _ = printWarning( - "No indicator found, or missing either a ThresholdMetric or RatioMetric, " + - "so using a default Indicator and MetricSource. Please update accordingly.", - ) - return nobl9v1alpha.Indicator{ - MetricSource: nobl9v1alpha.MetricSourceSpec{ - Project: metricSourceProject, - Name: "Changeme", - Kind: getKindFromAnnotations(metadata), - }, - } -} - -func getKindFromAnnotations(metadata v1.Metadata) string { - if value, ok := metadata.Annotations[n9KindAnnotation]; ok { - switch strings.ToLower(value) { - case "direct": - return kindDirect - case "agent": - return kindAgent - } - } - _ = printWarning( - "We set as default MetricSource Kind: Agent if you want to change it to Direct use can use annotation " + - n9KindAnnotation, - ) - return kindAgent -} - -// Return a list of nobl9v1alpha.Thresholds from a list of v1.Objectives. -func getN9Thresholds(o []v1.Objective, indicator v1.SLISpec) ([]nobl9v1alpha.Threshold, error) { - var t []nobl9v1alpha.Threshold //nolint:prealloc - for _, v := range o { - v := v // local copy - // if the operator isn't nil, then assign it, otherwise use default - var operator *string - defaultOp := "lt" - if v.Op != "" { - operator = &v.Op - } else { - operator = &defaultOp - } - - // if v.Value is set use that, otherwise default to 1 - var value float64 - if v.Value != 0 { - value = v.Value - } else { - value = 1 - } - - // start building our object - th := nobl9v1alpha.Threshold{ - ThresholdBase: nobl9v1alpha.ThresholdBase{ - DisplayName: v.DisplayName, - Value: value, - }, - Operator: operator, - BudgetTarget: &v.Target, - } - - // Get CountMetrics if we have a ratioMetric - if indicator.RatioMetric != nil { - c, err := getN9CountMetrics(*indicator.RatioMetric) - if err != nil { - return nil, fmt.Errorf("issue getting count metrics: %w", err) - } - th.CountMetrics = &c - } - - // Get thresholdMetrics - if indicator.ThresholdMetric != nil { - r, err := getN9RawMetrics(*indicator.ThresholdMetric) - if err != nil { - return nil, fmt.Errorf("issue getting raw metrics: %w", err) - } - th.RawMetric = &r - } - - t = append(t, th) - } - return t, nil -} - -func getN9RawMetrics(r v1.MetricSourceHolder) (nobl9v1alpha.RawMetricSpec, error) { - raw, err := getN9MetricSource(r.MetricSource) - if err != nil { - return nobl9v1alpha.RawMetricSpec{}, fmt.Errorf("issue getting raw metric source: %w", err) - } - - rm := nobl9v1alpha.RawMetricSpec{ - MetricQuery: &raw, - } - - return rm, nil -} - -func getN9CountMetrics(r v1.RatioMetric) (nobl9v1alpha.CountMetricsSpec, error) { - // Error if Bad is not nil, since Nobl9 doesn't support it. - if r.Bad != nil { - return nobl9v1alpha.CountMetricsSpec{}, fmt.Errorf("metric spec Bad is not supported in Nobl9") - } - - good, err := getN9MetricSource(r.Good.MetricSource) - if err != nil { - return nobl9v1alpha.CountMetricsSpec{}, fmt.Errorf("issue getting good metric source: %w", err) - } - - total, err := getN9MetricSource(r.Total.MetricSource) - if err != nil { - return nobl9v1alpha.CountMetricsSpec{}, fmt.Errorf("issue getting total metric source: %w", err) - } - - cm := nobl9v1alpha.CountMetricsSpec{ - Incremental: &r.Counter, - GoodMetric: &good, - TotalMetric: &total, - } - return cm, nil -} - -// Disabling the lint for this since theres not a really good way of doing this without a big switch statement. -// -//nolint:cyclop -func getN9MetricSource(m v1.MetricSource) (nobl9v1alpha.MetricSpec, error) { - // Nobl9 supported metric sources. - supportedMetricSources := map[string]string{ - "AmazonPrometheus": "AmazonPrometheus", - "AppDynamics": "AppDynamics", - "BigQuery": "BigQuery", - "CloudWatch": "CloudWatch", - "CloudWatchMetric": "CloudWatchMetric", - "Datadog": "Datadog", - "Dynatrace": "Dynatrace", - "Elasticsearch": "Elasticsearch", - "GoogleCloudMonitoring": "GoogleCloudMonitoring", - "GrafanaLoki": "GrafanaLoki", - "Graphite": "Graphite", - "Instana": "Instana", - "Lightstep": "Lightstep", - "NewRelic": "NewRelic", - "OpenTSDB": "OpenTSDB", - "Pingdom": "Pingdom", - "Prometheus": "Prometheus", - "Redshift": "Redshift", - "Splunk": "Splunk", - "SplunkObservability": "SplunkObservability", - "SumoLogic": "SumoLogic", - "ThousandEyes": "ThousandEyes", - } - var ms nobl9v1alpha.MetricSpec - switch m.Type { - case supportedMetricSources["Datadog"]: - query := m.MetricSourceSpec["query"] - ms = nobl9v1alpha.MetricSpec{ - Datadog: &nobl9v1alpha.DatadogMetric{ - Query: &query, - }, - } - case supportedMetricSources["Prometheus"]: - query := m.MetricSourceSpec["promql"] - ms = nobl9v1alpha.MetricSpec{ - Prometheus: &nobl9v1alpha.PrometheusMetric{ - PromQL: &query, - }, - } - case supportedMetricSources["AmazonPrometheus"]: - query := m.MetricSourceSpec["promql"] - ms = nobl9v1alpha.MetricSpec{ - AmazonPrometheus: &nobl9v1alpha.AmazonPrometheusMetric{ - PromQL: &query, - }, - } - case supportedMetricSources["NewRelic"]: - query := m.MetricSourceSpec["nrql"] - ms = nobl9v1alpha.MetricSpec{ - NewRelic: &nobl9v1alpha.NewRelicMetric{ - NRQL: &query, - }, - } - case supportedMetricSources["ThousandEyes"]: - id := m.MetricSourceSpec["TestID"] - testType := m.MetricSourceSpec["TestType"] - - // convert id (which is a string) to int64 - idInt, err := strconv.ParseInt(id, 10, 64) - if err != nil { - return nobl9v1alpha.MetricSpec{}, fmt.Errorf("issue converting test id to int64: %w", err) - } - - ms = nobl9v1alpha.MetricSpec{ - ThousandEyes: &nobl9v1alpha.ThousandEyesMetric{ - TestID: &idInt, - TestType: &testType, - }, - } - case supportedMetricSources["AppDynamics"]: - appName := m.MetricSourceSpec["applicationName"] - metricPath := m.MetricSourceSpec["metricPath"] - ms = nobl9v1alpha.MetricSpec{ - AppDynamics: &nobl9v1alpha.AppDynamicsMetric{ - ApplicationName: &appName, - MetricPath: &metricPath, - }, - } - case supportedMetricSources["Splunk"]: - query := m.MetricSourceSpec["query"] - ms = nobl9v1alpha.MetricSpec{ - Splunk: &nobl9v1alpha.SplunkMetric{ - Query: &query, - }, - } - case supportedMetricSources["Lightstep"]: - streamID := m.MetricSourceSpec["streamId"] - typeOfData := m.MetricSourceSpec["typeOfData"] - percentile := m.MetricSourceSpec["percentile"] - - // convert percentile (which is a string) to float64 - percentileFloat, err := strconv.ParseFloat(percentile, 64) - if err != nil { - return nobl9v1alpha.MetricSpec{}, fmt.Errorf("issue converting percentile to float64: %w", err) - } - - ms = nobl9v1alpha.MetricSpec{ - Lightstep: &nobl9v1alpha.LightstepMetric{ - StreamID: &streamID, - TypeOfData: &typeOfData, - Percentile: &percentileFloat, - }, - } - case supportedMetricSources["SplunkObservability"]: - program := m.MetricSourceSpec["program"] - ms = nobl9v1alpha.MetricSpec{ - SplunkObservability: &nobl9v1alpha.SplunkObservabilityMetric{ - Program: &program, - }, - } - case supportedMetricSources["Dynatrace"]: - metricSelector := m.MetricSourceSpec["metricSelector"] - ms = nobl9v1alpha.MetricSpec{ - Dynatrace: &nobl9v1alpha.DynatraceMetric{ - MetricSelector: &metricSelector, - }, - } - case supportedMetricSources["Elasticsearch"]: - index := m.MetricSourceSpec["index"] - query := m.MetricSourceSpec["query"] - - ms = nobl9v1alpha.MetricSpec{ - Elasticsearch: &nobl9v1alpha.ElasticsearchMetric{ - Index: &index, - Query: &query, - }, - } - case supportedMetricSources["CloudWatch"]: - namespace := m.MetricSourceSpec["namespace"] - metricName := m.MetricSourceSpec["metricName"] - region := m.MetricSourceSpec["region"] - - cwm := getN9CloudWatchQuery(m.MetricSourceSpec) - cwm.Namespace = &namespace - cwm.MetricName = &metricName - cwm.Region = ®ion - - ms = nobl9v1alpha.MetricSpec{ - CloudWatch: &cwm, - } - case supportedMetricSources["Redshift"]: - query := m.MetricSourceSpec["query"] - region := m.MetricSourceSpec["region"] - clusterID := m.MetricSourceSpec["clusterId"] - databaseName := m.MetricSourceSpec["databaseName"] - - ms = nobl9v1alpha.MetricSpec{ - Redshift: &nobl9v1alpha.RedshiftMetric{ - Query: &query, - Region: ®ion, - ClusterID: &clusterID, - DatabaseName: &databaseName, - }, - } - case supportedMetricSources["SumoLogic"]: - dataType := m.MetricSourceSpec["type"] - query := m.MetricSourceSpec["query"] - quantization := m.MetricSourceSpec["quantization"] - rollup := m.MetricSourceSpec["rollup"] - - ms = nobl9v1alpha.MetricSpec{ - SumoLogic: &nobl9v1alpha.SumoLogicMetric{ - Type: &dataType, - Query: &query, - Quantization: &quantization, - Rollup: &rollup, - }, - } - case supportedMetricSources["Instana"]: - metricType := m.MetricSourceSpec["metricType"] - metricSource, err := unflatten(m.MetricSourceSpec) - if err != nil { - return nobl9v1alpha.MetricSpec{}, fmt.Errorf("issue converting Instana to map: %w", err) - } - infrastructure := metricSource["infrastructure"].(map[string]interface{}) - metricRetrievalMethod := infrastructure["metricRetrievalMethod"].(string) - query := infrastructure["query"].(string) - snapshotID := infrastructure["snapshotId"].(string) - - application := metricSource["application"].(map[string]interface{}) - groupBy := application["groupBy"].(map[string]interface{}) - tag := groupBy["tag"].(string) - tagEntity := groupBy["tagEntity"].(string) - tagSecondLevelKey := groupBy["tagSecondLevelKey"].(string) - - ms = nobl9v1alpha.MetricSpec{ - Instana: &nobl9v1alpha.InstanaMetric{ - MetricType: metricType, - Infrastructure: &nobl9v1alpha.InstanaInfrastructureMetricType{ - MetricRetrievalMethod: metricRetrievalMethod, - Query: &query, - SnapshotID: &snapshotID, - MetricID: infrastructure["metricId"].(string), - PluginID: infrastructure["pluginId"].(string), - }, - Application: &nobl9v1alpha.InstanaApplicationMetricType{ - MetricID: application["metricId"].(string), - Aggregation: application["aggregation"].(string), - APIQuery: application["apiQuery"].(string), - GroupBy: nobl9v1alpha.InstanaApplicationMetricGroupBy{ - Tag: tag, - TagEntity: tagEntity, - TagSecondLevelKey: &tagSecondLevelKey, - }, - }, - }, - } - case supportedMetricSources["Pingdom"]: - checkID := m.MetricSourceSpec["checkId"] - checkType := m.MetricSourceSpec["checkType"] - status := m.MetricSourceSpec["status"] - - ms = nobl9v1alpha.MetricSpec{ - Pingdom: &nobl9v1alpha.PingdomMetric{ - CheckID: &checkID, - CheckType: &checkType, - Status: &status, - }, - } - case supportedMetricSources["Graphite"]: - metricPath := m.MetricSourceSpec["metricPath"] - - ms = nobl9v1alpha.MetricSpec{ - Graphite: &nobl9v1alpha.GraphiteMetric{ - MetricPath: &metricPath, - }, - } - case supportedMetricSources["BigQuery"]: - query := m.MetricSourceSpec["query"] - projectID := m.MetricSourceSpec["projectId"] - location := m.MetricSourceSpec["location"] - - ms = nobl9v1alpha.MetricSpec{ - BigQuery: &nobl9v1alpha.BigQueryMetric{ - Query: query, - ProjectID: projectID, - Location: location, - }, - } - case supportedMetricSources["OpenTSDB"]: - query := m.MetricSourceSpec["query"] - - ms = nobl9v1alpha.MetricSpec{ - OpenTSDB: &nobl9v1alpha.OpenTSDBMetric{ - Query: &query, - }, - } - case supportedMetricSources["GrafanaLoki"]: - logql := m.MetricSourceSpec["logql"] - - ms = nobl9v1alpha.MetricSpec{ - GrafanaLoki: &nobl9v1alpha.GrafanaLokiMetric{ - Logql: &logql, - }, - } - case supportedMetricSources["GoogleCloudMonitoring"]: - query := m.MetricSourceSpec["query"] - projectID := m.MetricSourceSpec["projectId"] - ms = nobl9v1alpha.MetricSpec{ - GoogleCloudMonitoring: &nobl9v1alpha.GoogleCloudMonitoringMetric{ - Query: &query, - ProjectID: &projectID, - }, - } - default: - // get the supportedMetricSources as a string - var supportedMetricSourcesString string - for k := range supportedMetricSources { - supportedMetricSourcesString += k + ", " - } - - return ms, fmt.Errorf( - "unsupported metric source kind %s. Supported types are %s", - m.Type, - supportedMetricSourcesString, - ) - } - return ms, nil -} - -func getN9CloudWatchQuery(m map[string]string) nobl9v1alpha.CloudWatchMetric { - switch { - case m["sql"] != "": - val := m["sql"] - return nobl9v1alpha.CloudWatchMetric{ - SQL: &val, - } - case m["json"] != "": - val := m["json"] - return nobl9v1alpha.CloudWatchMetric{ - JSON: &val, - } - case m["dimensions"] != "": - val, _ := getN9CloudWatchDims(m["dimensions"]) - stat := m["stat"] - return nobl9v1alpha.CloudWatchMetric{ - Stat: &stat, - Dimensions: val, - } - } - - return nobl9v1alpha.CloudWatchMetric{} -} - -func getN9CloudWatchDims(dimensions string) ([]nobl9v1alpha.CloudWatchMetricDimension, error) { - // split the incoming dimensions string into a CloudWatchMetricDimension - dimsPieces := strings.Split(dimensions, ";") - dims := make([]nobl9v1alpha.CloudWatchMetricDimension, 0, len(dimsPieces)) - for _, sequence := range dimsPieces { - // for the cloudwatch dimensions, we expect them in a single set of kv pairs, with name and value as the two keys - // example: 'name:foo,value:"foo";name:bar,value:"bar"' - cwDim := nobl9v1alpha.CloudWatchMetricDimension{} - for _, dimMap := range strings.Split(sequence, ",") { - kv := strings.Split(dimMap, ":") - if len(kv) != 2 { - return []nobl9v1alpha.CloudWatchMetricDimension{}, fmt.Errorf("invalid dimension: %s", dimMap) - } - - key := strings.TrimSpace(kv[0]) - val := strings.TrimSpace(kv[1]) - - if strings.ToLower(key) == "name" { - cwDim.Name = &val - } - - if strings.ToLower(key) == "value" { - cwDim.Value = &val - } - } - dims = append(dims, cwDim) - } - - return dims, nil -} - -func getN9SLISpec(o v1.SLOSpec, parsed []manifest.OpenSLOKind) (s v1.SLISpec) { - // if o.IndicatorRef is not nil, then return the indicator from the parsed list - if o.IndicatorRef != nil { - indicators := getObjectByKind("SLI", parsed) - - for _, i := range indicators { - ind := i.(v1.SLI) - if ind.Metadata.Name == *o.IndicatorRef { - s = ind.Spec - break - } - } - } else { - s = o.Indicator.Spec - } - return s -} - -// Function that returns an nobl9v1alpha.TimeWindow from a OpenSLO TimeWindow. -func getN9TimeWindow(tw []v1.TimeWindow) ([]nobl9v1alpha.TimeWindow, error) { - // return an error if the length of tw is greater than one, since we only support one TimeWindow - if len(tw) > 1 { - return nil, fmt.Errorf("OpenSLO only supports one TimeWindow") - } - - if len(tw) < 1 { - _ = printError("Nobl9 requires a TimeWindow defined.") - return nil, fmt.Errorf("no TimeWindow found") - } - - duration := tw[0].Duration - - unit, err := getDurationUnit(duration[len(duration)-1:]) - if err != nil { - return nil, fmt.Errorf("issue getting duration unit: %w", err) - } - - // Convert all but the last character of duration to an int. - durationInt, err := strconv.Atoi(duration[:len(duration)-1]) - if err != nil { - return nil, fmt.Errorf("issue converting duration to int: %w", err) - } - - rval := []nobl9v1alpha.TimeWindow{ - { - Unit: unit, - Count: durationInt, - IsRolling: tw[0].IsRolling, - }, - } - - // only add Calendar for Calendar aligned - if !tw[0].IsRolling { - rval[0].Calendar = &nobl9v1alpha.Calendar{ - TimeZone: tw[0].Calendar.TimeZone, - StartTime: tw[0].Calendar.StartTime, - } - } - - return rval, nil -} - -// Constructs Nobl9 AlertPolicy objects from our list of OpenSLOKinds. -func getN9AlertPolicyObjects( - parsed []manifest.OpenSLOKind, - rval *[]interface{}, - names *[]string, - project string, -) error { - // Get the alert policy object. - ap := getObjectByKind("AlertPolicy", parsed) - - // Return if ap is empty. - if len(ap) == 0 { - return nil - } - - // AlertCondition is required so get any from our parsed list. - ac := getObjectByKind("AlertCondition", parsed) - - // For each AlertPolicy - for _, o := range ap { - // Cast to OpenSLO service objects. - apObj, ok := o.(v1.AlertPolicy) - if !ok { - return fmt.Errorf("issue casting to AlertPolicy") - } - - // Gather the alert conditions. - var conditions []nobl9v1alpha.AlertCondition - for _, a := range apObj.Spec.Conditions { - err := getN9AlertCondition(a, &conditions, ac) - if err != nil { - return fmt.Errorf("issue getting alert condition: %w", err) - } - } - - // Construct the nobl9 AlertPolicy object from the OpenSLO AlertPolicy object. - _ = printWarning("Using default serverity of 'Medium' in AlertPolicy, because we don't have an exact mapping") - _ = printWarning("Using default CoolDownDuration in AlertPolicy, because OpenSLO doesn't support that") - *rval = append(*rval, nobl9v1alpha.AlertPolicy{ - ObjectHeader: getN9ObjectHeader( - "AlertPolicy", - apObj.Metadata.Name, - apObj.Metadata.DisplayName, - project, - apObj.Metadata.Labels, - ), - Spec: nobl9v1alpha.AlertPolicySpec{ - Description: apObj.Spec.Description, - Conditions: conditions, - Severity: "Medium", - CoolDownDuration: "5m", // default - }, - }) - - // Add the name to our list of names. - *names = append(*names, apObj.Metadata.Name) - } - return nil -} - -// returns an nobl9v1alpha.AlertCondition from an OpenSLO.AlertPolicyCondition. -func getN9AlertCondition( - apc v1.AlertPolicyCondition, - conditions *[]nobl9v1alpha.AlertCondition, - ac []manifest.OpenSLOKind, -) error { - // If we have an inline condition, we can use it. - //nolint: nestif - if apc.AlertConditionInline != nil { - _ = printWarning("using the default averageBurnRate in AlertCondition, since there isn't a direct match") - _ = printWarning("Using default operator in AlertCondition, because OpenSLO doesn't support that feature") - *conditions = append(*conditions, nobl9v1alpha.AlertCondition{ - Measurement: "averageBurnRate", - Value: apc.AlertConditionInline.Spec.Condition.Threshold, - LastsForDuration: apc.AlertConditionInline.Spec.Condition.AlertAfter, - Operation: "gt", - }) - } else { - // Error if we don't have any, since we need at least one. - if len(ac) == 0 { - return fmt.Errorf("no alert conditions found. Required for alert policy") - } - - // If we don't have an inline condition, we need to get the AlertCondition. - for _, c := range ac { - // Get the AlertCondition that matches the name. - acObj, err := c.(v1.AlertCondition) - if !err { - return fmt.Errorf("issue casting to AlertCondition") - } - if apc.AlertPolicyConditionSpec.ConditionRef == acObj.Metadata.Name { - _ = printWarning("Using default averageBurnRate in AlertCondition, since there isn't direct mapping") - _ = printWarning("Using default operator in AlertCondition, because OpenSLO doesn't support that feature") - *conditions = append(*conditions, nobl9v1alpha.AlertCondition{ - Measurement: "averageBurnRate", - Value: acObj.Spec.Condition.Threshold, - LastsForDuration: acObj.Spec.Condition.AlertAfter, - Operation: "gt", - }) - } else { - return fmt.Errorf("alert condition %s not found", apc.AlertPolicyConditionSpec.ConditionRef) - } - } - } - return nil -} - -// function that takes a manifest.OpenSLOKind and returns a nobl9v1alpha.ObjectHeader. -func getN9ObjectHeader(kind, name, displayName, project string, labels v1.Labels) nobl9v1alpha.ObjectHeader { - return nobl9v1alpha.ObjectHeader{ - ObjectHeader: nobl9manifest.ObjectHeader{ - APIVersion: nobl9v1alpha.APIVersion, - }, - Kind: kind, - MetadataHolder: nobl9v1alpha.MetadataHolder{ - Metadata: nobl9v1alpha.Metadata{ - Name: name, - DisplayName: displayName, - Project: project, - Labels: getN9Labels(labels), - }, - }, - } -} - -// getN9Labels takes a v1.Labels object and maps it to a nobl9v1alpha.Labels. -func getN9Labels(labels v1.Labels) map[string][]string { - if labels == nil { - return nil - } - rval := make(map[string][]string) - for k, v := range labels { - rval[k] = v - } - return rval -} - -// Constructs Nobl9 Service objects from our list of OpenSLOKinds. -func getN9ServiceObjects(parsed []manifest.OpenSLOKind, rval *[]interface{}, names *[]string, project string) error { - // Get the service object. - obj := getObjectByKind("Service", parsed) - - for _, o := range obj { - // Cast to OpenSLO service objects. - srvObj, ok := o.(v1.Service) - if !ok { - return fmt.Errorf("issue casting to service") - } - // Construct the nobl9 service object from the OpenSLO service object. - *rval = append(*rval, nobl9v1alpha.Service{ - ObjectHeader: getN9ObjectHeader( - "Service", - srvObj.Metadata.Name, - srvObj.Metadata.DisplayName, - project, - srvObj.Metadata.Labels, - ), - Spec: nobl9v1alpha.ServiceSpec{ - Description: srvObj.Spec.Description, - }, - }) - - // Add the name to the list of names. - *names = append(*names, srvObj.Metadata.Name) - } - return nil -} - -// ------------------------------------------------------------------------------ -// -// Helper functions. -func printYaml(out io.Writer, object interface{}) error { - // Convert parsed to yaml and print to out. - yml, err := yaml.Marshal(object) - if err != nil { - return fmt.Errorf("issue marshaling content: %w", err) - } - - if _, err = fmt.Fprint(out, "---\n"); err != nil { - return err - } - _, err = out.Write(yml) - if err != nil { - return fmt.Errorf("issue writing content: %w", err) - } - - return nil -} - -// Function to print warning messages to Stderr so that we can see them when -// when doing redirection in the console. -func printWarning(message string) error { - yellow := color.New(color.FgYellow).Add(color.Bold) - white := color.New(color.FgWhite).Add(color.Bold) - - yellow.EnableColor() - white.EnableColor() - - if _, err := yellow.Fprint(os.Stderr, "WARNING: "); err != nil { - return fmt.Errorf("issue printing warning: %w", err) - } - - if _, err := white.Fprintln(os.Stderr, message); err != nil { - return fmt.Errorf("issue printing warning: %w", err) - } - - yellow.DisableColor() - white.DisableColor() - - color.Unset() - return nil -} - -func printError(message string) error { - red := color.New(color.FgRed).Add(color.Bold) - white := color.New(color.FgWhite).Add(color.Bold) - - red.EnableColor() - white.EnableColor() - - if _, err := red.Fprint(os.Stderr, "ERROR: "); err != nil { - return fmt.Errorf("issue printing warning: %w", err) - } - - if _, err := white.Fprintln(os.Stderr, message); err != nil { - return fmt.Errorf("issue printing warning: %w", err) - } - - red.DisableColor() - white.DisableColor() - - color.Unset() - return nil -} - -// Function that unflattens json into nested maps. -func unflatten(json map[string]string) (map[string]interface{}, error) { - result := make(map[string]interface{}) - for key, value := range json { - keyParts := strings.Split(key, ".") - m := result - for i, k := range keyParts[:len(keyParts)-1] { - v, exists := m[k] - if !exists { - newMap := map[string]interface{}{} - m[k] = newMap - m = newMap - continue - } - - innerMap, ok := v.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("key=%v is not an object", strings.Join(keyParts[0:i+1], ".")) - } - m = innerMap - } - - leafKey := keyParts[len(keyParts)-1] - if _, exists := m[leafKey]; exists { - return nil, fmt.Errorf("key=%v already exists", key) - } - m[keyParts[len(keyParts)-1]] = value - } - - return result, nil -} - -// Function that takes a duration shorthand string and returns the unit of time, eg minute, hour, month. -func getDurationUnit(d string) (string, error) { - switch d { - case "m": - return "Minute", nil - case "h": - return "Hour", nil - case "d": - return "Day", nil - case "w": - return "Week", nil - case "M": - return "Month", nil - case "Q": - return "Quarter", nil - case "Y": - return "Year", nil - } - - return "", fmt.Errorf("duration unit not supported %s", d) -} - -// Checks that the given string is in the given slice. -func stringInSlice(a string, list []string) bool { - for _, b := range list { - if b == a { - return true - } - } - return false -} diff --git a/internal/convert/convert_test.go b/internal/convert/convert_test.go deleted file mode 100644 index 4fd57c3..0000000 --- a/internal/convert/convert_test.go +++ /dev/null @@ -1,1452 +0,0 @@ -/* -Package convert provides a command to convert from openslo to other formats. - -# Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package convert - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" - - nobl9v1alpha "github.com/OpenSLO/oslo/internal/manifest/nobl9/v1alpha" - "github.com/OpenSLO/oslo/pkg/manifest" - v1 "github.com/OpenSLO/oslo/pkg/manifest/v1" -) - -func Test_getCountMetrics(t *testing.T) { - t.Parallel() - tests := []struct { - name string - ratioMetric v1.RatioMetric - want string - wantErr bool - }{ - { - name: "Fail with Bad", - ratioMetric: v1.RatioMetric{ - Bad: &v1.MetricSourceHolder{}, - Total: v1.MetricSourceHolder{}, - }, - want: "", - wantErr: true, - }, - { - name: "RatioMetric", - ratioMetric: v1.RatioMetric{ - Good: &v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - Type: "Prometheus", - MetricSourceSpec: map[string]string{ - "promql": "sum(rate(container_cpu_usage_seconds_total{container_name!=\"POD\"}[1m])) by (container_name)", - }, - }, - }, - Total: v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - Type: "Prometheus", - MetricSourceSpec: map[string]string{ - "promql": "sum(rate(container_cpu_usage_seconds_total{container_name!=\"POD\"}[1m])) by (container_name)", - }, - }, - }, - }, - want: `incremental: false -good: - prometheus: - promql: sum(rate(container_cpu_usage_seconds_total{container_name!="POD"}[1m])) by (container_name) -total: - prometheus: - promql: sum(rate(container_cpu_usage_seconds_total{container_name!="POD"}[1m])) by (container_name) -`, - }, - } - - for _, tt := range tests { - tt := tt // https://gist.github.com/kunwardeep/80c2e9f3d3256c894898bae82d9f75d0 - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := getN9CountMetrics(tt.ratioMetric) - - if (err != nil) != tt.wantErr { - t.Errorf("getCountMetrics() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr { - y, _ := yaml.Marshal(got) - assert.Equal(t, tt.want, string(y)) - } - }) - } -} - -func Test_getMetricSource(t *testing.T) { - t.Parallel() - tests := []struct { - name string - args v1.MetricSource - want string - wantErr bool - }{ - { - name: "Unsupported", - args: v1.MetricSource{ - Type: "Unsupported", - }, - wantErr: true, - }, - { - name: "Prometheus", - args: v1.MetricSource{ - Type: "Prometheus", - MetricSourceSpec: map[string]string{ - "promql": "sum(rate(container_cpu_usage_seconds_total{container_name!=\"POD\"}[1m])) by (container_name)", - }, - }, - want: `prometheus: - promql: sum(rate(container_cpu_usage_seconds_total{container_name!="POD"}[1m])) by (container_name) -`, - }, - { - name: "AmazonPrometheus", - args: v1.MetricSource{ - Type: "AmazonPrometheus", - MetricSourceSpec: map[string]string{ - "promql": "myapp_server_requestMsec{host=\"*\",job=\"nginx\"}", - }, - }, - want: `amazonPrometheus: - promql: myapp_server_requestMsec{host="*",job="nginx"} -`, - }, - { - name: "Datadog", - args: v1.MetricSource{ - Type: "Datadog", - MetricSourceSpec: map[string]string{ - "query": "sum:rate:container.cpu{container_name!=\"POD\"} by {container_name}", - }, - }, - want: `datadog: - query: sum:rate:container.cpu{container_name!="POD"} by {container_name} -`, - }, - { - name: "NewRelic", - args: v1.MetricSource{ - Type: "NewRelic", - MetricSourceSpec: map[string]string{ - "nrql": "SELECT sum(duration) FROM Transaction WHERE name = 'WebTransaction'", - }, - }, - want: `newRelic: - nrql: SELECT sum(duration) FROM Transaction WHERE name = 'WebTransaction' -`, - }, - { - name: "ThousandEyes", - args: v1.MetricSource{ - Type: "ThousandEyes", - MetricSourceSpec: map[string]string{ - "TestID": "1234", - "TestType": "mytype", - }, - }, - want: `thousandEyes: - testID: 1234 - testType: mytype -`, - }, - { - name: "AppDynamics", - args: v1.MetricSource{ - Type: "AppDynamics", - MetricSourceSpec: map[string]string{ - "applicationName": "myapp", - "metricPath": "mypath", - }, - }, - want: `appDynamics: - applicationName: myapp - metricPath: mypath -`, - }, - { - name: "Splunk", - args: v1.MetricSource{ - Type: "Splunk", - MetricSourceSpec: map[string]string{ - "query": "mysplunkquery", - }, - }, - want: `splunk: - query: mysplunkquery -`, - }, - { - name: "Lightstep", - args: v1.MetricSource{ - Type: "Lightstep", - MetricSourceSpec: map[string]string{ - "streamId": "mystreamid", - "typeOfData": "mytypeofdata", - "percentile": "0.96", - }, - }, - want: `lightstep: - streamId: mystreamid - typeOfData: mytypeofdata - percentile: 0.96 -`, - }, - { - name: "SplunkObservability", - args: v1.MetricSource{ - Type: "SplunkObservability", - MetricSourceSpec: map[string]string{ - "program": "myprogram", - }, - }, - want: `splunkObservability: - program: myprogram -`, - }, - { - name: "Dynatrace", - args: v1.MetricSource{ - Type: "Dynatrace", - MetricSourceSpec: map[string]string{ - "metricSelector": "mymetricselector", - }, - }, - want: `dynatrace: - metricSelector: mymetricselector -`, - }, - { - name: "Elasticsearch", - args: v1.MetricSource{ - Type: "Elasticsearch", - MetricSourceSpec: map[string]string{ - "index": "myindex", - "query": "myquery", - }, - }, - want: `elasticsearch: - index: myindex - query: myquery -`, - }, - { - name: "CloudWatch", - args: v1.MetricSource{ - Type: "CloudWatch", - MetricSourceSpec: map[string]string{ - "namespace": "mynamespace", - "metricName": "mymetricname", - "region": "myregion", - "stat": "mystat", - "dimensions": "name:mydimensions,value:myvalue;name:mydimensions2,value:myvalue2", - }, - }, - want: `cloudWatch: - region: myregion - namespace: mynamespace - metricName: mymetricname - stat: mystat - dimensions: - - name: mydimensions - value: myvalue - - name: mydimensions2 - value: myvalue2 -`, - }, - { - name: "Redshift", - args: v1.MetricSource{ - Type: "Redshift", - MetricSourceSpec: map[string]string{ - "query": "myquery", - "region": "myregion", - "clusterId": "myclusterid", - "databaseName": "mydatabasename", - }, - }, - want: `redshift: - region: myregion - clusterId: myclusterid - databaseName: mydatabasename - query: myquery -`, - }, - { - name: "SumoLogic", - args: v1.MetricSource{ - Type: "SumoLogic", - MetricSourceSpec: map[string]string{ - "type": "mytype", - "query": "myquery", - "quantization": "myquantization", - "rollup": "myrollup", - }, - }, - want: `sumoLogic: - type: mytype - query: myquery - quantization: myquantization - rollup: myrollup -`, - }, - { - name: "Instana", - args: v1.MetricSource{ - Type: "Instana", - MetricSourceSpec: map[string]string{ - "metricType": "mymetrictype", - "infrastructure.metricRetrievalMethod": "myInfrastructureMetricRetrivalMethod", - "infrastructure.query": "myInfrastructureQuery", - "infrastructure.snapshotId": "myInfrastructureSnapshotId", - "infrastructure.metricId": "myInfrastructureMetricId", - "infrastructure.pluginId": "myInfrastructurePluginId", - "application.metricId": "myapplicationMetricId", - "application.aggregation": "myapplicationAggregation", - "application.groupBy.tag": "myapplicationGroupByTag", - "application.groupBy.tagEntity": "myapplicationGroupByTagEntity", - "application.groupBy.tagSecondLevelKey": "myapplicationTagSecondLevelKey", - "application.apiQuery": "myapplicationApiQuery", - "application.includeInternal": "true", - "application.includeSynthetic": "false", - }, - }, - want: `instana: - metricType: mymetrictype - infrastructure: - metricRetrievalMethod: myInfrastructureMetricRetrivalMethod - query: myInfrastructureQuery - snapshotId: myInfrastructureSnapshotId - metricId: myInfrastructureMetricId - pluginId: myInfrastructurePluginId - application: - metricId: myapplicationMetricId - aggregation: myapplicationAggregation - groupBy: - tag: myapplicationGroupByTag - tagEntity: myapplicationGroupByTagEntity - tagSecondLevelKey: myapplicationTagSecondLevelKey - apiQuery: myapplicationApiQuery -`, - }, - { - name: "Pingdom", - args: v1.MetricSource{ - Type: "Pingdom", - MetricSourceSpec: map[string]string{ - "checkId": "mycheckid", - "checkType": "mychecktype", - "status": "mystatus", - }, - }, - want: `pingdom: - checkId: mycheckid - checkType: mychecktype - status: mystatus -`, - }, - { - name: "Graphite", - args: v1.MetricSource{ - Type: "Graphite", - MetricSourceSpec: map[string]string{ - "metricPath": "mymetricpath", - }, - }, - want: `graphite: - metricPath: mymetricpath -`, - }, - { - name: "BigQuery", - args: v1.MetricSource{ - Type: "BigQuery", - MetricSourceSpec: map[string]string{ - "projectId": "myprojectid", - "query": "myquery", - "location": "mylocation", - }, - }, - want: `bigQuery: - query: myquery - projectId: myprojectid - location: mylocation -`, - }, - { - name: "OpenTSDB", - args: v1.MetricSource{ - Type: "OpenTSDB", - MetricSourceSpec: map[string]string{ - "query": "myquery", - }, - }, - want: `opentsdb: - query: myquery -`, - }, - { - name: "GrafanaLoki", - args: v1.MetricSource{ - Type: "GrafanaLoki", - MetricSourceSpec: map[string]string{ - "logql": "mylogql", - }, - }, - want: `grafanaLoki: - logql: mylogql -`, - }, - { - name: "GoogleCloudMonitoring", - args: v1.MetricSource{ - Type: "GoogleCloudMonitoring", - MetricSourceSpec: map[string]string{ - "projectId": "myprojectid", - "query": "myquery", - }, - }, - want: `gcm: - query: myquery - projectId: myprojectid -`, - }, - } - for _, tt := range tests { - tt := tt // https://gist.github.com/kunwardeep/80c2e9f3d3256c894898bae82d9f75d0 - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := getN9MetricSource(tt.args) - if (err != nil) != tt.wantErr { - t.Errorf("getMetricSource() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr { - y, _ := yaml.Marshal(got) - assert.Equal(t, tt.want, string(y)) - } - }) - } -} - -func Test_RemoveDuplicates(t *testing.T) { - t.Parallel() - tests := []struct { - name string - input []string - want []string - }{ - { - name: "empty array", - input: []string{}, - want: []string{}, - }, - { - name: "All unique", - input: []string{"a", "b", "c"}, - want: []string{"a", "b", "c"}, - }, - { - name: "Some dupes", - input: []string{"a", "b", "c", "b", "a"}, - want: []string{"a", "b", "c"}, - }, - { - name: "All the same", - input: []string{"a", "a", "a"}, - want: []string{"a"}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got := RemoveDuplicates(tt.input) - assert.Equal(t, tt.want, got) - }) - } -} - -func Test_getParsedObjects(t *testing.T) { - t.Parallel() - // needed farther down to test an empty list - var empty []manifest.OpenSLOKind - // needed here so we can pass in the pointer address later - sliName := "foo-sli" - - tests := []struct { - name string - args []string - want []manifest.OpenSLOKind - wantErr bool - }{ - { - name: "empty list", - args: []string{}, - want: empty, - }, - { - name: "Single DataSource per file", - args: []string{"../../test/v1/data-source/data-source.yaml"}, - want: []manifest.OpenSLOKind{ - v1.DataSource{ - ObjectHeader: v1.ObjectHeader{ - ObjectHeader: manifest.ObjectHeader{ - APIVersion: "openslo/v1", - }, - Kind: "DataSource", - MetadataHolder: v1.MetadataHolder{ - Metadata: v1.Metadata{ - Name: "TestDataSource", - DisplayName: "Test Data Source", - }, - }, - }, - Spec: v1.DataSourceSpec{ - Type: "CloudWatch", - ConnectionDetails: map[string]string{ - "accessKeyID": "accessKey", - "secretAccessKey": "secretAccessKey", - }, - }, - }, - }, - }, - { - name: "Single Service file", - args: []string{"../../test/v1/service/service.yaml"}, - want: []manifest.OpenSLOKind{ - v1.Service{ - ObjectHeader: v1.ObjectHeader{ - ObjectHeader: manifest.ObjectHeader{ - APIVersion: "openslo/v1", - }, - Kind: "Service", - MetadataHolder: v1.MetadataHolder{ - Metadata: v1.Metadata{ - Name: "my-rad-service", - DisplayName: "My Rad Service", - }, - }, - }, - Spec: v1.ServiceSpec{ - Description: "This is a great description of an even better service.", - }, - }, - }, - }, - { - name: "Single SLI per file", - args: []string{"../../test/v1/sli/sli-description-threshold-metricsourceref.yaml"}, - want: []manifest.OpenSLOKind{ - v1.SLI{ - ObjectHeader: v1.ObjectHeader{ - ObjectHeader: manifest.ObjectHeader{ - APIVersion: "openslo/v1", - }, - Kind: "SLI", - MetadataHolder: v1.MetadataHolder{ - Metadata: v1.Metadata{ - Name: "GreatSLI", - DisplayName: "Great SLI", - }, - }, - }, - Spec: v1.SLISpec{ - ThresholdMetric: &v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - MetricSourceRef: "redshift-datasource", - MetricSourceSpec: map[string]string{ - "clusterId": "metrics-cluster", - "databaseName": "metrics-db", - "query": "SELECT value, timestamp FROM metrics WHERE timestamp BETWEEN :date_from AND :date_to", - "region": "eu-central-1", - }, - }, - }, - }, - }, - }, - }, - { - name: "Multiple Files", - args: []string{ - "../../test/v1/service/service.yaml", - "../../test/v1/sli/sli-description-threshold-metricsourceref.yaml", - }, - want: []manifest.OpenSLOKind{ - v1.Service{ - ObjectHeader: v1.ObjectHeader{ - ObjectHeader: manifest.ObjectHeader{ - APIVersion: "openslo/v1", - }, - Kind: "Service", - MetadataHolder: v1.MetadataHolder{ - Metadata: v1.Metadata{ - Name: "my-rad-service", - DisplayName: "My Rad Service", - }, - }, - }, - Spec: v1.ServiceSpec{ - Description: "This is a great description of an even better service.", - }, - }, - v1.SLI{ - ObjectHeader: v1.ObjectHeader{ - ObjectHeader: manifest.ObjectHeader{ - APIVersion: "openslo/v1", - }, - Kind: "SLI", - MetadataHolder: v1.MetadataHolder{ - Metadata: v1.Metadata{ - Name: "GreatSLI", - DisplayName: "Great SLI", - }, - }, - }, - Spec: v1.SLISpec{ - ThresholdMetric: &v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - MetricSourceRef: "redshift-datasource", - MetricSourceSpec: map[string]string{ - "clusterId": "metrics-cluster", - "databaseName": "metrics-db", - "query": "SELECT value, timestamp FROM metrics WHERE timestamp BETWEEN :date_from AND :date_to", - "region": "eu-central-1", - }, - }, - }, - }, - }, - }, - }, - { - name: "Multiple definitions per file", - args: []string{"../../test/v1/multi.yaml"}, - want: []manifest.OpenSLOKind{ - v1.SLO{ - ObjectHeader: v1.ObjectHeader{ - ObjectHeader: manifest.ObjectHeader{ - APIVersion: "openslo/v1", - }, - Kind: "SLO", - MetadataHolder: v1.MetadataHolder{ - Metadata: v1.Metadata{ - Name: "foo-slo", - DisplayName: "FOO SLO", - }, - }, - }, - Spec: v1.SLOSpec{ - Description: "Foo SLO", - Service: "foo-slos", - IndicatorRef: &sliName, - BudgetingMethod: "Occurrences", - TimeWindow: []v1.TimeWindow{ - { - Duration: "28d", - IsRolling: true, - }, - }, - Objectives: []v1.Objective{ - { - DisplayName: "Test Objective", - Op: "gte", - Value: 10, - Target: 0.98, - }, - }, - AlertPolicies: []string{}, - }, - }, - v1.SLI{ - ObjectHeader: v1.ObjectHeader{ - ObjectHeader: manifest.ObjectHeader{ - APIVersion: "openslo/v1", - }, - Kind: "SLI", - MetadataHolder: v1.MetadataHolder{ - Metadata: v1.Metadata{ - Name: "foo-sli", - }, - }, - }, - Spec: v1.SLISpec{ - ThresholdMetric: &v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - MetricSourceRef: "foo-cloudwatch", - Type: "CloudWatch", - MetricSourceSpec: map[string]string{ - "dimensions": "name:CanaryName,value:web-app", - "metricName": "2xx", - "namespace": "CloudWatchSynthetics", - "region": "us-east-1", - "stat": "SampleCount", - }, - }, - }, - }, - }, - v1.DataSource{ - ObjectHeader: v1.ObjectHeader{ - ObjectHeader: manifest.ObjectHeader{ - APIVersion: "openslo/v1", - }, - Kind: "DataSource", - MetadataHolder: v1.MetadataHolder{ - Metadata: v1.Metadata{ - Name: "foo-cloudwatch", - }, - }, - }, - Spec: v1.DataSourceSpec{ - Type: "CloudWatch", - ConnectionDetails: map[string]string{ - "accessKeyID": "FOOBAR", - "secretAccessKey": "BAZBAT", - }, - }, - }, - }, - }, - } - for _, tt := range tests { - tt := tt // https://gist.github.com/kunwardeep/80c2e9f3d3256c894898bae82d9f75d0 - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := getParsedObjects(tt.args) - if (err != nil) != tt.wantErr { - t.Errorf("getParsedObjects() error = %v, wantErr %v", err, tt.wantErr) - return - } - assert.Equal(t, tt.want, got) - }) - } -} - -func Test_Nobl9(t *testing.T) { - t.Parallel() - type args struct { - filenames []string - project string - } - tests := []struct { - name string - args args - wantOut string - wantErr bool - }{ - { - name: "Single Service Single file", - args: args{ - filenames: []string{ - "../../test/v1/service/service-with-labels.yaml", - }, - project: "default", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: Service -metadata: - name: my-rad-service - displayName: My Rad Service - project: default - labels: - costCentre: - - project1 - serviceTier: - - tier-1 - team: - - identity - userImpacting: - - "true" -spec: - description: This is a great description of an even better service. -`, - }, - { - name: "Single SLO single file", - args: args{ - filenames: []string{ - "../../test/v1/slo/slo-indicatorref-rolling-alerts.yaml", - }, - project: "default", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: SLO -metadata: - name: TestSLO - displayName: Test SLO - project: default -spec: - description: This is a great description - indicator: - metricSource: - project: default - name: Changeme - kind: Agent - budgetingMethod: Occurrences - objectives: - - displayName: Foo Total Errors - value: 1 - target: 0.98 - op: lt - service: TheServiceName - timeWindows: - - unit: Day - count: 1 - isRolling: true - alertPolicies: - - FooAlertPolicy -`, - }, - { - name: "Multiple Kinds Single File", - args: args{ - filenames: []string{ - "../../test/v1/multi.yaml", - }, - project: "default", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: SLO -metadata: - name: foo-slo - displayName: FOO SLO - project: default -spec: - description: Foo SLO - indicator: - metricSource: - project: default - name: foo-cloudwatch - kind: Agent - budgetingMethod: Occurrences - objectives: - - displayName: Test Objective - value: 10 - target: 0.98 - rawMetric: - query: - cloudWatch: - region: us-east-1 - namespace: CloudWatchSynthetics - metricName: 2xx - stat: SampleCount - dimensions: - - name: CanaryName - value: web-app - op: gte - service: foo-slos - timeWindows: - - unit: Day - count: 28 - isRolling: true - alertPolicies: [] -`, - }, - { - name: "Multiple Kinds Multiple Files", - args: args{ - filenames: []string{ - "../../test/v1/slo/slo-indicatorRef-rolling-cloudwatch.yaml", - "../../test/v1/sli/sli-threshold-cloudwatch.yaml", - "../../test/v1/data-source/data-source-cloudwatch.yaml", - }, - project: "default", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: SLO -metadata: - name: foo-openslo-slo - displayName: FOO OPENSLO SLO - project: default -spec: - description: "" - indicator: - metricSource: - project: default - name: foo-cloudwatch - kind: Agent - budgetingMethod: Occurrences - objectives: - - displayName: Test Objective - value: 10 - target: 0.98 - rawMetric: - query: - cloudWatch: - region: us-east-1 - namespace: CloudWatchSynthetics - metricName: 2xx - stat: SampleCount - dimensions: - - name: CanaryName - value: web-app - op: gte - service: foo-slos - timeWindows: - - unit: Day - count: 28 - isRolling: true - alertPolicies: [] -`, - }, - { - name: "Test adding annotation to chose indicator kind", - args: args{ - filenames: []string{ - "../../test/v1/slo/slo-with-annotations.yaml", - }, - project: "default", - }, - wantOut: `--- -apiVersion: n9/v1alpha -kind: SLO -metadata: - name: monthy-openslo-slo - displayName: Python - project: default -spec: - description: "" - indicator: - metricSource: - project: default - name: Changeme - kind: Direct - budgetingMethod: Occurrences - objectives: - - displayName: Life of Brian - value: 10 - target: 0.98 - op: gte - service: foo-slos - timeWindows: - - unit: Day - count: 28 - isRolling: true - alertPolicies: [] -`, - }, - } - for _, tt := range tests { - tt := tt // https://gist.github.com/kunwardeep/80c2e9f3d3256c894898bae82d9f75d0 - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - out := &bytes.Buffer{} - if err := Nobl9(out, tt.args.filenames, tt.args.project); (err != nil) != tt.wantErr { - t.Errorf("Nobl9() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotOut := out.String(); gotOut != tt.wantOut { - assert.Equal(t, tt.wantOut, gotOut) - } - }) - } -} - -func Test_getN9Indicator(t *testing.T) { - t.Parallel() - var nilIndicator v1.SLISpec - type args struct { - indicator v1.SLISpec - project string - } - tests := []struct { - name string - args args - want nobl9v1alpha.Indicator - }{ - { - name: "nil Indicator", - args: args{ - indicator: nilIndicator, - project: "default", - }, - want: nobl9v1alpha.Indicator{ - MetricSource: nobl9v1alpha.MetricSourceSpec{ - Project: "default", - Name: "Changeme", - Kind: "Agent", - }, - }, - }, - { - name: "Empty Indicator", - args: args{ - indicator: nilIndicator, - }, - want: nobl9v1alpha.Indicator{ - MetricSource: nobl9v1alpha.MetricSourceSpec{ - Project: "default", - Name: "Changeme", - Kind: "Agent", - }, - }, - }, - { - name: "Empty RatioMetric", - args: args{ - indicator: v1.SLISpec{ - RatioMetric: &v1.RatioMetric{}, - }, - project: "FooBar", - }, - want: nobl9v1alpha.Indicator{ - MetricSource: nobl9v1alpha.MetricSourceSpec{ - Project: "FooBar", - Name: "Changeme", - Kind: "Agent", - }, - }, - }, - { - name: "Empty ThresholdMetric", - args: args{ - indicator: v1.SLISpec{ - ThresholdMetric: &v1.MetricSourceHolder{}, - }, - project: "FooBar", - }, - want: nobl9v1alpha.Indicator{ - MetricSource: nobl9v1alpha.MetricSourceSpec{ - Project: "FooBar", - Name: "Changeme", - Kind: "Agent", - }, - }, - }, - { - name: "Ratio Metric with good and total", - args: args{ - indicator: v1.SLISpec{ - RatioMetric: &v1.RatioMetric{ - Good: &v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - MetricSourceRef: "foo-bar-sli", - }, - }, - Total: v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - MetricSourceRef: "foo-bar-sli", - }, - }, - }, - }, - project: "FooBar", - }, - want: nobl9v1alpha.Indicator{ - MetricSource: nobl9v1alpha.MetricSourceSpec{ - Project: "FooBar", - Name: "foo-bar-sli", - Kind: "Agent", - }, - }, - }, - { - name: "Ratio Metric with bad and total", - args: args{ - indicator: v1.SLISpec{ - RatioMetric: &v1.RatioMetric{ - Bad: &v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - MetricSourceRef: "foo-bar-sli", - }, - }, - Total: v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - MetricSourceRef: "foo-bar-bad-sli", - }, - }, - }, - }, - project: "FooBar", - }, - want: nobl9v1alpha.Indicator{ - MetricSource: nobl9v1alpha.MetricSourceSpec{ - Project: "FooBar", - Name: "foo-bar-bad-sli", - Kind: "Agent", - }, - }, - }, - { - name: "Threshold Metric", - args: args{ - indicator: v1.SLISpec{ - ThresholdMetric: &v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - MetricSourceRef: "thresh-foo-sli", - }, - }, - }, - project: "FooBarThresh", - }, - want: nobl9v1alpha.Indicator{ - MetricSource: nobl9v1alpha.MetricSourceSpec{ - Project: "FooBarThresh", - Name: "thresh-foo-sli", - Kind: "Agent", - }, - }, - }, - } - for _, tt := range tests { - tt := tt // https://gist.github.com/kunwardeep/80c2e9f3d3256c894898bae82d9f75d0 - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got := getN9Indicator(tt.args.indicator, v1.Metadata{}, tt.args.project) - - assert.Equal(t, tt.want, got) - }) - } -} - -func Test_getN9Thresholds(t *testing.T) { - t.Parallel() - type args struct { - o []v1.Objective - indicator v1.SLISpec - } - tests := []struct { - name string - args args - want []nobl9v1alpha.Threshold - wantErr bool - }{ - { - name: "Single Objective, no Indicator", - args: args{ - o: []v1.Objective{ - { - DisplayName: "foo1", - Op: "gte", - Value: 0.01, - Target: 100, - }, - }, - }, - want: []nobl9v1alpha.Threshold{ - { - ThresholdBase: nobl9v1alpha.ThresholdBase{ - DisplayName: "foo1", - Value: 0.01, - }, - // anonymous function since we have to pass the address - BudgetTarget: func() *float64 { i := float64(100); return &i }(), - Operator: func() *string { i := "gte"; return &i }(), //nolint:goconst - }, - }, - }, - { - name: "Single Objective, RatioMetric Good Indicator", - args: args{ - o: []v1.Objective{ - { - DisplayName: "foo1", - Op: "gte", - Value: 0.01, - Target: 100, - }, - }, - indicator: v1.SLISpec{ - RatioMetric: &v1.RatioMetric{ - Counter: true, - Good: &v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - Type: "Datadog", - MetricSourceSpec: map[string]string{ - "query": "foo", - }, - }, - }, - Total: v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - Type: "Datadog", - MetricSourceSpec: map[string]string{ - "query": "bar", - }, - }, - }, - }, - }, - }, - want: []nobl9v1alpha.Threshold{ - { - ThresholdBase: nobl9v1alpha.ThresholdBase{ - DisplayName: "foo1", - Value: 0.01, - }, - // anonymous function since we have to pass the address - BudgetTarget: func() *float64 { i := float64(100); return &i }(), - CountMetrics: &nobl9v1alpha.CountMetricsSpec{ - Incremental: func() *bool { i := true; return &i }(), - GoodMetric: &nobl9v1alpha.MetricSpec{ - Datadog: &nobl9v1alpha.DatadogMetric{ - Query: func() *string { i := "foo"; return &i }(), - }, - }, - TotalMetric: &nobl9v1alpha.MetricSpec{ - Datadog: &nobl9v1alpha.DatadogMetric{ - Query: func() *string { i := "bar"; return &i }(), - }, - }, - }, - Operator: func() *string { i := "gte"; return &i }(), - }, - }, - }, - { - name: "Single Objective, RatioMetric Bad Indicator", - args: args{ - o: []v1.Objective{ - { - DisplayName: "foo1", - Op: "gte", - Value: 0.01, - Target: 100, - }, - }, - indicator: v1.SLISpec{ - RatioMetric: &v1.RatioMetric{ - Counter: true, - Bad: &v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - Type: "Datadog", - MetricSourceSpec: map[string]string{ - "query": "foo", - }, - }, - }, - Total: v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - Type: "Datadog", - MetricSourceSpec: map[string]string{ - "query": "bar", - }, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "Single Objective, Threshold Indicator", - args: args{ - o: []v1.Objective{ - { - DisplayName: "foo1", - Op: "gte", - Value: 0.01, - Target: 100, - }, - }, - indicator: v1.SLISpec{ - ThresholdMetric: &v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - Type: "NewRelic", - MetricSourceSpec: map[string]string{ - "nrql": "foo-bar", - }, - }, - }, - }, - }, - want: []nobl9v1alpha.Threshold{ - { - ThresholdBase: nobl9v1alpha.ThresholdBase{ - DisplayName: "foo1", - Value: 0.01, - }, - // anonymous function since we have to pass the address - BudgetTarget: func() *float64 { i := float64(100); return &i }(), - RawMetric: &nobl9v1alpha.RawMetricSpec{ - MetricQuery: &nobl9v1alpha.MetricSpec{ - NewRelic: &nobl9v1alpha.NewRelicMetric{ - NRQL: func() *string { i := "foo-bar"; return &i }(), //nolint:goconst - }, - }, - }, - Operator: func() *string { i := "gte"; return &i }(), - }, - }, - }, - { - name: "Multiple Objectives, Threshold Indicator", - args: args{ - o: []v1.Objective{ - { - DisplayName: "foo1", - Op: "gte", - Value: 0.98, - Target: 200, - }, - { - DisplayName: "foo1", - Op: "gte", - Value: 0.01, - Target: 100, - }, - }, - indicator: v1.SLISpec{ - ThresholdMetric: &v1.MetricSourceHolder{ - MetricSource: v1.MetricSource{ - Type: "NewRelic", - MetricSourceSpec: map[string]string{ - "nrql": "foo-bar", - }, - }, - }, - }, - }, - want: []nobl9v1alpha.Threshold{ - { - ThresholdBase: nobl9v1alpha.ThresholdBase{ - DisplayName: "foo1", - Value: 0.98, - }, - // anonymous function since we have to pass the address - BudgetTarget: func() *float64 { i := float64(200); return &i }(), - RawMetric: &nobl9v1alpha.RawMetricSpec{ - MetricQuery: &nobl9v1alpha.MetricSpec{ - NewRelic: &nobl9v1alpha.NewRelicMetric{ - NRQL: func() *string { i := "foo-bar"; return &i }(), - }, - }, - }, - Operator: func() *string { i := "gte"; return &i }(), - }, - { - ThresholdBase: nobl9v1alpha.ThresholdBase{ - DisplayName: "foo1", - Value: 0.01, - }, - // anonymous function since we have to pass the address - BudgetTarget: func() *float64 { i := float64(100); return &i }(), - RawMetric: &nobl9v1alpha.RawMetricSpec{ - MetricQuery: &nobl9v1alpha.MetricSpec{ - NewRelic: &nobl9v1alpha.NewRelicMetric{ - NRQL: func() *string { i := "foo-bar"; return &i }(), - }, - }, - }, - Operator: func() *string { i := "gte"; return &i }(), - }, - }, - }, - } - for _, tt := range tests { - tt := tt // https://gist.github.com/kunwardeep/80c2e9f3d3256c894898bae82d9f75d0 - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := getN9Thresholds(tt.args.o, tt.args.indicator) - if (err != nil) != tt.wantErr { - t.Errorf("getN9Thresholds() error = %v, wantErr %v", err, tt.wantErr) - return - } - assert.Equal(t, tt.want, got) - }) - } -} - -func Test_getN9CloudWatchQuery(t *testing.T) { - t.Parallel() - type args struct { - m map[string]string - } - tests := []struct { - name string - args args - want nobl9v1alpha.CloudWatchMetric - }{ - { - name: "has dimensions", - args: args{ - m: map[string]string{ - "namespace": "mynamespace", - "metricName": "mymetricname", - "region": "myregion", - "stat": "mystat", - "dimensions": "name:mydimensions,value:myvalue;name:mydimensions2,value:myvalue2", - }, - }, - want: nobl9v1alpha.CloudWatchMetric{ - Stat: func() *string { i := "mystat"; return &i }(), - Dimensions: []nobl9v1alpha.CloudWatchMetricDimension{ - { - Name: func() *string { i := "mydimensions"; return &i }(), - Value: func() *string { i := "myvalue"; return &i }(), - }, - { - Name: func() *string { i := "mydimensions2"; return &i }(), - Value: func() *string { i := "myvalue2"; return &i }(), - }, - }, - }, - }, - { - name: "has json", - args: args{ - m: map[string]string{ - "namespace": "mynamespace", - "metricName": "mymetricname", - "region": "myregion", - "json": "{\"foo\": \"bar\"}", - }, - }, - want: nobl9v1alpha.CloudWatchMetric{ - JSON: func() *string { i := "{\"foo\": \"bar\"}"; return &i }(), - }, - }, - { - name: "has SQL", - args: args{ - m: map[string]string{ - "namespace": "mynamespace", - "metricName": "mymetricname", - "region": "myregion", - "sql": "SELECT * FROM FOO", - }, - }, - want: nobl9v1alpha.CloudWatchMetric{ - SQL: func() *string { i := "SELECT * FROM FOO"; return &i }(), - }, - }, - } - for _, tt := range tests { - tt := tt // https://gist.github.com/kunwardeep/80c2e9f3d3256c894898bae82d9f75d0 - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got := getN9CloudWatchQuery(tt.args.m) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/pkg/discoverfiles/discoverfiles.go b/internal/files/discover.go similarity index 64% rename from pkg/discoverfiles/discoverfiles.go rename to internal/files/discover.go index b84ea53..d656e2b 100644 --- a/pkg/discoverfiles/discoverfiles.go +++ b/internal/files/discover.go @@ -1,50 +1,29 @@ -package discoverfiles +package files import ( "io/fs" "os" "path" "path/filepath" - - "github.com/spf13/cobra" - - "github.com/OpenSLO/oslo/internal/pathutil" ) -// RegisterFileRelatedFlags registers flags --file | -f and --recursive | -R for command -// passed as the argument and make them required. -func RegisterFileRelatedFlags(cmd *cobra.Command, filePaths *[]string, recursive *bool) { - const fileFlag = "file" - cmd.Flags().StringArrayVarP( - filePaths, fileFlag, "f", []string{}, - "The file(s) that contain the configurations.", - ) - if err := cmd.MarkFlagRequired(fileFlag); err != nil { - panic(err) - } - cmd.Flags().BoolVarP( - recursive, "recursive", "R", false, - "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.", //nolint:lll - ) -} - -// DiscoverFilePaths returns all file paths that come from file paths provided as the argument. +// Discover returns all file paths that come from file paths provided as the argument. // Return directly if they are standard files. For directories list all files available in its // root or recursively traverse all subdirectories and find files in them when the argument recursive // is true. For "-" path that indicates standard input or starts with http:// or https://, // it returns the path directly in the same way as other paths. -func DiscoverFilePaths(filePaths []string, recursive bool) ([]string, error) { //nolint:cyclop +func Discover(filePaths []string, recursive bool) ([]string, error) { //nolint:cyclop var discoveredPaths []string for _, p := range filePaths { // Indicates that a file should be read from standard input. // Code that consumes file paths needs to handle "-" // by reading from os.Stdin in such case. - if pathutil.IsStdin(p) { + if isStdin(p) { discoveredPaths = append(discoveredPaths, p) continue } // Content should be downloaded from this URL and treated as others. - if pathutil.IsURL(p) { + if isURL(p) { discoveredPaths = append(discoveredPaths, p) continue } diff --git a/pkg/discoverfiles/discoverfiles_test.go b/internal/files/discover_test.go similarity index 94% rename from pkg/discoverfiles/discoverfiles_test.go rename to internal/files/discover_test.go index 02fd202..9d316da 100644 --- a/pkg/discoverfiles/discoverfiles_test.go +++ b/internal/files/discover_test.go @@ -1,4 +1,4 @@ -package discoverfiles_test +package files_test import ( "io/fs" @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/OpenSLO/oslo/pkg/discoverfiles" + "github.com/OpenSLO/oslo/internal/files" ) // TestDiscoverFilePaths tests it on real filesystem, @@ -87,7 +87,7 @@ func TestDiscoverFilePaths(t *testing.T) { tC := tC t.Run(tC.name, func(t *testing.T) { t.Parallel() - res, err := discoverfiles.DiscoverFilePaths(tC.filePaths, tC.recursive) + res, err := files.Discover(tC.filePaths, tC.recursive) require.ErrorIs(t, err, tC.expectedError) require.Equal(t, tC.want, res) }) diff --git a/internal/files/format.go b/internal/files/format.go new file mode 100644 index 0000000..795e09f --- /dev/null +++ b/internal/files/format.go @@ -0,0 +1,54 @@ +/* +Package fmt handles formatting of the provided input. + +# Copyright © 2022 OpenSLO Team + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package files + +import ( + "fmt" + "io" + + "github.com/OpenSLO/OpenSLO/pkg/openslosdk" +) + +// Format formats multiple files and writes it to the provided writer, separated with "---". +func Format(out io.Writer, format openslosdk.ObjectFormat, sources []string) error { + for i, src := range sources { + if err := formatFile(out, format, src); err != nil { + return err + } + if i != len(sources)-1 { + if _, err := fmt.Fprintln(out, "---"); err != nil { + return err + } + } + } + return nil +} + +// formatFile formats a single formatFile and writes it to the provided writer. +func formatFile(out io.Writer, format openslosdk.ObjectFormat, source string) error { + content, err := readRawSchema(source) + if err != nil { + return fmt.Errorf("issue reading content: %w", err) + } + objects, err := readObjectsFromRawData(content) + if err != nil { + return fmt.Errorf("issue parsing objects: %w", err) + } + + return openslosdk.Encode(out, format, objects...) +} diff --git a/internal/files/format_test.go b/internal/files/format_test.go new file mode 100644 index 0000000..a8b0064 --- /dev/null +++ b/internal/files/format_test.go @@ -0,0 +1,119 @@ +/* +Copyright © 2022 OpenSLO Team + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package files_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/OpenSLO/OpenSLO/pkg/openslosdk" + "github.com/OpenSLO/oslo/internal/files" +) + +func TestFormatFiles(t *testing.T) { + t.Parallel() + tests := []struct { + name string + files []string + format openslosdk.ObjectFormat + wantOut string + wantErr bool + }{ + { + name: "Invalid file", + files: []string{"test/v1alpha/invalid-file.yaml"}, + format: openslosdk.FormatYAML, + wantErr: true, + wantOut: "", + }, + { + name: "Invalid content", + files: []string{"../../test/v1alpha/invalid-service.yaml"}, + format: openslosdk.FormatYAML, + wantErr: true, + wantOut: "", + }, + { + name: "Passes single file", + files: []string{"../../test/v1alpha/valid-service.yaml"}, + format: openslosdk.FormatYAML, + wantErr: false, + wantOut: `- apiVersion: openslo/v1alpha + kind: Service + metadata: + displayName: My Rad Service + name: my-rad-service + spec: + description: This is a great description of an even better service. +`, + }, + { + name: "Passes single JSON file", + files: []string{"../../test/v1alpha/valid-service.yaml"}, + format: openslosdk.FormatJSON, + wantErr: false, + wantOut: `[ + { + "apiVersion": "openslo/v1alpha", + "kind": "Service", + "metadata": { + "name": "my-rad-service", + "displayName": "My Rad Service" + }, + "spec": { + "description": "This is a great description of an even better service." + } + } +] +`, + }, + { + name: "Passes multiple files", + files: []string{"../../test/v1alpha/valid-service.yaml", "../../test/v1alpha/valid-service.yaml"}, + format: openslosdk.FormatYAML, + wantErr: false, + wantOut: `- apiVersion: openslo/v1alpha + kind: Service + metadata: + displayName: My Rad Service + name: my-rad-service + spec: + description: This is a great description of an even better service. +--- +- apiVersion: openslo/v1alpha + kind: Service + metadata: + displayName: My Rad Service + name: my-rad-service + spec: + description: This is a great description of an even better service. +`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + out := &bytes.Buffer{} + if err := files.Format(out, tc.format, tc.files); (err != nil) != tc.wantErr { + t.Errorf("fmtFile() error = %v, wantErr %v", err, tc.wantErr) + return + } + assert.Equal(t, tc.wantOut, out.String()) + }) + } +} diff --git a/internal/files/reader.go b/internal/files/reader.go new file mode 100644 index 0000000..6e76b87 --- /dev/null +++ b/internal/files/reader.go @@ -0,0 +1,91 @@ +/* +Package yamlutils provides functions to parse OpenSLO manifests. + +# Copyright © 2022 OpenSLO Team + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package files + +import ( + "bytes" + "io" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/OpenSLO/OpenSLO/pkg/openslo" + "github.com/OpenSLO/OpenSLO/pkg/openslosdk" +) + +// ReadObjects reads [openslo.Object] from the provided sources. +func ReadObjects(sources []string) ([]openslo.Object, error) { + var allObjects []openslo.Object + for _, src := range sources { + data, err := readRawSchema(src) + if err != nil { + return nil, err + } + objects, err := readObjectsFromRawData(data) + if err != nil { + return nil, err + } + allObjects = append(allObjects, objects...) + } + return allObjects, nil +} + +// readObjectsFromRawData reads [openslo.Object] from a byte slice. +func readObjectsFromRawData(data []byte) ([]openslo.Object, error) { + format := openslosdk.FormatYAML + if isJSONBuffer(data) { + format = openslosdk.FormatJSON + } + return openslosdk.Decode(bytes.NewReader(data), format) +} + +// readRawSchema reads raw OpenSLO schema from file path, HTTP address or stdin (path "-") to a byte slice. +func readRawSchema(path string) ([]byte, error) { + switch { + case isStdin(path): + return io.ReadAll(os.Stdin) + case isURL(path): + resp, err := http.Get(path) //nolint:gosec,noctx + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + return io.ReadAll(resp.Body) + default: + return os.ReadFile(filepath.Clean(path)) + } +} + +func isStdin(p string) bool { + return p == "-" +} + +func isURL(p string) bool { + return strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://") +} + +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) +} diff --git a/pkg/yamlutil/yamlutil_test.go b/internal/files/reader_test.go similarity index 52% rename from pkg/yamlutil/yamlutil_test.go rename to internal/files/reader_test.go index a14ef52..df4abea 100644 --- a/pkg/yamlutil/yamlutil_test.go +++ b/internal/files/reader_test.go @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -package yamlutil_test +package files import ( _ "embed" @@ -22,12 +22,7 @@ import ( "os" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/OpenSLO/oslo/pkg/manifest" - v1 "github.com/OpenSLO/oslo/pkg/manifest/v1" - "github.com/OpenSLO/oslo/pkg/yamlutil" ) //go:embed test-input @@ -41,7 +36,7 @@ func TestReadConf(t *testing.T) { //nolint:tparallel t.Run("from filepath successfully", func(t *testing.T) { t.Parallel() const filePath = "./test-input" - content, err := yamlutil.ReadConf(filePath) + content, err := readRawSchema(filePath) require.NoErrorf(t, err, "can't read content from filepath %q", filePath) require.Equal(t, expectedContent, content) }) @@ -54,7 +49,7 @@ func TestReadConf(t *testing.T) { //nolint:tparallel })) defer server.Close() - content, err := yamlutil.ReadConf(server.URL) + content, err := readRawSchema(server.URL) require.NoErrorf(t, err, "can't read content from URL of the test server: %q", server.URL) require.Equal(t, expectedContent, content) }) @@ -72,80 +67,8 @@ func TestReadConf(t *testing.T) { //nolint:tparallel os.Stdin = output const indicateStdin = "-" - content, err := yamlutil.ReadConf(indicateStdin) + content, err := readRawSchema(indicateStdin) require.NoError(t, err, "can't read content from stdin") require.Equal(t, expectedContent, content) }) } - -func TestParse(t *testing.T) { - t.Parallel() - type args struct { - fileContent []byte - filename string - } - tests := []struct { - name string - args args - want []manifest.OpenSLOKind - wantErr bool - }{ - { - name: "TestParse", - args: args{ - fileContent: []byte(testInput), - filename: "test.yaml", - }, - want: []manifest.OpenSLOKind{ - v1.Service{ - ObjectHeader: v1.ObjectHeader{ - ObjectHeader: manifest.ObjectHeader{ - APIVersion: "openslo/v1", - }, - Kind: "Service", - MetadataHolder: v1.MetadataHolder{ - Metadata: v1.Metadata{ - Name: "my-rad-service", - DisplayName: "My Rad Service", - }, - }, - }, - Spec: v1.ServiceSpec{ - Description: "This is a great description of an even better service.", - }, - }, - v1.Service{ - ObjectHeader: v1.ObjectHeader{ - ObjectHeader: manifest.ObjectHeader{ - APIVersion: "openslo/v1", - }, - Kind: "Service", - MetadataHolder: v1.MetadataHolder{ - Metadata: v1.Metadata{ - Name: "my-rad-service-deux", - DisplayName: "My Rad Service le Deux", - }, - }, - }, - Spec: v1.ServiceSpec{ - Description: "This is a great description of an even better service.", - }, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := yamlutil.Parse(tt.args.fileContent, tt.args.filename) - if (err != nil) != tt.wantErr { - t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) - return - } - assert.Equal(t, 2, len(got)) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/pkg/yamlutil/test-input b/internal/files/test-input similarity index 100% rename from pkg/yamlutil/test-input rename to internal/files/test-input diff --git a/pkg/discoverfiles/testfiles/a/a1.yml b/internal/files/testfiles/a/a1.yml similarity index 100% rename from pkg/discoverfiles/testfiles/a/a1.yml rename to internal/files/testfiles/a/a1.yml diff --git a/pkg/discoverfiles/testfiles/a/a2.yml b/internal/files/testfiles/a/a2.yml similarity index 100% rename from pkg/discoverfiles/testfiles/a/a2.yml rename to internal/files/testfiles/a/a2.yml diff --git a/pkg/discoverfiles/testfiles/a/b/b1.yml b/internal/files/testfiles/a/b/b1.yml similarity index 100% rename from pkg/discoverfiles/testfiles/a/b/b1.yml rename to internal/files/testfiles/a/b/b1.yml diff --git a/pkg/discoverfiles/testfiles/a/b/b2.yml b/internal/files/testfiles/a/b/b2.yml similarity index 100% rename from pkg/discoverfiles/testfiles/a/b/b2.yml rename to internal/files/testfiles/a/b/b2.yml diff --git a/pkg/discoverfiles/testfiles/aa/aa1.yml b/internal/files/testfiles/aa/aa1.yml similarity index 100% rename from pkg/discoverfiles/testfiles/aa/aa1.yml rename to internal/files/testfiles/aa/aa1.yml diff --git a/pkg/discoverfiles/testfiles/x.yml b/internal/files/testfiles/x.yml similarity index 100% rename from pkg/discoverfiles/testfiles/x.yml rename to internal/files/testfiles/x.yml diff --git a/pkg/discoverfiles/testfiles/y.yaml b/internal/files/testfiles/y.yaml similarity index 100% rename from pkg/discoverfiles/testfiles/y.yaml rename to internal/files/testfiles/y.yaml diff --git a/internal/fmt/fmt.go b/internal/fmt/fmt.go deleted file mode 100644 index 802f570..0000000 --- a/internal/fmt/fmt.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Package fmt handles formatting of the provided input. - -# Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package fmt - -import ( - "fmt" - "io" - - "gopkg.in/yaml.v3" - - "github.com/OpenSLO/oslo/pkg/yamlutil" -) - -// Files formats multiple files and writes it to the provided writer, separated with "---". -func Files(out io.Writer, sources []string) error { - count := len(sources) - for i := 0; i < count; i++ { - if err := file(out, sources[i]); err != nil { - return err - } - if i != count-1 { - if _, err := fmt.Fprintln(out, "---"); err != nil { - return err - } - } - } - return nil -} - -// file formats a single file and writes it to the provided writer. -func file(out io.Writer, source string) error { - // Get the file contents. - content, err := yamlutil.ReadConf(source) - if err != nil { - return fmt.Errorf("issue reading content: %w", err) - } - - // Parse the byte arrays to OpenSLOKind objects. - parsed, err := yamlutil.Parse(content, source) - if err != nil { - return fmt.Errorf("issue parsing content: %w", err) - } - - // New encoder that will write to the provided writer. - enc := yaml.NewEncoder(out) - enc.SetIndent(2) - - for _, o := range parsed { - // Encode the object to YAML. - if err := enc.Encode(o); err != nil { - return err - } - } - return nil -} diff --git a/internal/fmt/fmt_test.go b/internal/fmt/fmt_test.go deleted file mode 100644 index 0f9dc04..0000000 --- a/internal/fmt/fmt_test.go +++ /dev/null @@ -1,94 +0,0 @@ -/* -Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package fmt_test - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/OpenSLO/oslo/internal/fmt" -) - -func TestFiles(t *testing.T) { - t.Parallel() - tests := []struct { - name string - files []string - wantOut string - wantErr bool - }{ - { - name: "Invalid file", - files: []string{"test/v1alpha/invalid-file.yaml"}, - wantErr: true, - wantOut: "", - }, - { - name: "Invalid content", - files: []string{"../../test/v1alpha/invalid-service.yaml"}, - wantErr: true, - wantOut: "", - }, - { - name: "Passes single file", - files: []string{"../../test/v1alpha/valid-service.yaml"}, - wantErr: false, - wantOut: `apiVersion: openslo/v1alpha -kind: Service -metadata: - name: my-rad-service - displayName: My Rad Service -spec: - description: This is a great description of an even better service. -`, - }, - { - name: "Passes multiple files", - files: []string{"../../test/v1alpha/valid-service.yaml", "../../test/v1alpha/valid-service.yaml"}, - wantErr: false, - wantOut: `apiVersion: openslo/v1alpha -kind: Service -metadata: - name: my-rad-service - displayName: My Rad Service -spec: - description: This is a great description of an even better service. ---- -apiVersion: openslo/v1alpha -kind: Service -metadata: - name: my-rad-service - displayName: My Rad Service -spec: - description: This is a great description of an even better service. -`, - }, - } - for _, tt := range tests { - tt := tt // https://gist.github.com/kunwardeep/80c2e9f3d3256c894898bae82d9f75d0 - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - out := &bytes.Buffer{} - if err := fmt.Files(out, tt.files); (err != nil) != tt.wantErr { - t.Errorf("fmtFile() error = %v, wantErr %v", err, tt.wantErr) - return - } - assert.Equal(t, tt.wantOut, out.String()) - }) - } -} diff --git a/internal/manifest/nobl9/objects.go b/internal/manifest/nobl9/objects.go deleted file mode 100644 index 69dd7ad..0000000 --- a/internal/manifest/nobl9/objects.go +++ /dev/null @@ -1,35 +0,0 @@ -/* -Package manifest provides foundational structs. - -Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package nobl9manifest - -// ObjectHeader represents Header which is common for all available Objects. -type ObjectHeader struct { - APIVersion string `yaml:"apiVersion" validate:"required" example:"openslo/v1alpha"` -} - -// ObjectGeneric represents struct to which every Objects is parsable -// Specific types of Object have different structures as Spec. -type ObjectGeneric struct { - ObjectHeader `yaml:",inline"` -} - -// OpenSLOKind represents a type of object described by OpenSLO. -type OpenSLOKind interface { - Kind() string -} diff --git a/internal/manifest/nobl9/v1alpha/objects.go b/internal/manifest/nobl9/v1alpha/objects.go deleted file mode 100644 index 67f9f0a..0000000 --- a/internal/manifest/nobl9/v1alpha/objects.go +++ /dev/null @@ -1,380 +0,0 @@ -/* -Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package nobl9v1alpha - -import ( - nobl9manifest "github.com/OpenSLO/oslo/internal/manifest/nobl9" -) - -// apiVersion: n9/v1alpha -// kind: Service -// metadata: -// -// name: # string -// displayName: # string -// project: # string -// -// spec: -// -// description: # string -// serviceType: # string -const ( - APIVersion = "n9/v1alpha" -) - -// Possible values of field kind for valid Objects. -const ( - KindSLO = "SLO" - KindService = "Service" -) - -// Labels represents a set of labels. -type Labels map[string][]string - -// Metadata represents part of object which is is common for all available Objects, for internal usage. -type Metadata struct { - Name string `yaml:"name" validate:"required" example:"name"` - DisplayName string `yaml:"displayName,omitempty" validate:"omitempty,min=0,max=63" example:"Prometheus Source"` - Project string `yaml:"project,omitempty" validate:"objectName" example:"default"` - Labels Labels `yaml:"labels,omitempty" validate:"omitempty,labels"` -} - -// MetadataHolder is an intermediate structure that can provides metadata related -// field to other structures. -type MetadataHolder struct { - Metadata Metadata `yaml:"metadata"` -} - -// ObjectHeader is a header for all objects. -type ObjectHeader struct { - nobl9manifest.ObjectHeader `yaml:",inline"` - Kind string `yaml:"kind" validate:"required,oneof=Service SLO AlertNotificationTarget" example:"kind"` //nolint:lll - MetadataHolder `yaml:",inline"` -} - -// Service struct which mapped one to one with kind: service yaml definition. -type Service struct { - ObjectHeader `yaml:",inline"` - Spec ServiceSpec `yaml:"spec"` -} - -// Kind returns the name of this type. -func (Service) Kind() string { - return "Service" -} - -// ServiceSpec represents content of Spec typical for Service Object. -type ServiceSpec struct { - Description string `yaml:"description" validate:"max=1050" example:"Bleeding edge web app"` -} - -// AlertPolicy represents a set of conditions that can trigger an alert. -type AlertPolicy struct { - ObjectHeader `yaml:",inline"` - Spec AlertPolicySpec `yaml:"spec"` -} - -// AlertPolicySpec represents content of AlertPolicy's Spec. -type AlertPolicySpec struct { - Description string `yaml:"description" validate:"description" example:"Error budget is at risk"` - Severity string `yaml:"severity" validate:"required,severity" example:"High"` - CoolDownDuration string `yaml:"coolDown,omitempty" validate:"omitempty,validDuration" example:"5m"` //nolint:lll - Conditions []AlertCondition `yaml:"conditions" validate:"required,min=1,dive"` - AlertMethods []string `yaml:"alertMethods"` -} - -// AlertCondition represents a condition to meet to trigger an alert. -type AlertCondition struct { - Measurement string `yaml:"measurement" validate:"required,alertPolicyMeasurement" example:"BurnedBudget"` - Value interface{} `yaml:"value" validate:"required" swaggertype:"string" example:"0.97"` - LastsForDuration string `yaml:"lastsFor,omitempty" validate:"omitempty,validDuration,nonNegativeDuration" example:"15m"` //nolint:lll - Operation string `yaml:"op" validate:"required,alertOperation" example:"lt"` -} - -// AlertMethodAssignment represents an AlertMethod assigned to AlertPolicy. -type AlertMethodAssignment struct { - Project string `yaml:"project,omitempty" validate:"omitempty,objectName" example:"default"` - Name string `yaml:"name" validate:"required,objectName" example:"webhook-alertmethod"` -} - -// SLO struct which mapped one to one with kind: slo yaml definition, external usage. -type SLO struct { - ObjectHeader `yaml:",inline"` - Spec SLOSpec `yaml:"spec"` -} - -// SLOSpec represents content of Spec typical for SLO Object. -type SLOSpec struct { - Description string `yaml:"description" validate:"description" example:"Total count of server requests"` //nolint:lll - Indicator Indicator `yaml:"indicator"` - BudgetingMethod string `yaml:"budgetingMethod" validate:"required,budgetingMethod" example:"Occurrences"` - Thresholds []Threshold `yaml:"objectives" validate:"required,dive"` - Service string `yaml:"service" validate:"required,objectName" example:"webapp-service"` - TimeWindows []TimeWindow `yaml:"timeWindows" validate:"required,len=1,dive"` - AlertPolicies []string `yaml:"alertPolicies" validate:"omitempty"` - Attachments []Attachment `yaml:"attachments,omitempty" validate:"omitempty,len=1,dive"` - CreatedAt string `yaml:"createdAt,omitempty"` -} - -// ThresholdBase base structure representing a threshold. -type ThresholdBase struct { - DisplayName string `yaml:"displayName" validate:"objectiveDisplayName" example:"Good"` - Value float64 `yaml:"value" validate:"numeric" example:"100"` -} - -// Threshold represents single threshold for SLO, for internal usage. -type Threshold struct { - ThresholdBase `yaml:",inline"` - BudgetTarget *float64 `yaml:"target" validate:"required,numeric,gte=0,lt=1" example:"0.9"` - TimeSliceTarget *float64 `yaml:"timeSliceTarget,omitempty" example:"0.9"` - CountMetrics *CountMetricsSpec `yaml:"countMetrics,omitempty"` - RawMetric *RawMetricSpec `yaml:"rawMetric,omitempty"` - Operator *string `yaml:"op,omitempty" example:"lte"` - Name *string `yaml:"name,omitempty"` -} - -// Indicator represents integration with metric source can be. e.g. Prometheus, Datadog, for internal usage. -type Indicator struct { - MetricSource MetricSourceSpec `yaml:"metricSource" validate:"required"` - RawMetric MetricSpec `yaml:"rawMetric,omitempty"` -} - -// Attachment represents user defined URL attached to SLO. -type Attachment struct { - URL string `yaml:"url" validate:"required,url"` - DisplayName *string `yaml:"displayName,omitempty"` -} - -type Calendar struct { - StartTime string `yaml:"startTime" validate:"required,dateWithTime,minDateTime" example:"2020-01-21 12:30:00"` - TimeZone string `yaml:"timeZone" validate:"required,timeZone" example:"America/New_York"` -} - -// Period represents period of time. -type Period struct { - Begin string `yaml:"begin"` - End string `yaml:"end"` -} - -// TimeWindow represents content of time window. -type TimeWindow struct { - Unit string `yaml:"unit" validate:"required,timeUnit" example:"Week"` - Count int `yaml:"count" validate:"required,gt=0" example:"1"` - IsRolling bool `yaml:"isRolling" example:"true"` - Calendar *Calendar `yaml:"calendar,omitempty"` -} - -// CountMetricsSpec represents set of two time series of good and total counts. -type CountMetricsSpec struct { - Incremental *bool `yaml:"incremental" validate:"required"` - GoodMetric *MetricSpec `yaml:"good" validate:"required"` - TotalMetric *MetricSpec `yaml:"total" validate:"required"` -} - -// RawMetricSpec represents integration with a metric source for a particular threshold. -type RawMetricSpec struct { - MetricQuery *MetricSpec `yaml:"query" validate:"required"` -} - -type MetricSourceSpec struct { - Project string `yaml:"project,omitempty" validate:"omitempty,objectName" example:"default"` - Name string `yaml:"name" validate:"required,objectName" example:"prometheus-source"` - Kind string `yaml:"kind" validate:"omitempty,metricSourceKind" example:"Agent"` -} - -// MetricSpec defines single time series obtained from data source. -type MetricSpec struct { - Prometheus *PrometheusMetric `yaml:"prometheus,omitempty"` - Datadog *DatadogMetric `yaml:"datadog,omitempty"` - NewRelic *NewRelicMetric `yaml:"newRelic,omitempty"` - AppDynamics *AppDynamicsMetric `yaml:"appDynamics,omitempty"` - Splunk *SplunkMetric `yaml:"splunk,omitempty"` - Lightstep *LightstepMetric `yaml:"lightstep,omitempty"` - SplunkObservability *SplunkObservabilityMetric `yaml:"splunkObservability,omitempty"` - Dynatrace *DynatraceMetric `yaml:"dynatrace,omitempty"` - Elasticsearch *ElasticsearchMetric `yaml:"elasticsearch,omitempty"` - ThousandEyes *ThousandEyesMetric `yaml:"thousandEyes,omitempty"` - Graphite *GraphiteMetric `yaml:"graphite,omitempty"` - BigQuery *BigQueryMetric `yaml:"bigQuery,omitempty"` - OpenTSDB *OpenTSDBMetric `yaml:"opentsdb,omitempty"` - GrafanaLoki *GrafanaLokiMetric `yaml:"grafanaLoki,omitempty"` - CloudWatch *CloudWatchMetric `yaml:"cloudWatch,omitempty"` - Pingdom *PingdomMetric `yaml:"pingdom,omitempty"` - AmazonPrometheus *AmazonPrometheusMetric `yaml:"amazonPrometheus,omitempty"` - Redshift *RedshiftMetric `yaml:"redshift,omitempty"` - SumoLogic *SumoLogicMetric `yaml:"sumoLogic,omitempty"` - Instana *InstanaMetric `yaml:"instana,omitempty"` - GoogleCloudMonitoring *GoogleCloudMonitoringMetric `yaml:"gcm,omitempty"` -} - -// PrometheusMetric represents metric from Prometheus. -type PrometheusMetric struct { - PromQL *string `yaml:"promql" validate:"required" example:"cpu_usage_user{cpu=\"cpu-total\"}"` -} - -// AmazonPrometheusMetric represents metric from Amazon Managed Prometheus. -type AmazonPrometheusMetric struct { - PromQL *string `yaml:"promql" validate:"required" example:"cpu_usage_user{cpu=\"cpu-total\"}"` -} - -// DatadogMetric represents metric from Datadog. -type DatadogMetric struct { - Query *string `yaml:"query" validate:"required"` -} - -// NewRelicMetric represents metric from NewRelic. -type NewRelicMetric struct { - NRQL *string `yaml:"nrql" validate:"required"` -} - -// ThousandEyesMetric represents metric from ThousandEyes. -type ThousandEyesMetric struct { - TestID *int64 `yaml:"testID" validate:"required,gte=0"` - TestType *string `yaml:"testType" validate:"supportedThousandEyesTestType"` -} - -// AppDynamicsMetric represents metric from AppDynamics. -type AppDynamicsMetric struct { - ApplicationName *string `yaml:"applicationName" validate:"required,notEmpty"` - MetricPath *string `yaml:"metricPath" validate:"required,unambiguousAppDynamicMetricPath"` -} - -// SplunkMetric represents metric from Splunk. -type SplunkMetric struct { - Query *string `yaml:"query" validate:"required,notEmpty,splunkQueryValid"` -} - -// LightstepMetric represents metric from Lightstep. -type LightstepMetric struct { - StreamID *string `yaml:"streamId" validate:"required"` - TypeOfData *string `yaml:"typeOfData" validate:"required,oneof=latency error_rate good total"` - Percentile *float64 `yaml:"percentile,omitempty"` -} - -// SplunkObservabilityMetric represents metric from SplunkObservability. -type SplunkObservabilityMetric struct { - Program *string `yaml:"program" validate:"required"` -} - -// DynatraceMetric represents metric from Dynatrace. -type DynatraceMetric struct { - MetricSelector *string `yaml:"metricSelector" validate:"required"` -} - -// ElasticsearchMetric represents metric from Elasticsearch. -type ElasticsearchMetric struct { - Index *string `yaml:"index" validate:"required"` - Query *string `yaml:"query" validate:"required"` -} - -// CloudWatchMetric represents metric from CloudWatch. -type CloudWatchMetric struct { - Region *string `yaml:"region" validate:"required,max=255"` - Namespace *string `yaml:"namespace,omitempty"` - MetricName *string `yaml:"metricName,omitempty"` - Stat *string `yaml:"stat,omitempty"` - Dimensions []CloudWatchMetricDimension `yaml:"dimensions,omitempty" validate:"max=10,uniqueDimensionNames,dive"` - SQL *string `yaml:"sql,omitempty"` - JSON *string `yaml:"json,omitempty"` -} - -// RedshiftMetric represents metric from Redshift. -type RedshiftMetric struct { - Region *string `yaml:"region" validate:"required,max=255"` - ClusterID *string `yaml:"clusterId" validate:"required"` - DatabaseName *string `yaml:"databaseName" validate:"required"` - Query *string `yaml:"query" validate:"required,redshiftRequiredColumns"` -} - -// SumoLogicMetric represents metric from Sumo Logic. -type SumoLogicMetric struct { - Type *string `yaml:"type" validate:"required"` - Query *string `yaml:"query" validate:"required"` - Quantization *string `yaml:"quantization,omitempty"` - Rollup *string `yaml:"rollup,omitempty"` - // For struct level validation refer to sumoLogicStructValidation in pkg/manifest/v1alpha/validator.go -} - -// InstanaMetric represents metric from Redshift. -type InstanaMetric struct { - MetricType string `yaml:"metricType" validate:"required,oneof=infrastructure application"` //nolint:lll - Infrastructure *InstanaInfrastructureMetricType `yaml:"infrastructure,omitempty"` - Application *InstanaApplicationMetricType `yaml:"application,omitempty"` -} - -type InstanaInfrastructureMetricType struct { - MetricRetrievalMethod string `yaml:"metricRetrievalMethod" validate:"required,oneof=query snapshot"` - Query *string `yaml:"query,omitempty"` - SnapshotID *string `yaml:"snapshotId,omitempty"` - MetricID string `yaml:"metricId" validate:"required"` - PluginID string `yaml:"pluginId" validate:"required"` -} - -type InstanaApplicationMetricType struct { - MetricID string `yaml:"metricId" validate:"required,oneof=calls erroneousCalls errors latency"` //nolint:lll - Aggregation string `yaml:"aggregation" validate:"required"` - GroupBy InstanaApplicationMetricGroupBy `yaml:"groupBy" validate:"required"` - APIQuery string `yaml:"apiQuery" validate:"required,json"` - IncludeInternal bool `yaml:"includeInternal,omitempty"` - IncludeSynthetic bool `yaml:"includeSynthetic,omitempty"` -} - -type InstanaApplicationMetricGroupBy struct { - Tag string `yaml:"tag" validate:"required"` - TagEntity string `yaml:"tagEntity" validate:"required,oneof=DESTINATION SOURCE NOT_APPLICABLE"` - TagSecondLevelKey *string `yaml:"tagSecondLevelKey,omitempty"` -} - -// CloudWatchMetricDimension represents name/value pair that is part of the identity of a metric. -type CloudWatchMetricDimension struct { - Name *string `yaml:"name" validate:"required,max=255,ascii,notBlank"` - Value *string `yaml:"value" validate:"required,max=255,ascii,notBlank"` -} - -// PingdomMetric represents metric from Pingdom. -type PingdomMetric struct { - CheckID *string `yaml:"checkId" validate:"required,notBlank,numeric" example:"1234567"` - CheckType *string `yaml:"checkType" validate:"required,pingdomCheckTypeFieldValid" example:"uptime"` - Status *string `yaml:"status,omitempty" validate:"omitempty,pingdomStatusValid" example:"up,down"` -} - -// GraphiteMetric represents metric from Graphite. -type GraphiteMetric struct { - MetricPath *string `yaml:"metricPath" validate:"required,metricPathGraphite"` -} - -// BigQueryMetric represents metric from BigQuery. -type BigQueryMetric struct { - Query string `yaml:"query" validate:"required,bigQueryRequiredColumns"` - ProjectID string `yaml:"projectId" validate:"required"` - Location string `yaml:"location" validate:"required"` -} - -// OpenTSDBMetric represents metric from OpenTSDB. -type OpenTSDBMetric struct { - Query *string `yaml:"query" validate:"required"` -} - -// GrafanaLokiMetric represents metric from GrafanaLokiMetric. -type GrafanaLokiMetric struct { - Logql *string `yaml:"logql" validate:"required"` -} - -// GoogleCloudMonitoringMetric represents metric from GoogleCloudMonitoring. -type GoogleCloudMonitoringMetric struct { - Query *string `yaml:"query" validate:"required"` - ProjectID *string `yaml:"projectId" validate:"required"` -} diff --git a/internal/pathutil/pathutil.go b/internal/pathutil/pathutil.go deleted file mode 100644 index 35b28b5..0000000 --- a/internal/pathutil/pathutil.go +++ /dev/null @@ -1,13 +0,0 @@ -package pathutil - -import ( - "strings" -) - -func IsStdin(p string) bool { - return p == "-" -} - -func IsURL(p string) bool { - return strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://") -} diff --git a/pkg/manifest/models.go b/pkg/manifest/models.go deleted file mode 100644 index 1f55cc7..0000000 --- a/pkg/manifest/models.go +++ /dev/null @@ -1,35 +0,0 @@ -/* -Package manifest provides foundational structs. - -Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package manifest - -// ObjectHeader represents Header which is common for all available Objects. -type ObjectHeader struct { - APIVersion string `yaml:"apiVersion" validate:"required" example:"openslo/v1alpha"` -} - -// ObjectGeneric represents struct to which every Objects is parsable -// Specific types of Object have different structures as Spec. -type ObjectGeneric struct { - ObjectHeader `yaml:",inline"` -} - -// OpenSLOKind represents a type of object described by OpenSLO. -type OpenSLOKind interface { - Kind() string -} diff --git a/pkg/manifest/v1/alerts.go b/pkg/manifest/v1/alerts.go deleted file mode 100644 index 5467883..0000000 --- a/pkg/manifest/v1/alerts.go +++ /dev/null @@ -1,107 +0,0 @@ -/* -Package v1 contains all the types that are exported by the v1 API. -Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package v1 - -// AlertCondition is a condition that is used to trigger an alert. -type AlertCondition struct { - ObjectHeader `yaml:",inline"` - Spec AlertConditionSpec `yaml:"spec"` -} - -// AlertConditionInline is used for inline definitions. It is slightly -// different from the AlertCondition type because it does not have an APIVersion. -type AlertConditionInline struct { - Kind string `yaml:"kind" validate:"required"` - Metadata Metadata `yaml:"metadata" validate:"required"` - Spec AlertConditionSpec `yaml:"spec" validate:"required"` -} - -// AlertConditionSpec is the specification of an alert condition. -type AlertConditionSpec struct { - Description string `yaml:"description,omitempty" validate:"max=1050,omitempty" example:"If the CPU usage is too high for given period then it should alert"` //nolint:lll - Severity string `yaml:"severity" validate:"required" example:"page"` - Condition ConditionType `yaml:"condition" validate:"required"` -} - -// ConditionType is the type of a condition to trigger an alert. -type ConditionType struct { - Kind string `yaml:"kind" validate:"required" example:"burnrate"` - Threshold float64 `yaml:"threshold" validate:"required" example:"2"` - LookbackWindow string `yaml:"lookbackWindow" validate:"required,validDuration" example:"1h"` - AlertAfter string `yaml:"alertAfter" validate:"required,validDuration" example:"5m"` -} - -// AlertNotificationTarget is a target for sending alerts. -type AlertNotificationTarget struct { - ObjectHeader `yaml:",inline"` - Spec AlertNotificationTargetSpec `yaml:"spec"` -} - -// AlertNotificationTargetSpec is the specification of an alert notification target. -type AlertNotificationTargetSpec struct { - Target string `yaml:"target" validate:"required" example:"slack"` - Description string `yaml:"description,omitempty" validate:"max=1050,omitempty" example:"Sends P1 alert notifications to the slack channel"` //nolint:lll -} - -// AlertPolicy is a policy for sending alerts. -type AlertPolicy struct { - ObjectHeader `yaml:",inline"` - Spec AlertPolicySpec `yaml:"spec"` -} - -// AlertPolicyCondition is a condition that is used to trigger an alert in an alert policy. It can -// be either an inline condition or a reference to an alert condition. -type AlertPolicyCondition struct { - *AlertPolicyConditionSpec `yaml:",inline,omitempty" validate:"required_without=AlertConditionInline"` - *AlertConditionInline `yaml:",inline,omitempty" validate:"required_without=AlertPolicyConditionSpec"` -} - -// AlertPolicyConditionSpec is the specification of an alert policy condition. It is -// used to reference an AlertCondition. -type AlertPolicyConditionSpec struct { - ConditionRef string `yaml:"conditionRef" validate:"max=1050,required" example:"cpu-usage-breach"` -} - -// AlertPolicyNotificationTarget is a reference to an AlertNotificationTarget. -type AlertPolicyNotificationTarget struct { - TargetRef string `yaml:"targetRef" validate:"required" example:"OnCallDevopsMailNotification"` -} - -// AlertPolicySpec is the specification of an alert policy. -type AlertPolicySpec struct { - Description string `yaml:"description,omitempty" validate:"max=1050,omitempty" example:"Alert policy for cpu usage breaches, notifies on-call devops via email"` //nolint:lll - AlertWhenNoData bool `yaml:"alertWhenNoData"` - AlertWhenBreaching bool `yaml:"alertWhenBreaching"` - AlertWhenResolved bool `yaml:"alertWhenResolved"` - Conditions []AlertPolicyCondition `yaml:"conditions" validate:"required,len=1,dive"` - NotificationTargets []AlertPolicyNotificationTarget `yaml:"notificationTargets" validate:"required,dive"` -} - -// Kind returns the name of this type. -func (AlertNotificationTarget) Kind() string { - return "AlertNotificationTarget" -} - -// Kind returns the name of this type. -func (AlertCondition) Kind() string { - return "AlertCondition" -} - -// Kind returns the name of this type. -func (AlertPolicy) Kind() string { - return "AlertPolicy" -} diff --git a/pkg/manifest/v1/indicator.go b/pkg/manifest/v1/indicator.go deleted file mode 100644 index 77e4ded..0000000 --- a/pkg/manifest/v1/indicator.go +++ /dev/null @@ -1,137 +0,0 @@ -/* -Copyright © 2022 OpenSLO Team -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1 - -import ( - "fmt" - "strings" - - "gopkg.in/yaml.v3" -) - -// DataSource defines the data source for the SLI. -type DataSource struct { - ObjectHeader `yaml:",inline"` - Spec DataSourceSpec `yaml:"spec" validate:"required"` -} - -// DataSourceSpec defines the data source specification. -type DataSourceSpec struct { - Type string `yaml:"type" validate:"required"` - ConnectionDetails map[string]string `yaml:"connectionDetails"` -} - -// SLI represents the SLI. -type SLI struct { - ObjectHeader `yaml:",inline"` - Spec SLISpec `yaml:"spec" validate:"required"` -} - -// SLIInline represents the SLI inline. -type SLIInline struct { - Metadata Metadata `yaml:"metadata" validate:"required"` - Spec SLISpec `yaml:"spec" validate:"required"` -} - -// SLISpec defines the SLI specification. -type SLISpec struct { - ThresholdMetric *MetricSourceHolder `yaml:"thresholdMetric,omitempty" validate:"required_without=RatioMetric"` - RatioMetric *RatioMetric `yaml:"ratioMetric,omitempty" validate:"required_without=ThresholdMetric"` -} - -// RatioMetric represents the ratio metric. -type RatioMetric struct { - Counter bool `yaml:"counter" example:"true"` - Good *MetricSourceHolder `yaml:"good,omitempty" validate:"required_without=Bad"` - Bad *MetricSourceHolder `yaml:"bad,omitempty" validate:"required_without=Good"` - Total MetricSourceHolder `yaml:"total" validate:"required"` -} - -// MetricSourceHolder represents the metric source holder. -type MetricSourceHolder struct { - MetricSource MetricSource `yaml:"metricSource" validate:"required"` -} - -// MetricSource represents the metric source. -type MetricSource struct { - MetricSourceRef string `yaml:"metricSourceRef,omitempty" validate:"required_without=MetricSourceSpec"` - Type string `yaml:"type,omitempty" validate:"required_without=MetricSourceRef"` - MetricSourceSpec map[string]string `yaml:"spec" validate:"required_without=MetricSourceRef"` -} - -// UnmarshalYAML is used to override the default unmarshal behavior. -// MetricSources can have varying structures, so this method performs the following tasks: -// 1. Extracts the MetricSourceRef and Type separately, and assigns them to the MetricSource. -// 2. Attempts to unmarshal the MetricSourceSpec, which can be either a string, a list, or a more complex structure: -// 2a. If it's a scalar string, it is added directly as a single string. -// 2b. If it's a list of scalar values, they are concatenated into a single semicolon separated string. -// 2c. If it's a more complex sequence, the values are processed and flattened appropriately. -// -// This also assumes a certain flat structure that we can revisit if the need arises. - -func (m *MetricSource) UnmarshalYAML(value *yaml.Node) error { - // temp struct to unmarshal the string values - var tmpMetricSource struct { - MetricSourceRef string `yaml:"metricSourceRef,omitempty" validate:"required_without=MetricSourceSpec"` - Type string `yaml:"type,omitempty" validate:"required_without=MetricSourceRef"` - MetricSourceSpec map[string]yaml.Node `yaml:"spec"` - } - - if err := value.Decode(&tmpMetricSource); err != nil { - return err - } - - // no error with these, assign them - m.MetricSourceRef = tmpMetricSource.MetricSourceRef - m.Type = tmpMetricSource.Type - // initialize this so we can assign the values later - m.MetricSourceSpec = make(map[string]string) - - for k, v := range tmpMetricSource.MetricSourceSpec { - // simple use case - if v.Kind == yaml.ScalarNode { - m.MetricSourceSpec[k] = v.Value - } - - if v.Kind == yaml.SequenceNode { - // top level string that we will join with a semicolon - seqStrings := []string{} - for _, node := range v.Content { - if node.Kind == yaml.ScalarNode { - seqStrings = append(seqStrings, node.Value) - } else if node.Kind == yaml.MappingNode { - // each of these are k/v pairs that we will join with a comma - kvPairs := []string{} - for i := 0; i < len(node.Content); i += 2 { - kvPairs = append(kvPairs, fmt.Sprintf("%s:%s", node.Content[i].Value, node.Content[i+1].Value)) - } - seqStrings = append(seqStrings, strings.Join(kvPairs, ",")) - } - } - m.MetricSourceSpec[k] = strings.Join(seqStrings, ";") - } - } - - return nil -} - -// Kind returns the name of this type. -func (DataSource) Kind() string { - return "DataSource" -} - -// Kind returns the name of this type. -func (SLI) Kind() string { - return "SLI" -} diff --git a/pkg/manifest/v1/indicator_test.go b/pkg/manifest/v1/indicator_test.go deleted file mode 100644 index b987faf..0000000 --- a/pkg/manifest/v1/indicator_test.go +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright © 2022 OpenSLO Team -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package v1 - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" -) - -// spell-checker:disable -// - -func Test_MetricSource(t *testing.T) { - t.Parallel() - tests := []struct { - name string - yaml string - want MetricSource - }{ - { - name: "CloudWatch - One Dimension", - yaml: `metricSourceRef: usix-cloudwatch -type: CloudWatch -spec: - metricName: 2xx - namespace: CloudWatchSynthetics - region: us-east-1 - stat: SampleCount - dimensions: - - name: following - value: bar`, - want: MetricSource{ - MetricSourceRef: "usix-cloudwatch", - Type: "CloudWatch", - MetricSourceSpec: map[string]string{ - "metricName": "2xx", - "namespace": "CloudWatchSynthetics", - "region": "us-east-1", - "stat": "SampleCount", - "dimensions": "name:following,value:bar", - }, - }, - }, - { - name: "CloudWatch - Two Dimensions", - yaml: `metricSourceRef: usix-cloudwatch -type: CloudWatch -spec: - metricName: 2xx - namespace: CloudWatchSynthetics - region: us-east-1 - stat: SampleCount - dimensions: - - name: following - value: bar - - name: another - value: batz`, - want: MetricSource{ - MetricSourceRef: "usix-cloudwatch", - Type: "CloudWatch", - MetricSourceSpec: map[string]string{ - "metricName": "2xx", - "namespace": "CloudWatchSynthetics", - "region": "us-east-1", - "stat": "SampleCount", - "dimensions": "name:following,value:bar;name:another,value:batz", - }, - }, - }, - { - name: "Prometheus - Two labels", - yaml: `metricSourceRef: thanos -type: Prometheus -spec: - query: http_requests_total{} - dimensions: - - following - - another`, - want: MetricSource{ - MetricSourceRef: "thanos", - Type: "Prometheus", - MetricSourceSpec: map[string]string{ - "query": "http_requests_total{}", - "dimensions": "following;another", - }, - }, - }, - } - - for _, tt := range tests { - tt := tt // https://gist.github.com/kunwardeep/80c2e9f3d3256c894898bae82d9f75d0 - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var out MetricSource - if err := yaml.Unmarshal([]byte(tt.yaml), &out); err != nil { - t.Fatalf("Failed to unmarshal the yaml: %+v", err) - } - assert.Equal(t, tt.want, out) - }) - } -} - -// spell-checker:enable diff --git a/pkg/manifest/v1/objective.go b/pkg/manifest/v1/objective.go deleted file mode 100644 index 3015084..0000000 --- a/pkg/manifest/v1/objective.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Package v1 contains API Schema definitions for the slo v1 API group. - -# Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package v1 - -// Calendar struct represents calendar time window. -type Calendar struct { - StartTime string `yaml:"startTime" validate:"required,dateWithTime" example:"2020-01-21 12:30:00"` - TimeZone string `yaml:"timeZone" validate:"required,timeZone" example:"America/New_York"` -} - -// TimeWindow represents content of time window. -type TimeWindow struct { - Duration string `yaml:"duration" validate:"required,validDuration" example:"1h"` - IsRolling bool `yaml:"isRolling" example:"true"` - Calendar *Calendar `yaml:"calendar,omitempty" validate:"required_if=IsRolling false"` -} - -// Objective represents single threshold for SLO, for internal usage. -type Objective struct { - DisplayName string `yaml:"displayName,omitempty"` - Op string `yaml:"op,omitempty" example:"lte"` - Value float64 `yaml:"value,omitempty" validate:"numeric,omitempty"` - Target float64 `yaml:"target" validate:"required,numeric,gte=0,lt=1" example:"0.9"` - TimeSliceTarget float64 `yaml:"timeSliceTarget,omitempty" validate:"gte=0,lte=1,omitempty" example:"0.9"` - TimeSliceWindow string `yaml:"timeSliceWindow,omitempty" example:"5m"` - Indicator *SLIInline `yaml:"indicator,omitempty"` - IndicatorRef *string `yaml:"indicatorRef,omitempty"` - CompositeWeight float64 `yaml:"compositeWeight,omitempty" validate:"gte=0,omitempty"` -} - -// SLOSpec struct which mapped one to one with kind: slo yaml definition, internal use. -type SLOSpec struct { - Description string `yaml:"description,omitempty" validate:"max=1050,omitempty"` - Service string `yaml:"service" validate:"required" example:"webapp-service"` - Indicator *SLIInline `yaml:"indicator,omitempty"` - IndicatorRef *string `yaml:"indicatorRef,omitempty"` - BudgetingMethod string `yaml:"budgetingMethod" validate:"required,oneof=Occurrences Timeslices" example:"Occurrences"` //nolint:lll - TimeWindow []TimeWindow `yaml:"timeWindow" validate:"required,len=1,dive"` - Objectives []Objective `yaml:"objectives" validate:"required,dive"` - // We don't make clear in the spec if this is a ref or inline. - // We will make it a ref for now. - // https://github.com/OpenSLO/OpenSLO/issues/133 - AlertPolicies []string `yaml:"alertPolicies" validate:"dive"` -} - -// SLO struct which mapped one to one with kind: slo yaml definition, external usage. -type SLO struct { - ObjectHeader `yaml:",inline"` - Spec SLOSpec `yaml:"spec" validate:"required"` -} - -// Kind returns the name of this type. -func (SLO) Kind() string { - return "SLO" -} diff --git a/pkg/manifest/v1/objects.go b/pkg/manifest/v1/objects.go deleted file mode 100644 index 2266652..0000000 --- a/pkg/manifest/v1/objects.go +++ /dev/null @@ -1,159 +0,0 @@ -/* -Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package v1 - -import ( - "fmt" - - "gopkg.in/yaml.v3" - - "github.com/OpenSLO/oslo/pkg/manifest" -) - -// APIVersion is a value of valid apiVersions. -const ( - APIVersion = "openslo/v1" -) - -// Possible values of field kind for valid Objects. -const ( - KindAlertCondition = "AlertCondition" - KindAlertNotificationTarget = "AlertNotificationTarget" - KindAlertPolicy = "AlertPolicy" - KindDataSource = "DataSource" - KindSLI = "SLI" - KindSLO = "SLO" - KindService = "Service" -) - -// Parse is responsible for parsing all structs in this apiVersion. -func Parse(fileContent []byte, m ObjectGeneric, filename, kind string) (manifest.OpenSLOKind, error) { - switch kind { - case KindAlertCondition: - var content AlertCondition - err := yaml.Unmarshal(fileContent, &content) - return content, err - case KindAlertNotificationTarget: - var content AlertNotificationTarget - err := yaml.Unmarshal(fileContent, &content) - return content, err - case KindAlertPolicy: - var content AlertPolicy - err := yaml.Unmarshal(fileContent, &content) - return content, err - case KindDataSource: - var content DataSource - err := yaml.Unmarshal(fileContent, &content) - return content, err - case KindService: - var content Service - err := yaml.Unmarshal(fileContent, &content) - return content, err - case KindSLO: - var content SLO - err := yaml.Unmarshal(fileContent, &content) - return content, err - case KindSLI: - var content SLI - err := yaml.Unmarshal(fileContent, &content) - return content, err - default: - return nil, fmt.Errorf("unsupported kind: %s", m.Kind) - } -} - -// ---------------------------------------------------------------------------- -// Object definitions -// ---------------------------------------------------------------------------- - -type Label []string - -func (a *Label) UnmarshalYAML(value *yaml.Node) error { - var multi []string - if err := value.Decode(&multi); err != nil { - var single string - if err := value.Decode(&single); err != nil { - return err - } - *a = []string{single} - } else { - *a = multi - } - return nil -} - -type Labels map[string]Label - -// Annotations is a map of annotations. -type Annotations map[string]string - -// Metadata represents part of object which is is common for all available Objects, for internal usage. -type Metadata struct { - Name string `yaml:"name" validate:"required" example:"name"` - DisplayName string `yaml:"displayName,omitempty" validate:"omitempty,min=0,max=63" example:"Prometheus Source"` - Labels Labels `json:"labels,omitempty" validate:"omitempty"` - Annotations Annotations `json:"annotations,omitempty" validate:"omitempty"` -} - -// MetadataHolder is an intermediate structure that can provides metadata related -// field to other structures. -type MetadataHolder struct { - Metadata Metadata `yaml:"metadata"` -} - -// ObjectHeader represents Header which is common for all available Objects. -type ObjectHeader struct { - manifest.ObjectHeader `yaml:",inline"` - Kind string `yaml:"kind" validate:"required,oneof=Service SLO SLI AlertPolicy AlertNotificationTarget AlertCondition DataSource" example:"kind"` //nolint:lll - MetadataHolder `yaml:",inline"` -} - -// ObjectGeneric represents struct to which every Objects is parsable -// Specific types of Object have different structures as Spec. -type ObjectGeneric struct { - ObjectHeader `yaml:",inline"` -} - -// MetricSourceSpec represents the metric source. -type MetricSourceSpec struct { - Source string `yaml:"source" validate:"required,alpha"` - QueryType string `yaml:"queryType" validate:"required,alpha"` - Query string `yaml:"query" validate:"required"` -} - -// ObjectiveBase base structure representing a threshold. -type ObjectiveBase struct { - DisplayName string `yaml:"displayName" validate:"max=1050" example:"Good"` - Value float64 `yaml:"value" validate:"numeric" example:"100"` -} - -/*----- Service -----*/ - -// Service struct which mapped one to one with kind: service yaml definition. -type Service struct { - ObjectHeader `yaml:",inline"` - Spec ServiceSpec `yaml:"spec"` -} - -// Kind returns the name of this type. -func (Service) Kind() string { - return "Service" -} - -// ServiceSpec represents content of Spec typical for Service Object. -type ServiceSpec struct { - Description string `yaml:"description" validate:"max=1050" example:"Bleeding edge web app"` -} diff --git a/pkg/manifest/v1alpha/objects.go b/pkg/manifest/v1alpha/objects.go deleted file mode 100644 index a876e73..0000000 --- a/pkg/manifest/v1alpha/objects.go +++ /dev/null @@ -1,161 +0,0 @@ -/* -Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package v1alpha - -import ( - "fmt" - - "gopkg.in/yaml.v3" - - "github.com/OpenSLO/oslo/pkg/manifest" -) - -// APIVersion is a value of valid apiVersions. -const ( - APIVersion = "openslo/v1alpha" -) - -// Possible values of field kind for valid Objects. -const ( - KindSLO = "SLO" - KindService = "Service" -) - -// ObjectHeader is a header for all objects. -type ObjectHeader struct { - manifest.ObjectHeader `yaml:",inline"` - Kind string `yaml:"kind" validate:"required,oneof=Service SLO AlertNotificationTarget" example:"kind"` - MetadataHolder `yaml:",inline"` -} - -// Service struct which mapped one to one with kind: service yaml definition. -type Service struct { - ObjectHeader `yaml:",inline"` - Spec ServiceSpec `yaml:"spec"` -} - -// Kind returns the name of this type. -func (Service) Kind() string { - return "Service" -} - -// ServiceSpec represents content of Spec typical for Service Object. -type ServiceSpec struct { - Description string `yaml:"description" validate:"max=1050" example:"Bleeding edge web app"` -} - -// SLO struct which mapped one to one with kind: slo yaml definition, external usage. -type SLO struct { - ObjectHeader `yaml:",inline"` - Spec SLOSpec `yaml:"spec"` -} - -// Kind returns the name of this type. -func (SLO) Kind() string { - return "SLO" -} - -// SLOSpec represents content of Spec typical for SLO Object. -type SLOSpec struct { - TimeWindows []TimeWindow `yaml:"timeWindows" validate:"required,len=1,dive"` - BudgetingMethod string `yaml:"budgetingMethod" validate:"required,oneof=Occurrences Timeslices" example:"Occurrences"` //nolint: lll - Description string `yaml:"description" validate:"max=1050" example:"Total count of server requests"` - Indicator *Indicator `yaml:"indicator"` - Service string `yaml:"service" validate:"required" example:"webapp-service"` - Objectives []Objective `json:"objectives" validate:"required,dive"` -} - -// Indicator represents integration with metric source. -type Indicator struct { - ThresholdMetric MetricSourceSpec `yaml:"thresholdMetric" validate:"required"` -} - -// MetricSourceSpec represents the metric source. -type MetricSourceSpec struct { - Source string `yaml:"source" validate:"required,alpha"` - QueryType string `yaml:"queryType" validate:"required,alpha"` - Query string `yaml:"query" validate:"required"` -} - -// Objective represents single threshold for SLO, for internal usage. -type Objective struct { - ObjectiveBase `yaml:",inline"` - RatioMetrics *RatioMetrics `yaml:"ratioMetrics"` - BudgetTarget *float64 `yaml:"target" validate:"required,numeric,gte=0,lt=1" example:"0.9"` - TimeSliceTarget *float64 `yaml:"timeSliceTarget,omitempty" example:"0.9"` - Operator *string `yaml:"op,omitempty" example:"lte"` -} - -// RatioMetrics base struct for ratio metrics. -type RatioMetrics struct { - Good MetricSourceSpec `yaml:"good" validate:"required"` - Total MetricSourceSpec `yaml:"total" validate:"required"` - Counter bool `yaml:"counter" example:"true"` -} - -// ObjectiveBase base structure representing a threshold. -type ObjectiveBase struct { - DisplayName string `yaml:"displayName" validate:"max=1050" example:"Good"` - Value float64 `yaml:"value" validate:"numeric" example:"100"` -} - -// TimeWindow represents content of time window. -type TimeWindow struct { - Unit string `yaml:"unit" validate:"required,oneof=Second Quarter Month Week Day" example:"Week"` - Count int `yaml:"count" validate:"required,gt=0" example:"1"` - IsRolling bool `yaml:"isRolling" example:"true"` - Calendar *Calendar `yaml:"calendar,omitempty"` -} - -// Calendar struct represents calendar time window. -type Calendar struct { - StartTime string `yaml:"startTime" validate:"required,dateWithTime" example:"2020-01-21 12:30:00"` - TimeZone string `yaml:"timeZone" validate:"required,timeZone" example:"America/New_York"` -} - -// Metadata represents part of object which is is common for all available Objects, for internal usage. -type Metadata struct { - Name string `yaml:"name" validate:"required" example:"name"` - DisplayName string `yaml:"displayName,omitempty" validate:"omitempty,min=0,max=63" example:"Prometheus Source"` -} - -// MetadataHolder is an intermediate structure that can provides metadata related -// field to other structures. -type MetadataHolder struct { - Metadata Metadata `yaml:"metadata"` -} - -// ObjectGeneric represents struct to which every Objects is parsable -// Specific types of Object have different structures as Spec. -type ObjectGeneric struct { - ObjectHeader `yaml:",inline"` -} - -// Parse is responsible for parsing all structs in this apiVersion. -func Parse(fileContent []byte, m ObjectGeneric, filename string) (manifest.OpenSLOKind, error) { - switch m.Kind { - case KindService: - var content Service - err := yaml.Unmarshal(fileContent, &content) - return content, err - case KindSLO: - var content SLO - err := yaml.Unmarshal(fileContent, &content) - return content, err - default: - return nil, fmt.Errorf("unsupported kind: %s", m.Kind) - } -} diff --git a/pkg/validate/validate.go b/pkg/validate/validate.go deleted file mode 100644 index 84b1684..0000000 --- a/pkg/validate/validate.go +++ /dev/null @@ -1,104 +0,0 @@ -/* -Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package validate - -import ( - "errors" - "strings" - "time" - - "github.com/go-playground/validator/v10" - - "github.com/OpenSLO/oslo/pkg/manifest" - "github.com/OpenSLO/oslo/pkg/yamlutil" -) - -// validateStruct takes the given struct and validates it. -func validateStruct(c []manifest.OpenSLOKind) error { - validate := validator.New() - - _ = validate.RegisterValidation("dateWithTime", isDateWithTimeValid) - _ = validate.RegisterValidation("timeZone", isTimeZoneValid) - _ = validate.RegisterValidation("validDuration", isValidDurationString) - - var allErrors []string - for _, v := range c { - if err := validate.Struct(v); err != nil { - for _, err := range err.(validator.ValidationErrors) { //nolint: errorlint - allErrors = append(allErrors, err.Error()) - } - } - } - if len(allErrors) > 0 { - return errors.New(strings.Join(allErrors, "\n")) - } - return nil -} - -// Files validates the given array of filenames. -func Files(files []string) error { - var allErrors []string - for _, file := range files { - c, e := yamlutil.ReadConf(file) - if e != nil { - allErrors = append(allErrors, e.Error()) - break - } - - content, err := yamlutil.Parse(c, file) - if err != nil { - allErrors = append(allErrors, err.Error()) - break - } - if validationErrors := validateStruct(content); validationErrors != nil { - allErrors = append(allErrors, validationErrors.Error()) - } - } - if len(allErrors) > 0 { - return errors.New(strings.Join(allErrors, "\n")) - } - return nil -} - -func isValidDurationString(fl validator.FieldLevel) bool { - for _, s := range []string{"s", "m", "h", "d", "w", "M", "Q", "Y"} { - duration := fl.Field().String() - if strings.HasSuffix(duration, s) { - return true - } - } - return false -} - -func isDateWithTimeValid(fl validator.FieldLevel) bool { - if fl.Field().String() != "" { - _, err := time.Parse("2006-01-02T15:04:05Z", fl.Field().String()) - if err != nil { - return false - } - } - return true -} - -func isTimeZoneValid(fl validator.FieldLevel) bool { - if fl.Field().String() != "" { - _, err := time.LoadLocation(fl.Field().String()) - if err != nil { - return false - } - } - return true -} diff --git a/pkg/validate/validate_test.go b/pkg/validate/validate_test.go deleted file mode 100644 index c727bef..0000000 --- a/pkg/validate/validate_test.go +++ /dev/null @@ -1,283 +0,0 @@ -/* -Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package validate - -import ( - "testing" - - "github.com/go-playground/validator/v10" -) - -func Test_validateFiles(t *testing.T) { - t.Parallel() - type args struct { - files []string - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "invalid apiVersion", - args: args{ - files: []string{"../../test/invalid-apiversion.yaml"}, - }, - wantErr: true, - }, - { - name: "v1alpha gets v1 Kind", - args: args{ - files: []string{"../../test/v1/invalid-apiversion.yaml"}, - }, - wantErr: true, - }, - { - name: "v1alpha", - args: args{ - files: []string{ - "../../test/v1alpha/valid-service.yaml", - "../../test/v1alpha/valid-slos-ratio.yaml", - "../../test/v1alpha/valid-slos-threshold.yaml", - }, - }, - wantErr: false, - }, - { - name: "v1alpha single file invalid", - args: args{ - files: []string{"../../test/v1alpha/invalid-service.yaml"}, - }, - wantErr: true, - }, - { - name: "v1 AlertCondition", - args: args{ - files: []string{ - "../../test/v1/alert-condition/alert-condition.yaml", - "../../test/v1/alert-condition/alert-condition-no-description.yaml", - }, - }, - wantErr: false, - }, - { - name: "v1 AlertCondition invalid", - args: args{ - files: []string{ - "../../test/v1/alert-condition/alert-condition-no-condition.yaml", - "../../test/v1/alert-condition/alert-condition-no-sev.yaml", - }, - }, - wantErr: true, - }, - { - name: "v1 AlertNotificationTarget", - args: args{ - files: []string{ - "../../test/v1/alert-notification-target/alert-notification-target.yaml", - "../../test/v1/alert-notification-target/alert-notification-target-no-description.yaml", - }, - }, - wantErr: false, - }, - { - name: "v1 AlertNotificationTarget invalid", - args: args{ - files: []string{"../../test/v1/alert-notification-target/alert-notification-target-no-target.yaml"}, - }, - wantErr: true, - }, - { - name: "v1 AlertPolicy", - args: args{ - files: []string{ - "../../test/v1/alert-policy/alert-policy.yaml", - "../../test/v1/alert-policy/alert-policy-inline-cond.yaml", - "../../test/v1/alert-policy/alert-policy-many-notificationref.yaml", - }, - }, - wantErr: false, - }, - { - name: "v1 AlertPolicy invalid", - args: args{ - files: []string{ - "../../test/v1/alert-policy/alert-policy-malformed-cond.yaml", - "../../test/v1/alert-policy/alert-policy-malformed-targetref.yaml", - "../../test/v1/alert-policy/alert-policy-many-cond.yaml", - "../../test/v1/alert-policy/alert-policy-no-cond.yaml", - "../../test/v1/alert-policy/alert-policy-no-notification.yaml", - }, - }, - wantErr: true, - }, - { - name: "v1 single DataSource valid", - args: args{ - files: []string{"../../test/v1/data-source/data-source.yaml"}, - }, - wantErr: false, - }, - { - name: "v1 Service", - args: args{ - files: []string{ - "../../test/v1/service/service.yaml", - "../../test/v1/service/service-no-displayname.yaml", - }, - }, - wantErr: false, - }, - { - name: "v1 Service long description", - args: args{ - files: []string{"../../test/v1/service/service-long-description.yaml"}, - }, - wantErr: true, - }, - { - name: "v1 SLI", - args: args{ - files: []string{ - "../../test/v1/sli/sli-description-ratio-bad-inline-metricsource.yaml", - "../../test/v1/sli/sli-description-ratio-bad-metricsourceref.yaml", - "../../test/v1/sli/sli-description-ratio-good-inline-metricsource.yaml", - "../../test/v1/sli/sli-description-ratio-good-metricsourceref.yaml", - "../../test/v1/sli/sli-description-threshold-inline-metricsource.yaml", - "../../test/v1/sli/sli-description-threshold-metricsourceref.yaml", - "../../test/v1/sli/sli-no-description-ratio-bad-inline-metricsource.yaml", - "../../test/v1/sli/sli-no-description-ratio-bad-metricsourceref.yaml", - "../../test/v1/sli/sli-no-description-ratio-good-inline-metricsource.yaml", - "../../test/v1/sli/sli-no-description-ratio-good-metricsourceref.yaml", - "../../test/v1/sli/sli-no-description-threshold-inline-metricsource.yaml", - "../../test/v1/sli/sli-no-description-threshold-metricsourceref.yaml", - }, - }, - wantErr: false, - }, - { - name: "v1 SLO", - args: args{ - files: []string{ - "../../test/v1/slo/slo-indicatorref-calendar-alerts.yaml", - "../../test/v1/slo/slo-indicatorref-calendar-no-alerts.yaml", - "../../test/v1/slo/slo-indicatorref-rolling-alerts.yaml", - "../../test/v1/slo/slo-indicatorref-rolling-no-alerts.yaml", - "../../test/v1/slo/slo-no-indicatorref-calendar-alerts.yaml", - "../../test/v1/slo/slo-no-indicatorref-calendar-no-alerts.yaml", - "../../test/v1/slo/slo-no-indicatorref-rolling-alerts.yaml", - "../../test/v1/slo/slo-no-indicatorref-rolling-no-alerts.yaml", - "../../test/v1/slo/slo-composite-indicatorRef-rolling-no-alerts.yaml", - "../../test/v1/slo/slo-composite-no-indicatorRef-rolling-no-alerts.yaml", - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - if err := Files(tt.args.files); (err != nil) != tt.wantErr { - t.Errorf("Files() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_isValidDurationString(t *testing.T) { - t.Parallel() - type args struct { - durStr string - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "Seconds", - args: args{ - durStr: "1s", - }, - wantErr: false, - }, - { - name: "Minutes", - args: args{ - durStr: "5m", - }, - wantErr: false, - }, - { - name: "Hours", - args: args{ - durStr: "2h", - }, - wantErr: false, - }, - { - name: "Weeks", - args: args{ - durStr: "3w", - }, - wantErr: false, - }, - { - name: "Months", - args: args{ - durStr: "6M", - }, - wantErr: false, - }, - { - name: "Quarters", - args: args{ - durStr: "7Q", - }, - wantErr: false, - }, - { - name: "Years", - args: args{ - durStr: "8Y", - }, - wantErr: false, - }, - { - name: "Invalid", - args: args{ - durStr: "8y", - }, - wantErr: true, - }, - } - - validate := validator.New() - if err := validate.RegisterValidation("isValidDurationString", isValidDurationString); err != nil { - t.Errorf("unexpected error registering validation: %v", err) - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - if err := validate.Var(tt.args.durStr, "isValidDurationString"); (err != nil) != tt.wantErr { - t.Errorf("isValidDurationString() = %v, want %v", err, tt.wantErr) - } - }) - } -} diff --git a/pkg/yamlutil/yamlutil.go b/pkg/yamlutil/yamlutil.go deleted file mode 100644 index eca0ada..0000000 --- a/pkg/yamlutil/yamlutil.go +++ /dev/null @@ -1,137 +0,0 @@ -/* -Package yamlutils provides functions to parse OpenSLO manifests. - -# Copyright © 2022 OpenSLO Team - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package yamlutil - -import ( - "bytes" - "errors" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - - "github.com/hashicorp/go-multierror" - "gopkg.in/yaml.v3" - - "github.com/OpenSLO/oslo/internal/pathutil" - "github.com/OpenSLO/oslo/pkg/manifest" - v1 "github.com/OpenSLO/oslo/pkg/manifest/v1" - "github.com/OpenSLO/oslo/pkg/manifest/v1alpha" -) - -// ReadConf reads whole content from file path, HTTP address or stdin (path "-") to a byte slice. -func ReadConf(path string) (content []byte, err error) { - switch { - case pathutil.IsStdin(path): - return io.ReadAll(os.Stdin) - case pathutil.IsURL(path): - resp, err := http.Get(path) //nolint:gosec,noctx - if err != nil { - return nil, err - } - defer func() { - err = errors.Join(err, resp.Body.Close()) - }() - return io.ReadAll(resp.Body) - default: - return os.ReadFile(filepath.Clean(path)) - } -} - -// Parse takes the provided byte array, parses it, and returns an array of parsed struts. -// Ignoring the complexity linting errors for now, until we can figure -// out how to handle the complexity better. -func Parse(fileContent []byte, filename string) ( //nolint: gocognit, cyclop - parsedStructs []manifest.OpenSLOKind, - err error, -) { - var m manifest.ObjectGeneric - // unmarshal here to get the APIVersion so we can process the file correctly - if err = yaml.Unmarshal(fileContent, &m); err != nil { - return nil, fmt.Errorf("in file %q: %w", filename, err) - } - - var allErrors error - switch m.APIVersion { - // This is where we add new versions of the OpenSLO spec. - case v1alpha.APIVersion: - // unmarshal again to get the v1alpha struct - var o v1alpha.ObjectGeneric - if err := yaml.Unmarshal(fileContent, &o); err != nil { - return nil, fmt.Errorf("in file %q: %w", filename, err) - } - - // loop through and get all of the documents in the file - decoder := yaml.NewDecoder(bytes.NewReader(fileContent)) - for { - var i interface{} - err := decoder.Decode(&i) - if errors.Is(err, io.EOF) { - break - } - if err != nil { - return nil, fmt.Errorf("in file %q: %w", filename, err) - } - c, err := yaml.Marshal(&i) - if err != nil { - return nil, fmt.Errorf("in file %q: %w", filename, err) - } - - content, e := v1alpha.Parse(c, o, filename) - if e != nil { - allErrors = multierror.Append(allErrors, e) - } - parsedStructs = append(parsedStructs, content) - } - case v1.APIVersion: - // unmarshal again to get the v1 struct - var o v1.ObjectGeneric - if err := yaml.Unmarshal(fileContent, &o); err != nil { - return nil, fmt.Errorf("in file %q: %w", filename, err) - } - // loop through and get all of the documents in the file - decoder := yaml.NewDecoder(bytes.NewReader(fileContent)) - for { - var i interface{} - err := decoder.Decode(&i) - if errors.Is(err, io.EOF) { - break - } - if err != nil { - return nil, fmt.Errorf("in file %q: %w", filename, err) - } - c, err := yaml.Marshal(&i) - if err != nil { - return nil, fmt.Errorf("in file %q: %w", filename, err) - } - - kind := i.(map[string]interface{})["kind"].(string) - content, e := v1.Parse(c, o, filename, kind) - - if e != nil { - allErrors = multierror.Append(allErrors, fmt.Errorf("error in %q: %w", filename, e)) - } - parsedStructs = append(parsedStructs, content) - } - default: - allErrors = multierror.Append(allErrors, fmt.Errorf("unsupported API Version in file %s", filename)) - } - - return parsedStructs, allErrors -} diff --git a/test/cli/convert.bats b/test/cli/convert.bats deleted file mode 100644 index f84447e..0000000 --- a/test/cli/convert.bats +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bats - -setup_file() { - mkdir -p bin - # get the latest nobl9 cli tool for testing - curl https://api.github.com/repos/nobl9/sloctl/releases/latest | \ - grep "browser_download_url.*linux*" | \ - cut -d : -f 2,3 | tr -d \" | wget -O sloctl -qi - - chmod +x sloctl - mv sloctl bin/ -} - -setup() { - load 'test_helper/common-setup' - _common_setup -} - -teardown_file() { - rm ./bin/sloctl -} - -@test "oslo errors without output flag set" { - run sloctl convert - assert_equal $status 1 -} - -#-------------------- -# -# Nobl9 Coverting -# -@test "nobl9 - oslo fails when file doesn't exist" { - run oslo convert -f test/foo.yaml -o nobl9 - assert_equal $status 1 - assert_output --partial "no such file or directory" -} - -@test "nobl9 - oslo converts successfully" { - run oslo convert -f test/v1/service/service.yaml -o nobl9 - assert_equal $status 0 - assert_output -} - -@test "nobl9 - oslo converts file with multiple kinds successfully" { - run oslo convert -f test/v1/multi.yaml -o nobl9 - assert_equal $status 0 - assert_output --partial "apiVersion" -} - -@test "nobl9 - oslo converts multiple files successfully" { - run oslo convert \ - -f test/v1/slo/slo-indicatorRef-rolling-cloudwatch.yaml \ - -f test/v1/sli/sli-threshold-cloudwatch.yaml \ - -f test/v1/data-source/data-source-cloudwatch.yaml \ - -o nobl9 - assert_equal $status 0 - assert_output --partial "apiVersion" -} - -@test "nobl9 - sloctl parses converted openslo files successfully" { - skip "Skipping until sloctl can do offline validation" -} diff --git a/test/v1alpha/invalid-service.yaml b/test/v1alpha/invalid-service.yaml index bad6262..5f84b68 100644 --- a/test/v1alpha/invalid-service.yaml +++ b/test/v1alpha/invalid-service.yaml @@ -1,7 +1,7 @@ -apiVersion: foo +apiVersion: openslo/v1alpha kind: Service metadata: - name: my-rad-service + name: my-rad service displayName: My Rad Service spec: description: This is a great description of an even better service. From c4c1def769bdd0c946d1f4c28ff3d940ab2803cd Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Thu, 21 Nov 2024 15:18:05 +0100 Subject: [PATCH 2/5] fix linter --- .golangci.yml | 87 +++++++++++------------------------ cspell.json | 1 + internal/cli/fmt.go | 4 +- internal/cli/root_test.go | 2 +- internal/cli/validate.go | 2 +- internal/files/format_test.go | 2 +- 6 files changed, 32 insertions(+), 66 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 14474d0..78269f2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,107 +1,78 @@ run: timeout: 5m modules-download-mode: readonly - go: "1.20" + skip-dirs-use-default: true issues: # Enable all checks (which was as default disabled e.g. comments). exclude-use-default: false - exclude-rules: - # Documenting comments are not required except on packages. - - linters: - - golint - text: exported (function|method) .*? should have comment or be unexported - - linters: - - golint - text: exported const .*? should have comment (\(or a comment on this block\) )?or be unexported - linters: - - golint - text: exported type .*? should have comment or be unexported - # Stylecheck checks if at least one file has package comment but it's not required on all of them. + - revive + text: exported (function|method|type) .*? should have comment or be unexported - linters: - - golint - text: package comment should be of the form "Package .*? ..." + - revive + text: exported (const|var) .*? should have comment (\(or a comment on this block\) )?or be unexported - linters: - revive - text: package-comments + text: "if-return: redundant if ...; err != nil check, just return error instead." - linters: - - stylecheck - text: ST1000 + - revive + text: "^var-naming: .*" - linters: - - gocritic - text: whyNoLint - + - revive + text: "error-strings: error strings should not be capitalized or end with punctuation or a newline" # Value 0 means show all. max-issues-per-linter: 0 max-same-issues: 0 linters-settings: goimports: - # Put imports beginning with prefix after 3rd-party packages, - # it's a comma-separated list of prefixes. - local-prefixes: github.com/OpenSLO/oslo - golint: - min-confidence: 0 + local-prefixes: github.com/OpenSLO/OpenSLO govet: - # Report about shadowed variables. - check-shadowing: true + # False positives and reporting on error shadowing (which is intended). + # Quoting Robi Pike: + # The shadow code is marked experimental. + # It has too many false positives to be enabled by default, so this is not entirely unexpected, + # but don't expect a fix soon. The right way to detect shadowing without flow analysis is elusive. + # Few years later (comment from 2015) and the Shadow analyer is still experimental... + check-shadowing: false lll: line-length: 120 gocritic: enabled-tags: - - diagnostic - opinionated - - style + disabled-checks: + - singleCaseSwitch exhaustive: # In switch statement treat label default: as being exhaustive. default-signifies-exhaustive: true misspell: locale: US - forbidigo: - forbid: - - ^ioutil\..*$ # Package ioutil is deprecated. - gci: - sections: - - standard # Captures all standard packages if they do not match another section. - - default # Contains all imports that could not be matched to another section type. - - prefix(github.com/OpenSLO/oslo) # Groups all imports with the specified Prefix. + gocognit: + min-complexity: 30 revive: rules: - - name: package-comments - severity: ok + - name: unused-parameter + disabled: true + linters: - # Please, do not use `enable-all`: it's deprecated and will be removed soon. - # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint. disable-all: true enable: - # All linters from list https://golangci-lint.run/usage/linters/ are specifed here and explicit enable/disable. + # All linters from list https://golangci-lint.run/usage/linters/ are speciefed here and explicit enable/disable. - asciicheck - bodyclose - - cyclop - dogsled - - dupl - - durationcheck - errcheck - - errorlint - exhaustive - - exportloopref - - forbidigo - - gci - - gochecknoglobals - gochecknoinits - gocognit - - goconst - gocritic - gocyclo - - godot - - godox - gofmt - - gofumpt - goheader - goimports - goprintffuncname - - gosec - gosimple - govet - importas @@ -110,19 +81,13 @@ linters: - makezero - misspell - nakedret - - nestif - nilerr - - noctx - - nolintlint - - paralleltest - prealloc - predeclared - revive - rowserrcheck - sqlclosecheck - staticcheck - - stylecheck - - thelper - tparallel - typecheck - unconvert diff --git a/cspell.json b/cspell.json index 770d89d..37d9e95 100644 --- a/cspell.json +++ b/cspell.json @@ -123,6 +123,7 @@ "less", "make", "markdown", + "openslosdk", "php", "plaintext", "pug", diff --git a/internal/cli/fmt.go b/internal/cli/fmt.go index fb32883..7ec4e96 100644 --- a/internal/cli/fmt.go +++ b/internal/cli/fmt.go @@ -20,9 +20,9 @@ package cli import ( "fmt" + "github.com/OpenSLO/OpenSLO/pkg/openslosdk" "github.com/spf13/cobra" - "github.com/OpenSLO/OpenSLO/pkg/openslosdk" "github.com/OpenSLO/oslo/internal/files" ) @@ -57,7 +57,7 @@ func NewFmtCmd() *cobra.Command { } registerFileRelatedFlags(fmtCmd, &passedFilePaths, &recursive) fmtCmd.Flags().StringVarP( - &output, "ouput", "o", "yaml", + &output, "output", "o", "yaml", "The output format, one of [json, yaml].", ) return fmtCmd diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index f8ea427..3d62510 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -63,7 +63,7 @@ Usage: Flags: -f, --file stringArray The file(s) that contain the configurations. -h, --help help for fmt - -o, --ouput string The output format, one of [json, yaml]. (default "yaml") + -o, --output string The output format, one of [json, yaml]. (default "yaml") -R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory. `, wantErr: false, diff --git a/internal/cli/validate.go b/internal/cli/validate.go index 42c0d31..fdaa4ef 100644 --- a/internal/cli/validate.go +++ b/internal/cli/validate.go @@ -43,7 +43,7 @@ func NewValidateCmd() *cobra.Command { return err } for _, obj := range objects { - if err = obj.Validate(); err != nil { + if err := obj.Validate(); err != nil { return err } } diff --git a/internal/files/format_test.go b/internal/files/format_test.go index a8b0064..92795f9 100644 --- a/internal/files/format_test.go +++ b/internal/files/format_test.go @@ -19,9 +19,9 @@ import ( "bytes" "testing" + "github.com/OpenSLO/OpenSLO/pkg/openslosdk" "github.com/stretchr/testify/assert" - "github.com/OpenSLO/OpenSLO/pkg/openslosdk" "github.com/OpenSLO/oslo/internal/files" ) From 286005fecf71e4f970ba2b13b8d18c253b70734d Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Thu, 21 Nov 2024 15:25:14 +0100 Subject: [PATCH 3/5] fix spelling --- .golangci.yml | 2 +- cspell.json | 280 +++++++++++++++++++++++++------------------------- 2 files changed, 141 insertions(+), 141 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 78269f2..10513c7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -28,7 +28,7 @@ issues: linters-settings: goimports: - local-prefixes: github.com/OpenSLO/OpenSLO + local-prefixes: github.com/OpenSLO/oslo govet: # False positives and reporting on error shadowing (which is intended). # Quoting Robi Pike: diff --git a/cspell.json b/cspell.json index 37d9e95..c503001 100644 --- a/cspell.json +++ b/cspell.json @@ -1,142 +1,142 @@ { - "version": "0.2", - "language": "en_US", - "import": [ - "@cspell/dict-lorem-ipsum/cspell-ext.json" - ], - "dictionaries": [ - "bash", - "companies", - "css", - "go", - "html", - "misc", - "softwareTerms", - "lorem-ipsum" - ], - "useGitignore": true, - "ignorePaths": [ - ".vscode/", - "**/*.lock", - "**/*.mod", - "**/go.sum", - "package.json" - ], - "allowCompoundWords": true, - "words": [ - "bazbat", - "changeme", - "cyclop", - "distroless", - "Dynatrace", - "fatih", - "forbidigo", - "gochecknoglobals", - "gocognit", - "gocognitx", - "goconst", - "gocyclo", - "golangci", - "gopath", - "gosec", - "hashicorp", - "ibartholomew", - "ldflags", - "Logql", - "mitchellh", - "msec", - "myapp", - "myapplication", - "mycheckid", - "mychecktype", - "myclusterid", - "mydatabasename", - "mydimensions", - "myindex", - "myjson", - "mylocation", - "mylogql", - "mymetricname", - "mymetricpath", - "mymetricselector", - "mymetrictype", - "mypath", - "myprogram", - "myprojectid", - "myquantization", - "myquery", - "myregion", - "myrollup", - "myservice", - "mystatus", - "mystreamid", - "mytype", - "mytypeofdata", - "myvalue", - "nestif", - "nobl", - "noctx", - "nolint", - "nrql", - "NRQL", - "oneof", - "openslo", - "opentsdb", - "oslo", - "prealloc", - "promql", - "request", - "Rollup", - "rval", - "server", - "sloctl", - "slos", - "stretchr", - "structs", - "tada", - "tparallel", - "TSDB", - "unflatten", - "unflattens", - "unmarshals", - "usix" - ], - "enabledLanguageIds": [ - "asciidoc", - "c", - "cpp", - "csharp", - "css", - "git-commit", - "gitcommit", - "go", - "handlebars", - "haskell", - "html", - "jade", - "java", - "javascript", - "javascriptreact", - "json", - "jsonc", - "latex", - "less", - "make", - "markdown", - "openslosdk", - "php", - "plaintext", - "pug", - "python", - "restructuredtext", - "rust", - "scala", - "scss", - "text", - "typescript", - "typescriptreact", - "vim", - "yaml", - "yml" - ] + "language": "en_US", + "import": [ + "@cspell/dict-lorem-ipsum/cspell-ext.json" + ], + "dictionaries": [ + "bash", + "companies", + "css", + "go", + "html", + "misc", + "softwareTerms", + "lorem-ipsum" + ], + "useGitignore": true, + "ignorePaths": [ + ".vscode/", + "**/*.lock", + "**/*.mod", + "**/node_modules/**", + "**/go.sum", + "package.json" + ], + "allowCompoundWords": true, + "words": [ + "bazbat", + "changeme", + "cyclop", + "distroless", + "Dynatrace", + "fatih", + "forbidigo", + "gochecknoglobals", + "gocognit", + "gocognitx", + "goconst", + "gocyclo", + "golangci", + "gopath", + "gosec", + "hashicorp", + "ibartholomew", + "ldflags", + "Logql", + "mitchellh", + "msec", + "myapp", + "myapplication", + "mycheckid", + "mychecktype", + "myclusterid", + "mydatabasename", + "mydimensions", + "myindex", + "myjson", + "mylocation", + "mylogql", + "mymetricname", + "mymetricpath", + "mymetricselector", + "mymetrictype", + "mypath", + "myprogram", + "myprojectid", + "myquantization", + "myquery", + "myregion", + "myrollup", + "myservice", + "mystatus", + "mystreamid", + "mytype", + "mytypeofdata", + "myvalue", + "nestif", + "nobl", + "noctx", + "nolint", + "nrql", + "NRQL", + "oneof", + "openslo", + "openslosdk", + "opentsdb", + "oslo", + "prealloc", + "promql", + "request", + "Rollup", + "rval", + "server", + "sloctl", + "slos", + "stretchr", + "structs", + "tada", + "tparallel", + "TSDB", + "unflatten", + "unflattens", + "unmarshals", + "usix" + ], + "enabledLanguageIds": [ + "asciidoc", + "c", + "cpp", + "csharp", + "css", + "git-commit", + "gitcommit", + "go", + "handlebars", + "haskell", + "html", + "jade", + "java", + "javascript", + "javascriptreact", + "json", + "jsonc", + "latex", + "less", + "make", + "markdown", + "php", + "plaintext", + "pug", + "python", + "restructuredtext", + "rust", + "scala", + "scss", + "text", + "typescript", + "typescriptreact", + "vim", + "yaml", + "yml" + ] } From 2fe0a4d12917d7631a82f329bf19a60bc0421701 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Thu, 21 Nov 2024 15:26:38 +0100 Subject: [PATCH 4/5] format --- internal/cli/validate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/validate.go b/internal/cli/validate.go index fdaa4ef..95ab0cc 100644 --- a/internal/cli/validate.go +++ b/internal/cli/validate.go @@ -43,7 +43,7 @@ func NewValidateCmd() *cobra.Command { return err } for _, obj := range objects { - if err := obj.Validate(); err != nil { + if err := obj.Validate(); err != nil { return err } } From 929fae87b27b554737eeda5902f5870f74e47d9b Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Wed, 18 Dec 2024 10:42:54 +0100 Subject: [PATCH 5/5] fix typos --- .golangci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 10513c7..008c3ae 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -31,7 +31,7 @@ linters-settings: local-prefixes: github.com/OpenSLO/oslo govet: # False positives and reporting on error shadowing (which is intended). - # Quoting Robi Pike: + # Quoting Rob Pike: # The shadow code is marked experimental. # It has too many false positives to be enabled by default, so this is not entirely unexpected, # but don't expect a fix soon. The right way to detect shadowing without flow analysis is elusive. @@ -59,7 +59,7 @@ linters-settings: linters: disable-all: true enable: - # All linters from list https://golangci-lint.run/usage/linters/ are speciefed here and explicit enable/disable. + # All linters from list https://golangci-lint.run/usage/linters/ are specified here and explicit enable/disable. - asciicheck - bodyclose - dogsled