From 74499369d9af5497e14bd9e513c4cbc3d003387a Mon Sep 17 00:00:00 2001 From: Rocktavious Date: Thu, 31 Aug 2023 23:09:03 -0500 Subject: [PATCH] Initial start of big refactor to increase testing --- .../unreleased/Refactor-20230831-194854.yaml | 3 + .gitignore | 1 + src/cmd/collect.go | 21 +- src/cmd/config.go | 112 +- src/cmd/import.go | 7 +- src/cmd/preview.go | 8 +- src/cmd/reconcile.go | 22 +- src/cmd/root.go | 26 +- src/cmd/run.go | 22 + src/common/jqparser.go | 203 ---- src/common/service.go | 979 +++++++++--------- src/common/service_test.go | 99 +- src/jq/jq.go | 139 --- src/{ => pkg}/config/config.go | 16 +- src/pkg/config/config_samples.go | 102 ++ src/pkg/config/config_test.go | 47 + src/pkg/jq/jq.go | 117 +++ src/pkg/jq/jq_error.go | 43 + src/pkg/jq/jq_response.go | 68 ++ src/pkg/jq/jq_test.go | 123 +++ 20 files changed, 1098 insertions(+), 1060 deletions(-) create mode 100644 .changes/unreleased/Refactor-20230831-194854.yaml create mode 100644 src/cmd/run.go delete mode 100644 src/common/jqparser.go delete mode 100644 src/jq/jq.go rename src/{ => pkg}/config/config.go (88%) create mode 100644 src/pkg/config/config_samples.go create mode 100644 src/pkg/config/config_test.go create mode 100644 src/pkg/jq/jq.go create mode 100644 src/pkg/jq/jq_error.go create mode 100644 src/pkg/jq/jq_response.go create mode 100644 src/pkg/jq/jq_test.go diff --git a/.changes/unreleased/Refactor-20230831-194854.yaml b/.changes/unreleased/Refactor-20230831-194854.yaml new file mode 100644 index 00000000..5c58bf09 --- /dev/null +++ b/.changes/unreleased/Refactor-20230831-194854.yaml @@ -0,0 +1,3 @@ +kind: Refactor +body: Refactor the entire codebase to be easier to test and update +time: 2023-08-31T19:48:54.653125-05:00 diff --git a/.gitignore b/.gitignore index 6052d385..31f2aba3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /kubectl-opslevel src/dist .idea +.DS_Store diff --git a/src/cmd/collect.go b/src/cmd/collect.go index c0a2e95c..446d54dd 100644 --- a/src/cmd/collect.go +++ b/src/cmd/collect.go @@ -6,19 +6,13 @@ import ( "time" "github.com/opslevel/kubectl-opslevel/common" - "github.com/opslevel/kubectl-opslevel/config" - "github.com/opslevel/kubectl-opslevel/jq" "github.com/opslevel/kubectl-opslevel/k8sutils" + "github.com/opslevel/kubectl-opslevel/pkg/config" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" ) -var ( - collectResyncInterval int - collectBatchSize int -) - var collectCmd = &cobra.Command{ Use: "collect", Short: "Acts as a kubernetes controller to collect resources for submission to OpsLevel as custom event check payloads", @@ -27,20 +21,15 @@ var collectCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(collectCmd) + runCmd.AddCommand(collectCmd) collectCmd.Flags().StringP("integration-url", "i", "", "OpsLevel integration url (OPSLEVEL_INTEGRATION_URL)") - collectCmd.Flags().IntVar(&collectResyncInterval, "resync", 24, "The amount (in hours) before a full resync of the kubernetes cluster happens with OpsLevel. [default: 24]") - collectCmd.Flags().IntVar(&collectBatchSize, "batch", 500, "The max amount of k8s resources to batch process. Helps to speedup initial startup. [default: 500]") viper.BindEnv("integration-url", "OPSLEVEL_INTEGRATION_URL") } func runCollect(cmd *cobra.Command, args []string) { - config, configErr := config.New() - cobra.CheckErr(configErr) - - jq.ValidateInstalled() + config := getCfgFile() integrationUrl := viper.GetString("integration-url") if len(integrationUrl) <= 0 { @@ -49,7 +38,7 @@ func runCollect(cmd *cobra.Command, args []string) { k8sClient := k8sutils.CreateKubernetesClient() - resync := time.Hour * time.Duration(reconcileResyncInterval) + resync := time.Hour * time.Duration(resyncInterval) collectQueue := make(chan string, 1) for i, importConfig := range config.Service.Collect { @@ -64,7 +53,7 @@ func runCollect(cmd *cobra.Command, args []string) { continue } callback := createCollectHandler(fmt.Sprintf("service.import[%d]", i), importConfig, collectQueue) - controller := k8sutils.NewController(*gvr, resync, reconcileBatchSize) + controller := k8sutils.NewController(*gvr, resync, batchSize) controller.OnAdd = callback controller.OnUpdate = callback go controller.Start(1) diff --git a/src/cmd/config.go b/src/cmd/config.go index e28e7fc4..6d45db07 100644 --- a/src/cmd/config.go +++ b/src/cmd/config.go @@ -1,99 +1,17 @@ package cmd import ( - "bytes" "encoding/json" "fmt" yaml "gopkg.in/yaml.v3" "github.com/alecthomas/jsonschema" - "github.com/opslevel/kubectl-opslevel/config" + "github.com/opslevel/kubectl-opslevel/pkg/config" "github.com/spf13/cobra" "github.com/spf13/viper" ) -// Make sure we only use spaces inside of these samples -var configSimple = []byte(`#Simple Opslevel CLI Config -version: "1.2.0" -service: - import: - - selector: # This limits what data we look at in Kubernetes - apiVersion: apps/v1 # only supports resources found in 'kubectl api-resources --verbs="get,list"' - kind: Deployment - excludes: # filters out resources if any expression returns truthy - - .metadata.namespace == "kube-system" - - .metadata.annotations."opslevel.com/ignore" - opslevel: # This is how you map your kubernetes data to opslevel service - name: .metadata.name - owner: .metadata.namespace - aliases: # This are how we identify the services again during reconciliation - please make sure they are very unique - - '"k8s:\(.metadata.name)-\(.metadata.namespace)"' - tags: - assign: # tag with the same key name but with a different value will be updated on the service - - '{"imported": "kubectl-opslevel"}' - - .metadata.labels - create: # tag with the same key name but with a different value with be added to the service - - '{"environment": .spec.template.metadata.labels.environment}' - collect: - - selector: # This limits what data we look at in Kubernetes - apiVersion: apps/v1 # only supports resources found in 'kubectl api-resources --verbs="get,list"' - kind: Deployment - excludes: # filters out resources if any expression returns truthy - - .metadata.namespace == "kube-system" - - .metadata.annotations."opslevel.com/ignore" -`) - -var configSample = []byte(`#Sample Opslevel CLI Config -version: "1.2.0" -service: - import: - - selector: # This limits what data we look at in Kubernetes - apiVersion: apps/v1 # only supports resources found in 'kubectl api-resources --verbs="get,list"' - kind: Deployment - excludes: # filters out resources if any expression returns truthy - - .metadata.namespace == "kube-system" - - .metadata.annotations."opslevel.com/ignore" - opslevel: # This is how you map your kubernetes data to opslevel service - name: .metadata.name - description: .metadata.annotations."opslevel.com/description" - owner: .metadata.annotations."opslevel.com/owner" - lifecycle: .metadata.annotations."opslevel.com/lifecycle" - tier: .metadata.annotations."opslevel.com/tier" - product: .metadata.annotations."opslevel.com/product" - language: .metadata.annotations."opslevel.com/language" - framework: .metadata.annotations."opslevel.com/framework" - aliases: # This are how we identify the services again during reconciliation - please make sure they are very unique - - '"k8s:\(.metadata.name)-\(.metadata.namespace)"' - tags: - assign: # tag with the same key name but with a different value will be updated on the service - - '{"imported": "kubectl-opslevel"}' - # find annoations with format: opslevel.com/tags.: - - '.metadata.annotations | to_entries | map(select(.key | startswith("opslevel.com/tags"))) | map({(.key | split(".")[2]): .value})' - - .metadata.labels - create: # tag with the same key name but with a different value with be added to the service - - '{"environment": .spec.template.metadata.labels.environment}' - tools: - - '{"category": "other", "environment": "production", "displayName": "my-cool-tool", "url": .metadata.annotations."example.com/my-cool-tool"} | if .url then . else empty end' - # find annotations with format: opslevel.com/tools..: - - '.metadata.annotations | to_entries | map(select(.key | startswith("opslevel.com/tools"))) | map({"category": .key | split(".")[2], "displayName": .key | split(".")[3], "url": .value})' - # OR find annotations with format: opslevel.com/tools...: - # - '.metadata.annotations | to_entries | map(select(.key | startswith("opslevel.com/tools"))) | map({"category": .key | split(".")[2], "environment": .key | split(".")[3], "displayName": .key | split(".")[4], "url": .value})' - repositories: # attach repositories to the service using the opslevel repo alias - IE github.com:hashicorp/vault - - '{"name": "My Cool Repo", "directory": "/", "repo": .metadata.annotations.repo} | if .repo then . else empty end' - # if just the alias is returned as a single string we'll build the name for you and set the directory to "/" - - .metadata.annotations.repo - # find annotations with format: opslevel.com/repo..: - - '.metadata.annotations | to_entries | map(select(.key | startswith("opslevel.com/repos"))) | map({"name": .key | split(".")[2], "directory": .key | split(".")[3:] | join("/"), "repo": .value})' - collect: - - selector: # This limits what data we look at in Kubernetes - apiVersion: apps/v1 # only supports resources found in 'kubectl api-resources --verbs="get,list"' - kind: Deployment - excludes: # filters out resources if any expression returns truthy - - .metadata.namespace == "kube-system" - - .metadata.annotations."opslevel.com/ignore" -`) - var configCmd = &cobra.Command{ Use: "config", Short: "Commands for working with the opslevel configuration", @@ -117,8 +35,7 @@ var configViewCmd = &cobra.Command{ Short: "Print the final configuration result", Long: "Print the final configuration after loading all the overrides and defaults", Run: func(cmd *cobra.Command, args []string) { - conf, err := config.New() - cobra.CheckErr(err) + conf := getCfgFile() output, err2 := yaml.Marshal(conf) cobra.CheckErr(err2) fmt.Println(string(output)) @@ -130,7 +47,7 @@ var configSampleCmd = &cobra.Command{ Short: "Print a sample config file", Long: "Print a sample config file which could be used", Run: func(cmd *cobra.Command, args []string) { - fmt.Println(getSample(viper.GetBool("simple"))) + fmt.Println(config.GetSample(viper.GetBool("simple"))) }, } @@ -145,26 +62,3 @@ func init() { configSampleCmd.Flags().Bool("simple", false, "Adjust the sample config to a bit simpler") viper.BindPFlags(configSampleCmd.Flags()) } - -func getSample(simple bool) string { - var sample []byte - if simple == true { - sample = configSimple - } else { - sample = configSample - } - // we use yaml unmarshal to prove that the samples are valid yaml - var nodes yaml.Node - unmarshalErr := yaml.Unmarshal(sample, &nodes) - if unmarshalErr != nil { - return unmarshalErr.Error() - } - var b bytes.Buffer - yamlEncoder := yaml.NewEncoder(&b) - yamlEncoder.SetIndent(2) - encodeErr := yamlEncoder.Encode(&nodes) - if encodeErr != nil { - return encodeErr.Error() - } - return string(b.Bytes()) -} diff --git a/src/cmd/import.go b/src/cmd/import.go index 710775e7..19f5209d 100644 --- a/src/cmd/import.go +++ b/src/cmd/import.go @@ -4,8 +4,6 @@ import ( "sync" "github.com/opslevel/kubectl-opslevel/common" - "github.com/opslevel/kubectl-opslevel/config" - "github.com/opslevel/kubectl-opslevel/jq" "github.com/opslevel/opslevel-go/v2023" "github.com/rs/zerolog/log" @@ -24,10 +22,7 @@ func init() { } func runImport(cmd *cobra.Command, args []string) { - config, configErr := config.New() - cobra.CheckErr(configErr) - - jq.ValidateInstalled() + config := getCfgFile() services, servicesErr := common.GetAllServices(config) cobra.CheckErr(servicesErr) diff --git a/src/cmd/preview.go b/src/cmd/preview.go index 335c4ccd..86d94f89 100644 --- a/src/cmd/preview.go +++ b/src/cmd/preview.go @@ -9,9 +9,6 @@ import ( "time" "github.com/opslevel/kubectl-opslevel/common" - "github.com/opslevel/kubectl-opslevel/config" - "github.com/opslevel/kubectl-opslevel/jq" - _ "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -39,10 +36,7 @@ func runPreview(cmd *cobra.Command, args []string) { } } - config, err := config.New() - cobra.CheckErr(err) - - jq.ValidateInstalled() + config := getCfgFile() services, err2 := common.GetAllServices(config) cobra.CheckErr(err2) diff --git a/src/cmd/reconcile.go b/src/cmd/reconcile.go index 06a2168e..3dd4c44f 100644 --- a/src/cmd/reconcile.go +++ b/src/cmd/reconcile.go @@ -6,19 +6,13 @@ import ( "time" "github.com/opslevel/kubectl-opslevel/common" - "github.com/opslevel/kubectl-opslevel/config" - "github.com/opslevel/kubectl-opslevel/jq" "github.com/opslevel/kubectl-opslevel/k8sutils" + "github.com/opslevel/kubectl-opslevel/pkg/config" "github.com/opslevel/opslevel-go/v2023" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) -var ( - reconcileResyncInterval int - reconcileBatchSize int -) - var reconcileCmd = &cobra.Command{ Use: "reconcile", Short: "Run in the foreground as a kubernetes controller to reconcile data with service entries in OpsLevel", @@ -27,17 +21,11 @@ var reconcileCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(reconcileCmd) - - reconcileCmd.Flags().IntVar(&reconcileResyncInterval, "resync", 24, "The amount (in hours) before a full resync of the kubernetes cluster happens with OpsLevel. [default: 24]") - reconcileCmd.Flags().IntVar(&reconcileBatchSize, "batch", 500, "The max amount of k8s resources to batch process with jq. Helps to speedup initial startup. [default: 500]") + runCmd.AddCommand(reconcileCmd) } func runReconcile(cmd *cobra.Command, args []string) { - config, configErr := config.New() - cobra.CheckErr(configErr) - - jq.ValidateInstalled() + config := getCfgFile() k8sClient := k8sutils.CreateKubernetesClient() olClient := createOpslevelClient() @@ -46,7 +34,7 @@ func runReconcile(cmd *cobra.Command, args []string) { opslevel.Cache.CacheLifecycles(olClient) opslevel.Cache.CacheTeams(olClient) - resync := time.Hour * time.Duration(reconcileResyncInterval) + resync := time.Hour * time.Duration(resyncInterval) reconcileQueue := make(chan common.ServiceRegistration, 1) for i, importConfig := range config.Service.Import { @@ -61,7 +49,7 @@ func runReconcile(cmd *cobra.Command, args []string) { continue } callback := createHandler(fmt.Sprintf("service.import[%d]", i), importConfig, reconcileQueue) - controller := k8sutils.NewController(*gvr, resync, reconcileBatchSize) + controller := k8sutils.NewController(*gvr, resync, batchSize) controller.OnAdd = callback controller.OnUpdate = callback go controller.Start(1) diff --git a/src/cmd/root.go b/src/cmd/root.go index 68c48013..005624de 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "github.com/go-resty/resty/v2" + "github.com/opslevel/kubectl-opslevel/pkg/config" "os" "runtime" "strings" @@ -70,23 +71,6 @@ func initConfig() { } func readConfig() { - if cfgFile != "" { - if cfgFile == "." { - viper.SetConfigType("yaml") - viper.ReadConfig(os.Stdin) - return - } else { - viper.SetConfigFile(cfgFile) - } - } else { - home, err := os.UserHomeDir() - cobra.CheckErr(err) - - viper.SetConfigName("opslevel") - viper.SetConfigType("yaml") - viper.AddConfigPath(".") - viper.AddConfigPath(home) - } viper.SetEnvPrefix("OPSLEVEL") viper.AutomaticEnv() viper.ReadInConfig() @@ -179,3 +163,11 @@ func createOpslevelClient() *opslevel.Client { func createRestClient() *resty.Client { return opslevel.NewRestClient(opslevel.SetURL(viper.GetString("api-url"))) } + +func getCfgFile() *config.Config { + data, err := os.ReadFile(cfgFile) + cobra.CheckErr(err) + output, err := config.NewConfig(data) + cobra.CheckErr(err) + return output +} diff --git a/src/cmd/run.go b/src/cmd/run.go new file mode 100644 index 00000000..260e3992 --- /dev/null +++ b/src/cmd/run.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var ( + resyncInterval int + batchSize int +) + +var runCmd = &cobra.Command{ + Use: "run", + Short: "Commands for running a k8s controller", +} + +func init() { + rootCmd.AddCommand(runCmd) + + runCmd.PersistentFlags().IntVar(&resyncInterval, "resync", 24, "The amount (in hours) before a full resync of the kubernetes cluster happens with OpsLevel. [default: 24]") + runCmd.PersistentFlags().IntVar(&batchSize, "batch", 500, "The max amount of k8s resources to batch process with jq. Helps to speedup initial startup. [default: 500]") +} diff --git a/src/common/jqparser.go b/src/common/jqparser.go deleted file mode 100644 index e54f572b..00000000 --- a/src/common/jqparser.go +++ /dev/null @@ -1,203 +0,0 @@ -package common - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/opslevel/kubectl-opslevel/jq" - - "github.com/rs/zerolog/log" -) - -type JQParser struct { - JQ jq.JQ -} - -type JQResponseType int - -const ( - Empty JQResponseType = iota - String - StringArray - StringStringMap - StringStringMapArray - Bool - BoolArray - Unknown -) - -type JQResponse struct { - Bytes []byte - Type JQResponseType - StringObj string - StringArray []string - StringMap map[string]string - StringMapArray []map[string]string - BoolObj bool - BoolArray []bool -} - -type JQResponseMulti struct { - Bytes []byte - Objects []JQResponse -} - -func NewJQParser(filter string) JQParser { - parser := JQParser{JQ: jq.New(filter)} - return parser -} - -func NewJQParserMulti(filter string) JQParser { - parser := JQParser{JQ: jq.New(fmt.Sprintf("map((%s) // null)", filter))} - return parser -} - -func (parser *JQParser) doParse(field string, data []byte) []byte { - var bytes []byte - var err *jq.JQError - bytes, err = parser.JQ.Run(data) - if err != nil { - filter := strings.TrimSuffix(strings.TrimPrefix(parser.JQ.Filter(), "map(("), ") // null)") - log.Warn().Str("Field", field).Str("Filter", filter).Msgf(strings.ReplaceAll(err.Error(), parser.JQ.Filter(), "")) - return nil - } - return bytes -} - -func (parser *JQParser) Parse(field string, data []byte) *JQResponse { - var resp *JQResponse - if parser.JQ.Filter() == "" { - resp = &JQResponse{Bytes: []byte("")} - } else { - resp = &JQResponse{Bytes: parser.doParse(field, data)} - } - resp.Unmarshal() - return resp -} - -func (parser *JQParser) ParseMulti(field string, data []byte) *JQResponseMulti { - var resp *JQResponseMulti - if parser.JQ.Filter() == "map(() // null)" { - resp = &JQResponseMulti{Bytes: []byte("[]")} - } else { - resp = &JQResponseMulti{Bytes: parser.doParse(field, data)} - } - resp.Unmarshal() - return resp -} - -func (resp *JQResponse) Unmarshal() { - //fmt.Printf("Unmarshaling '%s'\n", string(resp.Bytes)) - if string(resp.Bytes) == "" { - resp.Type = Empty - return - } - - stringObjErr := json.Unmarshal(resp.Bytes, &resp.StringObj) - if stringObjErr == nil { - if resp.StringObj == "" { - resp.Type = Empty - return - } - resp.Type = String - return - } - - stringArrayErr := json.Unmarshal(resp.Bytes, &resp.StringArray) - if stringArrayErr == nil { - resp.Type = StringArray - return - } - - stringMapErr := json.Unmarshal(resp.Bytes, &resp.StringMap) - if stringMapErr == nil { - resp.Type = StringStringMap - return - } - - stringMapArrayErr := json.Unmarshal(resp.Bytes, &resp.StringMapArray) - if stringMapArrayErr == nil { - resp.Type = StringStringMapArray - return - } - - boolObjErr := json.Unmarshal(resp.Bytes, &resp.BoolObj) - if boolObjErr == nil { - resp.Type = Bool - return - } - - boolArrayErr := json.Unmarshal(resp.Bytes, &resp.BoolArray) - if boolArrayErr == nil { - resp.Type = BoolArray - return - } - - resp.Type = Unknown -} - -func (resp *JQResponseMulti) Unmarshal() { - //fmt.Printf("Unmarshaling '%s'\n", string(resp.Bytes)) - if string(resp.Bytes) == "[]" { - resp.Objects = nil - return - } - - var multi_stringObj []string - var multi_stringArray [][]string - var multi_stringMap []map[string]string - var multi_stringMapArray [][]map[string]string - var multi_boolObj []bool - var multi_boolArray [][]bool - - stringObjErr := json.Unmarshal(resp.Bytes, &multi_stringObj) - if stringObjErr == nil { - for _, item := range multi_stringObj { - resp.Objects = append(resp.Objects, JQResponse{Type: String, StringObj: item}) - } - return - } - - stringArrayErr := json.Unmarshal(resp.Bytes, &multi_stringArray) - if stringArrayErr == nil { - for _, item := range multi_stringArray { - resp.Objects = append(resp.Objects, JQResponse{Type: StringArray, StringArray: item}) - } - return - } - - stringMapErr := json.Unmarshal(resp.Bytes, &multi_stringMap) - if stringMapErr == nil { - for _, item := range multi_stringMap { - resp.Objects = append(resp.Objects, JQResponse{Type: StringStringMap, StringMap: item}) - } - return - } - - stringMapArrayErr := json.Unmarshal(resp.Bytes, &multi_stringMapArray) - if stringMapArrayErr == nil { - for _, item := range multi_stringMapArray { - resp.Objects = append(resp.Objects, JQResponse{Type: StringStringMapArray, StringMapArray: item}) - } - return - } - - boolObjErr := json.Unmarshal(resp.Bytes, &multi_boolObj) - if boolObjErr == nil { - for _, item := range multi_boolObj { - resp.Objects = append(resp.Objects, JQResponse{Type: Bool, BoolObj: item}) - } - return - } - - boolArrayErr := json.Unmarshal(resp.Bytes, &multi_boolArray) - if boolArrayErr == nil { - for _, item := range multi_boolArray { - resp.Objects = append(resp.Objects, JQResponse{Type: BoolArray, BoolArray: item}) - } - return - } - - resp.Objects = nil -} diff --git a/src/common/service.go b/src/common/service.go index 290220e8..f983df1c 100644 --- a/src/common/service.go +++ b/src/common/service.go @@ -2,19 +2,17 @@ package common import ( "encoding/json" - "fmt" - - "github.com/opslevel/kubectl-opslevel/config" "github.com/opslevel/kubectl-opslevel/k8sutils" + "github.com/opslevel/kubectl-opslevel/pkg/config" "github.com/opslevel/opslevel-go/v2023" - _ "github.com/rs/zerolog/log" ) -type SelectorParser struct { - Excludes []JQParser -} - +// +//type SelectorParser struct { +// Excludes []JQParser +//} +// type ServiceRegistration struct { Name string `json:",omitempty"` Description string `json:",omitempty"` @@ -36,491 +34,496 @@ func (s *ServiceRegistration) toPrettyJson() string { return string(prettyJSON) } -func (s *ServiceRegistration) mergeData(o ServiceRegistration) { - if s.Name == "" { - s.Name = o.Name - } - if s.Description == "" { - s.Description = o.Description - } - if s.Owner == "" { - s.Owner = o.Owner - } - if s.Lifecycle == "" { - s.Lifecycle = o.Lifecycle - } - if s.Tier == "" { - s.Tier = o.Tier - } - if s.Product == "" { - s.Product = o.Product - } - if s.Language == "" { - s.Language = o.Language - } - if s.Framework == "" { - s.Framework = o.Framework - } - for _, alias := range o.Aliases { - s.Aliases = append(s.Aliases, alias) - } - s.Aliases = removeDuplicates(s.Aliases) - for _, tag := range removeOverlappedKeys(s.TagAssigns, o.TagAssigns) { - s.TagAssigns = append(s.TagAssigns, tag) - } - for _, tag := range o.TagCreates { - s.TagCreates = append(s.TagCreates, tag) - } - s.TagAssigns = removeDuplicatesFromTagInputList(s.TagAssigns) - s.TagAssigns = removeOverlappedKeys(s.TagAssigns, s.TagCreates) - for _, tool := range o.Tools { - s.Tools = append(s.Tools, tool) - } - for _, repo := range o.Repositories { - s.Repositories = append(s.Repositories, repo) - } -} - -func parseField(field string, filter string, resources []byte) *JQResponseMulti { - parser := NewJQParserMulti(filter) - return parser.ParseMulti(field, resources) -} - -func parseFieldArray(field string, filters []string, resources []byte) []*JQResponseMulti { - var output []*JQResponseMulti - for i, filter := range filters { - output = append(output, parseField(fmt.Sprintf("%s[%d]", field, i+1), filter, resources)) - } - return output -} - -func contains(item opslevel.TagInput, data []opslevel.TagInput) bool { - for _, v := range data { - if item.Key == v.Key && item.Value == v.Value { - return true - } - } - return false -} - -func removeDuplicatesFromTagInputList(data []opslevel.TagInput) []opslevel.TagInput { - unique := []opslevel.TagInput{} - for _, entry := range data { - if contains(entry, unique) == false { - unique = append(unique, entry) - } - } - return unique -} - -// Also removes empty string values -func removeDuplicates(data []string) []string { - keys := make(map[string]bool) - list := []string{} - - for _, entry := range data { - if entry == "" { - continue - } - if _, value := keys[entry]; !value { - keys[entry] = true - list = append(list, entry) - } - } - return list -} - -func removeDuplicatesTags(data []opslevel.TagInput) (output []opslevel.TagInput) { - keys := make(map[string]bool) - - for _, entry := range data { - if entry.Key == "" { - continue - } - if _, value := keys[entry.Key]; !value { - keys[entry.Key] = true - output = append(output, entry) - } - } - return -} - -// https://github.com/OpsLevel/kubectl-opslevel/issues/41 -func removeOverlappedKeys(source []opslevel.TagInput, check []opslevel.TagInput) (output []opslevel.TagInput) { - for _, tagAssign := range source { - foundMatch := false - for _, tagCreate := range check { - if tagCreate.Key == tagAssign.Key { - foundMatch = true - break - } - } - if foundMatch == false { - output = append(output, tagAssign) - } - } - return -} - -func convertToToolCreateInput(data map[string]string) (*opslevel.ToolCreateInput, error) { - bytes, err := json.Marshal(data) - if err != nil { - return nil, err - } - tool := &opslevel.ToolCreateInput{} - if unmarshalErr := json.Unmarshal(bytes, tool); unmarshalErr != nil { - return nil, unmarshalErr - } - return tool, nil -} - -func convertToServiceRepositoryCreateInput(data map[string]string) *opslevel.ServiceRepositoryCreateInput { - var repoAlias string - baseDirectory := "" - displayName := "" - if val, ok := data["repo"]; ok { - repoAlias = val - } else { - return nil - } - if val, ok := data["directory"]; ok && val != "" { - baseDirectory = val - } - if val, ok := data["name"]; ok { - displayName = val - } - return &opslevel.ServiceRepositoryCreateInput{ - Repository: *opslevel.NewIdentifier(repoAlias), - BaseDirectory: baseDirectory, - DisplayName: displayName, - } -} - -func getString(index int, data *JQResponseMulti) string { - if index < len(data.Objects) { - return data.Objects[index].StringObj - } - return "" -} - -func getAliases(index int, data []*JQResponseMulti) []string { - output := []string{} - count := len(data) - for i := 0; i < count; i++ { - if data[i].Objects == nil { - continue - } - parsedData := data[i].Objects[index] - switch parsedData.Type { - case String: - if parsedData.StringObj == "" { - continue - } - output = append(output, parsedData.StringObj) - case StringArray: - for _, item := range parsedData.StringArray { - if item == "" { - continue - } - output = append(output, item) - } - // TODO: log warnings about a JQ filter that went unused because it returned an invalid type that we dont know how to handle - } - } - return removeDuplicates(output) -} - -func getTags(index int, data []*JQResponseMulti) (output []opslevel.TagInput) { - count := len(data) - for i := 0; i < count; i++ { - if data[i].Objects == nil { - continue - } - parsedData := data[i].Objects[index] - switch parsedData.Type { - case StringStringMap: - for k, v := range parsedData.StringMap { - if k == "" || v == "" { - continue - } - output = append(output, opslevel.TagInput{ - Key: k, - Value: v, - }) - } - case StringStringMapArray: - for _, item := range parsedData.StringMapArray { - for k, v := range item { - if k == "" || v == "" { - continue - } - output = append(output, opslevel.TagInput{ - Key: k, - Value: v, - }) - } - } - // TODO: log warnings about a JQ filter that went unused because it returned an invalid type that we dont know how to handle - } - } - return output -} - -func getTools(index int, data []*JQResponseMulti) []opslevel.ToolCreateInput { - output := []opslevel.ToolCreateInput{} - count := len(data) - for i := 0; i < count; i++ { - if data[i].Objects == nil { - continue - } - parsedData := data[i].Objects[index] - switch parsedData.Type { - case StringStringMap: - if parsedData.StringMap == nil { - continue - } - if input, err := convertToToolCreateInput(parsedData.StringMap); err == nil { - output = append(output, *input) - } - case StringStringMapArray: - for _, item := range parsedData.StringMapArray { - if item == nil { - continue - } - if input, err := convertToToolCreateInput(item); err == nil { - output = append(output, *input) - } - } - } - } - return output -} - -func getRepositories(index int, data []*JQResponseMulti) []opslevel.ServiceRepositoryCreateInput { - output := []opslevel.ServiceRepositoryCreateInput{} - count := len(data) - for i := 0; i < count; i++ { - if data[i].Objects == nil { - continue - } - parsedData := data[i].Objects[index] - switch parsedData.Type { - case String: - if parsedData.StringObj == "" { - continue - } - if input := convertToServiceRepositoryCreateInput(map[string]string{"repo": parsedData.StringObj}); input != nil { - output = append(output, *input) - } - case StringArray: - for _, item := range parsedData.StringArray { - if item == "" { - continue - } - if input := convertToServiceRepositoryCreateInput(map[string]string{"repo": item}); input != nil { - output = append(output, *input) - } - } - case StringStringMap: - if parsedData.StringMap == nil { - continue - } - if input := convertToServiceRepositoryCreateInput(parsedData.StringMap); input != nil { - output = append(output, *input) - } - case StringStringMapArray: - for _, item := range parsedData.StringMapArray { - if item == nil { - continue - } - if input := convertToServiceRepositoryCreateInput(item); input != nil { - output = append(output, *input) - } - } - } - } - return output -} - -var StartArray []byte = []byte(`[`) -var EndArray []byte = []byte(`]`) -var JoinItem []byte = []byte(`,`) - -func joinResourceBytes(resources [][]byte) []byte { - var output []byte - output = append(output, StartArray...) - count := len(resources) - 1 - for i, item := range resources { - output = append(output, item...) - if i < count { - output = append(output, JoinItem...) - } - } - output = append(output, EndArray...) - return output -} - -func anyIsTrue(resourceIndex int, filters []*JQResponseMulti) bool { - filtersCount := len(filters) - for filterIndex := 0; filterIndex < filtersCount; filterIndex++ { - results := filters[filterIndex].Objects - if results == nil { - return false - } - parsedData := results[resourceIndex] - switch parsedData.Type { - case Bool: - if parsedData.BoolObj { - return true - } - case BoolArray: - for _, value := range parsedData.BoolArray { - if value { - return true - } - } - } - } - return false - -} - +// +//func (s *ServiceRegistration) mergeData(o ServiceRegistration) { +// if s.Name == "" { +// s.Name = o.Name +// } +// if s.Description == "" { +// s.Description = o.Description +// } +// if s.Owner == "" { +// s.Owner = o.Owner +// } +// if s.Lifecycle == "" { +// s.Lifecycle = o.Lifecycle +// } +// if s.Tier == "" { +// s.Tier = o.Tier +// } +// if s.Product == "" { +// s.Product = o.Product +// } +// if s.Language == "" { +// s.Language = o.Language +// } +// if s.Framework == "" { +// s.Framework = o.Framework +// } +// for _, alias := range o.Aliases { +// s.Aliases = append(s.Aliases, alias) +// } +// s.Aliases = removeDuplicates(s.Aliases) +// for _, tag := range removeOverlappedKeys(s.TagAssigns, o.TagAssigns) { +// s.TagAssigns = append(s.TagAssigns, tag) +// } +// for _, tag := range o.TagCreates { +// s.TagCreates = append(s.TagCreates, tag) +// } +// s.TagAssigns = removeDuplicatesFromTagInputList(s.TagAssigns) +// s.TagAssigns = removeOverlappedKeys(s.TagAssigns, s.TagCreates) +// for _, tool := range o.Tools { +// s.Tools = append(s.Tools, tool) +// } +// for _, repo := range o.Repositories { +// s.Repositories = append(s.Repositories, repo) +// } +//} +// +//func parseField(field string, filter string, resources []byte) *JQResponseMulti { +// parser := NewJQParserMulti(filter) +// return parser.ParseMulti(field, resources) +//} +// +//func parseFieldArray(field string, filters []string, resources []byte) []*JQResponseMulti { +// var output []*JQResponseMulti +// for i, filter := range filters { +// output = append(output, parseField(fmt.Sprintf("%s[%d]", field, i+1), filter, resources)) +// } +// return output +//} +// +//func contains(item opslevel.TagInput, data []opslevel.TagInput) bool { +// for _, v := range data { +// if item.Key == v.Key && item.Value == v.Value { +// return true +// } +// } +// return false +//} +// +//func removeDuplicatesFromTagInputList(data []opslevel.TagInput) []opslevel.TagInput { +// unique := []opslevel.TagInput{} +// for _, entry := range data { +// if contains(entry, unique) == false { +// unique = append(unique, entry) +// } +// } +// return unique +//} +// +//// Also removes empty string values +//func removeDuplicates(data []string) []string { +// keys := make(map[string]bool) +// list := []string{} +// +// for _, entry := range data { +// if entry == "" { +// continue +// } +// if _, value := keys[entry]; !value { +// keys[entry] = true +// list = append(list, entry) +// } +// } +// return list +//} +// +//func removeDuplicatesTags(data []opslevel.TagInput) (output []opslevel.TagInput) { +// keys := make(map[string]bool) +// +// for _, entry := range data { +// if entry.Key == "" { +// continue +// } +// if _, value := keys[entry.Key]; !value { +// keys[entry.Key] = true +// output = append(output, entry) +// } +// } +// return +//} +// +//// https://github.com/OpsLevel/kubectl-opslevel/issues/41 +//func removeOverlappedKeys(source []opslevel.TagInput, check []opslevel.TagInput) (output []opslevel.TagInput) { +// for _, tagAssign := range source { +// foundMatch := false +// for _, tagCreate := range check { +// if tagCreate.Key == tagAssign.Key { +// foundMatch = true +// break +// } +// } +// if foundMatch == false { +// output = append(output, tagAssign) +// } +// } +// return +//} +// +//func convertToToolCreateInput(data map[string]string) (*opslevel.ToolCreateInput, error) { +// bytes, err := json.Marshal(data) +// if err != nil { +// return nil, err +// } +// tool := &opslevel.ToolCreateInput{} +// if unmarshalErr := json.Unmarshal(bytes, tool); unmarshalErr != nil { +// return nil, unmarshalErr +// } +// return tool, nil +//} +// +//func convertToServiceRepositoryCreateInput(data map[string]string) *opslevel.ServiceRepositoryCreateInput { +// var repoAlias string +// baseDirectory := "" +// displayName := "" +// if val, ok := data["repo"]; ok { +// repoAlias = val +// } else { +// return nil +// } +// if val, ok := data["directory"]; ok && val != "" { +// baseDirectory = val +// } +// if val, ok := data["name"]; ok { +// displayName = val +// } +// return &opslevel.ServiceRepositoryCreateInput{ +// Repository: *opslevel.NewIdentifier(repoAlias), +// BaseDirectory: baseDirectory, +// DisplayName: displayName, +// } +//} +// +//func getString(index int, data *JQResponseMulti) string { +// if index < len(data.Objects) { +// return data.Objects[index].StringObj +// } +// return "" +//} +// +//func getAliases(index int, data []*JQResponseMulti) []string { +// output := []string{} +// count := len(data) +// for i := 0; i < count; i++ { +// if data[i].Objects == nil { +// continue +// } +// parsedData := data[i].Objects[index] +// switch parsedData.Type { +// case String: +// if parsedData.StringObj == "" { +// continue +// } +// output = append(output, parsedData.StringObj) +// case StringArray: +// for _, item := range parsedData.StringArray { +// if item == "" { +// continue +// } +// output = append(output, item) +// } +// // TODO: log warnings about a JQ filter that went unused because it returned an invalid type that we dont know how to handle +// } +// } +// return removeDuplicates(output) +//} +// +//func getTags(index int, data []*JQResponseMulti) (output []opslevel.TagInput) { +// count := len(data) +// for i := 0; i < count; i++ { +// if data[i].Objects == nil { +// continue +// } +// parsedData := data[i].Objects[index] +// switch parsedData.Type { +// case StringStringMap: +// for k, v := range parsedData.StringMap { +// if k == "" || v == "" { +// continue +// } +// output = append(output, opslevel.TagInput{ +// Key: k, +// Value: v, +// }) +// } +// case StringStringMapArray: +// for _, item := range parsedData.StringMapArray { +// for k, v := range item { +// if k == "" || v == "" { +// continue +// } +// output = append(output, opslevel.TagInput{ +// Key: k, +// Value: v, +// }) +// } +// } +// // TODO: log warnings about a JQ filter that went unused because it returned an invalid type that we dont know how to handle +// } +// } +// return output +//} +// +//func getTools(index int, data []*JQResponseMulti) []opslevel.ToolCreateInput { +// output := []opslevel.ToolCreateInput{} +// count := len(data) +// for i := 0; i < count; i++ { +// if data[i].Objects == nil { +// continue +// } +// parsedData := data[i].Objects[index] +// switch parsedData.Type { +// case StringStringMap: +// if parsedData.StringMap == nil { +// continue +// } +// if input, err := convertToToolCreateInput(parsedData.StringMap); err == nil { +// output = append(output, *input) +// } +// case StringStringMapArray: +// for _, item := range parsedData.StringMapArray { +// if item == nil { +// continue +// } +// if input, err := convertToToolCreateInput(item); err == nil { +// output = append(output, *input) +// } +// } +// } +// } +// return output +//} +// +//func getRepositories(index int, data []*JQResponseMulti) []opslevel.ServiceRepositoryCreateInput { +// output := []opslevel.ServiceRepositoryCreateInput{} +// count := len(data) +// for i := 0; i < count; i++ { +// if data[i].Objects == nil { +// continue +// } +// parsedData := data[i].Objects[index] +// switch parsedData.Type { +// case String: +// if parsedData.StringObj == "" { +// continue +// } +// if input := convertToServiceRepositoryCreateInput(map[string]string{"repo": parsedData.StringObj}); input != nil { +// output = append(output, *input) +// } +// case StringArray: +// for _, item := range parsedData.StringArray { +// if item == "" { +// continue +// } +// if input := convertToServiceRepositoryCreateInput(map[string]string{"repo": item}); input != nil { +// output = append(output, *input) +// } +// } +// case StringStringMap: +// if parsedData.StringMap == nil { +// continue +// } +// if input := convertToServiceRepositoryCreateInput(parsedData.StringMap); input != nil { +// output = append(output, *input) +// } +// case StringStringMapArray: +// for _, item := range parsedData.StringMapArray { +// if item == nil { +// continue +// } +// if input := convertToServiceRepositoryCreateInput(item); input != nil { +// output = append(output, *input) +// } +// } +// } +// } +// return output +//} +// +//var StartArray []byte = []byte(`[`) +//var EndArray []byte = []byte(`]`) +//var JoinItem []byte = []byte(`,`) +// +//func joinResourceBytes(resources [][]byte) []byte { +// var output []byte +// output = append(output, StartArray...) +// count := len(resources) - 1 +// for i, item := range resources { +// output = append(output, item...) +// if i < count { +// output = append(output, JoinItem...) +// } +// } +// output = append(output, EndArray...) +// return output +//} +// +//func anyIsTrue(resourceIndex int, filters []*JQResponseMulti) bool { +// filtersCount := len(filters) +// for filterIndex := 0; filterIndex < filtersCount; filterIndex++ { +// results := filters[filterIndex].Objects +// if results == nil { +// return false +// } +// parsedData := results[resourceIndex] +// switch parsedData.Type { +// case Bool: +// if parsedData.BoolObj { +// return true +// } +// case BoolArray: +// for _, value := range parsedData.BoolArray { +// if value { +// return true +// } +// } +// } +// } +// return false +// +//} +// func FilterResources(selector k8sutils.KubernetesSelector, resources [][]byte) [][]byte { - var output [][]byte - resourceCount := len(resources) - // Parse - filterResults := parseFieldArray("selector.excludes", selector.Excludes, joinResourceBytes(resources)) - - // Aggregate - for resourceIndex := 0; resourceIndex < resourceCount; resourceIndex++ { - if anyIsTrue(resourceIndex, filterResults) { - continue - } - output = append(output, resources[resourceIndex]) - } - return output -} - -func aliasOverlaps(a []string, b []string) bool { - for _, i := range a { - for _, j := range b { - if i == j { - return true - } - } - } - return false -} - -// TODO: bubble up errors better -func parseResources(field string, c config.ServiceRegistrationConfig, count int, resources []byte) ([]ServiceRegistration, error) { - services := make([]ServiceRegistration, count) - - // Parse - Names := parseField(fmt.Sprintf("%s.name", field), c.Name, resources) - Descriptions := parseField(fmt.Sprintf("%s.description", field), c.Description, resources) - Owners := parseField(fmt.Sprintf("%s.owner", field), c.Owner, resources) - Lifecycles := parseField(fmt.Sprintf("%s.lifecycle", field), c.Lifecycle, resources) - Tiers := parseField(fmt.Sprintf("%s.tier", field), c.Tier, resources) - Products := parseField(fmt.Sprintf("%s.product", field), c.Product, resources) - Languages := parseField(fmt.Sprintf("%s.language", field), c.Language, resources) - Frameworks := parseField(fmt.Sprintf("%s.framework", field), c.Framework, resources) - Aliases := parseFieldArray(fmt.Sprintf("%s.aliases", field), c.Aliases, resources) - if len(Aliases) < 1 { - Aliases = append(Aliases, parseField("Auto Added Alias", "\"k8s:\\(.metadata.name)-\\(.metadata.namespace)\"", resources)) - } - TagAssigns := parseFieldArray(fmt.Sprintf("%s.tags.assign", field), c.Tags.Assign, resources) - TagCreates := parseFieldArray(fmt.Sprintf("%s.tags.create", field), c.Tags.Create, resources) - Tools := parseFieldArray(fmt.Sprintf("%s.tools", field), c.Tools, resources) - Repositories := parseFieldArray(fmt.Sprintf("%s.repository", field), c.Repositories, resources) - - // Aggregate - for i := 0; i < count; i++ { - service := &services[i] - - service.Name = getString(i, Names) - service.Description = getString(i, Descriptions) - service.Owner = getString(i, Owners) - service.Lifecycle = getString(i, Lifecycles) - service.Tier = getString(i, Tiers) - service.Product = getString(i, Products) - service.Language = getString(i, Languages) - service.Framework = getString(i, Frameworks) - service.Aliases = getAliases(i, Aliases) - service.TagAssigns = getTags(i, TagAssigns) - service.TagCreates = getTags(i, TagCreates) - service.TagCreates = removeDuplicatesTags(service.TagCreates) - service.TagAssigns = removeOverlappedKeys(service.TagAssigns, service.TagCreates) - service.Tools = getTools(i, Tools) - service.Repositories = getRepositories(i, Repositories) - } - - return services, nil -} - -func dedupServices(input []ServiceRegistration) ([]ServiceRegistration, error) { - var output []ServiceRegistration - for _, source := range input { - wasMerged := false - for i, dest := range output { - if aliasOverlaps(source.Aliases, dest.Aliases) { - dest.mergeData(source) - output[i] = dest - wasMerged = true - break - } - } - if !wasMerged { - output = append(output, source) - } - } - return output, nil -} - -func getServices(c *config.Config) ([]ServiceRegistration, error) { - var services []ServiceRegistration - k8sClient := k8sutils.CreateKubernetesClient() - for i, importConfig := range c.Service.Import { - selector := importConfig.SelectorConfig - if selectorErr := selector.Validate(); selectorErr != nil { - return services, selectorErr - } - - resources, queryErr := k8sClient.Query(selector) - if queryErr != nil { - return services, queryErr - } - - parsedServices, parsedServicesErr := ProcessResources(fmt.Sprintf("service.import[%d]", i+1), importConfig, resources) - if parsedServicesErr != nil { - return services, parsedServicesErr - } - - services = append(services, parsedServices...) - } - return services, nil + //var output [][]byte + //resourceCount := len(resources) + //// Parse + //filterResults := parseFieldArray("selector.excludes", selector.Excludes, joinResourceBytes(resources)) + // + //// Aggregate + //for resourceIndex := 0; resourceIndex < resourceCount; resourceIndex++ { + // if anyIsTrue(resourceIndex, filterResults) { + // continue + // } + // output = append(output, resources[resourceIndex]) + //} + //return output + return nil } +// +//func aliasOverlaps(a []string, b []string) bool { +// for _, i := range a { +// for _, j := range b { +// if i == j { +// return true +// } +// } +// } +// return false +//} +// +//// TODO: bubble up errors better +//func parseResources(field string, c config.ServiceRegistrationConfig, count int, resources []byte) ([]ServiceRegistration, error) { +// services := make([]ServiceRegistration, count) +// +// // Parse +// Names := parseField(fmt.Sprintf("%s.name", field), c.Name, resources) +// Descriptions := parseField(fmt.Sprintf("%s.description", field), c.Description, resources) +// Owners := parseField(fmt.Sprintf("%s.owner", field), c.Owner, resources) +// Lifecycles := parseField(fmt.Sprintf("%s.lifecycle", field), c.Lifecycle, resources) +// Tiers := parseField(fmt.Sprintf("%s.tier", field), c.Tier, resources) +// Products := parseField(fmt.Sprintf("%s.product", field), c.Product, resources) +// Languages := parseField(fmt.Sprintf("%s.language", field), c.Language, resources) +// Frameworks := parseField(fmt.Sprintf("%s.framework", field), c.Framework, resources) +// Aliases := parseFieldArray(fmt.Sprintf("%s.aliases", field), c.Aliases, resources) +// if len(Aliases) < 1 { +// Aliases = append(Aliases, parseField("Auto Added Alias", "\"k8s:\\(.metadata.name)-\\(.metadata.namespace)\"", resources)) +// } +// TagAssigns := parseFieldArray(fmt.Sprintf("%s.tags.assign", field), c.Tags.Assign, resources) +// TagCreates := parseFieldArray(fmt.Sprintf("%s.tags.create", field), c.Tags.Create, resources) +// Tools := parseFieldArray(fmt.Sprintf("%s.tools", field), c.Tools, resources) +// Repositories := parseFieldArray(fmt.Sprintf("%s.repository", field), c.Repositories, resources) +// +// // Aggregate +// for i := 0; i < count; i++ { +// service := &services[i] +// +// service.Name = getString(i, Names) +// service.Description = getString(i, Descriptions) +// service.Owner = getString(i, Owners) +// service.Lifecycle = getString(i, Lifecycles) +// service.Tier = getString(i, Tiers) +// service.Product = getString(i, Products) +// service.Language = getString(i, Languages) +// service.Framework = getString(i, Frameworks) +// service.Aliases = getAliases(i, Aliases) +// service.TagAssigns = getTags(i, TagAssigns) +// service.TagCreates = getTags(i, TagCreates) +// service.TagCreates = removeDuplicatesTags(service.TagCreates) +// service.TagAssigns = removeOverlappedKeys(service.TagAssigns, service.TagCreates) +// service.Tools = getTools(i, Tools) +// service.Repositories = getRepositories(i, Repositories) +// } +// +// return services, nil +//} +// +//func dedupServices(input []ServiceRegistration) ([]ServiceRegistration, error) { +// var output []ServiceRegistration +// for _, source := range input { +// wasMerged := false +// for i, dest := range output { +// if aliasOverlaps(source.Aliases, dest.Aliases) { +// dest.mergeData(source) +// output[i] = dest +// wasMerged = true +// break +// } +// } +// if !wasMerged { +// output = append(output, source) +// } +// } +// return output, nil +//} +// +//func getServices(c *config.Config) ([]ServiceRegistration, error) { +// var services []ServiceRegistration +// k8sClient := k8sutils.CreateKubernetesClient() +// for i, importConfig := range c.Service.Import { +// selector := importConfig.SelectorConfig +// if selectorErr := selector.Validate(); selectorErr != nil { +// return services, selectorErr +// } +// +// resources, queryErr := k8sClient.Query(selector) +// if queryErr != nil { +// return services, queryErr +// } +// +// parsedServices, parsedServicesErr := ProcessResources(fmt.Sprintf("service.import[%d]", i+1), importConfig, resources) +// if parsedServicesErr != nil { +// return services, parsedServicesErr +// } +// +// services = append(services, parsedServices...) +// } +// return services, nil +//} +// func GetAllServices(c *config.Config) ([]ServiceRegistration, error) { - services, err := getServices(c) - if err != nil { - return nil, err - } - return dedupServices(services) + //services, err := getServices(c) + //if err != nil { + // return nil, err + //} + //return dedupServices(services) + return nil, nil } func ProcessResources(field string, config config.Import, resources [][]byte) ([]ServiceRegistration, error) { - filtered := FilterResources(config.SelectorConfig, resources) - if len(filtered) < 1 { - return []ServiceRegistration{}, nil - } - parsed, parseError := parseResources(field, config.OpslevelConfig, len(filtered), joinResourceBytes(filtered)) - if parseError != nil { - return nil, parseError - } - deduped, dedupErr := dedupServices(parsed) - if dedupErr != nil { - return nil, dedupErr - } - return deduped, nil + //filtered := FilterResources(config.SelectorConfig, resources) + //if len(filtered) < 1 { + // return []ServiceRegistration{}, nil + //} + //parsed, parseError := parseResources(field, config.OpslevelConfig, len(filtered), joinResourceBytes(filtered)) + //if parseError != nil { + // return nil, parseError + //} + //deduped, dedupErr := dedupServices(parsed) + //if dedupErr != nil { + // return nil, dedupErr + //} + //return deduped, nil + return nil, nil } diff --git a/src/common/service_test.go b/src/common/service_test.go index cb572344..43cdf775 100644 --- a/src/common/service_test.go +++ b/src/common/service_test.go @@ -1,55 +1,48 @@ package common -import ( - "testing" - - "github.com/opslevel/opslevel-go/v2023" - "github.com/rocktavious/autopilot" -) - -func Test_RemoveDuplicatesTagAssign_WhenNoDuplicatesExist(t *testing.T) { - // Arrange - input := []opslevel.TagInput{ - { - Key: "foo", - Value: "bar", - }, - { - Key: "hello", - Value: "world", - }, - { - Key: "apple", - Value: "orange", - }, - } - - // Act - result := removeDuplicatesFromTagInputList(input) - - // Assert - autopilot.Equals(t, 3, len(result)) -} - -func Test_RemoveDuplicatesTagAssign_WhenDuplicatesExist(t *testing.T) { - // Arrange - input := []opslevel.TagInput{ - { - Key: "foo", - Value: "bar", - }, - { - Key: "hello", - Value: "world", - }, - { - Key: "foo", - Value: "bar", - }, - } - // Act - result := removeDuplicatesFromTagInputList(input) - - // Assert - autopilot.Equals(t, 2, len(result)) -} +//func Test_RemoveDuplicatesTagAssign_WhenNoDuplicatesExist(t *testing.T) { +// // Arrange +// input := []opslevel.TagInput{ +// { +// Key: "foo", +// Value: "bar", +// }, +// { +// Key: "hello", +// Value: "world", +// }, +// { +// Key: "apple", +// Value: "orange", +// }, +// } +// +// // Act +// result := removeDuplicatesFromTagInputList(input) +// +// // Assert +// autopilot.Equals(t, 3, len(result)) +//} +// +//func Test_RemoveDuplicatesTagAssign_WhenDuplicatesExist(t *testing.T) { +// // Arrange +// input := []opslevel.TagInput{ +// { +// Key: "foo", +// Value: "bar", +// }, +// { +// Key: "hello", +// Value: "world", +// }, +// { +// Key: "foo", +// Value: "bar", +// }, +// } +// // Act +// result := removeDuplicatesFromTagInputList(input) +// +// // Assert +// autopilot.Equals(t, 2, len(result)) +//} diff --git a/src/jq/jq.go b/src/jq/jq.go deleted file mode 100644 index d518ba58..00000000 --- a/src/jq/jq.go +++ /dev/null @@ -1,139 +0,0 @@ -package jq - -import ( - "bytes" - "context" - "fmt" - "io" - "io/ioutil" - "log" - "os/exec" - "strings" - "time" -) - -type JQ struct { - options []string - timeout time.Duration - writer io.Writer -} - -type JQOpt struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` -} - -type JQErrorType int - -const ( - EmptyFilter JQErrorType = iota - BadOptions - BadFilter - BadJSON - BadExcution - Unknown -) - -type JQError struct { - Message string - Type JQErrorType -} - -func (e *JQError) Error() string { - switch e.Type { - case EmptyFilter: - return "Empty JQ Filter" - case BadOptions: - return fmt.Sprintf("Invalid JQ Options %s", e.Message) - case BadFilter: - return fmt.Sprintf("Invalid JQ Filter %s", e.Message) - case BadJSON: - return fmt.Sprintf("Invalid Json %s", e.Message) - case BadExcution: - return fmt.Sprintf("Failed JQ Execution %s", strings.TrimSuffix(e.Message, "\n")) - case Unknown: - return fmt.Sprintf("Unknown JQ Error %s", e.Message) - } - panic(fmt.Sprintf("Unknown JQ Error %s", e.Message)) -} - -func (jq *JQ) Filter() string { - return jq.options[len(jq.options)-1] -} - -func (jq *JQ) Options() []string { - return jq.options[:len(jq.options)-1] -} - -func (jq *JQ) Commandline() string { - return fmt.Sprintf("jq %s", strings.Join(jq.options, " ")) -} - -func (jq *JQ) Run(json []byte) ([]byte, *JQError) { - var stderr bytes.Buffer - ctx, cancel := context.WithTimeout(context.Background(), jq.timeout) - //fmt.Printf("Exec: `jq %s`\n", strings.Join(jq.options, " ")) - cmd := exec.CommandContext(ctx, "jq", jq.options...) - cmd.Stdin = bytes.NewBuffer(json) - cmd.Stderr = &stderr - cmd.Env = make([]string, 0) - defer cancel() - out, err := cmd.Output() - if err != nil { - //fmt.Println("Got Error on JQ Execution") - //fmt.Println(err.Error()) - //fmt.Println(string(stderr.Bytes())) - // TODO: printing out that it couldn't find JQ binary - if err.Error() == "exit status 2" { - return nil, &JQError{Message: jq.Commandline(), Type: BadOptions} - } - if err.Error() == "exit status 3" { - return nil, &JQError{Message: jq.Filter(), Type: BadFilter} - } - if err.Error() == "exit status 4" { - return nil, &JQError{Message: string(json), Type: BadJSON} - } - if err.Error() == "exit status 5" { - return nil, &JQError{Message: string(stderr.Bytes()), Type: BadExcution} - } - return nil, &JQError{Message: string(stderr.Bytes()), Type: BadExcution} - } - return out, nil -} - -func (jq *JQ) Validate(json []byte) *JQError { - filter := jq.Filter() - if filter == "" { - return &JQError{Message: filter, Type: EmptyFilter} - } - _, err := jq.Run(json) - return err -} - -func ValidateInstalled() { - _, err := exec.LookPath("jq") - if err != nil { - log.Fatal(fmt.Errorf("%s\nPlease install 'jq' to use this tool - https://stedolan.github.io/jq/download/", err.Error())) - log.Fatal(err) - } -} - -func New(filter string) JQ { - return NewWithOptions(filter, 8*time.Second, nil) -} - -func NewWithOptions(filter string, timeout time.Duration, options []JQOpt) JQ { - opts := []string{} - for _, opt := range options { - if opt.Enabled { - opts = append(opts, fmt.Sprintf("--%s", opt.Name)) - } - } - opts = append(opts, fmt.Sprintf("%s", filter)) - jq := &JQ{ - options: opts, - timeout: timeout, - writer: ioutil.Discard, - } - return *jq -} diff --git a/src/config/config.go b/src/pkg/config/config.go similarity index 88% rename from src/config/config.go rename to src/pkg/config/config.go index dbec54ec..c2cd838d 100644 --- a/src/config/config.go +++ b/src/pkg/config/config.go @@ -3,15 +3,15 @@ package config import ( "errors" "fmt" + "gopkg.in/yaml.v3" "github.com/opslevel/kubectl-opslevel/k8sutils" "github.com/creasty/defaults" - "github.com/spf13/viper" ) var ( - ConfigCurrentVersion = "1.2.0" + ConfigCurrentVersion = "1.3.0" ) type TagRegistrationConfig struct { @@ -28,6 +28,7 @@ type ServiceRegistrationConfig struct { Product string `json:"product"` Language string `json:"language"` Framework string `json:"framework"` + System string `json:"system"` Aliases []string `json:"aliases"` // JQ expressions that return a single string or a []string Tags TagRegistrationConfig `json:"tags"` Tools []string `json:"tools"` // JQ expressions that return a single map[string]string or a []map[string]string @@ -57,15 +58,20 @@ type ConfigVersion struct { Version string } -func New() (*Config, error) { +func NewConfig(data []byte) (*Config, error) { v := &ConfigVersion{} - viper.Unmarshal(&v) + if err := yaml.Unmarshal(data, &v); err != nil { + return nil, err + } + if v.Version != ConfigCurrentVersion { return nil, errors.New(fmt.Sprintf("Supported config version is '%s' but found '%s' | Please update config file or create a new sample with `kubectl opslevel config sample`", ConfigCurrentVersion, v.Version)) } c := &Config{} - viper.Unmarshal(&c) + if err := yaml.Unmarshal(data, &c); err != nil { + return nil, err + } if err := defaults.Set(c); err != nil { return c, err } diff --git a/src/pkg/config/config_samples.go b/src/pkg/config/config_samples.go new file mode 100644 index 00000000..20253cf6 --- /dev/null +++ b/src/pkg/config/config_samples.go @@ -0,0 +1,102 @@ +package config + +import ( + "bytes" + "gopkg.in/yaml.v3" +) + +// Make sure we only use spaces inside of these samples +var Simple = []byte(`#Simple Opslevel CLI Config +version: "1.3.0" +service: + import: + - selector: &deployments + apiVersion: apps/v1 # only supports resources found in 'kubectl api-resources --verbs="get,list"' + kind: Deployment + opslevel: # This is how you map your kubernetes data to opslevel service using jq expressions + name: .metadata.name + owner: .metadata.namespace + aliases: # This are how we identify the services again during reconciliation - please make sure they are very unique + - '"k8s:\(.metadata.name)-\(.metadata.namespace)"' + tags: + assign: + - '{"imported": "kubectl-opslevel"}' + collect: + - selector: + !!merge <<: *deployments +`) + +var Sample = []byte(`#Sample Opslevel CLI Config +version: "1.3.0" +service: + import: + - selector: # This limits what data we look at in Kubernetes + apiVersion: apps/v1 # only supports resources found in 'kubectl api-resources --verbs="get,list"' + kind: Deployment + excludes: # filters out resources if any expression returns truthy + - .metadata.namespace == "kube-system" + - .metadata.annotations."opslevel.com/ignore" + opslevel: # This is how you map your kubernetes data to opslevel service using jq expressions + name: .metadata.name + description: .metadata.annotations."opslevel.com/description" + owner: .metadata.annotations."opslevel.com/owner" + lifecycle: .metadata.annotations."opslevel.com/lifecycle" + tier: .metadata.annotations."opslevel.com/tier" + product: .metadata.annotations."opslevel.com/product" + language: .metadata.annotations."opslevel.com/language" + framework: .metadata.annotations."opslevel.com/framework" + aliases: # This are how we identify the services again during reconciliation - please make sure they are very unique + - '"k8s:\(.metadata.name)-\(.metadata.namespace)"' + tags: + assign: # tag with the same key name but with a different value will be updated on the service + - '{"imported": "kubectl-opslevel"}' + # find annoations with format: opslevel.com/tags.: + - '.metadata.annotations | to_entries | map(select(.key | startswith("opslevel.com/tags"))) | map({(.key | split(".")[2]): .value})' + - .metadata.labels + create: # tag with the same key name but with a different value with be added to the service + - '{"environment": .spec.template.metadata.labels.environment}' + tools: + - '{"category": "other", "environment": "production", "displayName": "my-cool-tool", "url": .metadata.annotations."example.com/my-cool-tool"} | if .url then . else empty end' + # find annotations with format: opslevel.com/tools..: + - '.metadata.annotations | to_entries | map(select(.key | startswith("opslevel.com/tools"))) | map({"category": .key | split(".")[2], "displayName": .key | split(".")[3], "url": .value})' + # OR find annotations with format: opslevel.com/tools...: + # - '.metadata.annotations | to_entries | map(select(.key | startswith("opslevel.com/tools"))) | map({"category": .key | split(".")[2], "environment": .key | split(".")[3], "displayName": .key | split(".")[4], "url": .value})' + repositories: # attach repositories to the service using the opslevel repo alias - IE github.com:hashicorp/vault + - '{"name": "My Cool Repo", "directory": "/", "repo": .metadata.annotations.repo} | if .repo then . else empty end' + # if just the alias is returned as a single string we'll build the name for you and set the directory to "/" + - .metadata.annotations.repo + # find annotations with format: opslevel.com/repo..: + - '.metadata.annotations | to_entries | map(select(.key | startswith("opslevel.com/repos"))) | map({"name": .key | split(".")[2], "directory": .key | split(".")[3:] | join("/"), "repo": .value})' + collect: + - selector: # This limits what data we look at in Kubernetes + apiVersion: apps/v1 # only supports resources found in 'kubectl api-resources --verbs="get,list"' + kind: Deployment + excludes: # filters out resources if any expression returns truthy + - .metadata.namespace == "kube-system" + - .metadata.annotations."opslevel.com/ignore" +`) + +func pick(simple bool) []byte { + if simple == true { + return Simple + } else { + return Sample + } +} + +func GetSample(simple bool) string { + // we use yaml unmarshal to prove that the samples are valid yaml + var nodes yaml.Node + unmarshalErr := yaml.Unmarshal(pick(simple), &nodes) + if unmarshalErr != nil { + return unmarshalErr.Error() + } + var b bytes.Buffer + yamlEncoder := yaml.NewEncoder(&b) + yamlEncoder.SetIndent(2) + encodeErr := yamlEncoder.Encode(&nodes) + if encodeErr != nil { + return encodeErr.Error() + } + return string(b.Bytes()) +} diff --git a/src/pkg/config/config_test.go b/src/pkg/config/config_test.go new file mode 100644 index 00000000..4ef8be60 --- /dev/null +++ b/src/pkg/config/config_test.go @@ -0,0 +1,47 @@ +package config_test + +import ( + "github.com/opslevel/kubectl-opslevel/pkg/config" + "github.com/rocktavious/autopilot" + "testing" +) + +func TestConfig(t *testing.T) { + // Arrange + simple := config.GetSample(true) + sample := config.GetSample(false) + custom := []byte(`version: "1.3.0" +service: + import: + - selector: &deployments + apiVersion: apps/v1 # only supports resources found in 'kubectl api-resources --verbs="get,list"' + kind: Deployment + opslevel: + name: .metadata.name + collect: + - selector: + <<: *deployments +`) + // Act + cfg, err := config.NewConfig(custom) + // Assert + autopilot.Ok(t, err) + autopilot.Equals(t, simple, string(config.Simple)) + autopilot.Equals(t, sample, string(config.Sample)) + autopilot.Equals(t, config.ConfigCurrentVersion, cfg.Version) + autopilot.Equals(t, "Deployment", cfg.Service.Import[0].SelectorConfig.Kind) + autopilot.Equals(t, ".metadata.name", cfg.Service.Import[0].OpslevelConfig.Name) + autopilot.Equals(t, "Deployment", cfg.Service.Collect[0].SelectorConfig.Kind) +} + +func TestConfigErrors(t *testing.T) { + // Arrange + not_yaml := []byte(`{"version": "1.3.0" +service: +`) + // Act + cfg, err := config.NewConfig(not_yaml) + // Assert + autopilot.Assert(t, err != nil, "Parsed Invalid YAML") + autopilot.Assert(t, cfg == nil, "Returned non-nil Config") +} diff --git a/src/pkg/jq/jq.go b/src/pkg/jq/jq.go new file mode 100644 index 00000000..92bb62fc --- /dev/null +++ b/src/pkg/jq/jq.go @@ -0,0 +1,117 @@ +package jq + +import ( + "bytes" + "context" + "fmt" + "github.com/rs/zerolog/log" + "io" + "os/exec" + "strings" + "time" +) + +var _validated = false + +type JQ struct { + binary string + options []string + filter string + timeout time.Duration + writer io.Writer + validated bool +} + +func NewJQ() *JQ { + return &JQ{ + binary: "jq", + filter: ".", + options: []string{}, + timeout: 10 * time.Second, + writer: io.Discard, + } +} + +func (jq *JQ) validate() bool { + if jq.validated { + return true + } + _, err := exec.LookPath(jq.binary) + if err != nil { + return false + } + jq.validated = true + return true +} + +func (jq *JQ) WithBinary(binary string) *JQ { + jq.binary = binary + return jq +} + +func (jq *JQ) WithOption(option string) *JQ { + jq.options = append(jq.options, option) + return jq +} + +func (jq *JQ) Raw() *JQ { + return jq.WithOption("-r") +} + +func (jq *JQ) WithFilter(filter string) *JQ { + jq.filter = filter + return jq +} + +func (jq *JQ) WithTimeout(timeout time.Duration) *JQ { + jq.timeout = timeout + return jq +} + +func (jq *JQ) commandline() string { + return fmt.Sprintf("%s %s %s", jq.binary, strings.Join(jq.options, " "), jq.filter) +} + +func (jq *JQ) run(ctx context.Context, data []byte, stderr *bytes.Buffer) ([]byte, error) { + log.Debug().Msgf("Exec: '%s'", jq.commandline()) + cmd := exec.CommandContext(ctx, jq.binary, append(jq.options, jq.filter)...) + cmd.Stdin = bytes.NewBuffer(data) + cmd.Stderr = stderr + cmd.Env = make([]string, 0) + return cmd.Output() +} + +func (jq *JQ) Exec(data []byte) (*JQResponse, error) { + if jq.validate() == false { + //log.Fatal().Msgf("Please install '%s' to use this tool - https://stedolan.github.io/jq/download/", jq.binary) + return nil, JQError{Message: jq.binary, Type: ExecutableNotFound} + } + if jq.filter == "" { + return nil, JQError{Message: jq.filter, Type: EmptyFilter} + } + var stderr bytes.Buffer + ctx, cancel := context.WithTimeout(context.Background(), jq.timeout) + defer cancel() + out, err := jq.run(ctx, data, &stderr) + if err != nil { + log.Debug().Err(err).Str("stderr", stderr.String()).Msg("error occurred while executing JQ") + if err.Error() == "exit status 2" { + return nil, JQError{Message: jq.commandline(), Type: BadOptions} + } + if err.Error() == "exit status 3" { + return nil, JQError{Message: jq.filter, Type: BadFilter} + } + if err.Error() == "exit status 4" { + return nil, JQError{Message: string(data), Type: BadJSON} + } + if err.Error() == "exit status 5" { + return nil, JQError{Message: stderr.String(), Type: BadExcution} + } + return nil, JQError{Message: stderr.String(), Type: BadExcution} + } + return NewResponse(out), nil +} + +func (jq *JQ) Run(data string) (*JQResponse, error) { + return jq.Exec([]byte(data)) +} diff --git a/src/pkg/jq/jq_error.go b/src/pkg/jq/jq_error.go new file mode 100644 index 00000000..85c2e800 --- /dev/null +++ b/src/pkg/jq/jq_error.go @@ -0,0 +1,43 @@ +package jq + +import ( + "fmt" + "strings" +) + +type JQErrorType int + +const ( + EmptyFilter JQErrorType = iota + BadOptions + BadFilter + BadJSON + BadExcution + ExecutableNotFound + UnknownError +) + +type JQError struct { + Message string + Type JQErrorType +} + +func (e JQError) Error() string { + switch e.Type { + case EmptyFilter: + return "Empty JQ Filter" + case BadOptions: + return fmt.Sprintf("Invalid JQ Options %s", e.Message) + case BadFilter: + return fmt.Sprintf("Invalid JQ Filter %s", e.Message) + case BadJSON: + return fmt.Sprintf("Invalid Json %s", e.Message) + case BadExcution: + return fmt.Sprintf("Failed JQ Execution %s", strings.TrimSuffix(e.Message, "\n")) + case ExecutableNotFound: + return fmt.Sprintf("Executable Not Found %s", e.Message) + case UnknownError: + return fmt.Sprintf("Unknown JQ Error %s", e.Message) + } + panic(fmt.Sprintf("Unknown JQ Error %s", e.Message)) +} diff --git a/src/pkg/jq/jq_response.go b/src/pkg/jq/jq_response.go new file mode 100644 index 00000000..d29be67f --- /dev/null +++ b/src/pkg/jq/jq_response.go @@ -0,0 +1,68 @@ +package jq + +import "encoding/json" + +type JQResponseType int + +const ( + Empty JQResponseType = iota + String + Map + ArrayMap + Array + Unknown +) + +type JQResponse struct { + Type JQResponseType + Raw []byte + Data any +} + +func NewResponse(data []byte) (resp *JQResponse) { + var _obj string + var _map map[string]any + var _mapArray []map[string]any + var _array []any + + resp = &JQResponse{ + Type: Unknown, + Raw: data, + Data: data, + } + + if string(data) == "" { + resp.Type = Empty + return + } + + if err := json.Unmarshal(data, &_obj); err == nil { + resp.Data = _obj + if _obj == "" { + resp.Type = Empty + return + } + resp.Type = String + return + } + + if err := json.Unmarshal(data, &_map); err == nil { + resp.Data = _map + resp.Type = Map + return + } + + if err := json.Unmarshal(data, &_mapArray); err == nil { + resp.Data = _mapArray + resp.Type = ArrayMap + return + } + + if err := json.Unmarshal(data, &_array); err == nil { + resp.Data = _array + resp.Type = Array + return + } + + return +} diff --git a/src/pkg/jq/jq_test.go b/src/pkg/jq/jq_test.go new file mode 100644 index 00000000..0a2bc502 --- /dev/null +++ b/src/pkg/jq/jq_test.go @@ -0,0 +1,123 @@ +package jq_test + +import ( + "github.com/opslevel/kubectl-opslevel/pkg/jq" + "github.com/rocktavious/autopilot" + "testing" + "time" +) + +func TestJQ(t *testing.T) { + // Arrange + data := `{"metadata":{"name":"test"}}` + jq1 := jq.NewJQ().WithTimeout(1 * time.Second).WithFilter(".metadata.name") + // Act + result1, err1 := jq1.Run(data) + // Assert + autopilot.Ok(t, err1) + autopilot.Equals(t, "test", result1.Data) +} + +func TestJQRaw(t *testing.T) { + // Arrange + data := `{"metadata":{"name":"test"}}` + jq1 := jq.NewJQ().Raw().WithFilter(".metadata.name") + // Act + result1, err1 := jq1.Run(data) + // Assert + autopilot.Ok(t, err1) + autopilot.Equals(t, []byte("test\n"), result1.Data) +} + +func TestJQErrors(t *testing.T) { + // Arrange + data := `{"metadata":{"name":"test"}}` + bad := `{[}]` + jq1 := jq.NewJQ().WithTimeout(1).WithFilter(".metadata.name") + jq2 := jq.NewJQ().WithFilter("") + jq3 := jq.NewJQ().WithOption("--p") + jq4 := jq.NewJQ().WithFilter("..p") + jq5 := jq.NewJQ() + jq6 := jq.NewJQ().WithBinary("gojq") + // Act + _, err1 := jq1.Run(data) + _, err2 := jq2.Run(data) + _, err3 := jq3.Run(data) + _, err4 := jq4.Run(data) + _, err5 := jq5.Run(bad) + _, err6 := jq6.Run(data) + // Assert + autopilot.Assert(t, err1 != nil, "Timeout Didn't Happen") + autopilot.Assert(t, (err1.(jq.JQError)).Type == jq.BadExcution, "Error Type Was Wrong") + autopilot.Assert(t, err2 != nil, "Didn't Handle Empty Filter") + autopilot.Assert(t, (err2.(jq.JQError)).Type == jq.EmptyFilter, "Error Type Was Wrong") + autopilot.Assert(t, err3 != nil, "Didn't Handle Bad Options") + autopilot.Assert(t, (err3.(jq.JQError)).Type == jq.BadOptions, "Error Type Was Wrong") + autopilot.Assert(t, err4 != nil, "Didn't Handle Bad Filter") + autopilot.Assert(t, (err4.(jq.JQError)).Type == jq.BadFilter, "Error Type Was Wrong") + autopilot.Assert(t, err5 != nil, "Didn't Handle Bad JSON") + autopilot.Assert(t, (err5.(jq.JQError)).Type == jq.BadJSON, "Error Type Was Wrong") + autopilot.Assert(t, err6 != nil, "Didn't Handle Invalid Binary") + autopilot.Assert(t, (err6.(jq.JQError)).Type == jq.ExecutableNotFound, "Error Type Was Wrong") + +} + +func TestJQMulti(t *testing.T) { + // Arrange + jq1 := jq.NewJQ().WithFilter("map((.metadata.name) // null)") + // Act + result1, err1 := jq1.Run(`[{"metadata":{"name":"test1"}},{"metadata":{"name":"test2"}}]`) + // Assert + autopilot.Ok(t, err1) + autopilot.Equals(t, []any{"test1", "test2"}, result1.Data) +} + +func TestJQReponses_Empty(t *testing.T) { + // Arrange + data := `{"metadata":{"name":""}}` + jq1 := jq.NewJQ().WithFilter(".metadata.name") + jq2 := jq.NewJQ().WithFilter(".metadata.test") + // Act + result1, err1 := jq1.Run(data) + result2, err2 := jq2.Run(data) + // Assert + autopilot.Ok(t, err1) + autopilot.Equals(t, jq.Empty, result1.Type) + autopilot.Equals(t, "", result1.Data) + autopilot.Ok(t, err2) + autopilot.Equals(t, jq.Empty, result2.Type) + autopilot.Equals(t, "", result2.Data) +} + +func TestJQReponses_Map(t *testing.T) { + // Arrange + jq1 := jq.NewJQ().WithFilter("{\"key\": .metadata.name}") + // Act + result1, err1 := jq1.Run(`{"metadata":{"name":"test2"}}`) + // Assert + autopilot.Ok(t, err1) + autopilot.Equals(t, jq.Map, result1.Type) + autopilot.Equals(t, map[string]any{"key": "test2"}, result1.Data) +} + +func TestJQReponses_ArrayMap(t *testing.T) { + // Arrange + jq1 := jq.NewJQ().WithFilter("map({\"key\": .metadata.name} // null)") + // Act + result1, err1 := jq1.Run(`[{"metadata":{"name":"test1"}},{"metadata":{"name":"test2"}}]`) + // Assert + autopilot.Ok(t, err1) + autopilot.Equals(t, jq.ArrayMap, result1.Type) + autopilot.Equals(t, []map[string]any{{"key": "test1"}, {"key": "test2"}}, result1.Data) +} + +func TestJQReponses_Array(t *testing.T) { + // Arrange + jq1 := jq.NewJQ().WithFilter("map(.metadata.name // null)") + // Act + result1, err1 := jq1.Run(`[{"metadata":{"name":"test1"}},{"metadata":{"name":"test2"}}]`) + // Assert + autopilot.Ok(t, err1) + autopilot.Equals(t, jq.Array, result1.Type) + autopilot.Equals(t, []any{"test1", "test2"}, result1.Data) +}