diff --git a/.golangci.yml b/.golangci.yml index aea138d5..2be2ebf0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,8 @@ run: timeout: 5m modules-download-mode: readonly + skip-dirs: + - scripts skip-dirs-use-default: true skip-files: - old_sdk.go @@ -46,6 +48,8 @@ linters-settings: gocritic: enabled-tags: - opinionated + disabled-checks: + - singleCaseSwitch exhaustive: # In switch statement treat label default: as being exhaustive. default-signifies-exhaustive: true diff --git a/Makefile b/Makefile index 5de84e50..8b6042b2 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,7 @@ check/trailing: ## Check markdown files for potential issues with markdownlint. check/markdown: - $(call _print_check_step,Verifying Mardown files) + $(call _print_check_step,Verifying Markdown files) $(call _ensure_installed,yarn,markdownlint) yarn --silent markdownlint '*.md' --disable MD010 # MD010 does not handle code blocks well. diff --git a/cspell.yaml b/cspell.yaml index beb5d025..67173d20 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -28,6 +28,8 @@ ignorePaths: # TODO: Remove this exclusion after cleanup is done. - manifest/** words: + - GOFILE + - GOPACKAGE - auseg9kiegWKEtJZC416 - dynatrace - endef diff --git a/go.mod b/go.mod index 6059ff1d..9a6bb6a5 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/aws/aws-sdk-go v1.44.318 github.com/bmatcuk/doublestar/v4 v4.6.0 github.com/go-playground/validator/v10 v10.14.1 + github.com/goccy/go-yaml v1.11.0 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/hashicorp/go-retryablehttp v0.7.4 github.com/lestrrat-go/jwx v1.2.26 @@ -16,14 +17,12 @@ require ( github.com/stretchr/testify v1.8.4 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 golang.org/x/text v0.11.0 - gopkg.in/yaml.v3 v3.0.1 - k8s.io/apimachinery v0.27.3 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/fatih/color v1.14.1 // indirect + github.com/fatih/color v1.15.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -31,7 +30,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.2.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/kr/text v0.1.0 // indirect + github.com/kr/pretty v0.3.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect @@ -39,14 +38,14 @@ require ( github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect golang.org/x/crypto v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cc1e8e95..addb16be 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,7 @@ github.com/aws/aws-sdk-go v1.44.318/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8 github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc= github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= @@ -10,8 +11,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPc github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= -github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -23,9 +24,12 @@ github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+j github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= +github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= @@ -39,9 +43,11 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= @@ -65,8 +71,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= @@ -76,6 +82,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -122,8 +129,10 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -142,6 +151,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -152,9 +163,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM= -k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/manifest/doc.go b/manifest/doc.go new file mode 100644 index 00000000..abdd4f7b --- /dev/null +++ b/manifest/doc.go @@ -0,0 +1,2 @@ +// Package manifest defines the basic primitives for Nobl9 objects schema. +package manifest diff --git a/manifest/format.go b/manifest/format.go new file mode 100644 index 00000000..482de0de --- /dev/null +++ b/manifest/format.go @@ -0,0 +1,7 @@ +package manifest + +//go:generate ../bin/go-enum --nocase --lower --names + +// ObjectFormat represents the format of Object data representation. +// ENUM(JSON = 1, YAML) +type ObjectFormat int diff --git a/manifest/format_enum.go b/manifest/format_enum.go new file mode 100644 index 00000000..4f70b9b9 --- /dev/null +++ b/manifest/format_enum.go @@ -0,0 +1,74 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: 0.5.6 +// Revision: 97611fddaa414f53713597918c5e954646cb8623 +// Build Date: 2023-03-26T21:38:06Z +// Built By: goreleaser + +package manifest + +import ( + "fmt" + "strings" +) + +const ( + // ObjectFormatJSON is a ObjectFormat of type JSON. + ObjectFormatJSON ObjectFormat = iota + 1 + // ObjectFormatYAML is a ObjectFormat of type YAML. + ObjectFormatYAML +) + +var ErrInvalidObjectFormat = fmt.Errorf("not a valid ObjectFormat, try [%s]", strings.Join(_ObjectFormatNames, ", ")) + +const _ObjectFormatName = "JSONYAML" + +var _ObjectFormatNames = []string{ + _ObjectFormatName[0:4], + _ObjectFormatName[4:8], +} + +// ObjectFormatNames returns a list of possible string values of ObjectFormat. +func ObjectFormatNames() []string { + tmp := make([]string, len(_ObjectFormatNames)) + copy(tmp, _ObjectFormatNames) + return tmp +} + +var _ObjectFormatMap = map[ObjectFormat]string{ + ObjectFormatJSON: _ObjectFormatName[0:4], + ObjectFormatYAML: _ObjectFormatName[4:8], +} + +// String implements the Stringer interface. +func (x ObjectFormat) String() string { + if str, ok := _ObjectFormatMap[x]; ok { + return str + } + return fmt.Sprintf("ObjectFormat(%d)", x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x ObjectFormat) IsValid() bool { + _, ok := _ObjectFormatMap[x] + return ok +} + +var _ObjectFormatValue = map[string]ObjectFormat{ + _ObjectFormatName[0:4]: ObjectFormatJSON, + strings.ToLower(_ObjectFormatName[0:4]): ObjectFormatJSON, + _ObjectFormatName[4:8]: ObjectFormatYAML, + strings.ToLower(_ObjectFormatName[4:8]): ObjectFormatYAML, +} + +// ParseObjectFormat attempts to convert a string to a ObjectFormat. +func ParseObjectFormat(name string) (ObjectFormat, error) { + if x, ok := _ObjectFormatValue[name]; ok { + return x, nil + } + // Case insensitive parse, do a separate lookup to prevent unnecessary cost of lowercasing a string if we don't need to. + if x, ok := _ObjectFormatValue[strings.ToLower(name)]; ok { + return x, nil + } + return ObjectFormat(0), fmt.Errorf("%s is %w", name, ErrInvalidObjectFormat) +} diff --git a/manifest/models.go b/manifest/models.go deleted file mode 100644 index 4f633056..00000000 --- a/manifest/models.go +++ /dev/null @@ -1,121 +0,0 @@ -// Package manifest provides -package manifest - -import ( - "encoding/json" - "errors" - "fmt" - "strings" -) - -// StringInterpolationPlaceholder common symbol to use in strings for interpolation e.g. "My amazing {} Service" -const StringInterpolationPlaceholder = "{}" - -// ObjectInternal represents part of object which is only for internal usage, -// not exposed to the client, for internal usage -type ObjectInternal struct { - Organization string `json:"organization,omitempty" example:"nobl9-dev"` - ManifestSrc string `json:",omitempty" example:"x.yml"` - OktaClientID string `json:"-"` // used only by kind Agent -} - -type LabelKey = string -type LabelValue = string -type Labels map[LabelKey][]LabelValue - -// AlertSilenceMetadata defines only basic metadata fields - name and project which uniquely identifies -// object on project level. -type AlertSilenceMetadata struct { - Name string `json:"name" validate:"required,objectName" example:"name"` - Project string `json:"project,omitempty" validate:"objectName" example:"default"` -} - -// Metadata represents part of object which is common for all available Objects, for internal usage -type Metadata struct { - Name string `json:"name" validate:"required,objectName" example:"name"` - DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63" example:"Prometheus Source"` - Project string `json:"project,omitempty" validate:"objectName" example:"default"` - Labels Labels `json:"labels,omitempty" validate:"omitempty,labels"` -} - -// FullName returns full name of an object as `{name}.{project}` -func (m Metadata) FullName() string { - return fmt.Sprintf("%s.%s", m.Name, m.Project) -} - -// MetadataHolder is an intermediate structure that can provides metadata related -// field to other structures -type MetadataHolder struct { - Metadata Metadata `json:"metadata"` -} - -type ProjectMetadata struct { - Name string `json:"name" validate:"required,objectName" example:"name"` - DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63" example:"Shopping App"` - Labels Labels `json:"labels,omitempty" validate:"omitempty,labels"` -} - -type RoleBindingMetadata struct { - Name string `json:"name" validate:"required,objectName" example:"name"` -} - -// ObjectHeader represents Header which is common for all available Objects -type ObjectHeader struct { - APIVersion string `json:"apiVersion" validate:"required" example:"n9/v1alpha"` - Kind Kind `json:"kind" validate:"required" example:"kind"` - MetadataHolder - ObjectInternal -} - -// ObjectGeneric represents struct to which every Object is parsable -// Specific types of Object have different structures as Spec -type ObjectGeneric struct { - ObjectHeader - Spec json.RawMessage `json:"spec"` -} - -// JSONToGenericObjects parse JSON Array of Objects into generic objects -func JSONToGenericObjects(jsonPayload []byte) ([]ObjectGeneric, error) { - var objects []ObjectGeneric - if err := json.Unmarshal(jsonPayload, &objects); err != nil { - if stxErr, ok := err.(*json.SyntaxError); ok { - return nil, fmt.Errorf("malformed JSON payload, syntax error: %s offset: %d", stxErr.Error(), stxErr.Offset) - } - return nil, errors.New("malformed JSON payload - pass single list of valid JSON objects") - } - return objects, nil -} - -// UnsupportedKindErr returns appropriate error for missing value in field kind -// for not empty field kind returns always that is not supported for this apiVersion -// so have to be validated before -func UnsupportedKindErr(o ObjectGeneric) error { - if strings.TrimSpace(o.Kind.String()) == "" { - return EnhanceError(o, errors.New("missing or empty field kind for an Object")) - } - return EnhanceError(o, fmt.Errorf("invalid Object kind: %s for apiVersion: %s", o.Kind, o.APIVersion)) -} - -// UnsupportedAPIVersionErr returns appropriate error for missing value in field apiVersion -// for not empty field apiVersion returns always that this version is not supported so have to be -// validated before -func UnsupportedAPIVersionErr(o ObjectGeneric) error { - if strings.TrimSpace(o.APIVersion) == "" { - return EnhanceError(o, errors.New("missing or empty field apiVersion for an Object")) - } - return EnhanceError(o, fmt.Errorf("not supported apiVersion: %s", o.APIVersion)) -} - -// EnhanceError annotates error with path of manifest source, if it exists -// if not returns the same error as passed as argument -func EnhanceError(o ObjectGeneric, err error) error { - if err != nil && o.ManifestSrc != "" { - err = fmt.Errorf("%s: %w", o.ManifestSrc, err) - } - return err -} - -// StringInterpolation for arguments ("{}-my-{}-string-{}", "xd") returns string xd-my-xd-string-xd -func StringInterpolation(withPlaceholder, replacer string) string { - return strings.ReplaceAll(withPlaceholder, StringInterpolationPlaceholder, replacer) -} diff --git a/manifest/object.go b/manifest/object.go new file mode 100644 index 00000000..fe409bea --- /dev/null +++ b/manifest/object.go @@ -0,0 +1,142 @@ +package manifest + +import ( + "fmt" + "sort" + "strings" + + "github.com/pkg/errors" +) + +// Object represents a generic Nobl9 object definition. +// All Nobl9 objects implement this interface. +type Object interface { + // GetVersion returns the API version of the Object. + GetVersion() string + // GetKind returns the Kind of the Object. + GetKind() Kind + // GetName returns the name of the Object (RFC 1123 compliant DNS). + GetName() string + // Validate performs static validation of the Object. + Validate() error +} + +// ProjectScopedObject an Object which is tied to a specific KindProject. +// Example of such an object is v1alpha.SLO. +// On the other hand v1alpha.RoleBinding is an example of organization +// scoped Object which is not tied to any KindProject. +type ProjectScopedObject interface { + Object + // GetProject returns the name of the project which the ProjectScopedObject belongs to. + GetProject() string + // SetProject sets the name of the project which the ProjectScopedObject should belong to. + // It returns the copy of the Object with the updated Project. + SetProject(project string) Object +} + +// FilterByKind filters Object slice and returns its subset matching the type constraint. +func FilterByKind[T Object](objects []Object) []T { + var filtered []T + for i := range objects { + v, ok := objects[i].(T) + if ok { + filtered = append(filtered, v) + } + } + return filtered +} + +// Validate performs validation of all the provided objects. +// It aggregates the results into a single error. +func Validate(objects []Object) error { + errs := make([]string, 0) + for i := range objects { + if err := objects[i].Validate(); err != nil { + errs = append(errs, err.Error()) + } + } + if len(errs) > 0 { + return errors.New(strings.Join(errs, "\n")) + } + return validateObjectsUniqueness(objects) +} + +// SetDefaultProject sets the default project for each object only if the object is +// ProjectScopedObject, and it does not yet have project assigned to it. +func SetDefaultProject(objects []Object, project string) []Object { + for i := range objects { + v, ok := objects[i].(ProjectScopedObject) + if ok && v.GetProject() == "" { + objects[i] = v.SetProject(project) + } + } + return objects +} + +// validateObjectsUniqueness checks if all objects are uniquely named. +// The uniqueness key consists of the objects' kind, name and project. +// Project is only part of the key if Object does not implement ProjectScopedObject. +func validateObjectsUniqueness(objects []Object) (err error) { + type uniqueKey struct { + Kind Kind + Name string + Project string + } + + unique := make(map[uniqueKey]struct{}, len(objects)) + conflicts := make(map[Kind][]string) + for _, obj := range objects { + key := uniqueKey{ + Kind: obj.GetKind(), + Name: obj.GetName(), + } + if v, ok := obj.(ProjectScopedObject); ok { + key.Project = v.GetProject() + } + if _, isConflicting := unique[key]; isConflicting { + conflicts[obj.GetKind()] = append(conflicts[obj.GetKind()], uniquenessConflictDetails(obj, obj.GetKind())) + continue + } + unique[key] = struct{}{} + } + var errs []error + if len(conflicts) > 0 { + for kind, details := range conflicts { + errs = append(errs, fmt.Errorf( + `constraint "%s" was violated due to the following conflicts: [%s]`, + uniquenessConstraintDetails(kind), strings.Join(details, ", "))) + } + } + if len(errs) > 0 { + sort.Slice(errs, func(i, j int) bool { return errs[j].Error() > errs[i].Error() }) + builder := strings.Builder{} + for i, e := range errs { + builder.WriteString(e.Error()) + if i < len(errs)-1 { + builder.WriteString("; ") + } + } + return errors.New(builder.String()) + } + return nil +} + +// uniquenessConflictDetails creates a formatted string identifying a single conflict between two objects. +func uniquenessConflictDetails(object Object, kind Kind) string { + switch v := any(object).(type) { + case ProjectScopedObject: + return fmt.Sprintf(`{"Project": "%s", "%s": "%s"}`, v.GetProject(), kind, object.GetName()) + default: + return fmt.Sprintf(`"%s"`, object.GetName()) + } +} + +// uniquenessConstraintDetails creates a formatted string specifying the constraint which was broken. +func uniquenessConstraintDetails(kind Kind) string { + switch kind { + case KindProject, KindRoleBinding, KindUserGroup: + return fmt.Sprintf(`%s.metadata.name has to be unique`, kind) + default: + return fmt.Sprintf(`%s.metadata.name has to be unique across a single Project`, kind) + } +} diff --git a/manifest/object_test.go b/manifest/object_test.go new file mode 100644 index 00000000..9738343d --- /dev/null +++ b/manifest/object_test.go @@ -0,0 +1,170 @@ +package manifest + +import ( + _ "embed" + "strings" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestFilterByKind(t *testing.T) { + t.Run("nil objects slice", func(t *testing.T) { + objects := FilterByKind[customObject](nil) + assert.Nil(t, objects) + }) + + t.Run("empty objects slice", func(t *testing.T) { + objects := FilterByKind[customObject]([]Object{}) + assert.Nil(t, objects) + }) + + t.Run("no matching objects", func(t *testing.T) { + objects := FilterByKind[customObject]([]Object{ + customProjectScopedObject{}, + customProjectScopedObject{}, + }) + assert.Nil(t, objects) + }) + + t.Run("different objects", func(t *testing.T) { + objects := FilterByKind[customObject]([]Object{ + customObject{}, + customProjectScopedObject{}, + customObject{}, + customProjectScopedObject{}, + }) + assert.Len(t, objects, 2) + assert.IsType(t, []customObject{}, objects) + }) +} + +//go:embed test_data/expected_uniqueness_constraint_message.txt +var expectedUniquenessConstraintMessage string + +func TestValidate(t *testing.T) { + t.Run("nil objects slice", func(t *testing.T) { + err := Validate(nil) + assert.NoError(t, err) + }) + + t.Run("empty objects slice", func(t *testing.T) { + err := Validate([]Object{}) + assert.NoError(t, err) + }) + + t.Run("no errors", func(t *testing.T) { + err := Validate([]Object{ + customObject{kind: KindProject, name: "default"}, + customObject{kind: KindRoleBinding, name: "default"}, + }) + assert.NoError(t, err) + }) + + t.Run("errors", func(t *testing.T) { + err := Validate([]Object{ + customObject{}, + customObject{validationError: errors.New("I failed!")}, + customObject{validationError: errors.New("I failed too!")}, + }) + assert.Error(t, err) + assert.EqualError(t, err, "I failed!\nI failed too!") + }) + + t.Run("uniqueness constraint violated", func(t *testing.T) { + err := Validate([]Object{ + customObject{kind: KindProject, name: "sun"}, + customObject{kind: KindProject, name: "sun"}, + customObject{kind: KindProject, name: "moon"}, + customObject{kind: KindProject, name: "jupiter"}, + customObject{kind: KindProject, name: "sun"}, + customObject{kind: KindProject, name: "moon"}, + customObject{kind: KindRoleBinding, name: "sun"}, + customProjectScopedObject{customObject: customObject{ + kind: KindSLO, name: "sun"}, + project: "default"}, + customProjectScopedObject{customObject: customObject{ + kind: KindSLO, name: "sun"}, + project: "default"}, + customProjectScopedObject{customObject: customObject{ + kind: KindSLO, name: "sun"}, + project: "default"}, + customProjectScopedObject{customObject: customObject{ + kind: KindSLO, name: "jupiter"}, + project: "default"}, + customProjectScopedObject{customObject: customObject{ + kind: KindSLO, name: "jupiter"}, + project: "non-default"}, + customProjectScopedObject{customObject: customObject{ + kind: KindSLO, name: "moon"}, + project: "default"}, + customProjectScopedObject{customObject: customObject{ + kind: KindSLO, name: "moon"}, + project: "default"}, + customProjectScopedObject{customObject: customObject{ + kind: KindService, name: "jupiter"}, + project: "default"}, + }) + assert.Error(t, err) + assert.EqualError(t, err, strings.ReplaceAll(expectedUniquenessConstraintMessage, "\n", "; ")) + }) +} + +func TestSetDefaultProject(t *testing.T) { + for name, test := range map[string]struct { + Input []Object + Expected []Object + }{ + "nil objects slice": { + Input: nil, + Expected: nil, + }, + "empty objects slice": { + Input: []Object{}, + Expected: []Object{}, + }, + "different objects": { + Input: []Object{ + customProjectScopedObject{project: ""}, + customObject{}, + customProjectScopedObject{project: "this"}, + customProjectScopedObject{project: ""}, + }, + Expected: []Object{ + customProjectScopedObject{project: "default"}, + customObject{}, + customProjectScopedObject{project: "this"}, + customProjectScopedObject{project: "default"}, + }, + }, + } { + t.Run(name, func(t *testing.T) { + objects := SetDefaultProject(test.Input, "default") + assert.Equal(t, test.Expected, objects) + }) + } +} + +type customObject struct { + kind Kind + name string + validationError error +} + +func (c customObject) GetVersion() string { return "" } + +func (c customObject) GetKind() Kind { return c.kind } + +func (c customObject) GetName() string { return c.name } + +func (c customObject) Validate() error { return c.validationError } + +type customProjectScopedObject struct { + customObject + project string +} + +func (c customProjectScopedObject) GetProject() string { return c.project } + +func (c customProjectScopedObject) SetProject(project string) Object { c.project = project; return c } diff --git a/manifest/test_data/expected_uniqueness_constraint_message.txt b/manifest/test_data/expected_uniqueness_constraint_message.txt new file mode 100644 index 00000000..a7759439 --- /dev/null +++ b/manifest/test_data/expected_uniqueness_constraint_message.txt @@ -0,0 +1,2 @@ +constraint "Project.metadata.name has to be unique" was violated due to the following conflicts: ["sun", "sun", "moon"] +constraint "SLO.metadata.name has to be unique across a single Project" was violated due to the following conflicts: [{"Project": "default", "SLO": "sun"}, {"Project": "default", "SLO": "sun"}, {"Project": "default", "SLO": "moon"}] \ No newline at end of file diff --git a/manifest/v1alpha/agent.go b/manifest/v1alpha/agent.go index 54e7af03..00aa79c3 100644 --- a/manifest/v1alpha/agent.go +++ b/manifest/v1alpha/agent.go @@ -1,26 +1,31 @@ package v1alpha import ( - "encoding/json" - "github.com/pkg/errors" "github.com/nobl9/nobl9-go/manifest" ) -type AgentsSlice []Agent - -func (agents AgentsSlice) Clone() AgentsSlice { - clone := make([]Agent, len(agents)) - copy(clone, agents) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go Agent // Agent struct which mapped one to one with kind: Agent yaml definition type Agent struct { - manifest.ObjectHeader - Spec AgentSpec `json:"spec"` - Status AgentStatus `json:"status"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata AgentMetadata `json:"metadata"` + Spec AgentSpec `json:"spec"` + Status *AgentStatus `json:"status,omitempty"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` + OktaClientID string `json:"oktaClientID,omitempty"` +} + +type AgentMetadata struct { + Name string `json:"name" validate:"required,objectName"` + DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63"` + Project string `json:"project,omitempty" validate:"objectName"` + Labels Labels `json:"labels,omitempty" validate:"omitempty,labels"` } // AgentSpec represents content of Spec typical for Agent Object @@ -111,12 +116,6 @@ type AgentStatus struct { LastConnection string `json:"lastConnection,omitempty" example:"2020-08-31T14:26:13Z"` } -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (a Agent) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Name: a.Metadata.Name, Project: a.Metadata.Project} -} - // PrometheusAgentConfig represents content of Prometheus Configuration typical for Agent Object. type PrometheusAgentConfig struct { URL *string `json:"url,omitempty" example:"http://prometheus-service.monitoring:8080"` @@ -130,7 +129,7 @@ type DatadogAgentConfig struct { // NewRelicAgentConfig represents content of NewRelic Configuration typical for Agent Object. type NewRelicAgentConfig struct { - AccountID json.Number `json:"accountId,omitempty" example:"123654"` + AccountID int `json:"accountId,omitempty" example:"123654"` } // AmazonPrometheusAgentConfig represents content of Amazon Managed Service Configuration typical for Agent Object. @@ -230,27 +229,6 @@ type SplunkAgentConfig struct { URL string `json:"url,omitempty" example:"https://localhost:8089/servicesNS/admin/"` } -// genericToAgent converts ObjectGeneric to ObjectAgent -func genericToAgent(o manifest.ObjectGeneric, v validator, onlyHeader bool) (Agent, error) { - res := Agent{ - ObjectHeader: o.ObjectHeader, - } - if onlyHeader { - return res, nil - } - var resSpec AgentSpec - if err := json.Unmarshal(o.Spec, &resSpec); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - res.Spec = resSpec - if err := v.Check(res); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - return res, nil -} - // AgentWithSLOs struct which mapped one to one with kind: agent and slo yaml definition type AgentWithSLOs struct { Agent Agent `json:"agent"` diff --git a/manifest/v1alpha/agent_object.go b/manifest/v1alpha/agent_object.go new file mode 100644 index 00000000..6fd772d8 --- /dev/null +++ b/manifest/v1alpha/agent_object.go @@ -0,0 +1,53 @@ +// Code generated by "generate-object-impl Agent"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = Agent{} +var _ manifest.ProjectScopedObject = Agent{} +var _ ObjectContext = Agent{} + +func (a Agent) GetVersion() string { + return a.APIVersion +} + +func (a Agent) GetKind() manifest.Kind { + return a.Kind +} + +func (a Agent) GetName() string { + return a.Metadata.Name +} + +func (a Agent) Validate() error { + return validator.Check(a) +} + +func (a Agent) GetProject() string { + return a.Metadata.Project +} + +func (a Agent) SetProject(project string) manifest.Object { + a.Metadata.Project = project + return a +} + +func (a Agent) GetOrganization() string { + return a.Organization +} + +func (a Agent) SetOrganization(org string) manifest.Object { + a.Organization = org + return a +} + +func (a Agent) GetManifestSource() string { + return a.ManifestSource +} + +func (a Agent) SetManifestSource(src string) manifest.Object { + a.ManifestSource = src + return a +} diff --git a/manifest/v1alpha/alert.go b/manifest/v1alpha/alert.go index d0108ae7..49fd3b9a 100644 --- a/manifest/v1alpha/alert.go +++ b/manifest/v1alpha/alert.go @@ -2,34 +2,38 @@ package v1alpha import "github.com/nobl9/nobl9-go/manifest" -type AlertsSlice []Alert - -func (alerts AlertsSlice) Clone() AlertsSlice { - clone := make([]Alert, len(alerts)) - copy(clone, alerts) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go Alert // Alert represents triggered alert type Alert struct { - manifest.ObjectHeader - Spec AlertSpec `json:"spec"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata AlertMetadata `json:"metadata"` + Spec AlertSpec `json:"spec"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` +} + +type AlertMetadata struct { + Name string `json:"name" validate:"required,objectName"` + Project string `json:"project,omitempty" validate:"objectName"` } // AlertSpec represents content of Alert's Spec type AlertSpec struct { - AlertPolicy manifest.Metadata `json:"alertPolicy"` - SLO manifest.Metadata `json:"slo"` - Service manifest.Metadata `json:"service"` - Threshold AlertThreshold `json:"objective"` - Severity string `json:"severity" validate:"required,severity" example:"High"` - Status string `json:"status" example:"Resolved"` - TriggeredMetricTime string `json:"triggeredMetricTime"` - TriggeredClockTime string `json:"triggeredClockTime"` - ResolvedClockTime *string `json:"resolvedClockTime,omitempty"` - ResolvedMetricTime *string `json:"resolvedMetricTime,omitempty"` - CoolDown string `json:"coolDown"` - Conditions []AlertCondition `json:"conditions"` + AlertPolicy Metadata `json:"alertPolicy"` + SLO Metadata `json:"slo"` + Service Metadata `json:"service"` + Threshold AlertThreshold `json:"objective"` + Severity string `json:"severity" validate:"required,severity" example:"High"` + Status string `json:"status" example:"Resolved"` + TriggeredMetricTime string `json:"triggeredMetricTime"` + TriggeredClockTime string `json:"triggeredClockTime"` + ResolvedClockTime *string `json:"resolvedClockTime,omitempty"` + ResolvedMetricTime *string `json:"resolvedMetricTime,omitempty"` + CoolDown string `json:"coolDown"` + Conditions []AlertCondition `json:"conditions"` } type AlertThreshold struct { diff --git a/manifest/v1alpha/alert_method.go b/manifest/v1alpha/alert_method.go index fdfb0712..fd03fb2a 100644 --- a/manifest/v1alpha/alert_method.go +++ b/manifest/v1alpha/alert_method.go @@ -1,38 +1,33 @@ package v1alpha -import ( - "encoding/json" +import "github.com/nobl9/nobl9-go/manifest" - "github.com/nobl9/nobl9-go/manifest" -) - -type AlertMethodsSlice []AlertMethod - -func (alertMethods AlertMethodsSlice) Clone() AlertMethodsSlice { - clone := make([]AlertMethod, len(alertMethods)) - copy(clone, alertMethods) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go AlertMethod // AlertMethod represents the configuration required to send a notification to an external service // when an alert is triggered. type AlertMethod struct { - manifest.ObjectHeader - Spec AlertMethodSpec `json:"spec"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata AlertMethodMetadata `json:"metadata"` + Spec AlertMethodSpec `json:"spec"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` } -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (a AlertMethod) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Project: a.Metadata.Project, Name: a.Metadata.Name} +type AlertMethodMetadata struct { + Name string `json:"name" validate:"required,objectName"` + DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63"` + Project string `json:"project,omitempty" validate:"objectName"` } // PublicAlertMethod represents the configuration required to send a notification to an external service // when an alert is triggered. type PublicAlertMethod struct { - manifest.ObjectHeader - Spec PublicAlertMethodSpec `json:"spec"` - Status *PublicAlertMethodStatus `json:"status,omitempty"` + ObjectHeader `json:",inline"` + Spec PublicAlertMethodSpec `json:"spec"` + Status *PublicAlertMethodStatus `json:"status,omitempty"` } // PublicAlertMethodStatus represents content of Status optional for PublicAlertMethod Object @@ -190,27 +185,6 @@ type EmailAlertMethod struct { Body string `json:"body,omitempty" validate:"omitempty,max=2000,allowedAlertMethodEmailBodyFields"` } -// genericToAlertMethod converts ObjectGeneric to ObjectAlertMethod -func genericToAlertMethod(o manifest.ObjectGeneric, v validator, onlyHeader bool) (AlertMethod, error) { - res := AlertMethod{ - ObjectHeader: o.ObjectHeader, - } - if onlyHeader { - return res, nil - } - var resSpec AlertMethodSpec - if err := json.Unmarshal(o.Spec, &resSpec); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - res.Spec = resSpec - if err := v.Check(res); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - return res, nil -} - // AlertMethodWithAlertPolicy represents an AlertPolicies assigned to AlertMethod. type AlertMethodWithAlertPolicy struct { AlertMethod PublicAlertMethod `json:"alertMethod"` diff --git a/manifest/v1alpha/alert_method_object.go b/manifest/v1alpha/alert_method_object.go new file mode 100644 index 00000000..ac0e7f32 --- /dev/null +++ b/manifest/v1alpha/alert_method_object.go @@ -0,0 +1,53 @@ +// Code generated by "generate-object-impl AlertMethod"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = AlertMethod{} +var _ manifest.ProjectScopedObject = AlertMethod{} +var _ ObjectContext = AlertMethod{} + +func (a AlertMethod) GetVersion() string { + return a.APIVersion +} + +func (a AlertMethod) GetKind() manifest.Kind { + return a.Kind +} + +func (a AlertMethod) GetName() string { + return a.Metadata.Name +} + +func (a AlertMethod) Validate() error { + return validator.Check(a) +} + +func (a AlertMethod) GetProject() string { + return a.Metadata.Project +} + +func (a AlertMethod) SetProject(project string) manifest.Object { + a.Metadata.Project = project + return a +} + +func (a AlertMethod) GetOrganization() string { + return a.Organization +} + +func (a AlertMethod) SetOrganization(org string) manifest.Object { + a.Organization = org + return a +} + +func (a AlertMethod) GetManifestSource() string { + return a.ManifestSource +} + +func (a AlertMethod) SetManifestSource(src string) manifest.Object { + a.ManifestSource = src + return a +} diff --git a/manifest/v1alpha/alert_object.go b/manifest/v1alpha/alert_object.go new file mode 100644 index 00000000..d534b7c2 --- /dev/null +++ b/manifest/v1alpha/alert_object.go @@ -0,0 +1,53 @@ +// Code generated by "generate-object-impl Alert"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = Alert{} +var _ manifest.ProjectScopedObject = Alert{} +var _ ObjectContext = Alert{} + +func (a Alert) GetVersion() string { + return a.APIVersion +} + +func (a Alert) GetKind() manifest.Kind { + return a.Kind +} + +func (a Alert) GetName() string { + return a.Metadata.Name +} + +func (a Alert) Validate() error { + return validator.Check(a) +} + +func (a Alert) GetProject() string { + return a.Metadata.Project +} + +func (a Alert) SetProject(project string) manifest.Object { + a.Metadata.Project = project + return a +} + +func (a Alert) GetOrganization() string { + return a.Organization +} + +func (a Alert) SetOrganization(org string) manifest.Object { + a.Organization = org + return a +} + +func (a Alert) GetManifestSource() string { + return a.ManifestSource +} + +func (a Alert) SetManifestSource(src string) manifest.Object { + a.ManifestSource = src + return a +} diff --git a/manifest/v1alpha/alert_policy.go b/manifest/v1alpha/alert_policy.go index b43cc371..459f98b6 100644 --- a/manifest/v1alpha/alert_policy.go +++ b/manifest/v1alpha/alert_policy.go @@ -1,29 +1,25 @@ package v1alpha -import ( - "encoding/json" +import "github.com/nobl9/nobl9-go/manifest" - "github.com/nobl9/nobl9-go/manifest" -) - -type AlertPoliciesSlice []AlertPolicy - -func (alertPolicies AlertPoliciesSlice) Clone() AlertPoliciesSlice { - clone := make([]AlertPolicy, len(alertPolicies)) - copy(clone, alertPolicies) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go AlertPolicy // AlertPolicy represents a set of conditions that can trigger an alert. type AlertPolicy struct { - manifest.ObjectHeader - Spec AlertPolicySpec `json:"spec"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata AlertPolicyMetadata `json:"metadata"` + Spec AlertPolicySpec `json:"spec"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` } -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (a AlertPolicy) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Project: a.Metadata.Project, Name: a.Metadata.Name} +type AlertPolicyMetadata struct { + Name string `json:"name" validate:"required,objectName"` + DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63"` + Project string `json:"project,omitempty" validate:"objectName"` + Labels Labels `json:"labels,omitempty" validate:"omitempty,labels"` } // AlertPolicySpec represents content of AlertPolicy's Spec. @@ -53,34 +49,3 @@ type AlertPolicyWithSLOs struct { AlertPolicy AlertPolicy `json:"alertPolicy"` SLOs []SLO `json:"slos"` } - -// genericToAlertPolicy converts ObjectGeneric to ObjectAlertPolicy -func genericToAlertPolicy(o manifest.ObjectGeneric, v validator, onlyHeader bool) (AlertPolicy, error) { - res := AlertPolicy{ - ObjectHeader: o.ObjectHeader, - } - if onlyHeader { - return res, nil - } - var resSpec AlertPolicySpec - if err := json.Unmarshal(o.Spec, &resSpec); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - res.Spec = resSpec - if err := v.Check(res); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - - setAlertPolicyDefaults(&res) - return res, nil -} - -func setAlertPolicyDefaults(policy *AlertPolicy) { - for i, condition := range policy.Spec.Conditions { - if condition.AlertingWindow == "" && condition.LastsForDuration == "" { - policy.Spec.Conditions[i].LastsForDuration = DefaultAlertPolicyLastsForDuration - } - } -} diff --git a/manifest/v1alpha/alert_policy_object.go b/manifest/v1alpha/alert_policy_object.go new file mode 100644 index 00000000..ea1e4cea --- /dev/null +++ b/manifest/v1alpha/alert_policy_object.go @@ -0,0 +1,53 @@ +// Code generated by "generate-object-impl AlertPolicy"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = AlertPolicy{} +var _ manifest.ProjectScopedObject = AlertPolicy{} +var _ ObjectContext = AlertPolicy{} + +func (a AlertPolicy) GetVersion() string { + return a.APIVersion +} + +func (a AlertPolicy) GetKind() manifest.Kind { + return a.Kind +} + +func (a AlertPolicy) GetName() string { + return a.Metadata.Name +} + +func (a AlertPolicy) Validate() error { + return validator.Check(a) +} + +func (a AlertPolicy) GetProject() string { + return a.Metadata.Project +} + +func (a AlertPolicy) SetProject(project string) manifest.Object { + a.Metadata.Project = project + return a +} + +func (a AlertPolicy) GetOrganization() string { + return a.Organization +} + +func (a AlertPolicy) SetOrganization(org string) manifest.Object { + a.Organization = org + return a +} + +func (a AlertPolicy) GetManifestSource() string { + return a.ManifestSource +} + +func (a AlertPolicy) SetManifestSource(src string) manifest.Object { + a.ManifestSource = src + return a +} diff --git a/manifest/v1alpha/alert_silence.go b/manifest/v1alpha/alert_silence.go index e489dba3..ecc23960 100644 --- a/manifest/v1alpha/alert_silence.go +++ b/manifest/v1alpha/alert_silence.go @@ -1,34 +1,30 @@ package v1alpha import ( - "encoding/json" "time" "github.com/nobl9/nobl9-go/manifest" ) -type AlertSilencesSlice []AlertSilence - -func (alertSilences AlertSilencesSlice) Clone() AlertSilencesSlice { - clone := make([]AlertSilence, len(alertSilences)) - copy(clone, alertSilences) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go AlertSilence // AlertSilence represents alerts silencing configuration for given SLO and AlertPolicy. type AlertSilence struct { - manifest.ObjectInternal - APIVersion string `json:"apiVersion" validate:"required" example:"n9/v1alpha"` - Kind manifest.Kind `json:"kind" validate:"required" example:"kind"` - Metadata manifest.AlertSilenceMetadata `json:"metadata"` - Spec AlertSilenceSpec `json:"spec"` - Status AlertSilenceStatus `json:"status,omitempty"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata AlertSilenceMetadata `json:"metadata"` + Spec AlertSilenceSpec `json:"spec"` + Status *AlertSilenceStatus `json:"status,omitempty"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` } -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (a AlertSilence) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Project: a.Metadata.Project, Name: a.Metadata.Name} +// AlertSilenceMetadata defines only basic metadata fields - name and project which uniquely identifies +// object on project level. +type AlertSilenceMetadata struct { + Name string `json:"name" validate:"required,objectName" example:"name"` + Project string `json:"project,omitempty" validate:"objectName" example:"default"` } // AlertSilenceSpec represents content of AlertSilence's Spec. @@ -85,33 +81,3 @@ type AlertSilenceStatus struct { CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } - -// genericToAlertSilence converts ObjectGeneric to AlertSilence -func genericToAlertSilence(o manifest.ObjectGeneric, v validator, onlyHeader bool) (AlertSilence, error) { - res := AlertSilence{ - APIVersion: o.ObjectHeader.APIVersion, - Kind: o.ObjectHeader.Kind, - Metadata: manifest.AlertSilenceMetadata{ - Name: o.Metadata.Name, - Project: o.Metadata.Project, - }, - ObjectInternal: manifest.ObjectInternal{ - Organization: o.ObjectHeader.Organization, - ManifestSrc: o.ObjectHeader.ManifestSrc, - }, - } - if onlyHeader { - return res, nil - } - var resSpec AlertSilenceSpec - if err := json.Unmarshal(o.Spec, &resSpec); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - res.Spec = resSpec - if err := v.Check(res); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - return res, nil -} diff --git a/manifest/v1alpha/alert_silence_object.go b/manifest/v1alpha/alert_silence_object.go new file mode 100644 index 00000000..a961ef98 --- /dev/null +++ b/manifest/v1alpha/alert_silence_object.go @@ -0,0 +1,53 @@ +// Code generated by "generate-object-impl AlertSilence"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = AlertSilence{} +var _ manifest.ProjectScopedObject = AlertSilence{} +var _ ObjectContext = AlertSilence{} + +func (a AlertSilence) GetVersion() string { + return a.APIVersion +} + +func (a AlertSilence) GetKind() manifest.Kind { + return a.Kind +} + +func (a AlertSilence) GetName() string { + return a.Metadata.Name +} + +func (a AlertSilence) Validate() error { + return validator.Check(a) +} + +func (a AlertSilence) GetProject() string { + return a.Metadata.Project +} + +func (a AlertSilence) SetProject(project string) manifest.Object { + a.Metadata.Project = project + return a +} + +func (a AlertSilence) GetOrganization() string { + return a.Organization +} + +func (a AlertSilence) SetOrganization(org string) manifest.Object { + a.Organization = org + return a +} + +func (a AlertSilence) GetManifestSource() string { + return a.ManifestSource +} + +func (a AlertSilence) SetManifestSource(src string) manifest.Object { + a.ManifestSource = src + return a +} diff --git a/manifest/v1alpha/annotation.go b/manifest/v1alpha/annotation.go index 3dbea3eb..0a0a64b0 100644 --- a/manifest/v1alpha/annotation.go +++ b/manifest/v1alpha/annotation.go @@ -1,30 +1,27 @@ package v1alpha import ( - "encoding/json" "time" "github.com/nobl9/nobl9-go/manifest" ) -type AnnotationsSlice []Annotation - -func (annotations AnnotationsSlice) Clone() AnnotationsSlice { - clone := make([]Annotation, len(annotations)) - copy(clone, annotations) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go Annotation type Annotation struct { - manifest.ObjectHeader - Spec AnnotationSpec `json:"spec"` - Status AnnotationStatus `json:"status"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata AnnotationMetadata `json:"metadata"` + Spec AnnotationSpec `json:"spec"` + Status *AnnotationStatus `json:"status,omitempty"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` } -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (a Annotation) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Name: a.Metadata.Name, Project: a.Metadata.Project} +type AnnotationMetadata struct { + Name string `json:"name" validate:"required,objectName"` + Project string `json:"project,omitempty" validate:"objectName"` } type AnnotationSpec struct { @@ -48,23 +45,3 @@ func (a AnnotationSpec) GetParsedStartTime() (time.Time, error) { func (a AnnotationSpec) GetParsedEndTime() (time.Time, error) { return time.Parse(time.RFC3339, a.EndTime) } - -// genericToAnnotation converts ObjectGeneric to Annotation object -func genericToAnnotation(o manifest.ObjectGeneric, v validator) (Annotation, error) { - res := Annotation{ - ObjectHeader: o.ObjectHeader, - } - var resSpec AnnotationSpec - if err := json.Unmarshal(o.Spec, &resSpec); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - - res.Spec = resSpec - if err := v.Check(res); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - - return res, nil -} diff --git a/manifest/v1alpha/annotation_object.go b/manifest/v1alpha/annotation_object.go new file mode 100644 index 00000000..46c45c1f --- /dev/null +++ b/manifest/v1alpha/annotation_object.go @@ -0,0 +1,53 @@ +// Code generated by "generate-object-impl Annotation"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = Annotation{} +var _ manifest.ProjectScopedObject = Annotation{} +var _ ObjectContext = Annotation{} + +func (a Annotation) GetVersion() string { + return a.APIVersion +} + +func (a Annotation) GetKind() manifest.Kind { + return a.Kind +} + +func (a Annotation) GetName() string { + return a.Metadata.Name +} + +func (a Annotation) Validate() error { + return validator.Check(a) +} + +func (a Annotation) GetProject() string { + return a.Metadata.Project +} + +func (a Annotation) SetProject(project string) manifest.Object { + a.Metadata.Project = project + return a +} + +func (a Annotation) GetOrganization() string { + return a.Organization +} + +func (a Annotation) SetOrganization(org string) manifest.Object { + a.Organization = org + return a +} + +func (a Annotation) GetManifestSource() string { + return a.ManifestSource +} + +func (a Annotation) SetManifestSource(src string) manifest.Object { + a.ManifestSource = src + return a +} diff --git a/manifest/v1alpha/data_export.go b/manifest/v1alpha/data_export.go index 58b80e46..46b1c511 100644 --- a/manifest/v1alpha/data_export.go +++ b/manifest/v1alpha/data_export.go @@ -6,25 +6,31 @@ import ( "github.com/nobl9/nobl9-go/manifest" ) -type DataExportsSlice []DataExport +//go:generate go run ../../scripts/generate-object-impl.go DataExport -func (dataExports DataExportsSlice) Clone() DataExportsSlice { - clone := make([]DataExport, len(dataExports)) - copy(clone, dataExports) - return clone -} +const ( + DataExportTypeS3 string = "S3" + DataExportTypeSnowflake string = "Snowflake" + DataExportTypeGCS string = "GCS" +) // DataExport struct which mapped one to one with kind: DataExport yaml definition type DataExport struct { - manifest.ObjectHeader - Spec DataExportSpec `json:"spec"` - Status DataExportStatus `json:"status"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata DataExportMetadata `json:"metadata"` + Spec DataExportSpec `json:"spec"` + Status *DataExportStatus `json:"status"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` } -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (d DataExport) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Project: d.Metadata.Project, Name: d.Metadata.Name} +type DataExportMetadata struct { + Name string `json:"name" validate:"required,objectName"` + DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63"` + Project string `json:"project,omitempty" validate:"objectName"` + Labels Labels `json:"labels,omitempty" validate:"omitempty,labels"` } // DataExportSpec represents content of DataExport's Spec @@ -33,11 +39,28 @@ type DataExportSpec struct { Spec interface{} `json:"spec" validate:"required"` } -const ( - DataExportTypeS3 string = "S3" - DataExportTypeSnowflake string = "Snowflake" - DataExportTypeGCS string = "GCS" -) +func (d *DataExportSpec) UnmarshalJSON(bytes []byte) error { + var genericSpec struct { + ExportType string `json:"exportType" validate:"required,exportType" example:"Snowflake"` + Spec json.RawMessage `json:"spec"` + } + if err := json.Unmarshal(bytes, &genericSpec); err != nil { + return err + } + d.ExportType = genericSpec.ExportType + switch d.ExportType { + case DataExportTypeS3, DataExportTypeSnowflake: + d.Spec = &S3DataExportSpec{} + case DataExportTypeGCS: + d.Spec = &GCSDataExportSpec{} + } + if genericSpec.Spec != nil { + if err := json.Unmarshal(genericSpec.Spec, &d.Spec); err != nil { + return err + } + } + return nil +} // S3DataExportSpec represents content of Amazon S3 export type spec. type S3DataExportSpec struct { @@ -61,45 +84,3 @@ type DataExportStatusJob struct { Timestamp string `json:"timestamp,omitempty" example:"2021-02-09T10:43:07Z"` State string `json:"state" example:"finished"` } - -// dataExportGeneric represents struct to which every DataExport is parsable. -// Specific types of DataExport have different structures as Spec. -type dataExportGeneric struct { - ExportType string `json:"exportType" validate:"required,exportType" example:"Snowflake"` - Spec json.RawMessage `json:"spec"` -} - -// genericToDataExport converts ObjectGeneric to ObjectDataExport -func genericToDataExport(o manifest.ObjectGeneric, v validator, onlyHeader bool) (DataExport, error) { - res := DataExport{ - ObjectHeader: o.ObjectHeader, - } - if onlyHeader { - return res, nil - } - deg := dataExportGeneric{} - if err := json.Unmarshal(o.Spec, °); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - - resSpec := DataExportSpec{ExportType: deg.ExportType} - switch resSpec.ExportType { - case DataExportTypeS3, DataExportTypeSnowflake: - resSpec.Spec = &S3DataExportSpec{} - case DataExportTypeGCS: - resSpec.Spec = &GCSDataExportSpec{} - } - if deg.Spec != nil { - if err := json.Unmarshal(deg.Spec, &resSpec.Spec); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - } - res.Spec = resSpec - if err := v.Check(res); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - return res, nil -} diff --git a/manifest/v1alpha/data_export_object.go b/manifest/v1alpha/data_export_object.go new file mode 100644 index 00000000..31a8b336 --- /dev/null +++ b/manifest/v1alpha/data_export_object.go @@ -0,0 +1,53 @@ +// Code generated by "generate-object-impl DataExport"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = DataExport{} +var _ manifest.ProjectScopedObject = DataExport{} +var _ ObjectContext = DataExport{} + +func (d DataExport) GetVersion() string { + return d.APIVersion +} + +func (d DataExport) GetKind() manifest.Kind { + return d.Kind +} + +func (d DataExport) GetName() string { + return d.Metadata.Name +} + +func (d DataExport) Validate() error { + return validator.Check(d) +} + +func (d DataExport) GetProject() string { + return d.Metadata.Project +} + +func (d DataExport) SetProject(project string) manifest.Object { + d.Metadata.Project = project + return d +} + +func (d DataExport) GetOrganization() string { + return d.Organization +} + +func (d DataExport) SetOrganization(org string) manifest.Object { + d.Organization = org + return d +} + +func (d DataExport) GetManifestSource() string { + return d.ManifestSource +} + +func (d DataExport) SetManifestSource(src string) manifest.Object { + d.ManifestSource = src + return d +} diff --git a/manifest/v1alpha/data_sources.go b/manifest/v1alpha/data_sources.go index 2affec4f..b2dd0a0d 100644 --- a/manifest/v1alpha/data_sources.go +++ b/manifest/v1alpha/data_sources.go @@ -41,6 +41,8 @@ const ( GCM ) +const DatasourceStableChannel = "stable" + // HistoricalDataRetrieval represents optional parameters for agent to regard when configuring // TimeMachine-related SLO properties type HistoricalDataRetrieval struct { diff --git a/manifest/v1alpha/direct.go b/manifest/v1alpha/direct.go index 14de6d54..f93f124d 100644 --- a/manifest/v1alpha/direct.go +++ b/manifest/v1alpha/direct.go @@ -1,7 +1,6 @@ package v1alpha import ( - "encoding/json" "fmt" "strings" @@ -10,32 +9,34 @@ import ( "github.com/nobl9/nobl9-go/manifest" ) -type DirectsSlice []Direct - -func (directs DirectsSlice) Clone() DirectsSlice { - clone := make([]Direct, len(directs)) - copy(clone, directs) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go Direct // Direct struct which mapped one to one with kind: Direct yaml definition type Direct struct { - manifest.ObjectHeader - Spec DirectSpec `json:"spec"` - Status DirectStatus `json:"status"` -} + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata DirectMetadata `json:"metadata"` + Spec DirectSpec `json:"spec"` + Status *DirectStatus `json:"status,omitempty"` -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (d Direct) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Name: d.Metadata.Name, Project: d.Metadata.Project} + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` } // PublicDirect struct which mapped one to one with kind: Direct yaml definition without secrets type PublicDirect struct { - manifest.ObjectHeader - Spec PublicDirectSpec `json:"spec"` - Status DirectStatus `json:"status"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata DirectMetadata `json:"metadata"` + Spec PublicDirectSpec `json:"spec"` + Status *DirectStatus `json:"status,omitempty"` +} + +type DirectMetadata struct { + Name string `json:"name" validate:"required,objectName"` + DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63"` + Project string `json:"project,omitempty" validate:"objectName"` + Labels Labels `json:"labels,omitempty" validate:"omitempty,labels"` } // DirectSpec represents content of Spec typical for Direct Object @@ -173,14 +174,14 @@ type PublicDatadogDirectConfig struct { // NewRelicDirectConfig represents content of NewRelic Configuration typical for Direct Object. type NewRelicDirectConfig struct { - AccountID json.Number `json:"accountId" validate:"required" example:"123654"` - InsightsQueryKey string `json:"insightsQueryKey" validate:"newRelicApiKey" example:"secret"` + AccountID int `json:"accountId" validate:"required" example:"123654"` + InsightsQueryKey string `json:"insightsQueryKey" validate:"newRelicApiKey" example:"secret"` } // PublicNewRelicDirectConfig represents content of NewRelic Configuration typical for Direct Object without secrets. type PublicNewRelicDirectConfig struct { - AccountID json.Number `json:"accountId,omitempty" example:"123654"` - HiddenInsightsQueryKey string `json:"insightsQueryKey" example:"[hidden]"` + AccountID int `json:"accountId,omitempty" example:"123654"` + HiddenInsightsQueryKey string `json:"insightsQueryKey" example:"[hidden]"` } // PublicAppDynamicsDirectConfig represents public content of AppDynamics Configuration @@ -383,27 +384,6 @@ type PublicSumoLogicDirectConfig struct { URL string `json:"url"` } -// genericToDirect converts manifest.ObjectGeneric to Direct. -func genericToDirect(o manifest.ObjectGeneric, v validator, onlyHeader bool) (Direct, error) { - res := Direct{ - ObjectHeader: o.ObjectHeader, - } - if onlyHeader { - return res, nil - } - var resSpec DirectSpec - if err := json.Unmarshal(o.Spec, &resSpec); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - res.Spec = resSpec - if err := v.Check(res); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - return res, nil -} - // PublicDirectWithSLOs struct which mapped one to one with kind: direct and slo yaml definition type PublicDirectWithSLOs struct { Direct PublicDirect `json:"direct"` diff --git a/manifest/v1alpha/direct_object.go b/manifest/v1alpha/direct_object.go new file mode 100644 index 00000000..f18966f0 --- /dev/null +++ b/manifest/v1alpha/direct_object.go @@ -0,0 +1,53 @@ +// Code generated by "generate-object-impl Direct"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = Direct{} +var _ manifest.ProjectScopedObject = Direct{} +var _ ObjectContext = Direct{} + +func (d Direct) GetVersion() string { + return d.APIVersion +} + +func (d Direct) GetKind() manifest.Kind { + return d.Kind +} + +func (d Direct) GetName() string { + return d.Metadata.Name +} + +func (d Direct) Validate() error { + return validator.Check(d) +} + +func (d Direct) GetProject() string { + return d.Metadata.Project +} + +func (d Direct) SetProject(project string) manifest.Object { + d.Metadata.Project = project + return d +} + +func (d Direct) GetOrganization() string { + return d.Organization +} + +func (d Direct) SetOrganization(org string) manifest.Object { + d.Organization = org + return d +} + +func (d Direct) GetManifestSource() string { + return d.ManifestSource +} + +func (d Direct) SetManifestSource(src string) manifest.Object { + d.ManifestSource = src + return d +} diff --git a/manifest/v1alpha/labels.go b/manifest/v1alpha/labels.go new file mode 100644 index 00000000..52afeb75 --- /dev/null +++ b/manifest/v1alpha/labels.go @@ -0,0 +1,5 @@ +package v1alpha + +type LabelKey = string +type LabelValue = string +type Labels map[LabelKey][]LabelValue diff --git a/manifest/v1alpha/models.go b/manifest/v1alpha/models.go new file mode 100644 index 00000000..762c8976 --- /dev/null +++ b/manifest/v1alpha/models.go @@ -0,0 +1,54 @@ +package v1alpha + +import ( + "fmt" + "strings" + + "github.com/nobl9/nobl9-go/manifest" +) + +// StringInterpolationPlaceholder common symbol to use in strings for interpolation e.g. "My amazing {} Service" +const StringInterpolationPlaceholder = "{}" + +// ObjectInternal represents part of object which is only for internal usage, +// not exposed to the client +// Deprecated +type ObjectInternal struct { + Organization string `json:"organization,omitempty" example:"nobl9-dev"` + ManifestSrc string `json:",omitempty" example:"x.yml"` +} + +// Metadata represents part of object which is common for all available Objects, for internal usage +// Deprecated +type Metadata struct { + Name string `json:"name" validate:"required,objectName" example:"name"` + DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63" example:"Prometheus Source"` + Project string `json:"project,omitempty" validate:"objectName" example:"default"` + Labels Labels `json:"labels,omitempty" validate:"omitempty,labels"` +} + +// FullName returns full name of an object as `{name}.{project}` +func (m Metadata) FullName() string { + return fmt.Sprintf("%s.%s", m.Name, m.Project) +} + +// MetadataHolder is an intermediate structure that can provides metadata related +// field to other structures +// Deprecated +type MetadataHolder struct { + Metadata Metadata `json:"metadata"` +} + +// ObjectHeader represents Header which is common for all available Objects +// Deprecated +type ObjectHeader struct { + APIVersion string `json:"apiVersion" validate:"required" example:"n9/v1alpha"` + Kind manifest.Kind `json:"kind" validate:"required" example:"kind"` + MetadataHolder `json:",inline"` + ObjectInternal `json:",inline"` +} + +// StringInterpolation for arguments ("{}-my-{}-string-{}", "xd") returns string xd-my-xd-string-xd +func StringInterpolation(withPlaceholder, replacer string) string { + return strings.ReplaceAll(withPlaceholder, StringInterpolationPlaceholder, replacer) +} diff --git a/manifest/v1alpha/objects.go b/manifest/v1alpha/objects.go index f0fa7d42..fd071e06 100644 --- a/manifest/v1alpha/objects.go +++ b/manifest/v1alpha/objects.go @@ -2,262 +2,17 @@ package v1alpha import ( - "errors" - "fmt" - "strings" - "github.com/nobl9/nobl9-go/manifest" ) // APIVersion is a value of valid apiVersions const APIVersion = "n9/v1alpha" -// HiddenValue can be used as a value of a secret field and is ignored during saving -const HiddenValue = "[hidden]" - -const ( - DatasourceStableChannel = "stable" - DefaultAlertPolicyLastsForDuration = "0m" -) - -// APIObjects - all Objects available for this version of API -// Sorted in order of applying -type APIObjects struct { - SLOs SLOsSlice `json:"slos,omitempty"` - Services ServicesSlice `json:"services,omitempty"` - Agents AgentsSlice `json:"agents,omitempty"` - AlertPolicies AlertPoliciesSlice `json:"alertpolicies,omitempty"` - AlertSilences AlertSilencesSlice `json:"alertsilences,omitempty"` - Alerts AlertsSlice `json:"alerts,omitempty"` - AlertMethods AlertMethodsSlice `json:"alertmethods,omitempty"` - Directs DirectsSlice `json:"directs,omitempty"` - DataExports DataExportsSlice `json:"dataexports,omitempty"` - Projects ProjectsSlice `json:"projects,omitempty"` - RoleBindings RoleBindingsSlice `json:"rolebindings,omitempty"` - Annotations AnnotationsSlice `json:"annotations,omitempty"` - UserGroups UserGroupsSlice `json:"usergroups,omitempty"` -} - -func (o APIObjects) Clone() APIObjects { - return APIObjects{ - SLOs: o.SLOs.Clone(), - Services: o.Services.Clone(), - Agents: o.Agents.Clone(), - AlertPolicies: o.AlertPolicies.Clone(), - AlertSilences: o.AlertSilences.Clone(), - Alerts: o.Alerts.Clone(), - AlertMethods: o.AlertMethods.Clone(), - Directs: o.Directs.Clone(), - DataExports: o.DataExports.Clone(), - Projects: o.Projects.Clone(), - RoleBindings: o.RoleBindings.Clone(), - Annotations: o.Annotations.Clone(), - } -} - -func (o APIObjects) Len() int { - return len(o.SLOs) + - len(o.Services) + - len(o.Agents) + - len(o.AlertPolicies) + - len(o.AlertSilences) + - len(o.Alerts) + - len(o.AlertMethods) + - len(o.Directs) + - len(o.DataExports) + - len(o.Projects) + - len(o.RoleBindings) + - len(o.Annotations) -} - -// FilterEntry represents single metric label to be matched against value -type FilterEntry struct { - Label string `json:"label" validate:"required,prometheusLabelName"` - Value string `json:"value" validate:"required"` -} - -type OrganizationInformation struct { - ID string `json:"id"` - DisplayName *string `json:"displayName"` -} - -// Applying multiple Agents at once can cause timeout for whole sloctl apply command. -// This is caused by long request to Okta to create client credentials app. -// The same case is applicable for delete command. -const allowedAgentsToModify = 1 - -// Parse takes care of all Object supported by n9/v1alpha apiVersion -func Parse(o manifest.ObjectGeneric, parsedObjects *APIObjects, onlyHeaders bool) (err error) { - v := NewValidator() - switch o.Kind { - case manifest.KindSLO: - var slo SLO - slo, err = genericToSLO(o, v, onlyHeaders) - parsedObjects.SLOs = append(parsedObjects.SLOs, slo) - case manifest.KindService: - var service Service - service, err = genericToService(o, v, onlyHeaders) - parsedObjects.Services = append(parsedObjects.Services, service) - case manifest.KindAgent: - var agent Agent - if len(parsedObjects.Agents) >= allowedAgentsToModify { - err = manifest.EnhanceError(o, errors.New("only one Agent can be defined in this configuration")) - } else { - agent, err = genericToAgent(o, v, onlyHeaders) - parsedObjects.Agents = append(parsedObjects.Agents, agent) - } - case manifest.KindAlertPolicy: - var alertPolicy AlertPolicy - alertPolicy, err = genericToAlertPolicy(o, v, onlyHeaders) - parsedObjects.AlertPolicies = append(parsedObjects.AlertPolicies, alertPolicy) - case manifest.KindAlertSilence: - var alertSilence AlertSilence - alertSilence, err = genericToAlertSilence(o, v, onlyHeaders) - parsedObjects.AlertSilences = append(parsedObjects.AlertSilences, alertSilence) - case manifest.KindAlertMethod: - var alertMethod AlertMethod - alertMethod, err = genericToAlertMethod(o, v, onlyHeaders) - parsedObjects.AlertMethods = append(parsedObjects.AlertMethods, alertMethod) - case manifest.KindDirect: - var direct Direct - direct, err = genericToDirect(o, v, onlyHeaders) - parsedObjects.Directs = append(parsedObjects.Directs, direct) - case manifest.KindDataExport: - var dataExport DataExport - dataExport, err = genericToDataExport(o, v, onlyHeaders) - parsedObjects.DataExports = append(parsedObjects.DataExports, dataExport) - case manifest.KindProject: - var project Project - project, err = genericToProject(o, v, onlyHeaders) - parsedObjects.Projects = append(parsedObjects.Projects, project) - case manifest.KindRoleBinding: - var roleBinding RoleBinding - roleBinding, err = genericToRoleBinding(o, v) - parsedObjects.RoleBindings = append(parsedObjects.RoleBindings, roleBinding) - case manifest.KindAnnotation: - var annotation Annotation - annotation, err = genericToAnnotation(o, v) - parsedObjects.Annotations = append(parsedObjects.Annotations, annotation) - case manifest.KindUserGroup: - var group UserGroup - group, err = genericToUserGroup(o) - parsedObjects.UserGroups = append(parsedObjects.UserGroups, group) - // catching invalid kinds of objects for this apiVersion - default: - err = manifest.UnsupportedKindErr(o) - } - return err -} - -// Validate performs validation of parsed APIObjects. -func (o APIObjects) Validate() (err error) { - var errs []error - if err = validateUniquenessConstraints(manifest.KindSLO, o.SLOs); err != nil { - errs = append(errs, err) - } - if err = validateUniquenessConstraints(manifest.KindService, o.Services); err != nil { - errs = append(errs, err) - } - if err = validateUniquenessConstraints(manifest.KindProject, o.Projects); err != nil { - errs = append(errs, err) - } - if err = validateUniquenessConstraints(manifest.KindAgent, o.Agents); err != nil { - errs = append(errs, err) - } - if err = validateUniquenessConstraints(manifest.KindDirect, o.Directs); err != nil { - errs = append(errs, err) - } - if err = validateUniquenessConstraints(manifest.KindAlertMethod, o.AlertMethods); err != nil { - errs = append(errs, err) - } - if err = validateUniquenessConstraints(manifest.KindAlertPolicy, o.AlertPolicies); err != nil { - errs = append(errs, err) - } - if err = validateUniquenessConstraints(manifest.KindAlertSilence, o.AlertSilences); err != nil { - errs = append(errs, err) - } - if err = validateUniquenessConstraints(manifest.KindDataExport, o.DataExports); err != nil { - errs = append(errs, err) - } - if err = validateUniquenessConstraints(manifest.KindRoleBinding, o.RoleBindings); err != nil { - errs = append(errs, err) - } - if err = validateUniquenessConstraints(manifest.KindAnnotation, o.Annotations); err != nil { - errs = append(errs, err) - } - if err = validateUniquenessConstraints(manifest.KindUserGroup, o.UserGroups); err != nil { - errs = append(errs, err) - } - if len(errs) > 0 { - builder := strings.Builder{} - for i, err := range errs { - builder.WriteString(err.Error()) - if i < len(errs)-1 { - builder.WriteString("; ") - } - } - return errors.New(builder.String()) - } - return nil -} - -// uniqueIdentifiers holds metadata used to uniquely identify an object across a single organization. -// While Name is required, Project might not apply to all objects. -type uniqueIdentifiers struct { - Name string - Project string -} - -// uniqueIdentifiersGetter allows generics to be used when iterating -// over all Kind slices like SLOsSlice or ServicesSlice. -type uniqueIdentifiersGetter interface { - getUniqueIdentifiers() uniqueIdentifiers -} - -// validateUniquenessConstraints finds conflicting objects in a Kind slice. -// It returns an error if any conflicts were encountered. -// The error informs about the cause and lists ALL conflicts. -func validateUniquenessConstraints[T uniqueIdentifiersGetter](kind manifest.Kind, slice []T) error { - unique := make(map[string]struct{}, len(slice)) - var details []string - for i := range slice { - uid := slice[i].getUniqueIdentifiers() - key := uid.Project + uid.Name - if _, conflicts := unique[key]; conflicts { - details = append(details, conflictDetails(kind, uid)) - continue - } - unique[key] = struct{}{} - } - if len(details) > 0 { - return conflictError(kind, details) - } - return nil -} - -// conflictDetails creates a formatted string identifying a single conflict between two objects. -func conflictDetails(kind manifest.Kind, uid uniqueIdentifiers) string { - switch kind { - case manifest.KindProject, manifest.KindRoleBinding, manifest.KindUserGroup: - return fmt.Sprintf(`"%s"`, uid.Name) - default: - return fmt.Sprintf(`{"Project": "%s", "%s": "%s"}`, uid.Project, kind, uid.Name) - } -} - -// conflictError formats an error returned for a specific Kind with all it's conflicts listed as a JSON array. -// nolint: stylecheck -func conflictError(kind manifest.Kind, details []string) error { - return fmt.Errorf(`Constraint "%s" was violated due to the following conflicts: [%s]`, - constraintDetails(kind), strings.Join(details, ", ")) -} - -// constraintDetails creates a formatted string specifying the constraint which was broken. -func constraintDetails(kind manifest.Kind) string { - switch kind { - case manifest.KindProject, manifest.KindRoleBinding, manifest.KindUserGroup: - return fmt.Sprintf(`%s.metadata.name has to be unique`, kind) - default: - return fmt.Sprintf(`%s.metadata.name has to be unique across a single Project`, kind) - } +// ObjectContext defines method for interacting with contextual details of the Object +// which are not directly part of its manifest and are, from the users perspective, read only. +type ObjectContext interface { + GetOrganization() string + SetOrganization(org string) manifest.Object + GetManifestSource() string + SetManifestSource(src string) manifest.Object } diff --git a/manifest/v1alpha/objects_test.go b/manifest/v1alpha/objects_test.go deleted file mode 100644 index c1ccdc47..00000000 --- a/manifest/v1alpha/objects_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package v1alpha - -import ( - "embed" - "encoding/json" - "fmt" - "path" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" - - "github.com/nobl9/nobl9-go/manifest" -) - -const testDataDir = "test_data" - -//go:embed test_data -var testData embed.FS - -//go:embed test_data/expected_error_conflicting_slo.txt -var expectedError string - -func TestAPIObjects_Validate(t *testing.T) { - objects := APIObjects{} - for _, kind := range manifest.ApplicableKinds() { - require.Contains(t, - expectedError, - kind.String(), - "each applicable Kind must have a designated test file and appear in the expected error") - - data, err := testData.ReadFile(path.Join(testDataDir, - fmt.Sprintf("conflicting_%s.yaml", kind.ToLower()))) - require.NoError(t, err) - - var decodedYAML []map[string]interface{} - err = yaml.Unmarshal(data, &decodedYAML) - require.NoError(t, err) - - rawJSON, err := json.Marshal(decodedYAML) - require.NoError(t, err) - - var genericObjects []manifest.ObjectGeneric - err = json.Unmarshal(rawJSON, &genericObjects) - require.NoError(t, err) - require.Greater(t, len(genericObjects), 0) - - for _, object := range genericObjects { - // So that we can skip the Agent's constraints which allows only one to be applied (at the time being). - if object.Kind == manifest.KindAgent { - var agent Agent - agent, err = genericToAgent(object, NewValidator(), false) - require.NoError(t, err) - objects.Agents = append(objects.Agents, agent) - continue - } - err = Parse(object, &objects, false) - require.NoError(t, err) - } - } - - err := objects.Validate() - require.Error(t, err) - // Trim any trailing newlines from the file and replace the other newlines with '; ' - // just to make the test file a bit easier to read and work with. - expected := strings.Replace(strings.TrimSpace(expectedError), "\n", "; ", len(manifest.KindValues())) - assert.EqualError(t, err, expected) -} - -func TestSetAlertPolicyDefaults(t *testing.T) { - for _, testCase := range []struct { - desc string - in AlertPolicy - out AlertPolicy - }{ - { - desc: "when alertingWindow is defined, lastsFor default value should not be set", - in: AlertPolicy{ - Spec: AlertPolicySpec{ - Conditions: []AlertCondition{ - { - AlertingWindow: "30m", - }, - }, - }, - }, - out: AlertPolicy{ - Spec: AlertPolicySpec{ - Conditions: []AlertCondition{ - { - AlertingWindow: "30m", - }, - }, - }, - }, - }, - { - desc: "when alertingWindow is not defined and lastsFor is empty zero value should be set", - in: AlertPolicy{ - Spec: AlertPolicySpec{ - Conditions: []AlertCondition{ - {}, - }, - }, - }, - out: AlertPolicy{ - Spec: AlertPolicySpec{ - Conditions: []AlertCondition{ - { - LastsForDuration: "0m", - }, - }, - }, - }, - }, - { - desc: "when alertingWindow is not defined and lastsFor is not empty do not change lastsFor", - in: AlertPolicy{ - Spec: AlertPolicySpec{ - Conditions: []AlertCondition{ - { - LastsForDuration: "1h", - }, - }, - }, - }, - out: AlertPolicy{ - Spec: AlertPolicySpec{ - Conditions: []AlertCondition{ - { - LastsForDuration: "1h", - }, - }, - }, - }, - }, - } { - t.Run(testCase.desc, func(t *testing.T) { - setAlertPolicyDefaults(&testCase.in) - assert.Equal(t, testCase.out, testCase.in) - }) - } -} diff --git a/manifest/v1alpha/parser.go b/manifest/v1alpha/parser.go new file mode 100644 index 00000000..0a58f976 --- /dev/null +++ b/manifest/v1alpha/parser.go @@ -0,0 +1,81 @@ +package v1alpha + +import ( + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/pkg/errors" + + "github.com/nobl9/nobl9-go/manifest" +) + +type unmarshalFunc func(data []byte, v interface{}) error + +func ParseObject(data []byte, kind manifest.Kind, format manifest.ObjectFormat) (manifest.Object, error) { + var unmarshal unmarshalFunc + switch format { + case manifest.ObjectFormatJSON: + unmarshal = json.Unmarshal + case manifest.ObjectFormatYAML: + // Workaround for https://github.com/goccy/go-yaml/issues/313. + // If the library changes its interpretation of empty pointer fields, + // we should switch to native yaml.Unmarshal instead. + var err error + data, err = yaml.YAMLToJSON(data) + if err != nil { + return nil, errors.Wrap(err, "failed to convert YAML to JSON") + } + unmarshal = json.Unmarshal + default: + return nil, errors.Errorf("unsupported format: %s", format) + } + + var ( + object manifest.Object + err error + ) + //exhaustive:enforce + switch kind { + case manifest.KindService: + object, err = genericParseObject[Service](data, unmarshal) + case manifest.KindSLO: + object, err = genericParseObject[SLO](data, unmarshal) + case manifest.KindProject: + object, err = genericParseObject[Project](data, unmarshal) + case manifest.KindAgent: + object, err = genericParseObject[Agent](data, unmarshal) + case manifest.KindDirect: + object, err = genericParseObject[Direct](data, unmarshal) + case manifest.KindAlert: + object, err = genericParseObject[Alert](data, unmarshal) + case manifest.KindAlertMethod: + object, err = genericParseObject[AlertMethod](data, unmarshal) + case manifest.KindAlertPolicy: + object, err = genericParseObject[AlertPolicy](data, unmarshal) + case manifest.KindAlertSilence: + object, err = genericParseObject[AlertSilence](data, unmarshal) + case manifest.KindRoleBinding: + object, err = genericParseObject[RoleBinding](data, unmarshal) + case manifest.KindDataExport: + object, err = genericParseObject[DataExport](data, unmarshal) + case manifest.KindAnnotation: + object, err = genericParseObject[Annotation](data, unmarshal) + case manifest.KindUserGroup: + object, err = genericParseObject[UserGroup](data, unmarshal) + default: + return nil, fmt.Errorf("%s is %w", kind, manifest.ErrInvalidKind) + } + if err != nil { + return nil, err + } + return object, nil +} + +func genericParseObject[T manifest.Object](data []byte, unmarshal unmarshalFunc) (T, error) { + var object T + if err := unmarshal(data, &object); err != nil { + return object, err + } + return object, nil +} diff --git a/manifest/v1alpha/parser_test.go b/manifest/v1alpha/parser_test.go new file mode 100644 index 00000000..ad17f8f3 --- /dev/null +++ b/manifest/v1alpha/parser_test.go @@ -0,0 +1,44 @@ +package v1alpha + +import ( + "embed" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/nobl9/nobl9-go/manifest" +) + +//go:embed test_data/parser +var parserTestData embed.FS + +func TestParseObject(t *testing.T) { + for name, kind := range map[string]manifest.Kind{ + "cloudwatch_agent": manifest.KindAgent, + "redshift_agent": manifest.KindAgent, + } { + t.Run(strings.ReplaceAll(name, "_", " "), func(t *testing.T) { + jsonData, format := readParserTestFile(t, name+".json") + jsonObject, err := ParseObject(jsonData, kind, format) + require.NoError(t, err) + + yamlData, format := readParserTestFile(t, name+".yaml") + yamlObject, err := ParseObject(yamlData, kind, format) + require.NoError(t, err) + + assert.Equal(t, jsonObject, yamlObject) + }) + } +} + +func readParserTestFile(t *testing.T, filename string) ([]byte, manifest.ObjectFormat) { + t.Helper() + data, err := parserTestData.ReadFile(filepath.Join("test_data", "parser", filename)) + require.NoError(t, err) + format, err := manifest.ParseObjectFormat(filepath.Ext(filename)[1:]) + require.NoError(t, err) + return data, format +} diff --git a/manifest/v1alpha/project.go b/manifest/v1alpha/project.go index b2998def..47b72726 100644 --- a/manifest/v1alpha/project.go +++ b/manifest/v1alpha/project.go @@ -1,67 +1,27 @@ package v1alpha -import ( - "encoding/json" +import "github.com/nobl9/nobl9-go/manifest" - "github.com/nobl9/nobl9-go/manifest" -) - -type ProjectsSlice []Project - -func (projects ProjectsSlice) Clone() ProjectsSlice { - clone := make([]Project, len(projects)) - copy(clone, projects) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go Project // Project struct which mapped one to one with kind: project yaml definition. type Project struct { - manifest.ObjectInternal - APIVersion string `json:"apiVersion" validate:"required" example:"n9/v1alpha"` - Kind manifest.Kind `json:"kind" validate:"required" example:"kind"` - Metadata manifest.ProjectMetadata `json:"metadata"` - Spec ProjectSpec `json:"spec"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata ProjectMetadata `json:"metadata"` + Spec ProjectSpec `json:"spec"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` } -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (p Project) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Name: p.Metadata.Name} +type ProjectMetadata struct { + Name string `json:"name" validate:"required,objectName" example:"name"` + DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63" example:"Shopping App"` + Labels Labels `json:"labels,omitempty" validate:"omitempty,labels"` } // ProjectSpec represents content of Spec typical for Project Object. type ProjectSpec struct { Description string `json:"description" validate:"description" example:"Bleeding edge web app"` } - -// genericToProject converts ObjectGeneric to Project -func genericToProject(o manifest.ObjectGeneric, v validator, onlyHeader bool) (Project, error) { - res := Project{ - APIVersion: o.ObjectHeader.APIVersion, - Kind: o.ObjectHeader.Kind, - Metadata: manifest.ProjectMetadata{ - Name: o.Metadata.Name, - DisplayName: o.Metadata.DisplayName, - Labels: o.Metadata.Labels, - }, - ObjectInternal: manifest.ObjectInternal{ - Organization: o.ObjectHeader.Organization, - ManifestSrc: o.ObjectHeader.ManifestSrc, - }, - } - if onlyHeader { - return res, nil - } - - var resSpec ProjectSpec - if err := json.Unmarshal(o.Spec, &resSpec); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - res.Spec = resSpec - if err := v.Check(res); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - return res, nil -} diff --git a/manifest/v1alpha/project_object.go b/manifest/v1alpha/project_object.go new file mode 100644 index 00000000..ec5811ca --- /dev/null +++ b/manifest/v1alpha/project_object.go @@ -0,0 +1,43 @@ +// Code generated by "generate-object-impl Project"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = Project{} +var _ ObjectContext = Project{} + +func (p Project) GetVersion() string { + return p.APIVersion +} + +func (p Project) GetKind() manifest.Kind { + return p.Kind +} + +func (p Project) GetName() string { + return p.Metadata.Name +} + +func (p Project) Validate() error { + return validator.Check(p) +} + +func (p Project) GetOrganization() string { + return p.Organization +} + +func (p Project) SetOrganization(org string) manifest.Object { + p.Organization = org + return p +} + +func (p Project) GetManifestSource() string { + return p.ManifestSource +} + +func (p Project) SetManifestSource(src string) manifest.Object { + p.ManifestSource = src + return p +} diff --git a/manifest/v1alpha/reports.go b/manifest/v1alpha/reports.go index 6caa62c3..62de3ab9 100644 --- a/manifest/v1alpha/reports.go +++ b/manifest/v1alpha/reports.go @@ -1,8 +1,6 @@ // Package v1alpha represents objects available in API n9/v1alpha package v1alpha -import "github.com/nobl9/nobl9-go/manifest" - // UsageSummaryMetadata represents metadata part of usageSummary object type UsageSummaryMetadata struct { *Tier `json:"tier,omitempty" validate:"omitempty"` @@ -59,7 +57,7 @@ type SLOErrorBudgetStatusReport struct { } type SLOErrorBudgetStatus struct { - manifest.MetadataHolder + MetadataHolder TimeWindow TimeWindow `json:"timeWindow"` ThresholdSummary []ThresholdStatus `json:"thresholdSummary"` } diff --git a/manifest/v1alpha/role_binding.go b/manifest/v1alpha/role_binding.go index ae9f9424..f9955571 100644 --- a/manifest/v1alpha/role_binding.go +++ b/manifest/v1alpha/role_binding.go @@ -1,32 +1,22 @@ package v1alpha -import ( - "encoding/json" +import "github.com/nobl9/nobl9-go/manifest" - "github.com/nobl9/nobl9-go/manifest" -) - -type RoleBindingsSlice []RoleBinding - -func (roleBindings RoleBindingsSlice) Clone() RoleBindingsSlice { - clone := make([]RoleBinding, len(roleBindings)) - copy(clone, roleBindings) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go RoleBinding // RoleBinding represents relation of User and Role type RoleBinding struct { - manifest.ObjectInternal - APIVersion string `json:"apiVersion" validate:"required" example:"n9/v1alpha"` - Kind manifest.Kind `json:"kind" validate:"required" example:"kind"` - Metadata manifest.RoleBindingMetadata `json:"metadata"` - Spec RoleBindingSpec `json:"spec"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata RoleBindingMetadata `json:"metadata"` + Spec RoleBindingSpec `json:"spec"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` } -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (r RoleBinding) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Name: r.Metadata.Name} +type RoleBindingMetadata struct { + Name string `json:"name" validate:"required,objectName" example:"name"` } type RoleBindingSpec struct { @@ -35,30 +25,3 @@ type RoleBindingSpec struct { RoleRef string `json:"roleRef" validate:"required"` ProjectRef string `json:"projectRef,omitempty"` } - -// genericToRoleBinding converts ObjectGeneric to ObjectRoleBinding -// onlyHeader parameter is not supported for RoleBinding since ProjectRef is defined on Spec section. -func genericToRoleBinding(o manifest.ObjectGeneric, v validator) (RoleBinding, error) { - res := RoleBinding{ - APIVersion: o.ObjectHeader.APIVersion, - Kind: o.ObjectHeader.Kind, - Metadata: manifest.RoleBindingMetadata{ - Name: o.Metadata.Name, - }, - ObjectInternal: manifest.ObjectInternal{ - Organization: o.ObjectHeader.Organization, - ManifestSrc: o.ObjectHeader.ManifestSrc, - }, - } - var resSpec RoleBindingSpec - if err := json.Unmarshal(o.Spec, &resSpec); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - res.Spec = resSpec - if err := v.Check(res); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - return res, nil -} diff --git a/manifest/v1alpha/role_binding_object.go b/manifest/v1alpha/role_binding_object.go new file mode 100644 index 00000000..dc6bf39a --- /dev/null +++ b/manifest/v1alpha/role_binding_object.go @@ -0,0 +1,43 @@ +// Code generated by "generate-object-impl RoleBinding"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = RoleBinding{} +var _ ObjectContext = RoleBinding{} + +func (r RoleBinding) GetVersion() string { + return r.APIVersion +} + +func (r RoleBinding) GetKind() manifest.Kind { + return r.Kind +} + +func (r RoleBinding) GetName() string { + return r.Metadata.Name +} + +func (r RoleBinding) Validate() error { + return validator.Check(r) +} + +func (r RoleBinding) GetOrganization() string { + return r.Organization +} + +func (r RoleBinding) SetOrganization(org string) manifest.Object { + r.Organization = org + return r +} + +func (r RoleBinding) GetManifestSource() string { + return r.ManifestSource +} + +func (r RoleBinding) SetManifestSource(src string) manifest.Object { + r.ManifestSource = src + return r +} diff --git a/manifest/v1alpha/service.go b/manifest/v1alpha/service.go index 71e7a485..4195b90f 100644 --- a/manifest/v1alpha/service.go +++ b/manifest/v1alpha/service.go @@ -1,30 +1,26 @@ package v1alpha -import ( - "encoding/json" +import "github.com/nobl9/nobl9-go/manifest" - "github.com/nobl9/nobl9-go/manifest" -) - -type ServicesSlice []Service - -func (services ServicesSlice) Clone() ServicesSlice { - clone := make([]Service, len(services)) - copy(clone, services) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go Service // Service struct which mapped one to one with kind: service yaml definition type Service struct { - manifest.ObjectHeader - Spec ServiceSpec `json:"spec"` - Status *ServiceStatus `json:"status,omitempty"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata ServiceMetadata `json:"metadata"` + Spec ServiceSpec `json:"spec"` + Status *ServiceStatus `json:"status,omitempty"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` } -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (s Service) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Name: s.Metadata.Name, Project: s.Metadata.Project} +type ServiceMetadata struct { + Name string `json:"name" validate:"required,objectName"` + DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63"` + Project string `json:"project,omitempty" validate:"objectName"` + Labels Labels `json:"labels,omitempty" validate:"omitempty,labels"` } // ServiceStatus represents content of Status optional for Service Object. @@ -37,28 +33,6 @@ type ServiceSpec struct { Description string `json:"description" validate:"description" example:"Bleeding edge web app"` } -// genericToService converts ObjectGeneric to Object Service. -func genericToService(o manifest.ObjectGeneric, v validator, onlyHeader bool) (Service, error) { - res := Service{ - ObjectHeader: o.ObjectHeader, - } - if onlyHeader { - return res, nil - } - - var resSpec ServiceSpec - if err := json.Unmarshal(o.Spec, &resSpec); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - res.Spec = resSpec - if err := v.Check(res); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - return res, nil -} - // ServiceWithSLOs struct which mapped one to one with kind: service and slo yaml definition. type ServiceWithSLOs struct { Service Service `json:"service"` diff --git a/manifest/v1alpha/service_object.go b/manifest/v1alpha/service_object.go new file mode 100644 index 00000000..77e93368 --- /dev/null +++ b/manifest/v1alpha/service_object.go @@ -0,0 +1,53 @@ +// Code generated by "generate-object-impl Service"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = Service{} +var _ manifest.ProjectScopedObject = Service{} +var _ ObjectContext = Service{} + +func (s Service) GetVersion() string { + return s.APIVersion +} + +func (s Service) GetKind() manifest.Kind { + return s.Kind +} + +func (s Service) GetName() string { + return s.Metadata.Name +} + +func (s Service) Validate() error { + return validator.Check(s) +} + +func (s Service) GetProject() string { + return s.Metadata.Project +} + +func (s Service) SetProject(project string) manifest.Object { + s.Metadata.Project = project + return s +} + +func (s Service) GetOrganization() string { + return s.Organization +} + +func (s Service) SetOrganization(org string) manifest.Object { + s.Organization = org + return s +} + +func (s Service) GetManifestSource() string { + return s.ManifestSource +} + +func (s Service) SetManifestSource(src string) manifest.Object { + s.ManifestSource = src + return s +} diff --git a/manifest/v1alpha/slo.go b/manifest/v1alpha/slo.go index 451ce4ab..b763f0f1 100644 --- a/manifest/v1alpha/slo.go +++ b/manifest/v1alpha/slo.go @@ -1,123 +1,49 @@ package v1alpha import ( - "encoding/json" - "github.com/nobl9/nobl9-go/manifest" ) -type SLOsSlice []SLO - -func (slos SLOsSlice) Clone() SLOsSlice { - clone := make([]SLO, len(slos)) - copy(clone, slos) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go SLO // SLO struct which mapped one to one with kind: slo yaml definition, external usage type SLO struct { - manifest.ObjectHeader - Spec SLOSpec `json:"spec"` - Status *SLOStatus `json:"status,omitempty"` + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata SLOMetadata `json:"metadata"` + Spec SLOSpec `json:"spec"` + Status *SLOStatus `json:"status,omitempty"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` +} + +type SLOMetadata struct { + Name string `json:"name" validate:"required,objectName"` + DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63"` + Project string `json:"project,omitempty" validate:"objectName"` + Labels Labels `json:"labels,omitempty" validate:"omitempty,labels"` } // SLOSpec represents content of Spec typical for SLO Object type SLOSpec struct { - Description string `json:"description" validate:"description" example:"Total count of server requests"` //nolint:lll + Description string `json:"description,omitempty" validate:"description" example:"Total count of server requests"` //nolint:lll Indicator Indicator `json:"indicator"` BudgetingMethod string `json:"budgetingMethod" validate:"required,budgetingMethod" example:"Occurrences"` Thresholds []Threshold `json:"objectives" validate:"required,dive"` Service string `json:"service" validate:"required,objectName" example:"webapp-service"` TimeWindows []TimeWindow `json:"timeWindows" validate:"required,len=1,dive"` - AlertPolicies []string `json:"alertPolicies" validate:"omitempty"` + AlertPolicies []string `json:"alertPolicies,omitempty" validate:"omitempty"` Attachments []Attachment `json:"attachments,omitempty" validate:"omitempty,max=20,dive"` CreatedAt string `json:"createdAt,omitempty"` Composite *Composite `json:"composite,omitempty" validate:"omitempty"` AnomalyConfig *AnomalyConfig `json:"anomalyConfig,omitempty" validate:"omitempty"` } -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (s SLO) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Name: s.Metadata.Name, Project: s.Metadata.Project} -} - type SLOStatus struct { TimeTravelStatus *TimeTravelStatus `json:"timeTravel,omitempty"` } -// genericToSLO converts ObjectGeneric to Object SLO -func genericToSLO(o manifest.ObjectGeneric, v validator, onlyHeader bool) (SLO, error) { - res := SLO{ - ObjectHeader: o.ObjectHeader, - } - if onlyHeader { - return res, nil - } - var resSpec SLOSpec - if err := json.Unmarshal(o.Spec, &resSpec); err != nil { - return res, manifest.EnhanceError(o, err) - } - res.Spec = resSpec - - // to keep BC with the ThousandEyes initial implementation (that did not support passing TestType), - // we default `res.Spec.Indicator.RawMetrics.ThousandEyes.TestType` to a value that, until now, was implicitly assumed - setThousandEyesDefaults(&res) - - if err := v.Check(res); err != nil { - return res, manifest.EnhanceError(o, err) - } - - if res.Spec.Indicator.MetricSource.Project == "" { - res.Spec.Indicator.MetricSource.Project = res.Metadata.Project - } - if !res.Spec.Indicator.MetricSource.Kind.IsValid() { - res.Spec.Indicator.MetricSource.Kind = manifest.KindAgent - } - - // we're moving towards the version where raw metrics are defined on each objective, but for now, - // we have to make sure that old contract (with indicator defined directly on the SLO's spec) is also supported - if res.Spec.Indicator.RawMetric != nil { - for i := range res.Spec.Thresholds { - res.Spec.Thresholds[i].RawMetric = &RawMetricSpec{ - MetricQuery: res.Spec.Indicator.RawMetric, - } - } - } - - // AnomalyConfig will be moved into Anomaly Rules in PC-8502. - // Set the default value of all alert methods defined in anomaly config to the same project - // that is used by SLO. - if res.Spec.AnomalyConfig != nil && res.Spec.AnomalyConfig.NoData != nil { - for i := 0; i < len(res.Spec.AnomalyConfig.NoData.AlertMethods); i++ { - if res.Spec.AnomalyConfig.NoData.AlertMethods[i].Project == "" { - res.Spec.AnomalyConfig.NoData.AlertMethods[i].Project = res.Metadata.Project - } - } - } - - return res, nil -} - -func setThousandEyesDefaults(slo *SLO) { - if slo.Spec.Indicator.RawMetric != nil && - slo.Spec.Indicator.RawMetric.ThousandEyes != nil && - slo.Spec.Indicator.RawMetric.ThousandEyes.TestType == nil { - metricType := ThousandEyesNetLatency - slo.Spec.Indicator.RawMetric.ThousandEyes.TestType = &metricType - } - - for i, threshold := range slo.Spec.Thresholds { - if threshold.RawMetric != nil && - threshold.RawMetric.MetricQuery != nil && - threshold.RawMetric.MetricQuery.ThousandEyes != nil && - threshold.RawMetric.MetricQuery.ThousandEyes.TestType == nil { - metricType := ThousandEyesNetLatency - slo.Spec.Thresholds[i].RawMetric.MetricQuery.ThousandEyes.TestType = &metricType - } - } -} - // Calendar struct represents calendar time window type Calendar struct { StartTime string `json:"startTime" validate:"required,dateWithTime,minDateTime" example:"2020-01-21 12:30:00"` @@ -157,7 +83,7 @@ type ThresholdBase struct { // Threshold represents single threshold for SLO, for internal usage type Threshold struct { - ThresholdBase + ThresholdBase `json:",inline"` // BudgetTarget *float64 `json:"target" validate:"required,numeric,gte=0,lt=1" example:"0.9"` @@ -171,14 +97,14 @@ type Threshold struct { // Indicator represents integration with metric source can be. e.g. Prometheus, Datadog, for internal usage type Indicator struct { - MetricSource *MetricSourceSpec `json:"metricSource" validate:"required"` - RawMetric *MetricSpec `json:"rawMetric,omitempty"` + MetricSource MetricSourceSpec `json:"metricSource" validate:"required"` + RawMetric *MetricSpec `json:"rawMetric,omitempty"` } type MetricSourceSpec struct { Project string `json:"project,omitempty" validate:"omitempty,objectName" example:"default"` Name string `json:"name" validate:"required,objectName" example:"prometheus-source"` - Kind manifest.Kind `json:"kind" validate:"omitempty,metricSourceKind" example:"Agent"` + Kind manifest.Kind `json:"kind,omitempty" validate:"omitempty,metricSourceKind" example:"Agent"` } // Composite represents configuration for Composite SLO. diff --git a/manifest/v1alpha/slo_object.go b/manifest/v1alpha/slo_object.go new file mode 100644 index 00000000..3e7815c2 --- /dev/null +++ b/manifest/v1alpha/slo_object.go @@ -0,0 +1,53 @@ +// Code generated by "generate-object-impl SLO"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = SLO{} +var _ manifest.ProjectScopedObject = SLO{} +var _ ObjectContext = SLO{} + +func (s SLO) GetVersion() string { + return s.APIVersion +} + +func (s SLO) GetKind() manifest.Kind { + return s.Kind +} + +func (s SLO) GetName() string { + return s.Metadata.Name +} + +func (s SLO) Validate() error { + return validator.Check(s) +} + +func (s SLO) GetProject() string { + return s.Metadata.Project +} + +func (s SLO) SetProject(project string) manifest.Object { + s.Metadata.Project = project + return s +} + +func (s SLO) GetOrganization() string { + return s.Organization +} + +func (s SLO) SetOrganization(org string) manifest.Object { + s.Organization = org + return s +} + +func (s SLO) GetManifestSource() string { + return s.ManifestSource +} + +func (s SLO) SetManifestSource(src string) manifest.Object { + s.ManifestSource = src + return s +} diff --git a/manifest/v1alpha/test_data/conflicting_agent.yaml b/manifest/v1alpha/test_data/conflicting_agent.yaml deleted file mode 100644 index 4e05141d..00000000 --- a/manifest/v1alpha/test_data/conflicting_agent.yaml +++ /dev/null @@ -1,133 +0,0 @@ -- apiVersion: n9/v1alpha - kind: Agent - metadata: - name: single-conflict - project: default - spec: - datadog: - site: com - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - sourceOf: - - Metrics - status: - agentType: Datadog -- apiVersion: n9/v1alpha - kind: Agent - metadata: - name: single-conflict - project: default - spec: - datadog: - site: eu - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - sourceOf: - - Metrics - status: - agentType: Datadog -- apiVersion: n9/v1alpha - kind: Agent - metadata: - name: zero-conflict - project: default - spec: - datadog: - site: com - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - sourceOf: - - Metrics - status: - agentType: Datadog -- apiVersion: n9/v1alpha - kind: Agent - metadata: - name: zero-conflict - project: secret-agent - spec: - datadog: - site: eu - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - sourceOf: - - Metrics - status: - agentType: Datadog -- apiVersion: n9/v1alpha - kind: Agent - metadata: - name: double-conflict - project: not-so-default - spec: - datadog: - site: com - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - sourceOf: - - Metrics - status: - agentType: Datadog -- apiVersion: n9/v1alpha - kind: Agent - metadata: - name: double-conflict - project: not-so-default - spec: - datadog: - site: eu - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - sourceOf: - - Metrics - status: - agentType: Datadog -- apiVersion: n9/v1alpha - kind: Agent - metadata: - name: double-conflict - project: not-so-default - spec: - datadog: - site: com - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - sourceOf: - - Metrics - status: - agentType: Datadog diff --git a/manifest/v1alpha/test_data/conflicting_alertmethod.yaml b/manifest/v1alpha/test_data/conflicting_alertmethod.yaml deleted file mode 100644 index ad64d500..00000000 --- a/manifest/v1alpha/test_data/conflicting_alertmethod.yaml +++ /dev/null @@ -1,63 +0,0 @@ -- apiVersion: n9/v1alpha - kind: AlertMethod - metadata: - name: single-conflict - project: default - spec: - opsgenie: - auth: '[hidden]' - url: https://api.opsgenie.com -- apiVersion: n9/v1alpha - kind: AlertMethod - metadata: - name: single-conflict - project: default - spec: - opsgenie: - auth: '[hidden]' - url: https://api.opsgenie.eu -- apiVersion: n9/v1alpha - kind: AlertMethod - metadata: - name: zero-conflict - project: default - spec: - opsgenie: - auth: '[hidden]' - url: https://api.opsgenie.pl -- apiVersion: n9/v1alpha - kind: AlertMethod - metadata: - name: zero-conflict - project: super-project - spec: - opsgenie: - auth: '[hidden]' - url: https://api.opsgenie.ua -- apiVersion: n9/v1alpha - kind: AlertMethod - metadata: - name: double-conflict - project: non-default - spec: - opsgenie: - auth: '[hidden]' - url: https://api.opsgenie.gg -- apiVersion: n9/v1alpha - kind: AlertMethod - metadata: - name: double-conflict - project: non-default - spec: - opsgenie: - auth: '[hidden]' - url: https://api.opsgenie.gz -- apiVersion: n9/v1alpha - kind: AlertMethod - metadata: - name: double-conflict - project: non-default - spec: - opsgenie: - auth: '[hidden]' - url: https://api.opsgenie.gl diff --git a/manifest/v1alpha/test_data/conflicting_alertpolicy.yaml b/manifest/v1alpha/test_data/conflicting_alertpolicy.yaml deleted file mode 100644 index 6c513641..00000000 --- a/manifest/v1alpha/test_data/conflicting_alertpolicy.yaml +++ /dev/null @@ -1,98 +0,0 @@ -- apiVersion: n9/v1alpha - kind: AlertPolicy - metadata: - name: single-conflict - project: default - spec: - alertMethods: [] - conditions: - - lastsFor: 1h - measurement: timeToBurnBudget - op: lt - value: 72h - coolDown: 5m - severity: Medium -- apiVersion: n9/v1alpha - kind: AlertPolicy - metadata: - name: single-conflict - project: default - spec: - alertMethods: [] - conditions: - - lastsFor: 1h - measurement: timeToBurnBudget - op: lt - value: 72h - coolDown: 10m - severity: Medium -- apiVersion: n9/v1alpha - kind: AlertPolicy - metadata: - name: zero-conflict - project: default - spec: - alertMethods: [] - conditions: - - lastsFor: 1h - measurement: timeToBurnBudget - op: lt - value: 72h - coolDown: 15m - severity: Medium -- apiVersion: n9/v1alpha - kind: AlertPolicy - metadata: - name: zero-conflict - project: super-project - spec: - alertMethods: [] - conditions: - - lastsFor: 1h - measurement: timeToBurnBudget - op: lt - value: 72h - coolDown: 20m - severity: Medium -- apiVersion: n9/v1alpha - kind: AlertPolicy - metadata: - name: double-conflict - project: non-default - spec: - alertMethods: [] - conditions: - - lastsFor: 1h - measurement: timeToBurnBudget - op: lt - value: 72h - coolDown: 25m - severity: Medium -- apiVersion: n9/v1alpha - kind: AlertPolicy - metadata: - name: double-conflict - project: non-default - spec: - alertMethods: [] - conditions: - - lastsFor: 1h - measurement: timeToBurnBudget - op: lt - value: 72h - coolDown: 30m - severity: Medium -- apiVersion: n9/v1alpha - kind: AlertPolicy - metadata: - name: double-conflict - project: non-default - spec: - alertMethods: [] - conditions: - - lastsFor: 1h - measurement: timeToBurnBudget - op: lt - value: 72h - coolDown: 35m - severity: Medium diff --git a/manifest/v1alpha/test_data/conflicting_alertsilence.yaml b/manifest/v1alpha/test_data/conflicting_alertsilence.yaml deleted file mode 100644 index 968122c0..00000000 --- a/manifest/v1alpha/test_data/conflicting_alertsilence.yaml +++ /dev/null @@ -1,84 +0,0 @@ -- apiVersion: n9/v1alpha - kind: AlertSilence - metadata: - name: single-conflict - project: default - spec: - alertPolicy: - name: alert-police - period: - duration: 1h - startTime: "2022-12-16T13:38:18Z" - slo: newrelic-rolling-timeslices-ratio -- apiVersion: n9/v1alpha - kind: AlertSilence - metadata: - name: single-conflict - project: default - spec: - alertPolicy: - name: alert-police - period: - duration: 2h - startTime: "2022-12-16T13:38:18Z" - slo: newrelic-rolling-timeslices-ratio -- apiVersion: n9/v1alpha - kind: AlertSilence - metadata: - name: zero-conflict - project: default - spec: - alertPolicy: - name: alert-police - period: - duration: 3h - startTime: "2022-12-16T13:38:18Z" - slo: newrelic-rolling-timeslices-ratio -- apiVersion: n9/v1alpha - kind: AlertSilence - metadata: - name: zero-conflict - project: super-project - spec: - alertPolicy: - name: alert-police - period: - duration: 4h - startTime: "2022-12-16T13:38:18Z" - slo: newrelic-rolling-timeslices-ratio -- apiVersion: n9/v1alpha - kind: AlertSilence - metadata: - name: double-conflict - project: super-project - spec: - alertPolicy: - name: alert-police - period: - duration: 5h - startTime: "2022-12-16T13:38:18Z" - slo: newrelic-rolling-timeslices-ratio -- apiVersion: n9/v1alpha - kind: AlertSilence - metadata: - name: double-conflict - project: super-project - spec: - alertPolicy: - name: alert-police - period: - duration: 6h - startTime: "2022-12-16T13:38:18Z" - slo: newrelic-rolling-timeslices-ratio -- apiVersion: n9/v1alpha - kind: AlertSilence - metadata: - name: double-conflict - project: super-project - spec: - alertPolicy: - name: alert-police - period: - duration: 7h - startTime: "2022-12-16T13:38:18Z" - slo: newrelic-rolling-timeslices-ratio diff --git a/manifest/v1alpha/test_data/conflicting_annotation.yaml b/manifest/v1alpha/test_data/conflicting_annotation.yaml deleted file mode 100644 index 7de23dee..00000000 --- a/manifest/v1alpha/test_data/conflicting_annotation.yaml +++ /dev/null @@ -1,70 +0,0 @@ -- apiVersion: n9/v1alpha - kind: Annotation - metadata: - name: single-conflict - project: default - spec: - description: "i'm required..." - endTime: "2022-09-01T05:42:27Z" - slo: newrelic-rolling-occurrences-ratio - startTime: "2022-08-27T05:42:27Z" -- apiVersion: n9/v1alpha - kind: Annotation - metadata: - name: single-conflict - project: default - spec: - description: "i'm required..." - endTime: "2022-09-02T05:42:27Z" - slo: newrelic-rolling-occurrences-ratio - startTime: "2022-08-27T05:42:27Z" -- apiVersion: n9/v1alpha - kind: Annotation - metadata: - name: zero-conflict - project: default - spec: - description: "i'm required..." - endTime: "2022-09-02T05:42:27Z" - slo: newrelic-rolling-occurrences-ratio - startTime: "2022-08-27T05:42:27Z" -- apiVersion: n9/v1alpha - kind: Annotation - metadata: - name: zero-conflict - project: non-default - spec: - description: "i'm required..." - endTime: "2022-09-03T05:42:27Z" - slo: newrelic-rolling-occurrences-ratio - startTime: "2022-08-27T05:42:27Z" -- apiVersion: n9/v1alpha - kind: Annotation - metadata: - name: double-conflict - project: super-project - spec: - description: "i'm required..." - endTime: "2022-09-04T05:42:27Z" - slo: newrelic-rolling-occurrences-ratio - startTime: "2022-08-27T05:42:27Z" -- apiVersion: n9/v1alpha - kind: Annotation - metadata: - name: double-conflict - project: super-project - spec: - description: "i'm required..." - endTime: "2022-09-05T05:42:27Z" - slo: newrelic-rolling-occurrences-ratio - startTime: "2022-08-27T05:42:27Z" -- apiVersion: n9/v1alpha - kind: Annotation - metadata: - name: double-conflict - project: super-project - spec: - description: "i'm required..." - endTime: "2022-09-06T05:42:27Z" - slo: newrelic-rolling-occurrences-ratio - startTime: "2022-08-27T05:42:27Z" diff --git a/manifest/v1alpha/test_data/conflicting_dataexport.yaml b/manifest/v1alpha/test_data/conflicting_dataexport.yaml deleted file mode 100644 index 7d477a4e..00000000 --- a/manifest/v1alpha/test_data/conflicting_dataexport.yaml +++ /dev/null @@ -1,105 +0,0 @@ -- apiVersion: n9/v1alpha - kind: DataExport - metadata: - name: single-conflict - project: default - spec: - exportType: S3 - spec: - bucketName: dataexport-dev-orange - roleArn: arn:aws:iam::341861879477:role/1 - status: - awsExternalID: n9-integration-nobl9-dev - exportJob: - state: successful - timestamp: "2022-12-16T12:30:12Z" -- apiVersion: n9/v1alpha - kind: DataExport - metadata: - name: single-conflict - project: default - spec: - exportType: S3 - spec: - bucketName: dataexport-dev-orange - roleArn: arn:aws:iam::341861879477:role/2 - status: - awsExternalID: n9-integration-nobl9-dev - exportJob: - state: successful - timestamp: "2022-12-16T12:30:12Z" -- apiVersion: n9/v1alpha - kind: DataExport - metadata: - name: zero-conflicts - project: default - spec: - exportType: S3 - spec: - bucketName: dataexport-dev-orange - roleArn: arn:aws:iam::341861879477:role/3 - status: - awsExternalID: n9-integration-nobl9-dev - exportJob: - state: successful - timestamp: "2022-12-16T12:30:12Z" -- apiVersion: n9/v1alpha - kind: DataExport - metadata: - name: zero-conflicts - project: super-project - spec: - exportType: S3 - spec: - bucketName: dataexport-dev-orange - roleArn: arn:aws:iam::341861879477:role/4 - status: - awsExternalID: n9-integration-nobl9-dev - exportJob: - state: successful - timestamp: "2022-12-16T12:30:12Z" -- apiVersion: n9/v1alpha - kind: DataExport - metadata: - name: double-conflict - project: non-default - spec: - exportType: S3 - spec: - bucketName: dataexport-dev-orange - roleArn: arn:aws:iam::341861879477:role/5 - status: - awsExternalID: n9-integration-nobl9-dev - exportJob: - state: successful - timestamp: "2022-12-16T12:30:12Z" -- apiVersion: n9/v1alpha - kind: DataExport - metadata: - name: double-conflict - project: non-default - spec: - exportType: S3 - spec: - bucketName: dataexport-dev-orange - roleArn: arn:aws:iam::341861879477:role/6 - status: - awsExternalID: n9-integration-nobl9-dev - exportJob: - state: successful - timestamp: "2022-12-16T12:30:12Z" -- apiVersion: n9/v1alpha - kind: DataExport - metadata: - name: double-conflict - project: non-default - spec: - exportType: S3 - spec: - bucketName: dataexport-dev-orange - roleArn: arn:aws:iam::341861879477:role/7 - status: - awsExternalID: n9-integration-nobl9-dev - exportJob: - state: successful - timestamp: "2022-12-16T12:30:12Z" diff --git a/manifest/v1alpha/test_data/conflicting_direct.yaml b/manifest/v1alpha/test_data/conflicting_direct.yaml deleted file mode 100644 index ff946ffc..00000000 --- a/manifest/v1alpha/test_data/conflicting_direct.yaml +++ /dev/null @@ -1,140 +0,0 @@ -- apiVersion: n9/v1alpha - kind: Direct - metadata: - name: single-conflict - project: default - spec: - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - newRelic: - accountId: 1 - insightsQueryKey: 'NRIQ-hidden' - sourceOf: - - Metrics - status: - directType: NewRelic -- apiVersion: n9/v1alpha - kind: Direct - metadata: - name: single-conflict - project: default - spec: - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - newRelic: - accountId: 2 - insightsQueryKey: 'NRIQ-hidden' - sourceOf: - - Metrics - status: - directType: NewRelic -- apiVersion: n9/v1alpha - kind: Direct - metadata: - name: double-conflict - project: not-so-default - spec: - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - newRelic: - accountId: 3 - insightsQueryKey: 'NRIQ-hidden' - sourceOf: - - Metrics - status: - directType: NewRelic -- apiVersion: n9/v1alpha - kind: Direct - metadata: - name: double-conflict - project: not-so-default - spec: - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - newRelic: - accountId: 4 - insightsQueryKey: 'NRIQ-hidden' - sourceOf: - - Metrics - status: - directType: NewRelic -- apiVersion: n9/v1alpha - kind: Direct - metadata: - name: double-conflict - project: not-so-default - spec: - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - newRelic: - accountId: 5 - insightsQueryKey: 'NRIQ-hidden' - sourceOf: - - Metrics - status: - directType: NewRelic -- apiVersion: n9/v1alpha - kind: Direct - metadata: - name: zero-conflict - project: super-project - spec: - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - newRelic: - accountId: 6 - insightsQueryKey: 'NRIQ-hidden' - sourceOf: - - Metrics - status: - directType: NewRelic -- apiVersion: n9/v1alpha - kind: Direct - metadata: - name: zero-conflict - project: not-so-default - spec: - historicalDataRetrieval: - defaultDuration: - unit: Day - value: 0 - maxDuration: - unit: Day - value: 0 - newRelic: - accountId: 7 - insightsQueryKey: 'NRIQ-hidden' - sourceOf: - - Metrics - status: - directType: NewRelic diff --git a/manifest/v1alpha/test_data/conflicting_project.yaml b/manifest/v1alpha/test_data/conflicting_project.yaml deleted file mode 100644 index 2ca2d77b..00000000 --- a/manifest/v1alpha/test_data/conflicting_project.yaml +++ /dev/null @@ -1,42 +0,0 @@ -- apiVersion: n9/v1alpha - kind: Project - metadata: - name: test-project - spec: - description: "totally conflicting..." -- apiVersion: n9/v1alpha - kind: Project - metadata: - name: test-project - spec: - description: "we're conflicting here!" -- apiVersion: n9/v1alpha - kind: Project - metadata: - name: default - spec: - description: "not a conflict" -- apiVersion: n9/v1alpha - kind: Project - metadata: - name: test - spec: - description: "not a conflict either" -- apiVersion: n9/v1alpha - kind: Project - metadata: - name: conflict - spec: - description: "triple conflict! 1" -- apiVersion: n9/v1alpha - kind: Project - metadata: - name: conflict - spec: - description: "triple conflict! 2" -- apiVersion: n9/v1alpha - kind: Project - metadata: - name: conflict - spec: - description: "triple conflict! 3" diff --git a/manifest/v1alpha/test_data/conflicting_rolebinding.yaml b/manifest/v1alpha/test_data/conflicting_rolebinding.yaml deleted file mode 100644 index 94e2fbdd..00000000 --- a/manifest/v1alpha/test_data/conflicting_rolebinding.yaml +++ /dev/null @@ -1,48 +0,0 @@ -- apiVersion: n9/v1alpha - kind: RoleBinding - metadata: - name: single-conflict - spec: - projectRef: default - roleRef: project-viewer - user: darth-maul -- apiVersion: n9/v1alpha - kind: RoleBinding - metadata: - name: single-conflict - spec: - projectRef: default - roleRef: project-viewer - user: darth-vader -- apiVersion: n9/v1alpha - kind: RoleBinding - metadata: - name: not-a-conflict - spec: - projectRef: default - roleRef: project-viewer - user: darth-sidious -- apiVersion: n9/v1alpha - kind: RoleBinding - metadata: - name: double-conflict - spec: - projectRef: default - roleRef: project-viewer - user: obi-wan-kenobi -- apiVersion: n9/v1alpha - kind: RoleBinding - metadata: - name: double-conflict - spec: - projectRef: default - roleRef: project-viewer - user: luke-skywalker -- apiVersion: n9/v1alpha - kind: RoleBinding - metadata: - name: double-conflict - spec: - projectRef: default - roleRef: project-viewer - user: jar-jar-binks diff --git a/manifest/v1alpha/test_data/conflicting_service.yaml b/manifest/v1alpha/test_data/conflicting_service.yaml deleted file mode 100644 index 0cce5830..00000000 --- a/manifest/v1alpha/test_data/conflicting_service.yaml +++ /dev/null @@ -1,80 +0,0 @@ -- apiVersion: n9/v1alpha - kind: Service - metadata: - displayName: Not a test - name: test-service - project: default - spec: - description: "" - status: - sloCount: 0 -- apiVersion: n9/v1alpha - kind: Service - metadata: - displayName: Test Service - name: test-service - project: default - spec: - description: "" - status: - sloCount: 0 -- apiVersion: n9/v1alpha - kind: Service - metadata: - displayName: More Conflicts - name: test-service - project: default - spec: - description: "" - status: - sloCount: 0 -- apiVersion: n9/v1alpha - kind: Service - metadata: - displayName: Test Service - name: test-service-1 - project: default - spec: - description: "" - status: - sloCount: 0 -- apiVersion: n9/v1alpha - kind: Service - metadata: - displayName: I'm not conflicting - name: test-service - project: different - spec: - description: "" - status: - sloCount: 0 -- apiVersion: n9/v1alpha - kind: Service - metadata: - displayName: I'm not conflicting - name: test-service - project: not-default - spec: - description: "" - status: - sloCount: 0 -- apiVersion: n9/v1alpha - kind: Service - metadata: - displayName: Not a test - name: test-service - project: not-default - spec: - description: "" - status: - sloCount: 0 -- apiVersion: n9/v1alpha - kind: Service - metadata: - displayName: Test Service - name: test-service - project: not-default - spec: - description: "" - status: - sloCount: 0 diff --git a/manifest/v1alpha/test_data/conflicting_slo.yaml b/manifest/v1alpha/test_data/conflicting_slo.yaml deleted file mode 100644 index 9ed01431..00000000 --- a/manifest/v1alpha/test_data/conflicting_slo.yaml +++ /dev/null @@ -1,217 +0,0 @@ -- apiVersion: n9/v1alpha - kind: SLO - metadata: - name: vito - project: corleone - spec: - alertPolicies: [] - budgetingMethod: Occurrences - description: "" - indicator: - metricSource: - kind: Direct - name: datadog-eu - project: default - objectives: - - name: nyc - op: lt - rawMetric: - query: - datadog: - query: test - target: 0.9 - value: 12 - service: test-service-1 - timeWindows: - - count: 28 - isRolling: true - period: - begin: "2022-11-11T12:19:31Z" - end: "2022-12-09T12:19:31Z" - unit: Day -- apiVersion: n9/v1alpha - kind: SLO - metadata: - name: vito - project: corleone - spec: - alertPolicies: [] - budgetingMethod: Occurrences - description: "" - indicator: - metricSource: - kind: Direct - name: datadog-eu - project: default - objectives: - - name: nyc - op: lt - rawMetric: - query: - datadog: - query: test - target: 0.9 - value: 12 - service: test-service-2 - timeWindows: - - count: 28 - isRolling: true - period: - begin: "2022-11-11T12:19:31Z" - end: "2022-12-09T12:19:31Z" - unit: Day -- apiVersion: n9/v1alpha - kind: SLO - metadata: - name: sonny - project: corleone - spec: - alertPolicies: [] - budgetingMethod: Occurrences - description: "" - indicator: - metricSource: - kind: Direct - name: datadog-eu - project: default - objectives: - - name: nyc - op: lt - rawMetric: - query: - datadog: - query: test - target: 0.9 - value: 12 - service: test-service-1 - timeWindows: - - count: 28 - isRolling: true - period: - begin: "2022-11-11T12:19:31Z" - end: "2022-12-09T12:19:31Z" - unit: Day -- apiVersion: n9/v1alpha - kind: SLO - metadata: - name: vito - project: tattaglia - spec: - alertPolicies: [] - budgetingMethod: Occurrences - description: "" - indicator: - metricSource: - kind: Direct - name: datadog-eu - project: default - objectives: - - name: nyc - op: lt - rawMetric: - query: - datadog: - query: test - target: 0.9 - value: 12 - service: test-service-1 - timeWindows: - - count: 28 - isRolling: true - period: - begin: "2022-11-11T12:19:31Z" - end: "2022-12-09T12:19:31Z" - unit: Day -- apiVersion: n9/v1alpha - kind: SLO - metadata: - name: sonny - project: tattaglia - spec: - alertPolicies: [] - budgetingMethod: Occurrences - description: "" - indicator: - metricSource: - kind: Direct - name: datadog-eu - project: default - objectives: - - name: nyc - op: lt - rawMetric: - query: - datadog: - query: test - target: 0.9 - value: 12 - service: test-service-1 - timeWindows: - - count: 28 - isRolling: true - period: - begin: "2022-11-11T12:19:31Z" - end: "2022-12-09T12:19:31Z" - unit: Day -- apiVersion: n9/v1alpha - kind: SLO - metadata: - name: sonny - project: tattaglia - spec: - alertPolicies: [] - budgetingMethod: Occurrences - description: "" - indicator: - metricSource: - kind: Direct - name: datadog-eu - project: default - objectives: - - name: nyc - op: lt - rawMetric: - query: - datadog: - query: test - target: 0.9 - value: 12 - service: test-service-2 - timeWindows: - - count: 28 - isRolling: true - period: - begin: "2022-11-11T12:19:31Z" - end: "2022-12-09T12:19:31Z" - unit: Day -- apiVersion: n9/v1alpha - kind: SLO - metadata: - name: sonny - project: tattaglia - spec: - alertPolicies: [] - budgetingMethod: Occurrences - description: "" - indicator: - metricSource: - kind: Direct - name: datadog-eu - project: default - objectives: - - name: nyc - op: lt - rawMetric: - query: - datadog: - query: test - target: 0.9 - value: 12 - service: test-service-3 - timeWindows: - - count: 28 - isRolling: true - period: - begin: "2022-11-11T12:19:31Z" - end: "2022-12-09T12:19:31Z" - unit: Day diff --git a/manifest/v1alpha/test_data/conflicting_usergroup.yaml b/manifest/v1alpha/test_data/conflicting_usergroup.yaml deleted file mode 100644 index 094689f4..00000000 --- a/manifest/v1alpha/test_data/conflicting_usergroup.yaml +++ /dev/null @@ -1,42 +0,0 @@ -- apiVersion: n9/v1alpha - kind: UserGroup - metadata: - name: single-conflict - spec: - displayName: yellow Practical Steel Watch - members: [] -- apiVersion: n9/v1alpha - kind: UserGroup - metadata: - name: single-conflict - spec: - displayName: violet Durable Iron Computer - members: [] -- apiVersion: n9/v1alpha - kind: UserGroup - metadata: - name: all-good - spec: - displayName: pink Small Concrete Car - members: [] -- apiVersion: n9/v1alpha - kind: UserGroup - metadata: - name: double-conflict - spec: - displayName: something - members: [] -- apiVersion: n9/v1alpha - kind: UserGroup - metadata: - name: double-conflict - spec: - displayName: different - members: [] -- apiVersion: n9/v1alpha - kind: UserGroup - metadata: - name: double-conflict - spec: - displayName: each time - members: [] diff --git a/manifest/v1alpha/test_data/expected_error_conflicting_slo.txt b/manifest/v1alpha/test_data/expected_error_conflicting_slo.txt deleted file mode 100644 index 7a519f98..00000000 --- a/manifest/v1alpha/test_data/expected_error_conflicting_slo.txt +++ /dev/null @@ -1,12 +0,0 @@ -Constraint "SLO.metadata.name has to be unique across a single Project" was violated due to the following conflicts: [{"Project": "corleone", "SLO": "vito"}, {"Project": "tattaglia", "SLO": "sonny"}, {"Project": "tattaglia", "SLO": "sonny"}] -Constraint "Service.metadata.name has to be unique across a single Project" was violated due to the following conflicts: [{"Project": "default", "Service": "test-service"}, {"Project": "default", "Service": "test-service"}, {"Project": "not-default", "Service": "test-service"}, {"Project": "not-default", "Service": "test-service"}] -Constraint "Project.metadata.name has to be unique" was violated due to the following conflicts: ["test-project", "conflict", "conflict"] -Constraint "Agent.metadata.name has to be unique across a single Project" was violated due to the following conflicts: [{"Project": "default", "Agent": "single-conflict"}, {"Project": "not-so-default", "Agent": "double-conflict"}, {"Project": "not-so-default", "Agent": "double-conflict"}] -Constraint "Direct.metadata.name has to be unique across a single Project" was violated due to the following conflicts: [{"Project": "default", "Direct": "single-conflict"}, {"Project": "not-so-default", "Direct": "double-conflict"}, {"Project": "not-so-default", "Direct": "double-conflict"}] -Constraint "AlertMethod.metadata.name has to be unique across a single Project" was violated due to the following conflicts: [{"Project": "default", "AlertMethod": "single-conflict"}, {"Project": "non-default", "AlertMethod": "double-conflict"}, {"Project": "non-default", "AlertMethod": "double-conflict"}] -Constraint "AlertPolicy.metadata.name has to be unique across a single Project" was violated due to the following conflicts: [{"Project": "default", "AlertPolicy": "single-conflict"}, {"Project": "non-default", "AlertPolicy": "double-conflict"}, {"Project": "non-default", "AlertPolicy": "double-conflict"}] -Constraint "AlertSilence.metadata.name has to be unique across a single Project" was violated due to the following conflicts: [{"Project": "default", "AlertSilence": "single-conflict"}, {"Project": "super-project", "AlertSilence": "double-conflict"}, {"Project": "super-project", "AlertSilence": "double-conflict"}] -Constraint "DataExport.metadata.name has to be unique across a single Project" was violated due to the following conflicts: [{"Project": "default", "DataExport": "single-conflict"}, {"Project": "non-default", "DataExport": "double-conflict"}, {"Project": "non-default", "DataExport": "double-conflict"}] -Constraint "RoleBinding.metadata.name has to be unique" was violated due to the following conflicts: ["single-conflict", "double-conflict", "double-conflict"] -Constraint "Annotation.metadata.name has to be unique across a single Project" was violated due to the following conflicts: [{"Project": "default", "Annotation": "single-conflict"}, {"Project": "super-project", "Annotation": "double-conflict"}, {"Project": "super-project", "Annotation": "double-conflict"}] -Constraint "UserGroup.metadata.name has to be unique" was violated due to the following conflicts: ["single-conflict", "double-conflict", "double-conflict"] diff --git a/manifest/v1alpha/test_data/parser/cloudwatch_agent.json b/manifest/v1alpha/test_data/parser/cloudwatch_agent.json new file mode 100644 index 00000000..18493cdf --- /dev/null +++ b/manifest/v1alpha/test_data/parser/cloudwatch_agent.json @@ -0,0 +1,15 @@ +{ + "apiVersion": "n9/v1alpha", + "kind": "Agent", + "metadata": { + "name": "cloudwatch-agent", + "displayName": "cloudWatch-agent", + "project": "default" + }, + "spec": { + "cloudwatch": {}, + "sourceOf": [ + "Metrics" + ] + } +} diff --git a/manifest/v1alpha/test_data/parser/cloudwatch_agent.yaml b/manifest/v1alpha/test_data/parser/cloudwatch_agent.yaml new file mode 100644 index 00000000..72084113 --- /dev/null +++ b/manifest/v1alpha/test_data/parser/cloudwatch_agent.yaml @@ -0,0 +1,10 @@ +apiVersion: n9/v1alpha +kind: Agent +metadata: + name: cloudwatch-agent + displayName: cloudWatch-agent + project: default +spec: + cloudwatch: {} + sourceOf: + - Metrics \ No newline at end of file diff --git a/manifest/v1alpha/test_data/parser/redshift_agent.json b/manifest/v1alpha/test_data/parser/redshift_agent.json new file mode 100644 index 00000000..aab3e54a --- /dev/null +++ b/manifest/v1alpha/test_data/parser/redshift_agent.json @@ -0,0 +1,15 @@ +{ + "apiVersion": "n9/v1alpha", + "kind": "Agent", + "metadata": { + "name": "redshift-agent", + "displayName": "redshift-agent", + "project": "default" + }, + "spec": { + "cloudwatch": {}, + "sourceOf": [ + "Metrics" + ] + } +} diff --git a/manifest/v1alpha/test_data/parser/redshift_agent.yaml b/manifest/v1alpha/test_data/parser/redshift_agent.yaml new file mode 100644 index 00000000..b66b00dc --- /dev/null +++ b/manifest/v1alpha/test_data/parser/redshift_agent.yaml @@ -0,0 +1,10 @@ +apiVersion: n9/v1alpha +kind: Agent +metadata: + name: redshift-agent + displayName: redshift-agent + project: default +spec: + cloudwatch: {} + sourceOf: + - Metrics \ No newline at end of file diff --git a/manifest/v1alpha/ts.go b/manifest/v1alpha/ts.go index 02b97a46..ca12e534 100644 --- a/manifest/v1alpha/ts.go +++ b/manifest/v1alpha/ts.go @@ -1,14 +1,10 @@ // Package v1alpha represents objects available in API n9/v1alpha package v1alpha -import ( - "github.com/nobl9/nobl9-go/manifest" -) - // Time Series type SLOTimeSeries struct { - manifest.MetadataHolder + MetadataHolder TimeWindows []TimeWindowTimeSeries `json:"timewindows,omitempty"` Status *SLOStatus `json:"status,omitempty"` } @@ -108,7 +104,7 @@ type DownsamplingConfig struct { // SLO History Report type SLOHistoryReport struct { - manifest.MetadataHolder + MetadataHolder TimeWindows []TimeWindowHistoryReport `json:"timewindows,omitempty"` } diff --git a/manifest/v1alpha/user_groups.go b/manifest/v1alpha/user_groups.go index 33370bc5..265122d0 100644 --- a/manifest/v1alpha/user_groups.go +++ b/manifest/v1alpha/user_groups.go @@ -1,31 +1,17 @@ package v1alpha -import ( - "encoding/json" +import "github.com/nobl9/nobl9-go/manifest" - "github.com/nobl9/nobl9-go/manifest" -) - -type UserGroupsSlice []UserGroup - -func (u UserGroupsSlice) Clone() UserGroupsSlice { - clone := make([]UserGroup, len(u)) - copy(clone, u) - return clone -} +//go:generate go run ../../scripts/generate-object-impl.go UserGroup type UserGroup struct { - manifest.ObjectInternal - APIVersion string `json:"apiVersion" validate:"required" example:"n9/v1alpha"` - Kind manifest.Kind `json:"kind" validate:"required" example:"kind"` - Metadata GroupMetadata `json:"metadata"` - Spec UserGroupSpec `json:"spec"` -} + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata UserGroupMetadata `json:"metadata"` + Spec UserGroupSpec `json:"spec"` -// getUniqueIdentifiers returns uniqueIdentifiers used to check -// potential conflicts between simultaneously applied objects. -func (u UserGroup) getUniqueIdentifiers() uniqueIdentifiers { - return uniqueIdentifiers{Name: u.Metadata.Name} + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` } // UserGroupSpec represents content of UserGroup's Spec @@ -38,29 +24,6 @@ type Member struct { ID string `json:"id"` } -type GroupMetadata struct { +type UserGroupMetadata struct { Name string `json:"name" validate:"required,objectName" example:"name"` } - -// genericToUserGroup converts ObjectGeneric to UserGroup object -func genericToUserGroup(o manifest.ObjectGeneric) (UserGroup, error) { - res := UserGroup{ - APIVersion: o.ObjectHeader.APIVersion, - Kind: o.ObjectHeader.Kind, - Metadata: GroupMetadata{ - Name: o.Metadata.Name, - }, - ObjectInternal: manifest.ObjectInternal{ - Organization: o.ObjectHeader.Organization, - ManifestSrc: o.ObjectHeader.ManifestSrc, - }, - } - var resSpec UserGroupSpec - if err := json.Unmarshal(o.Spec, &resSpec); err != nil { - err = manifest.EnhanceError(o, err) - return res, err - } - res.Spec = resSpec - - return res, nil -} diff --git a/manifest/v1alpha/user_groups_object.go b/manifest/v1alpha/user_groups_object.go new file mode 100644 index 00000000..444daf94 --- /dev/null +++ b/manifest/v1alpha/user_groups_object.go @@ -0,0 +1,43 @@ +// Code generated by "generate-object-impl UserGroup"; DO NOT EDIT. + +package v1alpha + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = UserGroup{} +var _ ObjectContext = UserGroup{} + +func (u UserGroup) GetVersion() string { + return u.APIVersion +} + +func (u UserGroup) GetKind() manifest.Kind { + return u.Kind +} + +func (u UserGroup) GetName() string { + return u.Metadata.Name +} + +func (u UserGroup) Validate() error { + return validator.Check(u) +} + +func (u UserGroup) GetOrganization() string { + return u.Organization +} + +func (u UserGroup) SetOrganization(org string) manifest.Object { + u.Organization = org + return u +} + +func (u UserGroup) GetManifestSource() string { + return u.ManifestSource +} + +func (u UserGroup) SetManifestSource(src string) manifest.Object { + u.ManifestSource = src + return u +} diff --git a/manifest/v1alpha/validator.go b/manifest/v1alpha/validator.go index fc8d4133..70b2f612 100644 --- a/manifest/v1alpha/validator.go +++ b/manifest/v1alpha/validator.go @@ -75,6 +75,9 @@ const ( PingdomTypeTransaction = "transaction" ) +// HiddenValue can be used as a value of a secret field and is ignored during saving +const HiddenValue = "[hidden]" + //nolint:golint var ( ErrAgentTypeChanged = fmt.Errorf("cannot change agent type") @@ -109,11 +112,13 @@ type Validate struct { validate *v.Validate } -// Validator provides an abstraction on validator which is used for checking specific struct -type validator interface { - Check(interface{}) error +// Check performs validation, it accepts all possible structs and perform checks based on tags for structs fields +func (val *Validate) Check(s interface{}) error { + return val.validate.Struct(s) } +var validator = NewValidator() + // NewValidator returns an instance of preconfigured Validator for all available objects func NewValidator() *Validate { val := v.New() @@ -693,7 +698,18 @@ func isBadOverTotalEnabledForDataSource(spec SLOSpec) bool { } func hasOnlyOneRawMetricDefinitionTypeOrNone(spec SLOSpec) bool { - return spec.ObjectivesRawMetricsCount() == 0 || !spec.containsIndicatorRawMetric() + indicatorHasRawMetric := spec.containsIndicatorRawMetric() + if indicatorHasRawMetric { + for _, threshold := range spec.Thresholds { + if !threshold.HasRawMetricQuery() { + continue + } + if !reflect.DeepEqual(threshold.RawMetric.MetricQuery, spec.Indicator.RawMetric) { + return false + } + } + } + return true } func areRawMetricsSetForAllThresholdsOrNone(spec SLOSpec) bool { @@ -1597,12 +1613,12 @@ func validateURLDynatrace(validateURL string) bool { } func areLabelsValid(fl v.FieldLevel) bool { - labels := fl.Field().Interface().(manifest.Labels) + labels := fl.Field().Interface().(Labels) return validateLabels(labels) } -func validateLabels(labels manifest.Labels) bool { +func validateLabels(labels Labels) bool { for key, values := range labels { if !validateLabelKey(key) { return false @@ -2009,11 +2025,6 @@ func isValidReleaseChannel(releaseChannel ReleaseChannel) bool { return releaseChannel.IsValid() && releaseChannel != ReleaseChannelAlpha } -// Check performs validation, it accepts all possible structs and perform checks based on tags for structs fields -func (val *Validate) Check(s interface{}) error { - return val.validate.Struct(s) -} - func isBudgetingMethod(fl v.FieldLevel) bool { _, err := ParseBudgetingMethod(fl.Field().String()) return err == nil @@ -2237,12 +2248,12 @@ func isValidTimeSliceTargetValue(tsv float64) bool { func isValidObjectNameWithStringInterpolation(fl v.FieldLevel) bool { toCheck := fl.Field().String() - if !strings.Contains(toCheck, manifest.StringInterpolationPlaceholder) { + if !strings.Contains(toCheck, StringInterpolationPlaceholder) { return false } // During actual interpolation {} will be replaced with previous validated name, // replace here with test because valid DNS1123Label cannot contain {} and check - toCheck = manifest.StringInterpolation(toCheck, "test") + toCheck = StringInterpolation(toCheck, "test") return len(IsDNS1123Label(toCheck)) == 0 } diff --git a/manifest/v1alpha/validator_test.go b/manifest/v1alpha/validator_test.go index d7ee985a..64bc6c82 100644 --- a/manifest/v1alpha/validator_test.go +++ b/manifest/v1alpha/validator_test.go @@ -8,8 +8,6 @@ import ( v "github.com/go-playground/validator/v10" "github.com/stretchr/testify/assert" "golang.org/x/exp/slices" - - "github.com/nobl9/nobl9-go/manifest" ) func TestValidateURLDynatrace(t *testing.T) { @@ -74,7 +72,7 @@ func TestValidateURLDynatrace(t *testing.T) { func TestValidateLabels(t *testing.T) { testCases := []struct { desc string - labels manifest.Labels + labels Labels isValid bool }{ { diff --git a/manifest/version.go b/manifest/version.go new file mode 100644 index 00000000..3659ccdc --- /dev/null +++ b/manifest/version.go @@ -0,0 +1,7 @@ +package manifest + +//go:generate ../bin/go-enum --names --values --marshal + +// Version represents the specific version of the manifest. +// ENUM(v1alpha = n9/v1alpha) +type Version string diff --git a/manifest/version_enum.go b/manifest/version_enum.go new file mode 100644 index 00000000..14286767 --- /dev/null +++ b/manifest/version_enum.go @@ -0,0 +1,76 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: 0.5.6 +// Revision: 97611fddaa414f53713597918c5e954646cb8623 +// Build Date: 2023-03-26T21:38:06Z +// Built By: goreleaser + +package manifest + +import ( + "fmt" + "strings" +) + +const ( + // VersionV1alpha is a Version of type v1alpha. + VersionV1alpha Version = "n9/v1alpha" +) + +var ErrInvalidVersion = fmt.Errorf("not a valid Version, try [%s]", strings.Join(_VersionNames, ", ")) + +var _VersionNames = []string{ + string(VersionV1alpha), +} + +// VersionNames returns a list of possible string values of Version. +func VersionNames() []string { + tmp := make([]string, len(_VersionNames)) + copy(tmp, _VersionNames) + return tmp +} + +// VersionValues returns a list of the values for Version +func VersionValues() []Version { + return []Version{ + VersionV1alpha, + } +} + +// String implements the Stringer interface. +func (x Version) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x Version) IsValid() bool { + _, err := ParseVersion(string(x)) + return err == nil +} + +var _VersionValue = map[string]Version{ + "n9/v1alpha": VersionV1alpha, +} + +// ParseVersion attempts to convert a string to a Version. +func ParseVersion(name string) (Version, error) { + if x, ok := _VersionValue[name]; ok { + return x, nil + } + return Version(""), fmt.Errorf("%s is %w", name, ErrInvalidVersion) +} + +// MarshalText implements the text marshaller method. +func (x Version) MarshalText() ([]byte, error) { + return []byte(string(x)), nil +} + +// UnmarshalText implements the text unmarshaller method. +func (x *Version) UnmarshalText(text []byte) error { + tmp, err := ParseVersion(string(text)) + if err != nil { + return err + } + *x = tmp + return nil +} diff --git a/scripts/generate-object-impl.go b/scripts/generate-object-impl.go new file mode 100644 index 00000000..969bc780 --- /dev/null +++ b/scripts/generate-object-impl.go @@ -0,0 +1,166 @@ +package main + +import ( + "bytes" + _ "embed" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "html/template" + "os" + "path/filepath" + "strings" +) + +//go:embed generate-object-impl.tpl +var templateStr string + +type generator struct { + StructName string + Receiver string + IsProjectScopedObject bool + IsV1alphaObjectContext bool +} + +type Template struct { + Receiver string + StructName string + Package string + ProgramInvocation string + + GenerateObject bool + GenerateProjectScopedObject bool + GenerateV1alphaObjectContext bool +} + +func main() { + if len(os.Args) != 2 { + errFatal("you must provide struct name") + } + g := &generator{StructName: os.Args[1]} + + filename := os.Getenv("GOFILE") + pkg := os.Getenv("GOPACKAGE") + programName := filepath.Base(os.Args[0]) + fmt.Printf("%s [Struct: %s, File: %s, Package: %s]\n", programName, g.StructName, filename, pkg) + + cwd, err := os.Getwd() + if err != nil { + errFatal(err.Error()) + } + + fst := token.NewFileSet() + + af, err := parser.ParseFile(fst, filepath.Join(cwd, filename), nil, 0) + if err != nil { + errFatal(err.Error()) + } + + ast.Inspect(af, g.genDecl) + + tpl, err := template.New("generator").Parse(templateStr) + if err != nil { + errFatal(err.Error()) + } + buf := new(bytes.Buffer) + err = tpl.Execute(buf, Template{ + Receiver: string(strings.ToLower(g.StructName)[0]), + StructName: g.StructName, + Package: pkg, + ProgramInvocation: fmt.Sprintf("%s %s", programName, strings.Join(os.Args[1:], " ")), + GenerateObject: true, + GenerateProjectScopedObject: g.IsProjectScopedObject, + GenerateV1alphaObjectContext: g.IsV1alphaObjectContext && strings.Contains(cwd, "v1alpha"), + }) + if err != nil { + errFatal(err.Error()) + } + formatted, err := format.Source(buf.Bytes()) + if err != nil { + errFatal(err.Error()) + } + outputName := filepath.Join(cwd, fmt.Sprintf("%s_object.go", strings.TrimSuffix(filename, ".go"))) + if err = os.WriteFile(outputName, formatted, 0600); err != nil { + errFatal(err.Error()) + } +} + +func (g *generator) genDecl(node ast.Node) bool { + decl, ok := node.(*ast.GenDecl) + if !ok || decl.Tok != token.TYPE { + // We only care about type declarations. + return true + } + if len(decl.Specs) != 1 { + return false + } + spec, ok := decl.Specs[0].(*ast.TypeSpec) + if !ok { + return false + } + structType, isStruct := spec.Type.(*ast.StructType) + if !isStruct || spec.Name.Name != g.StructName { + return false + } + g.IsProjectScopedObject = g.hasProjectInMetadata(structType.Fields) + g.IsV1alphaObjectContext = g.hasOrganizationAndManifestSource(structType.Fields) + return false +} + +func (g *generator) hasProjectInMetadata(fields *ast.FieldList) bool { + hasProjectRef := false + for _, field := range fields.List { + if len(field.Names) == 0 { + continue + } + if field.Names[0].Name != "Metadata" { + continue + } + metadata, ok := field.Type.(*ast.Ident). + Obj.Decl.(*ast.TypeSpec). + Type.(*ast.StructType) + if !ok { + continue + } + for _, mf := range metadata.Fields.List { + if len(mf.Names) == 0 { + continue + } + if mf.Names[0].Name == "Project" { + hasProjectRef = true + break + } + } + } + return hasProjectRef +} + +func (g *generator) hasOrganizationAndManifestSource(fields *ast.FieldList) bool { + hasOrganization := false + hasManifestSource := false + for _, field := range fields.List { + if len(field.Names) == 0 { + continue + } + if field.Names[0].Name == "Organization" { + hasOrganization = true + continue + } + if field.Names[0].Name == "ManifestSource" { + hasManifestSource = true + continue + } + } + return hasOrganization && hasManifestSource +} + +func errFatal(f string, a ...interface{}) { + if len(a) == 0 { + fmt.Fprintln(os.Stderr, f) + } else { + fmt.Fprintf(os.Stderr, f+"\n", a...) + } + os.Exit(1) +} diff --git a/scripts/generate-object-impl.tpl b/scripts/generate-object-impl.tpl new file mode 100644 index 00000000..bd4fe0a1 --- /dev/null +++ b/scripts/generate-object-impl.tpl @@ -0,0 +1,66 @@ +// Code generated by "{{ .ProgramInvocation }}"; DO NOT EDIT. + +package {{ .Package }} + +import "github.com/nobl9/nobl9-go/manifest" + +// Ensure interfaces are implemented. +var _ manifest.Object = {{ .StructName }}{} +{{- if .GenerateProjectScopedObject }} +var _ manifest.ProjectScopedObject = {{ .StructName }}{} +{{- end }} +{{- if .GenerateV1alphaObjectContext }} +var _ ObjectContext = {{ .StructName }}{} +{{- end }} + +{{- if .GenerateObject }} + +func ({{ .Receiver }} {{ .StructName }}) GetVersion() string { + return {{ .Receiver }}.APIVersion +} + +func ({{ .Receiver }} {{ .StructName }}) GetKind() manifest.Kind { + return {{ .Receiver }}.Kind +} + +func ({{ .Receiver }} {{ .StructName }}) GetName() string { + return {{ .Receiver }}.Metadata.Name +} + +func ({{ .Receiver }} {{ .StructName }}) Validate() error { + return validator.Check({{ .Receiver }}) +} +{{- end }} + +{{- if .GenerateProjectScopedObject }} + +func ({{ .Receiver }} {{ .StructName }}) GetProject() string { + return {{ .Receiver }}.Metadata.Project +} + +func ({{ .Receiver }} {{ .StructName }}) SetProject(project string) manifest.Object { + {{ .Receiver }}.Metadata.Project = project + return {{ .Receiver }} +} +{{- end }} + +{{- if .GenerateV1alphaObjectContext }} + +func ({{ .Receiver }} {{ .StructName }}) GetOrganization() string { + return {{ .Receiver }}.Organization +} + +func ({{ .Receiver }} {{ .StructName }}) SetOrganization(org string) manifest.Object { + {{ .Receiver }}.Organization = org + return {{ .Receiver }} +} + +func ({{ .Receiver }} {{ .StructName }}) GetManifestSource() string { + return {{ .Receiver }}.ManifestSource +} + +func ({{ .Receiver }} {{ .StructName }}) SetManifestSource(src string) manifest.Object { + {{ .Receiver }}.ManifestSource = src + return {{ .Receiver }} +} +{{- end }} diff --git a/sdk/client.go b/sdk/client.go index 276ea9d6..1502a924 100644 --- a/sdk/client.go +++ b/sdk/client.go @@ -18,6 +18,8 @@ import ( "github.com/pkg/errors" "github.com/nobl9/nobl9-go/manifest" + "github.com/nobl9/nobl9-go/manifest/v1alpha" + "github.com/nobl9/nobl9-go/sdk/definitions" "github.com/nobl9/nobl9-go/sdk/retryhttp" ) @@ -68,7 +70,7 @@ const ( ) type Response struct { - Objects []AnyJSONObj + Objects []manifest.Object TruncatedMax int } @@ -78,9 +80,6 @@ type M2MAppCredentials struct { ClientSecret string `json:"client_secret"` } -// AnyJSONObj can store a generic representation on any valid JSON. -type AnyJSONObj = map[string]interface{} - // Client represents API high level client. type Client struct { HTTP *http.Client @@ -171,7 +170,6 @@ const ( apiApply = "apply" apiDelete = "delete" apiGet = "get" - apiInputData = "input/data" apiGetGroups = "/usrmgmt/groups" ) @@ -183,7 +181,7 @@ func (c *Client) GetObjects( kind manifest.Kind, filterLabel map[string][]string, names ...string, -) ([]AnyJSONObj, error) { +) ([]manifest.Object, error) { q := url.Values{} if len(names) > 0 { q[QueryKeyName] = names @@ -218,11 +216,10 @@ func (c *Client) GetObjectsWithParams( switch { case resp.StatusCode == http.StatusOK: - content, err := decodeJSONResponse(resp.Body) - if err != nil { + response.Objects, err = definitions.ReadSources(ctx, definitions.NewReaderSource(resp.Body, "")) + if err != nil && !errors.Is(err, definitions.ErrNoDefinitionsFound) { return response, fmt.Errorf("cannot decode response from API: %w", err) } - response.Objects = content if _, exists := resp.Header[HeaderTruncatedLimitMax]; !exists { return response, nil } @@ -275,20 +272,30 @@ func (c *Client) prepareFilterLabelsString(filterLabel map[string][]string) stri } // ApplyObjects applies (create or update) list of objects passed as argument via API. -func (c *Client) ApplyObjects(ctx context.Context, objects []AnyJSONObj, dryRun bool) error { +func (c *Client) ApplyObjects(ctx context.Context, objects []manifest.Object, dryRun bool) error { return c.applyOrDeleteObjects(ctx, objects, apiApply, dryRun) } // DeleteObjects deletes list of objects passed as argument via API. -func (c *Client) DeleteObjects(ctx context.Context, objects []AnyJSONObj, dryRun bool) error { +func (c *Client) DeleteObjects(ctx context.Context, objects []manifest.Object, dryRun bool) error { return c.applyOrDeleteObjects(ctx, objects, apiDelete, dryRun) } // applyOrDeleteObjects applies or deletes list of objects // depending on apiMode parameter. -func (c *Client) applyOrDeleteObjects(ctx context.Context, objects []AnyJSONObj, apiMode string, dryRun bool) error { +func (c *Client) applyOrDeleteObjects( + ctx context.Context, + objects []manifest.Object, + apiMode string, + dryRun bool, +) error { + var err error + objects, err = c.setOrganizationForObjects(ctx, objects) + if err != nil { + return err + } buf := new(bytes.Buffer) - if err := json.NewEncoder(buf).Encode(objects); err != nil { + if err = json.NewEncoder(buf).Encode(objects); err != nil { return fmt.Errorf("cannot marshal: %w", err) } @@ -326,6 +333,21 @@ func (c *Client) applyOrDeleteObjects(ctx context.Context, objects []AnyJSONObj, } } +func (c *Client) setOrganizationForObjects(ctx context.Context, objects []manifest.Object) ([]manifest.Object, error) { + // Make sure the organization is available. + if err := c.preRequestOnce(ctx); err != nil { + return nil, err + } + for i := range objects { + objCtx, ok := objects[i].(v1alpha.ObjectContext) + if !ok { + continue + } + objects[i] = objCtx.SetOrganization(c.Credentials.Organization) + } + return objects, nil +} + func (c *Client) GetAWSExternalID(ctx context.Context, project string) (string, error) { req, err := c.createRequest(ctx, http.MethodGet, "/get/dataexport/aws-external-id", project, nil, nil) if err != nil { @@ -476,13 +498,3 @@ func getResponseServerError(resp *http.Response) error { } return fmt.Errorf(msg) } - -// decodeJSONResponse assumes that passed body is an array of JSON objects. -func decodeJSONResponse(r io.Reader) ([]AnyJSONObj, error) { - dec := json.NewDecoder(r) - var parsed []AnyJSONObj - if err := dec.Decode(&parsed); err != nil { - return nil, err - } - return parsed, nil -} diff --git a/sdk/client_test.go b/sdk/client_test.go index f0771fde..60985100 100644 --- a/sdk/client_test.go +++ b/sdk/client_test.go @@ -18,24 +18,26 @@ import ( "github.com/stretchr/testify/require" "github.com/nobl9/nobl9-go/manifest" + "github.com/nobl9/nobl9-go/manifest/v1alpha" + "github.com/nobl9/nobl9-go/sdk/definitions" ) func TestClient_GetObjects(t *testing.T) { - responsePayload := []AnyJSONObj{ - { - "apiVersion": "v1alpha", - "kind": "Service", - "metadata": map[string]interface{}{ - "name": "service1", - "project": "default", + responsePayload := []manifest.Object{ + v1alpha.Service{ + APIVersion: v1alpha.APIVersion, + Kind: manifest.KindService, + Metadata: v1alpha.ServiceMetadata{ + Name: "service1", + Project: "default", }, }, - { - "apiVersion": "v1alpha", - "kind": "Service", - "metadata": map[string]interface{}{ - "name": "service2", - "project": "default", + v1alpha.Service{ + APIVersion: v1alpha.APIVersion, + Kind: manifest.KindService, + Metadata: v1alpha.ServiceMetadata{ + Name: "service2", + Project: "default", }, }, } @@ -75,13 +77,51 @@ func TestClient_GetObjects(t *testing.T) { assert.Equal(t, responsePayload, objects) } -func TestClient_GetObjects_GroupsEndpoint(t *testing.T) { +func TestClient_GetObjects_NoObjectsInResponse(t *testing.T) { + responsePayload := make([]manifest.Object, 0) + + client, srv := prepareTestClient(t, endpointConfig{ + // Define endpoint response. + Path: "api/get/service", + ResponseFunc: func(t *testing.T, w http.ResponseWriter) { + require.NoError(t, json.NewEncoder(w).Encode(responsePayload)) + }, + }) + + // Start and close the test server. + srv.Start() + defer srv.Close() + + // Run the API method. + objects, err := client.GetObjects( + context.Background(), + ProjectsWildcard, + manifest.KindService, + nil, + "service1", + ) + // Verify response handling. + require.NoError(t, err) + require.Len(t, objects, 0) +} + +func TestClient_GetObjects_UserGroupsEndpoint(t *testing.T) { + responsePayload := []manifest.Object{ + v1alpha.UserGroup{ + APIVersion: v1alpha.APIVersion, + Kind: manifest.KindService, + Metadata: v1alpha.UserGroupMetadata{ + Name: "service1", + }, + }, + } + calledTimes := 0 client, srv := prepareTestClient(t, endpointConfig{ // Define endpoint response. Path: "api/usrmgmt/groups", ResponseFunc: func(t *testing.T, w http.ResponseWriter) { - require.NoError(t, json.NewEncoder(w).Encode([]AnyJSONObj{})) + require.NoError(t, json.NewEncoder(w).Encode(responsePayload)) }, // Verify request parameters. TestRequestFunc: func(t *testing.T, r *http.Request) { @@ -101,16 +141,17 @@ func TestClient_GetObjects_GroupsEndpoint(t *testing.T) { } func TestClient_ApplyObjects(t *testing.T) { - requestPayload := []AnyJSONObj{ - { - "apiVersion": "v1alpha", - "kind": "Service", - "metadata": map[string]interface{}{ - "name": "service1", - "project": "default", + requestPayload := []manifest.Object{ + v1alpha.Service{ + APIVersion: v1alpha.APIVersion, + Kind: manifest.KindService, + Metadata: v1alpha.ServiceMetadata{ + Name: "service1", + Project: "default", }, }, } + expected := addOrganization(requestPayload, "my-org") client, srv := prepareTestClient(t, endpointConfig{ // Define endpoint response. @@ -123,9 +164,9 @@ func TestClient_ApplyObjects(t *testing.T) { assert.Equal(t, http.MethodPut, r.Method) assert.Equal(t, "", r.Header.Get(HeaderProject)) assert.Equal(t, url.Values{QueryKeyDryRun: {"true"}}, r.URL.Query()) - var objects []AnyJSONObj - require.NoError(t, json.NewDecoder(r.Body).Decode(&objects)) - assert.Equal(t, requestPayload, objects) + objects, err := definitions.ReadSources(context.Background(), definitions.NewReaderSource(r.Body, "")) + require.NoError(t, err) + assert.Equal(t, expected, objects) }, }) @@ -140,16 +181,17 @@ func TestClient_ApplyObjects(t *testing.T) { } func TestClient_DeleteObjects(t *testing.T) { - requestPayload := []AnyJSONObj{ - { - "apiVersion": "v1alpha", - "kind": "Service", - "metadata": map[string]interface{}{ - "name": "service1", - "project": "default", + requestPayload := []manifest.Object{ + v1alpha.Service{ + APIVersion: v1alpha.APIVersion, + Kind: manifest.KindService, + Metadata: v1alpha.ServiceMetadata{ + Name: "service1", + Project: "default", }, }, } + expected := addOrganization(requestPayload, "my-org") client, srv := prepareTestClient(t, endpointConfig{ // Define endpoint response. @@ -162,6 +204,9 @@ func TestClient_DeleteObjects(t *testing.T) { assert.Equal(t, http.MethodDelete, r.Method) assert.Equal(t, "", r.Header.Get(HeaderProject)) assert.Equal(t, url.Values{QueryKeyDryRun: {"true"}}, r.URL.Query()) + objects, err := definitions.ReadSources(context.Background(), definitions.NewReaderSource(r.Body, "")) + require.NoError(t, err) + assert.Equal(t, expected, objects) }, }) @@ -276,6 +321,16 @@ type endpointConfig struct { TestRequestFunc func(*testing.T, *http.Request) } +func addOrganization(objects []manifest.Object, org string) []manifest.Object { + result := make([]manifest.Object, 0, len(objects)) + for _, obj := range objects { + if objCtx, ok := obj.(v1alpha.ObjectContext); ok { + result = append(result, objCtx.SetOrganization(org)) + } + } + return result +} + func prepareTestClient(t *testing.T, endpoint endpointConfig) (client *Client, srv *httptest.Server) { t.Helper() urlScheme = "http" @@ -342,7 +397,9 @@ func prepareTestClient(t *testing.T, endpoint endpointConfig) (client *Client, s assert.Equal(t, userAgent, r.Header.Get(HeaderUserAgent)) assert.Equal(t, "Bearer "+token, r.Header.Get(HeaderAuthorization)) // Endpoint specific tests. - endpoint.TestRequestFunc(t, r) + if endpoint.TestRequestFunc != nil { + endpoint.TestRequestFunc(t, r) + } // Record response. endpoint.ResponseFunc(t, w) default: diff --git a/sdk/definitions/metadata.go b/sdk/definitions/metadata.go deleted file mode 100644 index 637fe260..00000000 --- a/sdk/definitions/metadata.go +++ /dev/null @@ -1,52 +0,0 @@ -package definitions - -import ( - "fmt" - - "github.com/nobl9/nobl9-go/manifest" - "github.com/nobl9/nobl9-go/sdk" -) - -// MetadataAnnotations defines a set of annotations appended to applied objects definitions. -// These annotations are only set if the resource definition does not contain them already. -type MetadataAnnotations struct { - Organization string - Project string - // When using Read or ReadSources this field is set by these functions, - // anything here will provided here will be overwritten. - ManifestSource string -} - -// AnnotateObject annotates an sdk.Kind with additional metadata. -// If objects does not contain project - default value is added. -// If value 'metadata.project' in the definition is different from -// the Project provided in MetadataAnnotations, an error is returned. -func (ma MetadataAnnotations) AnnotateObject(object sdk.AnyJSONObj) (sdk.AnyJSONObj, error) { - if object["organization"] == nil && ma.Organization != "" { - object["organization"] = ma.Organization - } - if object["manifestSrc"] == nil && ma.ManifestSource != "" { - object["manifestSrc"] = ma.ManifestSource - } - meta, ok := object["metadata"].(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("cannot retrieve metadata section") - } - kindStr, ok := object["kind"].(string) - if !ok { - return nil, fmt.Errorf("cannot retrieve object kind") - } - kind, err := manifest.ParseKind(kindStr) - if err != nil { - return nil, err - } - switch kind { - case manifest.KindProject, manifest.KindRoleBinding, manifest.KindUserGroup: - // Do not append the project name. - default: - if meta["project"] == nil && ma.Project != "" { - meta["project"] = ma.Project - } - } - return object, nil -} diff --git a/sdk/definitions/metadata_test.go b/sdk/definitions/metadata_test.go deleted file mode 100644 index 4483cbd3..00000000 --- a/sdk/definitions/metadata_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package definitions - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/nobl9/nobl9-go/sdk" -) - -func TestMetadataAnnotations_AnnotateObject(t *testing.T) { - t.Run("fill missing fields", func(t *testing.T) { - result, err := MetadataAnnotations{ - Organization: "my-org", - Project: "default", - ManifestSource: "my-source", - }.AnnotateObject(sdk.AnyJSONObj{ - "kind": "SLO", - "metadata": map[string]interface{}{}, - }) - require.NoError(t, err) - expected := sdk.AnyJSONObj{ - "kind": "SLO", - "organization": "my-org", - "manifestSrc": "my-source", - "metadata": map[string]interface{}{"project": "default"}, - } - assert.Equal(t, expected, result) - }) - - t.Run("don't fill fields if annotations are not provided", func(t *testing.T) { - result, err := MetadataAnnotations{ - Organization: "", - Project: "", - ManifestSource: "", - }.AnnotateObject(sdk.AnyJSONObj{ - "kind": "SLO", - "metadata": map[string]interface{}{}, - }) - require.NoError(t, err) - expected := sdk.AnyJSONObj{ - "kind": "SLO", - "metadata": map[string]interface{}{}, - } - assert.Equal(t, expected, result) - }) - - t.Run("don't fill fields if they are set already", func(t *testing.T) { - result, err := MetadataAnnotations{ - Organization: "different-org", - Project: "non-default", - ManifestSource: "other-source", - }.AnnotateObject(sdk.AnyJSONObj{ - "kind": "SLO", - "organization": "my-org", - "manifestSrc": "my-source", - "metadata": map[string]interface{}{"project": "default"}, - }) - require.NoError(t, err) - expected := sdk.AnyJSONObj{ - "kind": "SLO", - "manifestSrc": "my-source", - "organization": "my-org", - "metadata": map[string]interface{}{"project": "default"}, - } - assert.Equal(t, expected, result) - }) -} diff --git a/sdk/definitions/parser.go b/sdk/definitions/parser.go index b8d2b397..4676948b 100644 --- a/sdk/definitions/parser.go +++ b/sdk/definitions/parser.go @@ -1,75 +1,249 @@ package definitions import ( + "bufio" "bytes" + "encoding/json" "fmt" - "io" + "regexp" + "github.com/goccy/go-yaml" "github.com/pkg/errors" - "k8s.io/apimachinery/pkg/util/yaml" - "github.com/nobl9/nobl9-go/sdk" + "github.com/nobl9/nobl9-go/manifest" + "github.com/nobl9/nobl9-go/manifest/v1alpha" ) -var ( - errNoDefinitionsInInput = errors.New("no definitions in input") - errMalformedInput = errors.New("malformed input") -) +var ErrNoDefinitionsFound = errors.New("no definitions in input") + +// Decode reads objects from the provided bytes slice. +// It detects if the input is in JSON (manifest.RawObjectFormatJSON) or YAML (manifest.RawObjectFormatYAML format. +func Decode(data []byte) ([]manifest.Object, error) { + if isJSONBuffer(data) { + return decodeJSON(data) + } + return decodeYAML(data) +} + +// DecodeSingle returns a single, concrete object implementing manifest.Object. +// It expects exactly one object in the decoded byte slice. +func DecodeSingle[T manifest.Object](data []byte) (object T, err error) { + objects, err := Decode(data) + if err != nil { + return object, err + } + if len(objects) != 1 { + return object, fmt.Errorf("unexpected number of objects: %d, expected exactly one", len(objects)) + } + var isOfType bool + object, isOfType = objects[0].(T) + if !isOfType { + return object, fmt.Errorf("object of type %T is not of type %T", objects[0], *new(T)) + } + return object, nil +} -// processRawDefinitionsToJSONArray function converts raw definitions to JSON array. -func processRawDefinitionsToJSONArray(a MetadataAnnotations, rds rawDefinitions) ([]sdk.AnyJSONObj, error) { - jsonArray := make([]sdk.AnyJSONObj, 0, len(rds)) +// processRawDefinitions function converts raw definitions to a slice of manifest.Object. +func processRawDefinitions(rds rawDefinitions) ([]manifest.Object, error) { + result := make([]manifest.Object, 0, len(rds)) for _, rd := range rds { - a.ManifestSource = rd.ResolvedSource - defsInJSON, err := decodeYAMLToJSON(rd.Definition) + objects, err := Decode(rd.Definition) if err != nil { return nil, fmt.Errorf("%s: %w", rd.ResolvedSource, err) } - for _, defInJSON := range defsInJSON { - annotated, err := a.AnnotateObject(defInJSON) - if err != nil { - return nil, err + for _, obj := range objects { + if obj == nil { + continue } - jsonArray = append(jsonArray, annotated) + result = append(result, annotateWithManifestSource(obj, rd.ResolvedSource)) } } - return jsonArray, nil + return result, nil } -func decodeYAMLToJSON(content []byte) ([]sdk.AnyJSONObj, error) { - s := yaml.NewYAMLToJSONDecoder(bytes.NewReader(content)) - var jsonArray []sdk.AnyJSONObj - for { - var rawData interface{} - if err := s.Decode(&rawData); err != nil { - if err == io.EOF { - break +// annotateWithManifestSource annotates manifest.Object with the manifest definition source. +func annotateWithManifestSource(object manifest.Object, source string) manifest.Object { + switch object.GetVersion() { + case "n9/v1alpha": + if v, ok := object.(v1alpha.ObjectContext); ok { + if v.GetManifestSource() == "" && source != "" { + object = v.SetManifestSource(source) } + } + } + return object +} + +func decodeJSON(data []byte) ([]manifest.Object, error) { + var res []genericObject + switch getJsonIdent(data) { + case identArray: + if err := json.Unmarshal(data, &res); err != nil { + return nil, err + } + case identObject: + var object genericObject + if err := json.Unmarshal(data, &object); err != nil { return nil, err } - switch obj := rawData.(type) { - case map[string]interface{}: - if len(obj) > 0 { - jsonArray = append(jsonArray, obj) + res = append(res, object) + } + if len(res) == 0 { + return nil, ErrNoDefinitionsFound + } + objects := make([]manifest.Object, 0, len(res)) + for i := range res { + objects = append(objects, res[i].Object) + } + return objects, nil +} + +func decodeYAML(data []byte) ([]manifest.Object, error) { + scanner := bufio.NewScanner(bytes.NewBuffer(data)) + scanner.Split(splitYAMLDocument) + var res []genericObject + for scanner.Scan() { + doc := scanner.Bytes() + if len(bytes.TrimSpace(doc)) == 0 { + continue + } + switch getYamlIdent(doc) { + case identArray: + var a []genericObject + if err := yaml.Unmarshal(doc, &a); err != nil { + return nil, err } - case []interface{}: - for _, def := range obj { - switch o := def.(type) { - case sdk.AnyJSONObj: - if len(o) > 0 { - jsonArray = append(jsonArray, o) - } - default: - return nil, errMalformedInput - } + res = append(res, a...) + case identObject: + var object genericObject + if err := yaml.Unmarshal(doc, &object); err != nil { + return nil, err + } + res = append(res, object) + } + } + if len(res) == 0 { + return nil, ErrNoDefinitionsFound + } + objects := make([]manifest.Object, 0, len(res)) + for i := range res { + objects = append(objects, res[i].Object) + } + return objects, nil +} + +// genericObject is a container for manifest.Object which helps in decoding process. +type genericObject struct { + Object manifest.Object +} + +// UnmarshalJSON implements json.Unmarshaler. +func (o *genericObject) UnmarshalJSON(data []byte) error { + return o.unmarshalGeneric(data, manifest.ObjectFormatJSON) +} + +// UnmarshalYAML implements yaml.BytesUnmarshaler. +func (o *genericObject) UnmarshalYAML(data []byte) error { + return o.unmarshalGeneric(data, manifest.ObjectFormatYAML) +} + +// unmarshalGeneric decodes a single raw manifest.Object representation into respective manifest.ObjectFormat. +// It uses an intermediate decoding step to extract manifest.Version and manifest.Kind from the object. +// Decoding is then delegated to the parser for specific manifest.Version. +func (o *genericObject) unmarshalGeneric(data []byte, format manifest.ObjectFormat) error { + var object struct { + ApiVersion manifest.Version `json:"apiVersion" yaml:"apiVersion"` + Kind manifest.Kind `json:"kind" yaml:"kind"` + } + var unmarshal func(data []byte, v interface{}) error + //exhaustive: enforce + switch format { + case manifest.ObjectFormatJSON: + unmarshal = json.Unmarshal + case manifest.ObjectFormatYAML: + unmarshal = yaml.Unmarshal + } + if err := unmarshal(data, &object); err != nil { + return err + } + switch object.ApiVersion { + case manifest.VersionV1alpha: + parsed, err := v1alpha.ParseObject(data, object.Kind, format) + if err != nil { + return err + } + o.Object = parsed + default: + return manifest.ErrInvalidVersion + } + return nil +} + +var jsonBufferRegex = regexp.MustCompile(`^\s*\[?\s*{`) + +// isJSONBuffer scans the provided buffer, looking for an open brace indicating this is JSON. +// While a simple list like ["a", "b", "c"] is still a valid JSON, +// it does not really concern us when processing complex objects. +func isJSONBuffer(buf []byte) bool { + return jsonBufferRegex.Match(buf) +} + +type ident uint8 + +const ( + identArray = iota + 1 + identObject +) + +var jsonArrayIdentRegex = regexp.MustCompile(`^\s*\[`) + +func getJsonIdent(data []byte) ident { + if jsonArrayIdentRegex.Match(data) { + return identArray + } + return identObject +} + +var yamlArrayIdentRegex = regexp.MustCompile(`(?m)^- `) + +func getYamlIdent(data []byte) ident { + // If we encounter square brackets array syntax, well... let's still recognize it's a valid array + // but obviously it cannot be a complex object as this syntax won't allow it. + if yamlArrayIdentRegex.Match(data) || jsonArrayIdentRegex.Match(data) { + return identArray + } + return identObject +} + +// yamlDocSep includes a prefixed newline character as we do now want to split on the first +// document separator located at the beginning of the file. +const yamlDocSep = "\n---" + +// splitYAMLDocument is a bufio.SplitFunc for splitting YAML streams into individual documents. +func splitYAMLDocument(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + // We have a potential document terminator. + if i := bytes.Index(data, []byte(yamlDocSep)); i >= 0 { + sep := len(yamlDocSep) + i += sep + after := data[i:] + if len(after) == 0 { + if atEOF { + return len(data), data[:len(data)-sep], nil } - case nil: - default: - return nil, errMalformedInput + return 0, nil, nil + } + if j := bytes.IndexByte(after, '\n'); j >= 0 { + return i + j + 1, data[0 : i-sep], nil } + return 0, nil, nil } - if len(jsonArray) == 0 { - return nil, errNoDefinitionsInInput + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil } - return jsonArray, nil + // Request more data. + return 0, nil, nil } diff --git a/sdk/definitions/parser_test.go b/sdk/definitions/parser_test.go new file mode 100644 index 00000000..c415532c --- /dev/null +++ b/sdk/definitions/parser_test.go @@ -0,0 +1,145 @@ +package definitions + +import ( + "embed" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/nobl9/nobl9-go/manifest" + "github.com/nobl9/nobl9-go/manifest/v1alpha" +) + +//go:embed test_data/parser +var parserTestData embed.FS + +func TestDecode(t *testing.T) { + for _, test := range []struct { + Input string + ExpectedObjectsLen int + ExpectedNames []string + Format manifest.ObjectFormat + }{ + { + Input: "list_of_objects.yaml", + ExpectedObjectsLen: 2, + ExpectedNames: []string{"default0", "default1"}, + Format: manifest.ObjectFormatYAML, + }, + { + Input: "list_of_objects_with_whitespace.yaml", + ExpectedObjectsLen: 2, + ExpectedNames: []string{"default0", "default1"}, + Format: manifest.ObjectFormatYAML, + }, + { + Input: "list_of_objects_with_comments.yaml", + ExpectedObjectsLen: 2, + ExpectedNames: []string{"default0", "default1"}, + Format: manifest.ObjectFormatYAML, + }, + { + Input: "multiple_documents.yaml", + ExpectedObjectsLen: 3, + ExpectedNames: []string{"default0", "default1", "default2"}, + Format: manifest.ObjectFormatYAML, + }, + { + Input: "single_document.yaml", + ExpectedObjectsLen: 1, + ExpectedNames: []string{"default"}, + Format: manifest.ObjectFormatYAML, + }, + { + Input: "single_document_with_document_separators.yaml", + ExpectedObjectsLen: 1, + ExpectedNames: []string{"default"}, + Format: manifest.ObjectFormatYAML, + }, + { + Input: "compacted_list_of_objects.json", + ExpectedObjectsLen: 2, + ExpectedNames: []string{"default0", "default1"}, + Format: manifest.ObjectFormatJSON, + }, + { + Input: "compacted_single_object.json", + ExpectedObjectsLen: 1, + ExpectedNames: []string{"default"}, + Format: manifest.ObjectFormatJSON, + }, + { + Input: "list_of_objects.json", + ExpectedObjectsLen: 2, + ExpectedNames: []string{"default0", "default1"}, + Format: manifest.ObjectFormatJSON, + }, + { + Input: "list_of_objects_with_whitespace.json", + ExpectedObjectsLen: 2, + ExpectedNames: []string{"default0", "default1"}, + Format: manifest.ObjectFormatJSON, + }, + { + Input: "single_object.json", + ExpectedObjectsLen: 1, + ExpectedNames: []string{"default"}, + Format: manifest.ObjectFormatJSON, + }, + } { + t.Run(test.Input, func(t *testing.T) { + data := readInputFile(t, test.Input) + + isJSON := isJSONBuffer(data) + switch test.Format { + case manifest.ObjectFormatJSON: + assert.True(t, isJSON, "expected the file contents to be interpreted as JSON") + case manifest.ObjectFormatYAML: + assert.False(t, isJSON, "expected the file contents to be interpreted as YAML") + } + + objects, err := Decode(data) + require.NoError(t, err) + assert.Len(t, objects, test.ExpectedObjectsLen) + assert.IsType(t, v1alpha.Project{}, objects[0]) + + objectNames := make([]string, 0, len(objects)) + for _, object := range objects { + objectNames = append(objectNames, object.GetName()) + } + for _, name := range test.ExpectedNames { + assert.Contains(t, objectNames, name) + } + }) + } +} + +func TestDecodeSingle(t *testing.T) { + t.Run("golden path", func(t *testing.T) { + project, err := DecodeSingle[v1alpha.Project](readInputFile(t, "single_project.yaml")) + require.NoError(t, err) + assert.NotZero(t, project) + assert.Equal(t, "default", project.GetName()) + }) + + t.Run("multiple objects, return error", func(t *testing.T) { + _, err := DecodeSingle[v1alpha.Project](readInputFile(t, "two_projects.yaml")) + require.Error(t, err) + assert.EqualError(t, err, "unexpected number of objects: 2, expected exactly one") + }) + + t.Run("invalid type, return error", func(t *testing.T) { + _, err := DecodeSingle[v1alpha.Service](readInputFile(t, "single_project.yaml")) + require.Error(t, err) + assert.EqualError(t, err, "object of type v1alpha.Project is not of type v1alpha.Service") + }) +} + +func readInputFile(t *testing.T, name string) []byte { + t.Helper() + data, err := parserTestData.ReadFile(filepath.Join("test_data", "parser", name)) + require.NoError(t, err) + return data +} diff --git a/sdk/definitions/reader.go b/sdk/definitions/reader.go index 69ebd0c4..2b0eecc0 100644 --- a/sdk/definitions/reader.go +++ b/sdk/definitions/reader.go @@ -10,10 +10,11 @@ import ( "regexp" "sort" "strings" + "time" "github.com/pkg/errors" - "github.com/nobl9/nobl9-go/sdk" + "github.com/nobl9/nobl9-go/manifest" "github.com/nobl9/nobl9-go/sdk/retryhttp" ) @@ -22,7 +23,7 @@ type ( // - file path (SourceTypeFile or SourceTypeDirectory) // - glob pattern (SourceTypeGlobPattern) // - URL (SourceTypeURL) - // - input provided via io.Reader, like os.Stdin (SourceTypeInput) + // - input provided via io.Reader, like os.Stdin (SourceTypeReader) RawSource = string // rawDefinition stores both the resolved source and raw resource definition. @@ -37,16 +38,18 @@ type ( ) // Read resolves the RawSource(s) it receives and calls ReadSources on the resolved Source(s). -func Read(ctx context.Context, annotations MetadataAnnotations, rawSources ...RawSource) ([]sdk.AnyJSONObj, error) { +func Read(ctx context.Context, rawSources ...RawSource) ([]manifest.Object, error) { sources, err := ResolveSources(rawSources...) if err != nil { return nil, errors.Wrap(err, "failed to resolve all raw sources") } - return ReadSources(ctx, annotations, sources...) + return ReadSources(ctx, sources...) } +const unknownSource = "-" + // ReadSources reads from the provided Source(s) based on the SourceType. -// For SourceTypeInput it will read directly from Source.Reader, +// For SourceTypeReader it will read directly from Source.Reader, // otherwise it reads from all the Source.Paths. It calculates a sum for // each definition read from Source and won't create duplicates. This // allows the user to combine Source(s) with possibly overlapping paths. @@ -55,7 +58,7 @@ func Read(ctx context.Context, annotations MetadataAnnotations, rawSources ...Ra // type SourceTypeGlobPattern or SourceTypeDirectory and a file does not // contain the required APIVersionRegex, it is skipped. However in case // of SourceTypeFile, it will thrown ErrInvalidFile error. -func ReadSources(ctx context.Context, annotations MetadataAnnotations, sources ...*Source) ([]sdk.AnyJSONObj, error) { +func ReadSources(ctx context.Context, sources ...*Source) ([]manifest.Object, error) { sort.Slice(sources, func(i, j int) bool { return sources[i].Raw > sources[j].Raw }) @@ -65,9 +68,19 @@ func ReadSources(ctx context.Context, annotations MetadataAnnotations, sources . def []byte ) for _, src := range sources { + if src.Type == SourceTypeReader { + switch len(src.Paths) { + case 0: + src.Paths = []string{unknownSource} + case 1: + break + default: + return nil, ErrSourceTypeReaderPath + } + } for _, path := range src.Paths { switch src.Type { - case SourceTypeInput: + case SourceTypeReader: def, err = readFromReader(src.Reader) case SourceTypeURL: def, err = readFromURL(ctx, path) @@ -88,7 +101,7 @@ func ReadSources(ctx context.Context, annotations MetadataAnnotations, sources . appendUniqueDefinition(definitions, path, def) } } - return processRawDefinitionsToJSONArray(annotations, definitions) + return processRawDefinitions(definitions) } var ( @@ -99,7 +112,8 @@ var ( matchingRulesDisclaimer) ErrInvalidFile = errors.Errorf("valid Nobl9 resource definition must match against the following regex: '%s'", APIVersionRegex) - ErrInvalidSourceType = errors.New("invalid SourceType provided") + ErrInvalidSourceType = errors.New("invalid SourceType provided") + ErrSourceTypeReaderPath = errors.New("SourceTypeReader Source may define at most a single Source.Path") matchingRulesDisclaimer = fmt.Sprintf( "valid resource definition file must have one of the extensions: [%s]", @@ -127,7 +141,7 @@ func readFromReader(in io.Reader) ([]byte, error) { // concurrently safe by design. // The factory is defined in a package variable to allow testing of HTTPS requests with httptest package. var httpClientFactory = func(url string) *http.Client { - return retryhttp.NewClient(sdk.Timeout, nil) + return retryhttp.NewClient(10*time.Second, nil) } func readFromURL(ctx context.Context, url string) ([]byte, error) { diff --git a/sdk/definitions/reader_test.go b/sdk/definitions/reader_test.go index 20625b9b..217f9943 100644 --- a/sdk/definitions/reader_test.go +++ b/sdk/definitions/reader_test.go @@ -16,17 +16,17 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/nobl9/nobl9-go/sdk" + "github.com/nobl9/nobl9-go/manifest" ) -//go:embed test_data -var testData embed.FS +//go:embed test_data/reader +var readerTestData embed.FS var templates *template.Template func TestMain(m *testing.M) { // Register templates. var err error - templates, err = template.ParseFS(testData, "test_data/expected/*.tpl.json") + templates, err = template.ParseFS(readerTestData, "test_data/reader/expected/*.tpl.json") if err != nil { panic(err) } @@ -99,8 +99,7 @@ func TestReadDefinitions_FromReader(t *testing.T) { t.Run("read definitions from reader", func(t *testing.T) { definitions, err := ReadSources( context.Background(), - MetadataAnnotations{Organization: "my-org"}, - NewInputSource(readTestFile(t, "service_and_agent.yaml"), "stdin")) + NewReaderSource(readTestFile(t, "service_and_agent.yaml"), "stdin")) require.NoError(t, err) definitionsMatchExpected(t, definitions, expectedMeta{Name: "service_and_agent", ManifestSrc: "stdin"}) }) @@ -108,19 +107,40 @@ func TestReadDefinitions_FromReader(t *testing.T) { t.Run("read definitions from reader for empty source", func(t *testing.T) { definitions, err := ReadSources( context.Background(), - MetadataAnnotations{Organization: "org"}, - NewInputSource(readTestFile(t, "service_and_agent.yaml"), "test")) + NewReaderSource(readTestFile(t, "service_and_agent.yaml"), "test")) require.NoError(t, err) definitionsMatchExpected(t, definitions, - expectedMeta{Name: "service_and_agent", ManifestSrc: "test", Organization: "org"}) + expectedMeta{Name: "service_and_agent", ManifestSrc: "test"}) + }) + + t.Run("fill in path for empty Source.Paths", func(t *testing.T) { + definitions, err := ReadSources( + context.Background(), + &Source{ + Reader: readTestFile(t, "service_and_agent.yaml"), + Type: SourceTypeReader, + }) + require.NoError(t, err) + definitionsMatchExpected(t, definitions, expectedMeta{Name: "service_and_agent", ManifestSrc: unknownSource}) }) t.Run("report an error when io.Reader is nil", func(t *testing.T) { - _, err := ReadSources(context.Background(), MetadataAnnotations{}, NewInputSource(nil, "nil")) + _, err := ReadSources(context.Background(), NewReaderSource(nil, "nil")) require.Error(t, err) assert.ErrorIs(t, err, ErrIoReaderIsNil) }) + + t.Run("report an error when more than one Source.Path provided", func(t *testing.T) { + _, err := ReadSources(context.Background(), + &Source{ + Reader: readTestFile(t, "service_and_agent.yaml"), + Type: SourceTypeReader, + Paths: []string{"this", "that"}, + }) + require.Error(t, err) + assert.ErrorIs(t, err, ErrSourceTypeReaderPath) + }) } func TestReadDefinitions_FromURL(t *testing.T) { @@ -133,7 +153,7 @@ func TestReadDefinitions_FromURL(t *testing.T) { defer srv.Close() require.Regexp(t, "^http://", srv.URL) - definitions, err := Read(context.Background(), MetadataAnnotations{Organization: "my-org"}, srv.URL) + definitions, err := Read(context.Background(), srv.URL) require.NoError(t, err) definitionsMatchExpected(t, definitions, expectedMeta{Name: "annotations", ManifestSrc: srv.URL}) }) @@ -148,11 +168,11 @@ func TestReadDefinitions_FromURL(t *testing.T) { httpClientFactory = func(url string) *http.Client { return srv.Client() } require.Regexp(t, "^https://", srv.URL) - definitions, err := Read(context.Background(), MetadataAnnotations{Organization: "org"}, srv.URL) + definitions, err := Read(context.Background(), srv.URL) require.NoError(t, err) definitionsMatchExpected(t, definitions, - expectedMeta{Name: "annotations", ManifestSrc: srv.URL, Organization: "org"}, + expectedMeta{Name: "annotations", ManifestSrc: srv.URL}, ) }) @@ -163,7 +183,7 @@ func TestReadDefinitions_FromURL(t *testing.T) { httpClientFactory = func(url string) *http.Client { return srv.Client() } defer srv.Close() - _, err := Read(context.Background(), MetadataAnnotations{Organization: "my-org"}, srv.URL) + _, err := Read(context.Background(), srv.URL) require.Error(t, err) assert.ErrorContains(t, err, fmt.Sprintf("GET %s response: 403 some error reason", srv.URL)) }) @@ -177,7 +197,7 @@ func TestReadDefinitions_FromURL(t *testing.T) { var err error ctx, cancel := context.WithCancel(context.Background()) cancel() - _, err = Read(ctx, MetadataAnnotations{Organization: "my-org"}, srv.URL) + _, err = Read(ctx, srv.URL) require.Error(t, err) assert.ErrorIs(t, err, context.Canceled) @@ -253,16 +273,15 @@ func TestReadDefinitions_FromFS(t *testing.T) { {Name: "annotations", ManifestSrc: tmpDir("more-yaml/even-more-definitions/annotations.yaml")}, {Name: "project", ManifestSrc: tmpDir("more-yaml/even-more-definitions/project.json")}, } - // Prepare expected files located in pkg/definitions/test_data. + // Prepare expected files located in ./test_data/reader. allNobl9RelFiles := []expectedMeta{ - {Name: "slo", ManifestSrc: workingDir("test_data/inputs/slo.yaml")}, - {Name: "service_and_agent", ManifestSrc: workingDir("test_data/inputs/service_and_agent.yaml")}, - {Name: "projects_and_direct", ManifestSrc: workingDir("test_data/inputs/projects_and_direct.yml")}, - {Name: "annotations", ManifestSrc: workingDir("test_data/inputs/annotations.yaml")}, - {Name: "project", ManifestSrc: workingDir("test_data/inputs/project.json")}, + {Name: "slo", ManifestSrc: workingDir("test_data/reader/inputs/slo.yaml")}, + {Name: "service_and_agent", ManifestSrc: workingDir("test_data/reader/inputs/service_and_agent.yaml")}, + {Name: "projects_and_direct", ManifestSrc: workingDir("test_data/reader/inputs/projects_and_direct.yml")}, + {Name: "annotations", ManifestSrc: workingDir("test_data/reader/inputs/annotations.yaml")}, + {Name: "project", ManifestSrc: workingDir("test_data/reader/inputs/project.json")}, } - const organization = "my-org" for name, test := range map[string]struct { Sources []RawSource Expected []expectedMeta @@ -295,7 +314,7 @@ func TestReadDefinitions_FromFS(t *testing.T) { Expected: []expectedMeta{{Name: "projects_and_direct", ManifestSrc: tmpDir("more-yaml/projects_and_direct.yml")}}, }, "read test_data directory files with a relative path": { - Sources: []RawSource{"test_data/inputs"}, + Sources: []RawSource{"test_data/reader/inputs"}, Expected: allNobl9RelFiles, }, "read a single directory by name": { @@ -310,7 +329,7 @@ func TestReadDefinitions_FromFS(t *testing.T) { Expected: allNobl9TmpFiles, }, "recurse the whole relative FS tree with a wildcard": { - Sources: []RawSource{workingDir("test_data/inputs/**")}, + Sources: []RawSource{workingDir("test_data/reader/inputs/**")}, Expected: allNobl9RelFiles, }, "double wildcard inside the pattern": { @@ -347,7 +366,7 @@ func TestReadDefinitions_FromFS(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - definitions, err := Read(ctx, MetadataAnnotations{Organization: organization}, test.Sources...) + definitions, err := Read(ctx, test.Sources...) require.NoError(t, err) definitionsMatchExpected(t, definitions, test.Expected...) @@ -368,7 +387,7 @@ func TestReadDefinitions_FromFS(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - _, err = Read(ctx, MetadataAnnotations{Organization: organization}, test.Sources...) + _, err = Read(ctx, test.Sources...) require.Error(t, err) assert.ErrorIs(t, err, test.Expected) }) @@ -376,18 +395,21 @@ func TestReadDefinitions_FromFS(t *testing.T) { } type expectedMeta struct { - Name string - Organization string - ManifestSrc string + Name string + ManifestSrc string } -func definitionsMatchExpected(t *testing.T, definitions []sdk.AnyJSONObj, meta ...expectedMeta) { +func definitionsMatchExpected(t *testing.T, definitions []manifest.Object, meta ...expectedMeta) { t.Helper() - expected := make([]sdk.AnyJSONObj, 0, len(definitions)) + + rawActual, err := json.Marshal(definitions) + require.NoError(t, err) + var actual []interface{} + err = json.Unmarshal(rawActual, &actual) + require.NoError(t, err) + + expectedAcc := make([]interface{}, 0, len(definitions)) for _, m := range meta { - if len(m.Organization) == 0 { - m.Organization = "my-org" - } buf := bytes.NewBuffer([]byte{}) err := templates.ExecuteTemplate(buf, m.Name+".tpl.json", m) require.NoError(t, err) @@ -397,21 +419,21 @@ func definitionsMatchExpected(t *testing.T, definitions []sdk.AnyJSONObj, meta . switch v := decoded.(type) { case []interface{}: for _, i := range v { - expected = append(expected, i.(map[string]interface{})) + expectedAcc = append(expectedAcc, i.(map[string]interface{})) } case map[string]interface{}: - expected = append(expected, v) + expectedAcc = append(expectedAcc, v) } } - require.Equal(t, len(expected), len(definitions)) + require.NoError(t, err) - assert.ElementsMatch(t, expected, definitions) + assert.ElementsMatch(t, expectedAcc, actual) } // readTestFile attempts to read the designated file from test_data folder. func readTestFile(t *testing.T, filename string) *bytes.Buffer { t.Helper() - data, err := testData.ReadFile(filepath.Join("test_data", "inputs", filename)) + data, err := readerTestData.ReadFile(filepath.Join("test_data", "reader", "inputs", filename)) require.NoError(t, err) return bytes.NewBuffer(data) } diff --git a/sdk/definitions/source.go b/sdk/definitions/source.go index 9e83a51b..aeecd432 100644 --- a/sdk/definitions/source.go +++ b/sdk/definitions/source.go @@ -27,7 +27,7 @@ func ResolveSources(rawSources ...RawSource) ([]*Source, error) { // ResolveSource attempts to resolve a single RawSource producing a Source instance read to be passed to ReadSources. // It interprets the provided URI and associates it with a specific SourceType. -// If you wish to create a SourceTypeInput Source you should use a separate method: NewInputSource. +// If you wish to create a SourceTypeReader Source you should use a separate method: NewReaderSource. func ResolveSource(rawSource RawSource) (src *Source, err error) { src = &Source{Raw: rawSource} switch { @@ -43,11 +43,11 @@ func ResolveSource(rawSource RawSource) (src *Source, err error) { return src, err } -// NewInputSource creates a special instance of Source with SourceTypeInput. +// NewReaderSource creates a special instance of Source with SourceTypeReader. // ReadSources will process the Source by reading form the provided io.Reader. -func NewInputSource(r io.Reader, source RawSource) *Source { +func NewReaderSource(r io.Reader, source RawSource) *Source { return &Source{ - Type: SourceTypeInput, + Type: SourceTypeReader, Paths: []string{source}, Reader: r, Raw: source, @@ -60,7 +60,7 @@ type Source struct { Type SourceType // Paths lists all resolved URIs the Source points at. Paths []string - // Reader may be optionally provided with SourceTypeInput for ReadSources to read from the io.Reader. + // Reader may be optionally provided with SourceTypeReader for ReadSources to read from the io.Reader. Reader io.Reader // Raw is the original, unresolved RawSource, an example might be a relative path // which was resolved to its absolute form. @@ -74,7 +74,7 @@ const ( SourceTypeDirectory SourceTypeGlobPattern SourceTypeURL - SourceTypeInput + SourceTypeReader ) func (s Source) String() string { diff --git a/sdk/definitions/sourcetype_string.go b/sdk/definitions/sourcetype_string.go index 9370807b..88fbe287 100644 --- a/sdk/definitions/sourcetype_string.go +++ b/sdk/definitions/sourcetype_string.go @@ -12,7 +12,7 @@ func _() { _ = x[SourceTypeDirectory-1] _ = x[SourceTypeGlobPattern-2] _ = x[SourceTypeURL-3] - _ = x[SourceTypeInput-4] + _ = x[SourceTypeReader-4] } const _SourceType_name = "FileDirectoryGlobPatternURLInput" diff --git a/sdk/definitions/test_data/parser/compacted_list_of_objects.json b/sdk/definitions/test_data/parser/compacted_list_of_objects.json new file mode 100644 index 00000000..65b2914f --- /dev/null +++ b/sdk/definitions/test_data/parser/compacted_list_of_objects.json @@ -0,0 +1 @@ +[{"apiVersion":"n9/v1alpha","kind":"Project","metadata":{"name":"default0"},"spec":{"description":"default project"}},{"apiVersion":"n9/v1alpha","kind":"Project","metadata":{"name":"default1"},"spec":{"description":"default project"}}] diff --git a/sdk/definitions/test_data/parser/compacted_single_object.json b/sdk/definitions/test_data/parser/compacted_single_object.json new file mode 100644 index 00000000..8d3c77e8 --- /dev/null +++ b/sdk/definitions/test_data/parser/compacted_single_object.json @@ -0,0 +1 @@ +{"apiVersion":"n9/v1alpha","kind":"Project","metadata":{"name":"default"},"spec":{"description":"default project"}} diff --git a/sdk/definitions/test_data/parser/list_of_objects.json b/sdk/definitions/test_data/parser/list_of_objects.json new file mode 100644 index 00000000..e9c3ee43 --- /dev/null +++ b/sdk/definitions/test_data/parser/list_of_objects.json @@ -0,0 +1,22 @@ +[ + { + "apiVersion": "n9/v1alpha", + "kind": "Project", + "metadata": { + "name": "default0" + }, + "spec": { + "description": "default project" + } + }, + { + "apiVersion": "n9/v1alpha", + "kind": "Project", + "metadata": { + "name": "default1" + }, + "spec": { + "description": "default project" + } + } +] diff --git a/sdk/definitions/test_data/parser/list_of_objects.yaml b/sdk/definitions/test_data/parser/list_of_objects.yaml new file mode 100644 index 00000000..09d6b03b --- /dev/null +++ b/sdk/definitions/test_data/parser/list_of_objects.yaml @@ -0,0 +1,12 @@ +- apiVersion: n9/v1alpha + kind: Project + metadata: + name: default0 + spec: + description: default project +- apiVersion: n9/v1alpha + kind: Project + metadata: + name: default1 + spec: + description: default project diff --git a/sdk/definitions/test_data/parser/list_of_objects_with_comments.yaml b/sdk/definitions/test_data/parser/list_of_objects_with_comments.yaml new file mode 100644 index 00000000..f14564ce --- /dev/null +++ b/sdk/definitions/test_data/parser/list_of_objects_with_comments.yaml @@ -0,0 +1,16 @@ +########## +# Projects: +########## +- apiVersion: n9/v1alpha + kind: Project + metadata: + name: default0 + spec: + description: default project +# project without any resources - not visible on SLO grid +- apiVersion: n9/v1alpha + kind: Project + metadata: + name: default1 + spec: + description: default project diff --git a/sdk/definitions/test_data/parser/list_of_objects_with_whitespace.json b/sdk/definitions/test_data/parser/list_of_objects_with_whitespace.json new file mode 100644 index 00000000..9497c852 --- /dev/null +++ b/sdk/definitions/test_data/parser/list_of_objects_with_whitespace.json @@ -0,0 +1,22 @@ + [ + { + "apiVersion": "n9/v1alpha", + "kind": "Project", + "metadata": { + "name": "default0" + }, + "spec": { + "description": "default project" + } + }, + { + "apiVersion": "n9/v1alpha", + "kind": "Project", + "metadata": { + "name": "default1" + }, + "spec": { + "description": "default project" + } + } +] diff --git a/sdk/definitions/test_data/parser/list_of_objects_with_whitespace.yaml b/sdk/definitions/test_data/parser/list_of_objects_with_whitespace.yaml new file mode 100644 index 00000000..b8655a46 --- /dev/null +++ b/sdk/definitions/test_data/parser/list_of_objects_with_whitespace.yaml @@ -0,0 +1,17 @@ + + +- apiVersion: n9/v1alpha + kind: Project + metadata: + name: default0 + spec: + description: default project + + + +- apiVersion: n9/v1alpha + kind: Project + metadata: + name: default1 + spec: + description: default project diff --git a/sdk/definitions/test_data/parser/multiple_documents.yaml b/sdk/definitions/test_data/parser/multiple_documents.yaml new file mode 100644 index 00000000..ae5d77eb --- /dev/null +++ b/sdk/definitions/test_data/parser/multiple_documents.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: n9/v1alpha +kind: Project +metadata: + name: default0 +spec: + description: default project +--- +- apiVersion: n9/v1alpha + kind: Project + metadata: + name: default1 + spec: + description: default project +- apiVersion: n9/v1alpha + kind: Project + metadata: + name: default2 + spec: + description: default project diff --git a/sdk/definitions/test_data/parser/single_document.yaml b/sdk/definitions/test_data/parser/single_document.yaml new file mode 100644 index 00000000..df2e64cf --- /dev/null +++ b/sdk/definitions/test_data/parser/single_document.yaml @@ -0,0 +1,6 @@ +apiVersion: n9/v1alpha +kind: Project +metadata: + name: default +spec: + description: default project \ No newline at end of file diff --git a/sdk/definitions/test_data/parser/single_document_with_document_separators.yaml b/sdk/definitions/test_data/parser/single_document_with_document_separators.yaml new file mode 100644 index 00000000..51a91fc9 --- /dev/null +++ b/sdk/definitions/test_data/parser/single_document_with_document_separators.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: n9/v1alpha +kind: Project +metadata: + name: default +spec: + description: default project +--- diff --git a/sdk/definitions/test_data/parser/single_object.json b/sdk/definitions/test_data/parser/single_object.json new file mode 100644 index 00000000..d9fd5bf4 --- /dev/null +++ b/sdk/definitions/test_data/parser/single_object.json @@ -0,0 +1,10 @@ +{ + "apiVersion": "n9/v1alpha", + "kind": "Project", + "metadata": { + "name": "default" + }, + "spec": { + "description": "default project" + } +} diff --git a/sdk/definitions/test_data/parser/single_project.yaml b/sdk/definitions/test_data/parser/single_project.yaml new file mode 100644 index 00000000..ed1e1e3a --- /dev/null +++ b/sdk/definitions/test_data/parser/single_project.yaml @@ -0,0 +1,6 @@ +apiVersion: n9/v1alpha +kind: Project +metadata: + name: default +spec: + description: default project diff --git a/sdk/definitions/test_data/parser/two_projects.yaml b/sdk/definitions/test_data/parser/two_projects.yaml new file mode 100644 index 00000000..09d6b03b --- /dev/null +++ b/sdk/definitions/test_data/parser/two_projects.yaml @@ -0,0 +1,12 @@ +- apiVersion: n9/v1alpha + kind: Project + metadata: + name: default0 + spec: + description: default project +- apiVersion: n9/v1alpha + kind: Project + metadata: + name: default1 + spec: + description: default project diff --git a/sdk/definitions/test_data/expected/annotations.tpl.json b/sdk/definitions/test_data/reader/expected/annotations.tpl.json similarity index 80% rename from sdk/definitions/test_data/expected/annotations.tpl.json rename to sdk/definitions/test_data/reader/expected/annotations.tpl.json index fe47ef0f..929df0a2 100644 --- a/sdk/definitions/test_data/expected/annotations.tpl.json +++ b/sdk/definitions/test_data/reader/expected/annotations.tpl.json @@ -12,8 +12,7 @@ "startTime": "2006-01-02T17:10:05Z", "endTime": "2006-01-02T17:10:05Z" }, - "manifestSrc": "{{ .ManifestSrc }}", - "organization": "{{ .Organization }}" + "manifestSrc": "{{ .ManifestSrc }}" }, { "apiVersion": "n9/v1alpha", @@ -28,7 +27,6 @@ "startTime": "2006-01-02T17:20:05Z", "endTime": "2006-01-02T17:30:05Z" }, - "manifestSrc": "{{ .ManifestSrc }}", - "organization": "{{ .Organization }}" + "manifestSrc": "{{ .ManifestSrc }}" } ] diff --git a/sdk/definitions/test_data/expected/project.tpl.json b/sdk/definitions/test_data/reader/expected/project.tpl.json similarity index 64% rename from sdk/definitions/test_data/expected/project.tpl.json rename to sdk/definitions/test_data/reader/expected/project.tpl.json index 3e1ead23..5f9079d3 100644 --- a/sdk/definitions/test_data/expected/project.tpl.json +++ b/sdk/definitions/test_data/reader/expected/project.tpl.json @@ -7,6 +7,5 @@ "spec": { "description": "" }, - "manifestSrc": "{{ .ManifestSrc }}", - "organization": "{{ .Organization }}" + "manifestSrc": "{{ .ManifestSrc }}" } diff --git a/sdk/definitions/test_data/expected/projects_and_direct.tpl.json b/sdk/definitions/test_data/reader/expected/projects_and_direct.tpl.json similarity index 75% rename from sdk/definitions/test_data/expected/projects_and_direct.tpl.json rename to sdk/definitions/test_data/reader/expected/projects_and_direct.tpl.json index 62ae7f07..2ee3c4ab 100644 --- a/sdk/definitions/test_data/expected/projects_and_direct.tpl.json +++ b/sdk/definitions/test_data/reader/expected/projects_and_direct.tpl.json @@ -18,8 +18,7 @@ "insightsQueryKey": "" } }, - "manifestSrc": "{{ .ManifestSrc }}", - "organization": "{{ .Organization }}" + "manifestSrc": "{{ .ManifestSrc }}" }, { "apiVersion": "n9/v1alpha", @@ -30,8 +29,7 @@ "spec": { "description": "" }, - "manifestSrc": "{{ .ManifestSrc }}", - "organization": "{{ .Organization }}" + "manifestSrc": "{{ .ManifestSrc }}" }, { "apiVersion": "n9/v1alpha", @@ -42,7 +40,6 @@ "spec": { "description": "" }, - "manifestSrc": "{{ .ManifestSrc }}", - "organization": "{{ .Organization }}" + "manifestSrc": "{{ .ManifestSrc }}" } ] diff --git a/sdk/definitions/test_data/expected/service_and_agent.tpl.json b/sdk/definitions/test_data/reader/expected/service_and_agent.tpl.json similarity index 72% rename from sdk/definitions/test_data/expected/service_and_agent.tpl.json rename to sdk/definitions/test_data/reader/expected/service_and_agent.tpl.json index fb6f9ac9..98aa91b1 100644 --- a/sdk/definitions/test_data/expected/service_and_agent.tpl.json +++ b/sdk/definitions/test_data/reader/expected/service_and_agent.tpl.json @@ -8,15 +8,14 @@ }, "spec": { "appDynamics": { - "URL": "https://nobl9.saas.appdynamics.com" + "url": "https://nobl9.saas.appdynamics.com" }, "sourceOf": [ "Metrics", "Services" ] }, - "manifestSrc": "{{ .ManifestSrc }}", - "organization": "{{ .Organization }}" + "manifestSrc": "{{ .ManifestSrc }}" }, { "apiVersion": "n9/v1alpha", @@ -29,7 +28,6 @@ "spec": { "description": "polakpotrafi website" }, - "manifestSrc": "{{ .ManifestSrc }}", - "organization": "{{ .Organization }}" + "manifestSrc": "{{ .ManifestSrc }}" } ] diff --git a/sdk/definitions/test_data/expected/slo.tpl.json b/sdk/definitions/test_data/reader/expected/slo.tpl.json similarity index 87% rename from sdk/definitions/test_data/expected/slo.tpl.json rename to sdk/definitions/test_data/reader/expected/slo.tpl.json index 1b50bf42..1f5701a2 100644 --- a/sdk/definitions/test_data/expected/slo.tpl.json +++ b/sdk/definitions/test_data/reader/expected/slo.tpl.json @@ -29,7 +29,7 @@ "rawMetric": { "query": { "lightstep": { - "streamID": "DzpxcSRh", + "streamId": "DzpxcSRh", "typeOfData": "latency", "percentile": 95 } @@ -40,7 +40,6 @@ } ] }, - "manifestSrc": "{{ .ManifestSrc }}", - "organization": "{{ .Organization }}" + "manifestSrc": "{{ .ManifestSrc }}" } ] diff --git a/sdk/definitions/test_data/inputs/annotations.yaml b/sdk/definitions/test_data/reader/inputs/annotations.yaml similarity index 100% rename from sdk/definitions/test_data/inputs/annotations.yaml rename to sdk/definitions/test_data/reader/inputs/annotations.yaml diff --git a/sdk/definitions/test_data/inputs/k8s.yaml b/sdk/definitions/test_data/reader/inputs/k8s.yaml similarity index 100% rename from sdk/definitions/test_data/inputs/k8s.yaml rename to sdk/definitions/test_data/reader/inputs/k8s.yaml diff --git a/sdk/definitions/test_data/inputs/project.json b/sdk/definitions/test_data/reader/inputs/project.json similarity index 100% rename from sdk/definitions/test_data/inputs/project.json rename to sdk/definitions/test_data/reader/inputs/project.json diff --git a/sdk/definitions/test_data/inputs/projects_and_direct.yml b/sdk/definitions/test_data/reader/inputs/projects_and_direct.yml similarity index 100% rename from sdk/definitions/test_data/inputs/projects_and_direct.yml rename to sdk/definitions/test_data/reader/inputs/projects_and_direct.yml diff --git a/sdk/definitions/test_data/inputs/run.sh b/sdk/definitions/test_data/reader/inputs/run.sh similarity index 100% rename from sdk/definitions/test_data/inputs/run.sh rename to sdk/definitions/test_data/reader/inputs/run.sh diff --git a/sdk/definitions/test_data/inputs/service_and_agent.yaml b/sdk/definitions/test_data/reader/inputs/service_and_agent.yaml similarity index 89% rename from sdk/definitions/test_data/inputs/service_and_agent.yaml rename to sdk/definitions/test_data/reader/inputs/service_and_agent.yaml index a7566ab2..cd63b2f3 100644 --- a/sdk/definitions/test_data/inputs/service_and_agent.yaml +++ b/sdk/definitions/test_data/reader/inputs/service_and_agent.yaml @@ -6,7 +6,7 @@ metadata: project: appdynamics spec: appDynamics: - URL: https://nobl9.saas.appdynamics.com + url: https://nobl9.saas.appdynamics.com sourceOf: - Metrics - Services diff --git a/sdk/definitions/test_data/inputs/slo.yaml b/sdk/definitions/test_data/reader/inputs/slo.yaml similarity index 94% rename from sdk/definitions/test_data/inputs/slo.yaml rename to sdk/definitions/test_data/reader/inputs/slo.yaml index 867e1d17..51d89379 100644 --- a/sdk/definitions/test_data/inputs/slo.yaml +++ b/sdk/definitions/test_data/reader/inputs/slo.yaml @@ -20,7 +20,7 @@ spec: rawMetric: query: lightstep: - streamID: DzpxcSRh + streamId: DzpxcSRh typeOfData: latency percentile: 95 value: 150