diff --git a/client/client_test.go b/client/client_test.go index 3598f52..7085b56 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -126,6 +126,52 @@ func TestApplyShouldWorkWithExternalAuthMode(t *testing.T) { } } +func TestApplyShouldWorkWithQueryParams(t *testing.T) { + defer httpmock.Reset() + baseUrl := "http://baseUrl" + apiKey := "aToken" + client, err := Make(ApiParameter{ + ApiKey: apiKey, + BaseUrl: baseUrl, + }) + if err != nil { + panic(err) + } + client.setAuthMethodFromEnvIfNeeded() + httpmock.ActivateNonDefault( + client.client.GetClient(), + ) + responder := httpmock.NewStringResponder(200, `{"upsertResult": "NotChanged"}`) + + topic := resource.Resource{ + Json: []byte(`{"yolo": "data"}`), + Kind: "Alert", + Name: "my-alert", + Version: "v3", + Metadata: map[string]interface{}{ + "appInstance": "my-app", + }, + } + + httpmock.RegisterMatcherResponderWithQuery( + "PUT", + "http://baseUrl/api/public/monitoring/v3/alert?appInstance=my-app", + nil, + httpmock.HeaderIs("Authorization", "Bearer "+apiKey). + And(httpmock.HeaderIs("X-CDK-CLIENT", "CLI/unknown")). + And(httpmock.BodyContainsBytes(topic.Json)), + responder, + ) + + body, err := client.Apply(&topic, false) + if err != nil { + t.Error(err) + } + if body != "NotChanged" { + t.Errorf("Bad result expected NotChanged got: %s", body) + } +} + func TestApplyWithDryModeShouldWork(t *testing.T) { defer httpmock.Reset() baseUrl := "http://baseUrl" @@ -237,7 +283,7 @@ func TestGetShouldWork(t *testing.T) { ) app := client.GetKinds()["Application"] - result, err := client.Get(&app, []string{}, nil) + result, err := client.Get(&app, []string{}, []string{}, nil) if err != nil { t.Error(err) } @@ -274,7 +320,7 @@ func TestGetShouldFailIfN2xx(t *testing.T) { ) app := client.GetKinds()["Application"] - _, err = client.Get(&app, []string{}, nil) + _, err = client.Get(&app, []string{}, []string{}, nil) if err == nil { t.Failed() } @@ -309,7 +355,7 @@ func TestDescribeShouldWork(t *testing.T) { ) app := client.GetKinds()["Application"] - result, err := client.Describe(&app, []string{}, "yo") + result, err := client.Describe(&app, []string{}, []string{}, "yo") if err != nil { t.Error(err) } @@ -347,7 +393,7 @@ func TestDescribeShouldFailIfNo2xx(t *testing.T) { ) app := client.GetKinds()["Application"] - _, err = client.Describe(&app, []string{}, "yo") + _, err = client.Describe(&app, []string{}, []string{}, "yo") if err == nil { t.Failed() } @@ -382,7 +428,7 @@ func TestDeleteShouldWork(t *testing.T) { ) app := client.GetKinds()["Application"] - err = client.Delete(&app, []string{}, "yo") + err = client.Delete(&app, []string{}, []string{}, "yo") if err != nil { t.Error(err) } @@ -453,7 +499,7 @@ func TestDeleteShouldFailOnNot2XX(t *testing.T) { ) app := client.GetKinds()["Application"] - err = client.Delete(&app, []string{}, "yo") + err = client.Delete(&app, []string{}, []string{}, "yo") if err == nil { t.Fail() } diff --git a/client/console_client.go b/client/console_client.go index d7c89b7..5f636d5 100644 --- a/client/console_client.go +++ b/client/console_client.go @@ -254,12 +254,15 @@ func (client *Client) Apply(resource *resource.Resource, dryMode bool) (string, if !ok { return "", fmt.Errorf("kind %s not found", resource.Kind) } - applyPath, err := kind.ApplyPath(resource) + applyQueryInfo, err := kind.ApplyPath(resource) if err != nil { return "", err } - url := client.baseUrl + applyPath + url := client.baseUrl + applyQueryInfo.Path builder := client.client.R().SetBody(resource.Json) + for _, param := range applyQueryInfo.QueryParams { + builder = builder.SetQueryParam(param.Name, param.Value) + } if dryMode { builder = builder.SetQueryParam("dryMode", "true") } @@ -279,11 +282,15 @@ func (client *Client) Apply(resource *resource.Resource, dryMode bool) (string, return upsertResponse.UpsertResult, nil } -func (client *Client) Get(kind *schema.Kind, parentPathValue []string, queryParams map[string]string) ([]resource.Resource, error) { +func (client *Client) Get(kind *schema.Kind, parentPathValue []string, parentQueryValue []string, queryParams map[string]string) ([]resource.Resource, error) { var result []resource.Resource client.setAuthMethodFromEnvIfNeeded() - url := client.baseUrl + kind.ListPath(parentPathValue) + queryInfo := kind.ListPath(parentPathValue, parentQueryValue) + url := client.baseUrl + queryInfo.Path requestBuilder := client.client.R() + for _, p := range queryInfo.QueryParams { + requestBuilder = requestBuilder.SetQueryParam(p.Name, p.Value) + } if queryParams != nil { requestBuilder = requestBuilder.SetQueryParams(queryParams) } @@ -318,11 +325,16 @@ func (client *Client) Login(username, password string) (LoginResult, error) { return result, nil } -func (client *Client) Describe(kind *schema.Kind, parentPathValue []string, name string) (resource.Resource, error) { +func (client *Client) Describe(kind *schema.Kind, parentPathValue []string, parentQueryValue []string, name string) (resource.Resource, error) { var result resource.Resource client.setAuthMethodFromEnvIfNeeded() - url := client.baseUrl + kind.DescribePath(parentPathValue, name) - resp, err := client.client.R().Get(url) + queryInfo := kind.DescribePath(parentPathValue, parentQueryValue, name) + url := client.baseUrl + queryInfo.Path + requestBuilder := client.client.R() + for _, p := range queryInfo.QueryParams { + requestBuilder = requestBuilder.SetQueryParam(p.Name, p.Value) + } + resp, err := requestBuilder.Get(url) if err != nil { return result, err } else if resp.IsError() { @@ -332,10 +344,15 @@ func (client *Client) Describe(kind *schema.Kind, parentPathValue []string, name return result, err } -func (client *Client) Delete(kind *schema.Kind, parentPathValue []string, name string) error { +func (client *Client) Delete(kind *schema.Kind, parentPathValue []string, parentQueryValue []string, name string) error { client.setAuthMethodFromEnvIfNeeded() - url := client.baseUrl + kind.DescribePath(parentPathValue, name) - resp, err := client.client.R().Delete(url) + queryInfo := kind.DescribePath(parentPathValue, parentQueryValue, name) + url := client.baseUrl + queryInfo.Path + requestBuilder := client.client.R() + for _, p := range queryInfo.QueryParams { + requestBuilder = requestBuilder.SetQueryParam(p.Name, p.Value) + } + resp, err := requestBuilder.Delete(url) if err != nil { return err } else if resp.IsError() { diff --git a/client/gateway_client.go b/client/gateway_client.go index 8499afb..d75e90c 100644 --- a/client/gateway_client.go +++ b/client/gateway_client.go @@ -76,10 +76,14 @@ func MakeGatewayClientFromEnv() (*GatewayClient, error) { return client, nil } -func (client *GatewayClient) Get(kind *schema.Kind, parentPathValue []string, queryParams map[string]string) ([]resource.Resource, error) { +func (client *GatewayClient) Get(kind *schema.Kind, parentPathValue []string, parentQueryValue []string, queryParams map[string]string) ([]resource.Resource, error) { var result []resource.Resource - url := client.baseUrl + kind.ListPath(parentPathValue) + queryInfo := kind.ListPath(parentPathValue, parentQueryValue) + url := client.baseUrl + queryInfo.Path requestBuilder := client.client.R() + for _, p := range queryInfo.QueryParams { + requestBuilder = requestBuilder.SetQueryParam(p.Name, p.Value) + } if queryParams != nil { requestBuilder = requestBuilder.SetQueryParams(queryParams) } @@ -93,10 +97,15 @@ func (client *GatewayClient) Get(kind *schema.Kind, parentPathValue []string, qu return result, err } -func (client *GatewayClient) Describe(kind *schema.Kind, parentPathValue []string, name string) (resource.Resource, error) { +func (client *GatewayClient) Describe(kind *schema.Kind, parentPathValue []string, parentQueryValue []string, name string) (resource.Resource, error) { var result resource.Resource - url := client.baseUrl + kind.DescribePath(parentPathValue, name) - resp, err := client.client.R().Get(url) + queryInfo := kind.DescribePath(parentPathValue, parentQueryValue, name) + url := client.baseUrl + queryInfo.Path + requestBuilder := client.client.R() + for _, p := range queryInfo.QueryParams { + requestBuilder = requestBuilder.SetQueryParam(p.Name, p.Value) + } + resp, err := requestBuilder.Get(url) if err != nil { return result, err } else if resp.IsError() { @@ -106,9 +115,14 @@ func (client *GatewayClient) Describe(kind *schema.Kind, parentPathValue []strin return result, err } -func (client *GatewayClient) Delete(kind *schema.Kind, parentPathValue []string, name string) error { - url := client.baseUrl + kind.DescribePath(parentPathValue, name) - resp, err := client.client.R().Delete(url) +func (client *GatewayClient) Delete(kind *schema.Kind, parentPathValue []string, parentQueryValue []string, name string) error { + queryInfo := kind.DescribePath(parentPathValue, parentQueryValue, name) + url := client.baseUrl + queryInfo.Path + requestBuilder := client.client.R() + for _, p := range queryInfo.QueryParams { + requestBuilder = requestBuilder.SetQueryParam(p.Name, p.Value) + } + resp, err := requestBuilder.Delete(url) if err != nil { return err } else if resp.IsError() { @@ -154,8 +168,8 @@ func (client *GatewayClient) DeleteResourceByNameAndVCluster(resource *resource. if !ok { return fmt.Errorf("kind %s not found", resource.Kind) } - deletePath := kind.ListPath(nil) - url := client.baseUrl + deletePath + deletePath := kind.ListPath(nil, nil) + url := client.baseUrl + deletePath.Path resp, err := client.client.R().SetBody(map[string]string{"name": name, "vCluster": vCluster.(string)}).Delete(url) if err != nil { return err @@ -226,7 +240,7 @@ func (client *GatewayClient) DeleteResourceInterceptors(resource *resource.Resou } func (client *GatewayClient) DeleteKindByNameAndVCluster(kind *schema.Kind, param map[string]string) error { - url := client.baseUrl + kind.ListPath(nil) + url := client.baseUrl + kind.ListPath(nil, nil).Path req := client.client.R() req.SetBody(param) resp, err := req.Delete(url) @@ -242,7 +256,7 @@ func (client *GatewayClient) DeleteKindByNameAndVCluster(kind *schema.Kind, para } func (client *GatewayClient) DeleteInterceptor(kind *schema.Kind, name string, param map[string]string) error { - url := client.baseUrl + kind.ListPath(nil) + "/" + name + url := client.baseUrl + kind.ListPath(nil, nil).Path + "/" + name req := client.client.R() var bodyParams = make(map[string]interface{}) for k, v := range param { @@ -275,12 +289,15 @@ func (client *GatewayClient) Apply(resource *resource.Resource, dryMode bool) (s if !ok { return "", fmt.Errorf("kind %s not found", resource.Kind) } - applyPath, err := kind.ApplyPath(resource) + applyQueryInfo, err := kind.ApplyPath(resource) if err != nil { return "", err } - url := client.baseUrl + applyPath + url := client.baseUrl + applyQueryInfo.Path builder := client.client.R().SetBody(resource.Json) + for _, param := range applyQueryInfo.QueryParams { + builder = builder.SetQueryParam(param.Name, param.Value) + } if dryMode { builder = builder.SetQueryParam("dryMode", "true") } diff --git a/client/gateway_client_test.go b/client/gateway_client_test.go index b936c1b..8d98283 100644 --- a/client/gateway_client_test.go +++ b/client/gateway_client_test.go @@ -153,7 +153,7 @@ func TestGwGetShouldWork(t *testing.T) { ) vClusterKind := gatewayClient.GetKinds()["VirtualCluster"] - result, err := gatewayClient.Get(&vClusterKind, []string{}, nil) + result, err := gatewayClient.Get(&vClusterKind, []string{}, []string{}, nil) if err != nil { t.Error(err) } @@ -191,7 +191,7 @@ func TestGwGetShouldFailIfN2xx(t *testing.T) { ) vClusterKind := gatewayClient.GetKinds()["VirtualCluster"] - _, err = gatewayClient.Get(&vClusterKind, []string{}, nil) + _, err = gatewayClient.Get(&vClusterKind, []string{}, []string{}, nil) if err == nil { t.Failed() } @@ -227,7 +227,7 @@ func TestGwDeleteShouldWork(t *testing.T) { ) vClusters := gatewayClient.GetKinds()["VirtualCluster"] - err = gatewayClient.Delete(&vClusters, []string{}, "vcluster1") + err = gatewayClient.Delete(&vClusters, []string{}, []string{}, "vcluster1") if err != nil { t.Error(err) } @@ -262,7 +262,7 @@ func TestGwDeleteShouldFailOnNot2XX(t *testing.T) { ) vClusterKind := gatewayClient.GetKinds()["VirtualCluster"] - err = gatewayClient.Delete(&vClusterKind, []string{}, "vcluster1") + err = gatewayClient.Delete(&vClusterKind, []string{}, []string{}, "vcluster1") if err == nil { t.Fail() } diff --git a/cmd/delete.go b/cmd/delete.go index 0f45f50..846f127 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -60,7 +60,9 @@ func initDelete(kinds schema.KindCatalog, strict bool) { deleteCmd.AddCommand(interceptorsDeleteCmd) } else { flags := kind.GetParentFlag() + parentQueryFlags := kind.GetParentQueryFlag() parentFlagValue := make([]*string, len(flags)) + parentQueryFlagValue := make([]*string, len(parentQueryFlags)) kindCmd := &cobra.Command{ Use: fmt.Sprintf("%s [name]", name), Short: "Delete resource of kind " + name, @@ -68,14 +70,19 @@ func initDelete(kinds schema.KindCatalog, strict bool) { Aliases: buildAlias(name), Run: func(cmd *cobra.Command, args []string) { parentValue := make([]string, len(parentFlagValue)) + parentQueryValue := make([]string, len(parentQueryFlagValue)) for i, v := range parentFlagValue { parentValue[i] = *v } + for i, v := range parentQueryFlagValue { + parentQueryValue[i] = *v + } + var err error if isGatewayKind(kind) { - err = gatewayApiClient().Delete(&kind, parentValue, args[0]) + err = gatewayApiClient().Delete(&kind, parentValue, parentQueryValue, args[0]) } else { - err = consoleApiClient().Delete(&kind, parentValue, args[0]) + err = consoleApiClient().Delete(&kind, parentValue, parentQueryValue, args[0]) } if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) @@ -87,6 +94,9 @@ func initDelete(kinds schema.KindCatalog, strict bool) { parentFlagValue[i] = kindCmd.Flags().String(flag, "", "Parent "+flag) kindCmd.MarkFlagRequired(flag) } + for i, flag := range parentQueryFlags { + parentQueryFlagValue[i] = kindCmd.Flags().String(flag, "", "Parent "+flag) + } deleteCmd.AddCommand(kindCmd) } } diff --git a/cmd/get.go b/cmd/get.go index 46f2452..1a3ebea 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -127,14 +127,14 @@ func initGet(kinds schema.KindCatalog) { kind := kinds[key] // keep only the Kinds where kind.GetParentFlag() is empty and not of GatewayKind (demands extra configuration, TODO fix if config is provided) - if len(kind.GetParentFlag()) > 0 { + if len(kind.GetParentFlag())+len(kind.GetParentQueryFlag()) > 0 { continue } if _, isGatewayKind := kind.GetLatestKindVersion().(*schema.GatewayKindVersion); isGatewayKind { continue } - resources, err := consoleApiClient().Get(&kind, []string{}, map[string]string{}) + resources, err := consoleApiClient().Get(&kind, []string{}, []string{}, map[string]string{}) if err != nil { fmt.Fprintf(os.Stderr, "Error fetching resource %s: %s\n", kind.GetName(), err) continue @@ -161,8 +161,10 @@ func initGet(kinds schema.KindCatalog) { use = fmt.Sprintf("%s", name) } parentFlags := kind.GetParentFlag() + parentQueryFlags := kind.GetParentQueryFlag() listFlags := kind.GetListFlag() parentFlagValue := make([]*string, len(parentFlags)) + parentQueryFlagValue := make([]*string, len(parentQueryFlags)) listFlagValue := make(map[string]interface{}, len(listFlags)) kindCmd := &cobra.Command{ Use: use, @@ -172,18 +174,23 @@ func initGet(kinds schema.KindCatalog) { Aliases: buildAlias(name), Run: func(cmd *cobra.Command, args []string) { parentValue := make([]string, len(parentFlagValue)) + parentQueryValue := make([]string, len(parentQueryFlagValue)) queryParams := buildQueryParams(listFlagValue) for i, v := range parentFlagValue { parentValue[i] = *v } + for i, v := range parentQueryFlagValue { + parentQueryValue[i] = *v + } + var err error if len(args) == 0 { var result []resource.Resource if isGatewayKind { - result, err = gatewayApiClient().Get(&kind, parentValue, queryParams) + result, err = gatewayApiClient().Get(&kind, parentValue, parentQueryValue, queryParams) } else { - result, err = consoleApiClient().Get(&kind, parentValue, queryParams) + result, err = consoleApiClient().Get(&kind, parentValue, parentQueryValue, queryParams) } if err != nil { fmt.Fprintf(os.Stderr, "Error fetching resources: %s\n", err) @@ -193,9 +200,9 @@ func initGet(kinds schema.KindCatalog) { } else if len(args) == 1 { var result resource.Resource if isGatewayKind { - result, err = gatewayApiClient().Describe(&kind, parentValue, args[0]) + result, err = gatewayApiClient().Describe(&kind, parentValue, parentQueryValue, args[0]) } else { - result, err = consoleApiClient().Describe(&kind, parentValue, args[0]) + result, err = consoleApiClient().Describe(&kind, parentValue, parentQueryValue, args[0]) } if err != nil { fmt.Fprintf(os.Stderr, "Error describing resource: %s\n", err) @@ -213,6 +220,9 @@ func initGet(kinds schema.KindCatalog) { parentFlagValue[i] = kindCmd.Flags().String(flag, "", "Parent "+flag) kindCmd.MarkFlagRequired(flag) } + for i, flag := range parentQueryFlags { + parentQueryFlagValue[i] = kindCmd.Flags().String(flag, "", "Parent "+flag) + } for key, flag := range listFlags { var isFlagSet bool if flag.Type == "string" { diff --git a/schema/console-default-schema.json b/schema/console-default-schema.json index b63c9d3..c0bec30 100644 --- a/schema/console-default-schema.json +++ b/schema/console-default-schema.json @@ -1 +1 @@ -{"Alert":{"Versions":{"2":{"ListPath":"/public/monitoring/v2/cluster/{cluster}/alert","Name":"Alert","ParentPathParam":["cluster"],"ListQueryParamter":{"alertType":{"FlagName":"alert-type","Required":false,"Type":"string"},"connect":{"FlagName":"connect","Required":false,"Type":"string"},"connector":{"FlagName":"connector","Required":false,"Type":"string"},"consumerGroup":{"FlagName":"consumer-group","Required":false,"Type":"string"},"topic":{"FlagName":"topic","Required":false,"Type":"string"}},"ApplyExample":"null\n","Order":15}}},"Application":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application","Name":"Application","ParentPathParam":[],"ListQueryParamter":{},"ApplyExample":"null\n","Order":6}}},"ApplicationGroup":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application-group","Name":"ApplicationGroup","ParentPathParam":[],"ListQueryParamter":{},"ApplyExample":"null\n","Order":9}}},"ApplicationInstance":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application-instance","Name":"ApplicationInstance","ParentPathParam":[],"ListQueryParamter":{"application":{"FlagName":"application","Required":false,"Type":"string"}},"ApplyExample":"null\n","Order":7}}},"ApplicationInstancePermission":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application-instance-permission","Name":"ApplicationInstancePermission","ParentPathParam":[],"ListQueryParamter":{"filterByApplication":{"FlagName":"application","Required":false,"Type":"string"},"filterByApplicationInstance":{"FlagName":"application-instance","Required":false,"Type":"string"},"filterByGrantedTo":{"FlagName":"granted-to","Required":false,"Type":"string"}},"ApplyExample":"null\n","Order":8}}},"Connector":{"Versions":{"2":{"ListPath":"/public/kafka/v2/cluster/{cluster}/connect/{connectCluster}/connector","Name":"Connector","ParentPathParam":["cluster","connectCluster"],"ListQueryParamter":{},"ApplyExample":"null\n","Order":13}}},"Group":{"Versions":{"2":{"ListPath":"/public/iam/v2/group","Name":"Group","ParentPathParam":[],"ListQueryParamter":{},"ApplyExample":"null\n","Order":1}}},"IndexedTopic":{"Versions":{"1":{"ListPath":"/public/sql/v1/cluster/{cluster}/indexed_topic","Name":"IndexedTopic","ParentPathParam":["cluster"],"ListQueryParamter":{},"ApplyExample":"null\n","Order":14}}},"KafkaCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/kafka-cluster","Name":"KafkaCluster","ParentPathParam":[],"ListQueryParamter":{},"ApplyExample":"null\n","Order":2}}},"KafkaConnectCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/cluster/{cluster}/kafka-connect","Name":"KafkaConnectCluster","ParentPathParam":["cluster"],"ListQueryParamter":{},"ApplyExample":"null\n","Order":3}}},"KsqlDBCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/cluster/{cluster}/ksqldb","Name":"KsqlDBCluster","ParentPathParam":["cluster"],"ListQueryParamter":{},"ApplyExample":"null\n","Order":4}}},"ServiceAccount":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/cluster/{cluster}/service-account","Name":"ServiceAccount","ParentPathParam":["cluster"],"ListQueryParamter":{},"ApplyExample":"null\n","Order":10}}},"Subject":{"Versions":{"2":{"ListPath":"/public/kafka/v2/cluster/{cluster}/subject","Name":"Subject","ParentPathParam":["cluster"],"ListQueryParamter":{},"ApplyExample":"null\n","Order":12}}},"Topic":{"Versions":{"2":{"ListPath":"/public/kafka/v2/cluster/{cluster}/topic","Name":"Topic","ParentPathParam":["cluster"],"ListQueryParamter":{},"ApplyExample":"null\n","Order":11}}},"TopicPolicy":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/topic-policy","Name":"TopicPolicy","ParentPathParam":[],"ListQueryParamter":{"app-instance":{"FlagName":"application-instance","Required":false,"Type":"string"}},"ApplyExample":"null\n","Order":5}}},"User":{"Versions":{"2":{"ListPath":"/public/iam/v2/user","Name":"User","ParentPathParam":[],"ListQueryParamter":{},"ApplyExample":"null\n","Order":0}}}} \ No newline at end of file +{"Alert":{"Versions":{"2":{"ListPath":"/public/monitoring/v2/cluster/{cluster}/alert","Name":"Alert","ParentPathParam":["cluster"],"ParentQueryParam":null,"ListQueryParamter":{"alertType":{"FlagName":"alert-type","Required":false,"Type":"string"},"connect":{"FlagName":"connect","Required":false,"Type":"string"},"connector":{"FlagName":"connector","Required":false,"Type":"string"},"consumerGroup":{"FlagName":"consumer-group","Required":false,"Type":"string"},"topic":{"FlagName":"topic","Required":false,"Type":"string"}},"ApplyExample":"apiVersion: v2\nkind: Alert\nmetadata:\n name: alert\n cluster: cluster\nspec:\n connectName: connect\n connectorName: connector\n threshold: 1\n operator: GreaterThan\n metric: FailedTaskCount\n type: KafkaConnectAlert\n","Order":15},"3":{"ListPath":"/public/monitoring/v3/alert","Name":"Alert","ParentPathParam":[],"ParentQueryParam":["appInstance","group","user"],"ListQueryParamter":{"alertType":{"FlagName":"alert-type","Required":false,"Type":"string"},"appInstance":{"FlagName":"application-instance","Required":false,"Type":"string"},"cluster":{"FlagName":"cluster","Required":false,"Type":"string"},"connect":{"FlagName":"connect","Required":false,"Type":"string"},"connector":{"FlagName":"connector","Required":false,"Type":"string"},"consumerGroup":{"FlagName":"consumer-group","Required":false,"Type":"string"},"group":{"FlagName":"group","Required":false,"Type":"string"},"topic":{"FlagName":"topic","Required":false,"Type":"string"},"user":{"FlagName":"user","Required":false,"Type":"string"}},"ApplyExample":"apiVersion: v3\nkind: Alert\nmetadata:\n name: alert\n appInstance: my-app\nspec:\n cluster: cluster\n connectName: connect\n connectorName: connector\n threshold: 1\n operator: GreaterThan\n metric: FailedTaskCount\n type: KafkaConnectAlert\n","Order":15}}},"Application":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application","Name":"Application","ParentPathParam":[],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v1\nkind: Application\nmetadata:\n name: my-application\nspec:\n title: My Application\n owner: me\n","Order":6}}},"ApplicationGroup":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application-group","Name":"ApplicationGroup","ParentPathParam":[],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v1\nkind: ApplicationGroup\nmetadata:\n application: clickstream-app\n name: clickstream-support\nspec:\n displayName: Support Clickstream\n description: |-\n Members of the Support Group are allowed:\n Read access on all the resources\n Can restart owned connectors\n Can reset offsets\n permissions:\n - appInstance: clickstream-app-dev\n patternType: LITERAL\n name: '*'\n permissions:\n - topicConsume\n - topicViewConfig\n resourceType: TOPIC\n members:\n - user1@company.org\n - user2@company.org\n externalGroups:\n - support\n","Order":9}}},"ApplicationInstance":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application-instance","Name":"ApplicationInstance","ParentPathParam":[],"ParentQueryParam":null,"ListQueryParamter":{"application":{"FlagName":"application","Required":false,"Type":"string"}},"ApplyExample":"apiVersion: v1\nkind: ApplicationInstance\nmetadata:\n name: my-app-instance-prod\n application: my-app\nspec:\n cluster: prod-cluster\n topicPolicyRef:\n - my-topic-policy\n resources:\n - type: TOPIC\n name: my-topic\n patternType: LITERAL\n - type: CONSUMER_GROUP\n name: my-consumer-group\n patternType: LITERAL\n serviceAccount: my-service-account\n defaultCatalogVisibility: PUBLIC\n","Order":7}}},"ApplicationInstancePermission":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application-instance-permission","Name":"ApplicationInstancePermission","ParentPathParam":[],"ParentQueryParam":null,"ListQueryParamter":{"filterByApplication":{"FlagName":"application","Required":false,"Type":"string"},"filterByApplicationInstance":{"FlagName":"application-instance","Required":false,"Type":"string"},"filterByGrantedTo":{"FlagName":"granted-to","Required":false,"Type":"string"}},"ApplyExample":"apiVersion: v1\nkind: ApplicationInstancePermission\nmetadata:\n application: test\n appInstance: test\n name: test\nspec:\n resource:\n type: TOPIC\n name: test\n patternType: LITERAL\n permission: READ\n grantedTo: test\n","Order":8}}},"Connector":{"Versions":{"2":{"ListPath":"/public/kafka/v2/cluster/{cluster}/connect/{connectCluster}/connector","Name":"Connector","ParentPathParam":["cluster","connectCluster"],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v2\nkind: Connector\nmetadata:\n name: my-connector\n cluster: my-cluster\n connectCluster: my-connect\n autoRestart:\n enabled: true\n frequencySeconds: 600\n description: My connector\nspec:\n config: {}\n","Order":13}}},"Group":{"Versions":{"2":{"ListPath":"/public/iam/v2/group","Name":"Group","ParentPathParam":[],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v2\nkind: Group\nmetadata:\n name: group\nspec:\n displayName: test\n description: description\n externalGroups:\n - test\n members:\n - user@conduktor.io\n membersFromExternalGroups: []\n permissions:\n - resourceType: TOPIC\n cluster: '*'\n name: test\n patternType: LITERAL\n permissions:\n - topicConsume\n - resourceType: TOPIC\n cluster: '*'\n name: test2\n patternType: PREFIXED\n permissions:\n - topicConsume\n","Order":1}}},"IndexedTopic":{"Versions":{"1":{"ListPath":"/public/sql/v1/cluster/{cluster}/indexed_topic","Name":"IndexedTopic","ParentPathParam":["cluster"],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v1\nkind: IndexedTopic\nmetadata:\n cluster: my-cluster\n name: my-topic\nspec:\n retentionTimeInSecond: 3600\n","Order":14}}},"KafkaCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/kafka-cluster","Name":"KafkaCluster","ParentPathParam":[],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v2\nkind: KafkaCluster\nmetadata:\n name: my-kafka-cluster\n labels:\n env: prod\nspec:\n displayName: yo\n bootstrapServers: localhost:9092\n properties:\n sasl.jaas.config: org.apache.kafka.common.security.plain.PlainLoginModule required username=\"admin\" password=\"admin-secret\";\n color: '#FF0000'\n icon: icon\n schemaRegistry:\n url: https://my-schema-registry:8081\n security:\n username: admin\n password: admin-secret\n type: BasicAuth\n ignoreUntrustedCertificate: false\n type: ConfluentLike\n","Order":2}}},"KafkaConnectCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/cluster/{cluster}/kafka-connect","Name":"KafkaConnectCluster","ParentPathParam":["cluster"],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v2\nkind: KafkaConnectCluster\nmetadata:\n name: connect-1\n cluster: my-cloud\n labels:\n user-labels: I am a user label\nspec:\n displayName: My kafka connect\n urls: http://localhost:8083\n headers:\n a: b\n c: d\n ignoreUntrustedCertificate: true\n security:\n username: user\n password: password\n type: BasicAuth\n","Order":3}}},"KsqlDBCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/cluster/{cluster}/ksqldb","Name":"KsqlDBCluster","ParentPathParam":["cluster"],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v2\nkind: KsqlDBCluster\nmetadata:\n name: connect-1\n cluster: my-cloud\nspec:\n displayName: My kafka connect\n url: http://localhost:8083\n headers:\n a: b\n c: d\n ignoreUntrustedCertificate: true\n security:\n username: user\n password: password\n type: BasicAuth\n","Order":4}}},"ServiceAccount":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/cluster/{cluster}/service-account","Name":"ServiceAccount","ParentPathParam":["cluster"],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v1\nkind: ServiceAccount\nmetadata:\n appInstance: my-app-instance-dev\n cluster: my-kafka-cluster\n labels:\n conduktor.io/application: application-a\n conduktor.io/application-instance: dev\n user-labels: I am a user label\n name: sa-clicko-dev\nspec:\n authorization:\n acls:\n - type: TOPIC\n name: click.\n patternType: Prefixed\n operations:\n - Write\n host: '*'\n permission: Allow\n","Order":10}}},"Subject":{"Versions":{"2":{"ListPath":"/public/kafka/v2/cluster/{cluster}/subject","Name":"Subject","ParentPathParam":["cluster"],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v2\nkind: Subject\nmetadata:\n name: my-subject\n cluster: my-cluster\n labels:\n conduktor.io/application: application-a\n conduktor.io/application-instance: staging\nspec:\n format: AVRO\n compatibility: BACKWARD_TRANSITIVE\n schema: '{\"type\": \"long\"}'\n","Order":12}}},"Topic":{"Versions":{"2":{"ListPath":"/public/kafka/v2/cluster/{cluster}/topic","Name":"Topic","ParentPathParam":["cluster"],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v2\nkind: Topic\nmetadata:\n name: my-topic\n cluster: my-cluster\n labels:\n conduktor.io/application: application-a\n conduktor.io/application-instance: staging\n user-labels: I am a user label\n catalogVisibility: PUBLIC\n descriptionIsEditable: true\n description: This is a topic description\n sqlStorage:\n retentionTimeInSecond: 42\nspec:\n partitions: 1\n replicationFactor: 1\n configs:\n cleanup.policy: delete\n retention.ms: '86400000'\n","Order":11}}},"TopicPolicy":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/topic-policy","Name":"TopicPolicy","ParentPathParam":[],"ParentQueryParam":null,"ListQueryParamter":{"app-instance":{"FlagName":"application-instance","Required":false,"Type":"string"}},"ApplyExample":"apiVersion: v1\nkind: TopicPolicy\nmetadata:\n name: my-app-instance-prod\nspec:\n policies:\n my-policy:\n constraint: OneOf\n optional: true\n values:\n - value1\n - value2\n","Order":5}}},"User":{"Versions":{"2":{"ListPath":"/public/iam/v2/user","Name":"User","ParentPathParam":[],"ParentQueryParam":null,"ListQueryParamter":{},"ApplyExample":"apiVersion: v2\nkind: User\nmetadata:\n name: user@conduktor.io\nspec:\n firstName: description\n lastName: test\n permissions: []\n","Order":0}}}} \ No newline at end of file diff --git a/schema/kind.go b/schema/kind.go index a7c6c10..acc8677 100644 --- a/schema/kind.go +++ b/schema/kind.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "regexp" + "slices" "strconv" "strings" @@ -17,6 +18,7 @@ type KindVersion interface { GetListPath() string GetName() string GetParentPathParam() []string + GetParentQueryParam() []string GetOrder() int GetListQueryParamter() map[string]QueryParameterOption GetApplyExample() string @@ -38,6 +40,7 @@ type ConsoleKindVersion struct { ListPath string Name string ParentPathParam []string + ParentQueryParam []string ListQueryParamter map[string]QueryParameterOption ApplyExample string Order int `json:1000` //same value DefaultPriority @@ -59,6 +62,10 @@ func (c *ConsoleKindVersion) GetParentPathParam() []string { return c.ParentPathParam } +func (c *ConsoleKindVersion) GetParentQueryParam() []string { + return c.ParentQueryParam +} + func (c *ConsoleKindVersion) GetOrder() int { return c.Order } @@ -76,6 +83,7 @@ type GatewayKindVersion struct { ListPath string Name string ParentPathParam []string + ParentQueryParam []string ListQueryParameter map[string]QueryParameterOption GetAvailable bool ApplyExample string @@ -94,6 +102,10 @@ func (g *GatewayKindVersion) GetParentPathParam() []string { return g.ParentPathParam } +func (g *GatewayKindVersion) GetParentQueryParam() []string { + return g.ParentQueryParam +} + func (g *GatewayKindVersion) GetApplyExample() string { return g.ApplyExample } @@ -192,9 +204,22 @@ func (kind *Kind) GetParentFlag() []string { return kindVersion.GetParentPathParam() } +func (kind *Kind) GetParentQueryFlag() []string { + kindVersion := kind.GetLatestKindVersion() + return kindVersion.GetParentQueryParam() +} + func (kind *Kind) GetListFlag() map[string]QueryParameterOption { kindVersion := kind.GetLatestKindVersion() - return kindVersion.GetListQueryParamter() + kindVersion.GetParentQueryParam() + flags := make(map[string]QueryParameterOption) + // Filter out query params from parent to avoid duplicates + for k, v := range kindVersion.GetListQueryParamter() { + if !slices.Contains(kindVersion.GetParentQueryParam(), k) { + flags[k] = v + } + } + return flags } func (kind *Kind) MaxVersion() int { @@ -222,36 +247,77 @@ func (Kind *Kind) GetName() string { panic("No kindVersion in kind") //should never happen } -func (kind *Kind) ListPath(parentPathValues []string) string { +type QueryInfo struct { + Path string + QueryParams []QueryParam +} + +type QueryParam struct { + Name string + Value string +} + +func (kind *Kind) ListPath(parentValues []string, parentQueryValues []string) QueryInfo { kindVersion := kind.GetLatestKindVersion() - if len(parentPathValues) != len(kindVersion.GetParentPathParam()) { - panic(fmt.Sprintf("For kind %s expected %d parent apiVersion values, got %d", kindVersion.GetName(), len(kindVersion.GetParentPathParam()), len(parentPathValues))) + if len(parentValues) != len(kindVersion.GetParentPathParam()) { + panic(fmt.Sprintf("For kind %s expected %d parent apiVersion values, got %d", kindVersion.GetName(), len(kindVersion.GetParentPathParam()), len(parentValues))) } path := kindVersion.GetListPath() - for i, pathValue := range parentPathValues { + for i, pathValue := range parentValues { path = strings.Replace(path, fmt.Sprintf("{%s}", kindVersion.GetParentPathParam()[i]), pathValue, 1) } - return path + + if len(parentQueryValues) != len(kindVersion.GetParentQueryParam()) { + panic(fmt.Sprintf("For kind %s expected %d parent query parameter values, got %d", kindVersion.GetName(), len(kindVersion.GetParentPathParam()), len(parentValues))) + } + var params []QueryParam + for i, value := range parentQueryValues { + if value != "" { + params = append(params, QueryParam{ + Name: kindVersion.GetParentQueryParam()[i], + Value: value, + }) + } + } + + return QueryInfo{ + Path: path, + QueryParams: params, + } } -func (kind *Kind) DescribePath(parentPathValues []string, name string) string { - return kind.ListPath(parentPathValues) + "/" + name +func (kind *Kind) DescribePath(parentPathValues []string, parentQueryValues []string, name string) QueryInfo { + queryInfo := kind.ListPath(parentPathValues, parentQueryValues) + return QueryInfo{ + Path: queryInfo.Path + "/" + name, + QueryParams: queryInfo.QueryParams, + } } -func (kind *Kind) ApplyPath(resource *resource.Resource) (string, error) { +func (kind *Kind) ApplyPath(resource *resource.Resource) (QueryInfo, error) { kindVersion, ok := kind.Versions[extractVersionFromApiVersion(resource.Version)] if !ok { - return "", fmt.Errorf("Could not find version %s for kind %s", resource.Version, resource.Kind) + return QueryInfo{}, fmt.Errorf("Could not find version %s for kind %s", resource.Version, resource.Kind) } parentPathValues := make([]string, len(kindVersion.GetParentPathParam())) + var parentQueryValues []string var err error for i, param := range kindVersion.GetParentPathParam() { parentPathValues[i], err = resource.StringFromMetadata(param) if err != nil { - return "", err + return QueryInfo{}, err + } + } + for _, param := range kindVersion.GetParentQueryParam() { + var value string + value, err = resource.StringFromMetadata(param) + if err == nil { + parentQueryValues = append(parentQueryValues, value) + } else { + parentQueryValues = append(parentQueryValues, "") } } - return kind.ListPath(parentPathValues), nil + return kind.ListPath(parentPathValues, parentQueryValues), nil } func (kind *Kind) DeletePath(resource *resource.Resource) (string, error) { @@ -260,5 +326,5 @@ func (kind *Kind) DeletePath(resource *resource.Resource) (string, error) { return "", err } - return applyPath + "/" + resource.Name, nil + return applyPath.Path + "/" + resource.Name, nil } diff --git a/schema/kind_test.go b/schema/kind_test.go index c972409..bf7ee83 100644 --- a/schema/kind_test.go +++ b/schema/kind_test.go @@ -1,6 +1,7 @@ package schema import ( + "reflect" "testing" ) @@ -49,21 +50,27 @@ func TestKindGetFlagWhenNoFlag(t *testing.T) { } func TestKindListPath(t *testing.T) { - t.Run("replaces parent parameters in ListPath", func(t *testing.T) { + t.Run("replaces parent parameters in ListPath and add parameters", func(t *testing.T) { kind := Kind{ Versions: map[int]KindVersion{ 1: &ConsoleKindVersion{ - ListPath: "/ListPath/{param-1}/{param-2}", - ParentPathParam: []string{"param-1", "param-2"}, + ListPath: "/ListPath/{param-1}/{param-2}", + ParentPathParam: []string{"param-1", "param-2"}, + ParentQueryParam: []string{"param-3"}, }, }, } - got := kind.ListPath([]string{"value1", "value2"}) - want := "/ListPath/value1/value2" + got := kind.ListPath([]string{"value1", "value2"}, []string{"value3"}) + wantPath := "/ListPath/value1/value2" + wantParams := []QueryParam{{Name: "param-3", Value: "value3"}} - if got != want { - t.Errorf("got ListPath %q, want %q", got, want) + if got.Path != wantPath { + t.Errorf("got ListPath %q, want %q", got.Path, wantPath) + } + + if !reflect.DeepEqual(got.QueryParams, wantParams) { + t.Errorf("got ListPath params %q, want %q", got.QueryParams, wantParams) } }) @@ -83,6 +90,26 @@ func TestKindListPath(t *testing.T) { } }() - kind.ListPath([]string{"value1"}) + kind.ListPath([]string{"value1"}, []string{}) + }) + + t.Run("panics when parent paths and parameters length mismatch", func(t *testing.T) { + kind := Kind{ + Versions: map[int]KindVersion{ + 1: &ConsoleKindVersion{ + ListPath: "/Test", + ParentPathParam: []string{}, + ParentQueryParam: []string{"param1", "param2"}, + }, + }, + } + + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } + }() + + kind.ListPath([]string{}, []string{"value1"}) }) } diff --git a/schema/schema.go b/schema/schema.go index e335ed7..f0d7429 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -82,9 +82,10 @@ func buildConsoleKindVersion(s *Schema, path, kind string, order int, put *v3hig for _, putParameter := range put.Parameters { if putParameter.In == "path" && *putParameter.Required { newKind.ParentPathParam = append(newKind.ParentPathParam, putParameter.Name) - } - + if putParameter.In == "query" && putParameter.Name != "dryMode" { + newKind.ParentQueryParam = append(newKind.ParentQueryParam, putParameter.Name) + } } for _, getParameter := range get.Parameters { if getParameter.In == "query" {