diff --git a/client/client_test.go b/client/client_test.go index ab33976..1912721 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -188,7 +188,7 @@ func TestGetShouldWork(t *testing.T) { ) app := client.GetKinds()["Application"] - result, err := client.Get(&app, []string{}) + result, err := client.Get(&app, []string{}, nil) if err != nil { t.Error(err) } @@ -225,7 +225,7 @@ func TestGetShouldFailIfN2xx(t *testing.T) { ) app := client.GetKinds()["Application"] - _, err = client.Get(&app, []string{}) + _, err = client.Get(&app, []string{}, nil) if err == nil { t.Failed() } diff --git a/client/client.go b/client/console_client.go similarity index 96% rename from client/client.go rename to client/console_client.go index a2065de..9f0e923 100644 --- a/client/client.go +++ b/client/console_client.go @@ -97,7 +97,7 @@ func Make(apiParameter ApiParameter) (*Client, error) { err := result.initKindFromApi() if err != nil { fmt.Fprintf(os.Stderr, "Cannot access the Conduktor API: %s\nUsing offline defaults.\n", err) - result.kinds = schema.DefaultKind() + result.kinds = schema.ConsoleDefaultKind() } return result, nil @@ -106,7 +106,7 @@ func Make(apiParameter ApiParameter) (*Client, error) { func MakeFromEnv() (*Client, error) { apiParameter := ApiParameter{ BaseUrl: os.Getenv("CDK_BASE_URL"), - Debug: strings.ToLower(os.Getenv("CDK_DEBUG")) == "true", + Debug: utils.CdkDebug(), Cert: os.Getenv("CDK_CERT"), Cacert: os.Getenv("CDK_CACERT"), ApiKey: os.Getenv("CDK_API_KEY"), @@ -201,11 +201,15 @@ func (client *Client) Apply(resource *resource.Resource, dryMode bool) (string, return upsertResponse.UpsertResult, nil } -func (client *Client) Get(kind *schema.Kind, parentPathValue []string) ([]resource.Resource, error) { +func (client *Client) Get(kind *schema.Kind, parentPathValue []string, queryParams map[string]string) ([]resource.Resource, error) { var result []resource.Resource client.setApiKeyFromEnvIfNeeded() url := client.baseUrl + kind.ListPath(parentPathValue) - resp, err := client.client.R().Get(url) + requestBuilder := client.client.R() + if queryParams != nil { + requestBuilder = requestBuilder.SetQueryParams(queryParams) + } + resp, err := requestBuilder.Get(url) if err != nil { return result, err } else if resp.IsError() { @@ -310,7 +314,7 @@ func (client *Client) initKindFromApi() error { return fmt.Errorf("Cannot parse openapi: %s", err) } strict := false - client.kinds, err = schema.GetKinds(strict) + client.kinds, err = schema.GetConsoleKinds(strict) if err != nil { fmt.Errorf("Cannot extract kinds from openapi: %s", err) } diff --git a/client/gateway_client.go b/client/gateway_client.go new file mode 100644 index 0000000..ceb2a0c --- /dev/null +++ b/client/gateway_client.go @@ -0,0 +1,334 @@ +package client + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "os" + + "github.com/conduktor/ctl/resource" + "github.com/conduktor/ctl/schema" + "github.com/conduktor/ctl/utils" + "github.com/go-resty/resty/v2" +) + +type GatewayClient struct { + cdkGatewayUser string + cdkGatewayPassword string + baseUrl string + client *resty.Client + kinds schema.KindCatalog +} + +type GatewayApiParameter struct { + BaseUrl string + Debug bool + CdkGatewayUser string + CdkGatewayPassword string +} + +func MakeGateway(apiParameter GatewayApiParameter) (*GatewayClient, error) { + restyClient := resty.New().SetDebug(apiParameter.Debug).SetHeader("X-CDK-CLIENT", "CLI/"+utils.GetConduktorVersion()) + + if apiParameter.BaseUrl == "" { + return nil, fmt.Errorf("Please set CDK_GATEWAY_BASE_URL") + } + + if apiParameter.CdkGatewayUser == "" || apiParameter.CdkGatewayPassword == "" { + return nil, fmt.Errorf("CDK_GATEWAY_USER and CDK_GATEWAY_PASSWORD must be provided") + } + + result := &GatewayClient{ + cdkGatewayUser: apiParameter.CdkGatewayUser, + cdkGatewayPassword: apiParameter.CdkGatewayPassword, + baseUrl: apiParameter.BaseUrl, + client: restyClient, + kinds: nil, + } + + result.client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) + result.client.SetDisableWarn(true) + result.client.SetBasicAuth(apiParameter.CdkGatewayUser, apiParameter.CdkGatewayPassword) + + err := result.initKindFromApi() + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot access the Gateway Conduktor API: %s\nUsing offline defaults.\n", err) + result.kinds = schema.GatewayDefaultKind() + } + + return result, nil +} + +func MakeGatewayClientFromEnv() (*GatewayClient, error) { + apiParameter := GatewayApiParameter{ + BaseUrl: os.Getenv("CDK_GATEWAY_BASE_URL"), + Debug: utils.CdkDebug(), + CdkGatewayUser: os.Getenv("CDK_GATEWAY_USER"), + CdkGatewayPassword: os.Getenv("CDK_GATEWAY_PASSWORD"), + } + + client, err := MakeGateway(apiParameter) + if err != nil { + return nil, fmt.Errorf("Cannot create client: %s", err) + } + return client, nil +} + +func (client *GatewayClient) Get(kind *schema.Kind, parentPathValue []string, queryParams map[string]string) ([]resource.Resource, error) { + var result []resource.Resource + url := client.baseUrl + kind.ListPath(parentPathValue) + requestBuilder := client.client.R() + if queryParams != nil { + requestBuilder = requestBuilder.SetQueryParams(queryParams) + } + resp, err := requestBuilder.Get(url) + if err != nil { + return result, err + } else if resp.IsError() { + return result, fmt.Errorf(extractApiError(resp)) + } + err = json.Unmarshal(resp.Body(), &result) + return result, err +} + +func (client *GatewayClient) Describe(kind *schema.Kind, parentPathValue []string, name string) (resource.Resource, error) { + var result resource.Resource + url := client.baseUrl + kind.DescribePath(parentPathValue, name) + resp, err := client.client.R().Get(url) + if err != nil { + return result, err + } else if resp.IsError() { + return result, fmt.Errorf("error describing resources %s/%s, got status code: %d:\n %s", kind.GetName(), name, resp.StatusCode(), string(resp.Body())) + } + err = json.Unmarshal(resp.Body(), &result) + 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) + if err != nil { + return err + } else if resp.IsError() { + return fmt.Errorf(extractApiError(resp)) + } else { + fmt.Printf("%s/%s deleted\n", kind.GetName(), name) + } + + return err +} + +func (client *GatewayClient) DeleteResourceByName(resource *resource.Resource) error { + kinds := client.GetKinds() + kind, ok := kinds[resource.Kind] + if !ok { + return fmt.Errorf("kind %s not found", resource.Kind) + } + deletePath, err := kind.DeletePath(resource) + if err != nil { + return err + } + url := client.baseUrl + deletePath + resp, err := client.client.R().Delete(url) + if err != nil { + return err + } else if resp.IsError() { + return fmt.Errorf(extractApiError(resp)) + } else { + fmt.Printf("%s/%s deleted\n", kind.GetName(), resource.Name) + } + + return err +} + +func (client *GatewayClient) DeleteResourceByNameAndVCluster(resource *resource.Resource) error { + kinds := client.GetKinds() + kind, ok := kinds[resource.Kind] + name := resource.Name + vCluster := resource.Metadata["vCluster"] + if vCluster == nil { + vCluster = "passthrough" + } + if !ok { + return fmt.Errorf("kind %s not found", resource.Kind) + } + deletePath := kind.ListPath(nil) + url := client.baseUrl + deletePath + resp, err := client.client.R().SetBody(map[string]string{"name": name, "vCluster": vCluster.(string)}).Delete(url) + if err != nil { + return err + } else if resp.IsError() { + return fmt.Errorf(extractApiError(resp)) + } else { + fmt.Printf("%s/%s deleted\n", kind.GetName(), resource.Name) + } + + return err +} + +type DeleteInterceptorPayload struct { + VCluster *string `json:"vCluster"` + Group *string `json:"group"` + Username *string `json:"username"` +} + +func (client *GatewayClient) DeleteResourceInterceptors(resource *resource.Resource) error { + kinds := client.GetKinds() + kind, ok := kinds[resource.Kind] + scope := resource.Metadata["scope"] + passthrough := "passthrough" + var deleteInterceptorPayload DeleteInterceptorPayload + if scope == nil { + deleteInterceptorPayload = DeleteInterceptorPayload{ + VCluster: &passthrough, + Group: nil, + Username: nil, + } + } else { + vCluster := scope.(map[string]interface{})["vCluster"] + var vClusterValue string + if vCluster != nil && vCluster.(string) != "" { + vClusterValue = vCluster.(string) + deleteInterceptorPayload.VCluster = &vClusterValue + } else { + deleteInterceptorPayload.VCluster = &passthrough + } + group := scope.(map[string]interface{})["group"] + var groupValue string + if group != nil && group.(string) != "" { + groupValue = group.(string) + deleteInterceptorPayload.Group = &groupValue + } + username := scope.(map[string]interface{})["username"] + var usernameValue string + if username != nil && username.(string) != "" { + usernameValue = username.(string) + deleteInterceptorPayload.Username = &usernameValue + } + } + if !ok { + return fmt.Errorf("kind %s not found", resource.Kind) + } + deletePath, err := kind.DeletePath(resource) + if err != nil { + return err + } + url := client.baseUrl + deletePath + resp, err := client.client.R().SetBody(deleteInterceptorPayload).Delete(url) + if err != nil { + return err + } else if resp.IsError() { + return fmt.Errorf(extractApiError(resp)) + } else { + fmt.Printf("%s/%s deleted\n", kind.GetName(), resource.Name) + } + + return err +} + +func (client *GatewayClient) DeleteKindByNameAndVCluster(kind *schema.Kind, param map[string]string) error { + url := client.baseUrl + kind.ListPath(nil) + req := client.client.R() + req.SetBody(param) + resp, err := req.Delete(url) + if err != nil { + return err + } else if resp.IsError() { + return fmt.Errorf(extractApiError(resp)) + } else { + fmt.Printf("%s/%s deleted\n", kind.GetName(), param) + } + + return err +} + +func (client *GatewayClient) DeleteInterceptor(kind *schema.Kind, name string, param map[string]string) error { + url := client.baseUrl + kind.ListPath(nil) + "/" + name + req := client.client.R() + var bodyParams = make(map[string]interface{}) + for k, v := range param { + if v == "" { + bodyParams[k] = nil + } else { + bodyParams[k] = v + } + } + req.SetBody(bodyParams) + resp, err := req.Delete(url) + if err != nil { + return err + } else if resp.IsError() { + return fmt.Errorf(extractApiError(resp)) + } else { + fmt.Printf("%s/%s deleted\n", kind.GetName(), param) + } + + return err +} + +func (client *GatewayClient) ActivateDebug() { + client.client.SetDebug(true) +} + +func (client *GatewayClient) Apply(resource *resource.Resource, dryMode bool) (string, error) { + kinds := client.GetKinds() + kind, ok := kinds[resource.Kind] + if !ok { + return "", fmt.Errorf("kind %s not found", resource.Kind) + } + applyPath, err := kind.ApplyPath(resource) + if err != nil { + return "", err + } + url := client.baseUrl + applyPath + builder := client.client.R().SetBody(resource.Json) + if dryMode { + builder = builder.SetQueryParam("dryMode", "true") + } + resp, err := builder.Put(url) + if err != nil { + return "", err + } else if resp.IsError() { + return "", fmt.Errorf(extractApiError(resp)) + } + bodyBytes := resp.Body() + var upsertResponse UpsertResponse + err = json.Unmarshal(bodyBytes, &upsertResponse) + //in case backend format change (not json string anymore). Let not fail the client for that + if err != nil { + return resp.String(), nil + } + return upsertResponse.UpsertResult, nil +} + +func (client *GatewayClient) GetOpenApi() ([]byte, error) { + url := client.baseUrl + "/gateway/v2/docs" + resp, err := client.client.R().Get(url) + if err != nil { + return nil, err + } else if resp.IsError() { + return nil, fmt.Errorf(resp.String()) + } + return resp.Body(), nil +} + +func (client *GatewayClient) initKindFromApi() error { + data, err := client.GetOpenApi() + if err != nil { + return fmt.Errorf("Cannot get openapi: %s", err) + } + schema, err := schema.New(data) + if err != nil { + return fmt.Errorf("Cannot parse openapi: %s", err) + } + strict := false + client.kinds, err = schema.GetGatewayKinds(strict) + if err != nil { + fmt.Errorf("Cannot extract kinds from openapi: %s", err) + } + return nil +} + +func (client *GatewayClient) GetKinds() schema.KindCatalog { + return client.kinds +} diff --git a/client/gateway_client_test.go b/client/gateway_client_test.go new file mode 100644 index 0000000..ef75201 --- /dev/null +++ b/client/gateway_client_test.go @@ -0,0 +1,269 @@ +package client + +import ( + "reflect" + "testing" + + "github.com/conduktor/ctl/resource" + "github.com/jarcoal/httpmock" +) + +var aVClusterResource = resource.Resource{ + Version: "gateway/v2", + Kind: "VClusters", + Name: "vcluster1", + Metadata: map[string]interface{}{"name": "vcluster1"}, + Json: []byte(`{"apiVersion":"v1","kind":"VClusters","metadata":{"name":"vcluster1"},"spec":{"prefix":"vcluster1_"}}`), +} + +func TestGwApplyShouldWork(t *testing.T) { + defer httpmock.Reset() + baseUrl := "http://baseUrl" + gatewayClient, err := MakeGateway(GatewayApiParameter{ + BaseUrl: baseUrl, + Debug: false, + CdkGatewayUser: "admin", + CdkGatewayPassword: "conduktor", + }) + if err != nil { + panic(err) + } + httpmock.ActivateNonDefault( + gatewayClient.client.GetClient(), + ) + responder := httpmock.NewStringResponder(200, `{"upsertResult": "NotChanged"}`) + + httpmock.RegisterMatcherResponderWithQuery( + "PUT", + "http://baseUrl/gateway/v2/vclusters", + nil, + httpmock.HeaderIs("Authorization", "Basic YWRtaW46Y29uZHVrdG9y"). + And(httpmock.HeaderIs("X-CDK-CLIENT", "CLI/unknown")). + And(httpmock.BodyContainsBytes(aVClusterResource.Json)), + responder, + ) + + body, err := gatewayClient.Apply(&aVClusterResource, false) + if err != nil { + t.Error(err) + } + if body != "NotChanged" { + t.Errorf("Bad result expected NotChanged got: %s", body) + } +} + +func TestGwApplyWithDryModeShouldWork(t *testing.T) { + defer httpmock.Reset() + baseUrl := "http://baseUrl" + gatewayClient, err := MakeGateway(GatewayApiParameter{ + BaseUrl: baseUrl, + Debug: false, + CdkGatewayUser: "admin", + CdkGatewayPassword: "conduktor", + }) + if err != nil { + panic(err) + } + httpmock.ActivateNonDefault( + gatewayClient.client.GetClient(), + ) + responder := httpmock.NewStringResponder(200, `{"upsertResult": "NotChanged"}`) + + httpmock.RegisterMatcherResponderWithQuery( + "PUT", + "http://baseUrl/gateway/v2/vclusters", + "dryMode=true", + httpmock.HeaderIs("Authorization", "Basic YWRtaW46Y29uZHVrdG9y"). + And(httpmock.BodyContainsBytes(aVClusterResource.Json)), + responder, + ) + + body, err := gatewayClient.Apply(&aVClusterResource, true) + if err != nil { + t.Error(err) + } + if body != "NotChanged" { + t.Errorf("Bad result expected NotChanged got: %s", body) + } +} + +func TestGwApplyShouldFailIfNo2xx(t *testing.T) { + defer httpmock.Reset() + baseUrl := "http://baseUrl" + gatewayClient, err := MakeGateway(GatewayApiParameter{ + BaseUrl: baseUrl, + Debug: false, + CdkGatewayUser: "admin", + CdkGatewayPassword: "conduktor", + }) + + if err != nil { + panic(err) + } + httpmock.ActivateNonDefault( + gatewayClient.client.GetClient(), + ) + responder, err := httpmock.NewJsonResponder(400, "") + if err != nil { + panic(err) + } + + httpmock.RegisterMatcherResponderWithQuery( + "PUT", + "http://baseUrl/gateway/v2/vclusters", + nil, + httpmock.HeaderIs("Authorization", "Basic YWRtaW46Y29uZHVrdG9y"). + And(httpmock.BodyContainsBytes(aVClusterResource.Json)), + responder, + ) + + _, err = gatewayClient.Apply(&aVClusterResource, false) + if err == nil { + t.Failed() + } +} + +func TestGwGetShouldWork(t *testing.T) { + defer httpmock.Reset() + baseUrl := "http://baseUrl" + gatewayClient, err := MakeGateway(GatewayApiParameter{ + BaseUrl: baseUrl, + Debug: false, + CdkGatewayUser: "admin", + CdkGatewayPassword: "conduktor", + }) + if err != nil { + panic(err) + } + httpmock.ActivateNonDefault( + gatewayClient.client.GetClient(), + ) + responder, err := httpmock.NewJsonResponder(200, []resource.Resource{aVClusterResource}) + if err != nil { + panic(err) + } + + httpmock.RegisterMatcherResponderWithQuery( + "GET", + "http://baseUrl/gateway/v2/vclusters", + nil, + httpmock.HeaderIs("Authorization", "Basic YWRtaW46Y29uZHVrdG9y"). + And(httpmock.HeaderIs("X-CDK-CLIENT", "CLI/unknown")), + responder, + ) + + vClusterKind := gatewayClient.GetKinds()["VClusters"] + result, err := gatewayClient.Get(&vClusterKind, []string{}, nil) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(result[0].Json, aVClusterResource.Json) { + t.Errorf("Bad result expected %v got: %v", aVClusterResource, result) + } +} + +func TestGwGetShouldFailIfN2xx(t *testing.T) { + defer httpmock.Reset() + baseUrl := "http://baseUrl" + gatewayClient, err := MakeGateway(GatewayApiParameter{ + BaseUrl: baseUrl, + Debug: false, + CdkGatewayUser: "admin", + CdkGatewayPassword: "conduktor", + }) + if err != nil { + panic(err) + } + httpmock.ActivateNonDefault( + gatewayClient.client.GetClient(), + ) + responder, err := httpmock.NewJsonResponder(404, "") + if err != nil { + panic(err) + } + + httpmock.RegisterMatcherResponderWithQuery( + "GET", + "http://baseUrl/gateway/v2/vclusters", + nil, + httpmock.HeaderIs("Authorization", "Basic changeme"), + responder, + ) + + vClusterKind := gatewayClient.GetKinds()["VClusters"] + _, err = gatewayClient.Get(&vClusterKind, []string{}, nil) + if err == nil { + t.Failed() + } +} + +func TestGwDeleteShouldWork(t *testing.T) { + defer httpmock.Reset() + baseUrl := "http://baseUrl" + gatewayClient, err := MakeGateway(GatewayApiParameter{ + BaseUrl: baseUrl, + Debug: false, + CdkGatewayUser: "admin", + CdkGatewayPassword: "conduktor", + }) + if err != nil { + panic(err) + } + httpmock.ActivateNonDefault( + gatewayClient.client.GetClient(), + ) + responder, err := httpmock.NewJsonResponder(200, "[]") + if err != nil { + panic(err) + } + + httpmock.RegisterMatcherResponderWithQuery( + "DELETE", + "http://baseUrl/gateway/v2/vclusters/vcluster1", + nil, + httpmock.HeaderIs("Authorization", "Basic YWRtaW46Y29uZHVrdG9y"). + And(httpmock.HeaderIs("X-CDK-CLIENT", "CLI/unknown")), + responder, + ) + + vClusters := gatewayClient.GetKinds()["VClusters"] + err = gatewayClient.Delete(&vClusters, []string{}, "vcluster1") + if err != nil { + t.Error(err) + } +} + +func TestGwDeleteShouldFailOnNot2XX(t *testing.T) { + defer httpmock.Reset() + baseUrl := "http://baseUrl" + gatewayClient, err := MakeGateway(GatewayApiParameter{ + BaseUrl: baseUrl, + Debug: false, + CdkGatewayUser: "admin", + CdkGatewayPassword: "conduktor", + }) + if err != nil { + panic(err) + } + httpmock.ActivateNonDefault( + gatewayClient.client.GetClient(), + ) + responder, err := httpmock.NewJsonResponder(404, "[]") + if err != nil { + panic(err) + } + + httpmock.RegisterMatcherResponderWithQuery( + "DELETE", + "http://baseUrl/gateway/v2/vclusters/vcluster1", + nil, + httpmock.HeaderIs("Authorization", "Basic YWRtaW46Y29uZHVrdG9y"), + responder, + ) + + vClusterKind := gatewayClient.GetKinds()["VClusters"] + err = gatewayClient.Delete(&vClusterKind, []string{}, "vcluster1") + if err == nil { + t.Fail() + } +} diff --git a/cmd/apply.go b/cmd/apply.go index 6f78b4e..852999a 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -36,7 +36,13 @@ func initApply(kinds schema.KindCatalog) { schema.SortResourcesForApply(kinds, resources, *debug) allSuccess := true for _, resource := range resources { - upsertResult, err := apiClient().Apply(&resource, *dryRun) + var upsertResult string + var err error + if isGatewayResource(resource, kinds) { + upsertResult, err = gatewayApiClient().Apply(&resource, *dryRun) + } else { + upsertResult, err = consoleApiClient().Apply(&resource, *dryRun) + } if err != nil { fmt.Fprintf(os.Stderr, "Could not apply resource %s/%s: %s\n", resource.Kind, resource.Name, err) allSuccess = false diff --git a/cmd/consoleMkKind.go b/cmd/consoleMkKind.go new file mode 100644 index 0000000..3a600c1 --- /dev/null +++ b/cmd/consoleMkKind.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "github.com/conduktor/ctl/schema" + "github.com/conduktor/ctl/utils" + "github.com/spf13/cobra" +) + +func initConsoleMkKind() { + var prettyPrint *bool + var nonStrict *bool + + var makeKind = &cobra.Command{ + Use: "makeKind [file]", + Short: "Make kind json from openapi file if file not given it will read from api", + Long: ``, + Aliases: []string{"mkKind", "makeConsoleKind"}, + Args: cobra.RangeArgs(0, 1), + Hidden: !utils.CdkDebug(), + Run: func(cmd *cobra.Command, args []string) { + runMkKind(cmd, args, *prettyPrint, *nonStrict, func() ([]byte, error) { return consoleApiClient().GetOpenApi() }, func(schema *schema.Schema, strict bool) (schema.KindCatalog, error) { + return schema.GetConsoleKinds(strict) + }) + }, + } + rootCmd.AddCommand(makeKind) + + prettyPrint = makeKind.Flags().BoolP("pretty", "p", false, "Pretty print the output") + nonStrict = makeKind.Flags().BoolP("non-strict", "n", false, "Don't be strict on the parsing of the schema") +} diff --git a/cmd/delete.go b/cmd/delete.go index c26ef02..1dc5496 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -3,7 +3,6 @@ package cmd import ( "fmt" "os" - "strings" "github.com/conduktor/ctl/schema" "github.com/spf13/cobra" @@ -22,7 +21,19 @@ func initDelete(kinds schema.KindCatalog) { schema.SortResourcesForDelete(kinds, resources, *debug) allSuccess := true for _, resource := range resources { - err := apiClient().DeleteResource(&resource) + var err error + kind := kinds[resource.Kind] + if isGatewayKind(kind) { + if isResourceIdentifiedByName(resource) { + err = gatewayApiClient().DeleteResourceByName(&resource) + } else if isResourceIdentifiedByNameAndVCluster(resource) { + err = gatewayApiClient().DeleteResourceByNameAndVCluster(&resource) + } else if isResourceInterceptor(resource) { + err = gatewayApiClient().DeleteResourceInterceptors(&resource) + } + } else { + err = consoleApiClient().DeleteResource(&resource) + } if err != nil { fmt.Fprintf(os.Stderr, "Could not delete resource %s/%s: %s\n", resource.Kind, resource.Name, err) allSuccess = false @@ -41,29 +52,42 @@ func initDelete(kinds schema.KindCatalog) { deleteCmd.MarkFlagRequired("file") for name, kind := range kinds { - flags := kind.GetFlag() - parentFlagValue := make([]*string, len(flags)) - kindCmd := &cobra.Command{ - Use: fmt.Sprintf("%s [name]", name), - Short: "Delete resource of kind " + name, - Args: cobra.MatchAll(cobra.ExactArgs(1)), - Aliases: []string{strings.ToLower(name), strings.ToLower(name) + "s", name + "s"}, - Run: func(cmd *cobra.Command, args []string) { - parentValue := make([]string, len(parentFlagValue)) - for i, v := range parentFlagValue { - parentValue[i] = *v - } - err := apiClient().Delete(&kind, parentValue, args[0]) - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - os.Exit(1) - } - }, - } - for i, flag := range kind.GetFlag() { - parentFlagValue[i] = kindCmd.Flags().String(flag, "", "Parent "+flag) - kindCmd.MarkFlagRequired(flag) + if isKindIdentifiedByNameAndVCluster(kind) { + byVClusterAndNameDeleteCmd := buildDeleteByVClusterAndNameCmd(kind) + deleteCmd.AddCommand(byVClusterAndNameDeleteCmd) + } else if isKindInterceptor(kind) { + interceptorsDeleteCmd := buildDeleteInterceptorsCmd(kind) + deleteCmd.AddCommand(interceptorsDeleteCmd) + } else { + flags := kind.GetParentFlag() + parentFlagValue := make([]*string, len(flags)) + kindCmd := &cobra.Command{ + Use: fmt.Sprintf("%s [name]", name), + Short: "Delete resource of kind " + name, + Args: cobra.MatchAll(cobra.ExactArgs(1)), + Aliases: buildAlias(name), + Run: func(cmd *cobra.Command, args []string) { + parentValue := make([]string, len(parentFlagValue)) + for i, v := range parentFlagValue { + parentValue[i] = *v + } + var err error + if isGatewayKind(kind) { + err = gatewayApiClient().Delete(&kind, parentValue, args[0]) + } else { + err = consoleApiClient().Delete(&kind, parentValue, args[0]) + } + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + }, + } + for i, flag := range kind.GetParentFlag() { + parentFlagValue[i] = kindCmd.Flags().String(flag, "", "Parent "+flag) + kindCmd.MarkFlagRequired(flag) + } + deleteCmd.AddCommand(kindCmd) } - deleteCmd.AddCommand(kindCmd) } } diff --git a/cmd/delete_gateway_specifics.go b/cmd/delete_gateway_specifics.go new file mode 100644 index 0000000..f8b7867 --- /dev/null +++ b/cmd/delete_gateway_specifics.go @@ -0,0 +1,82 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/conduktor/ctl/schema" + "github.com/spf13/cobra" +) + +func buildDeleteByVClusterAndNameCmd(kind schema.Kind) *cobra.Command { + const vClusterFlag = "vcluster" + name := kind.GetName() + var vClusterValue string + var deleteCmd = &cobra.Command{ + Use: fmt.Sprintf("%s [name]", name), + Short: "Delete resource of kind " + name, + Args: cobra.ExactArgs(1), + Aliases: buildAlias(name), + Run: func(cmd *cobra.Command, args []string) { + var err error + bodyParams := make(map[string]string) + nameValue := args[0] + if nameValue != "" { + bodyParams["name"] = nameValue + } + bodyParams["vCluster"] = vClusterValue + + err = gatewayApiClient().DeleteKindByNameAndVCluster(&kind, bodyParams) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + }, + } + + deleteCmd.Flags().StringVar(&vClusterValue, vClusterFlag, "passthrough", "vCluster of the "+name) + + return deleteCmd +} + +func buildDeleteInterceptorsCmd(kind schema.Kind) *cobra.Command { + const vClusterFlag = "vcluster" + const groupFlag = "group" + const usernameFlag = "username" + var vClusterValue string + var groupValue string + var usernameValue string + name := kind.GetName() + var interceptorDeleteCmd = &cobra.Command{ + Use: fmt.Sprintf("%s [name]", name), + Short: "Delete resource of kind " + name, + Args: cobra.ExactArgs(1), + Aliases: buildAlias(name), + Run: func(cmd *cobra.Command, args []string) { + var err error + bodyParams := make(map[string]string) + nameValue := args[0] + if vClusterValue != "" { + bodyParams["vCluster"] = vClusterValue + } + if groupValue != "" { + bodyParams["group"] = groupValue + } + if usernameValue != "" { + bodyParams["username"] = usernameValue + } + + err = gatewayApiClient().DeleteInterceptor(&kind, nameValue, bodyParams) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + }, + } + + interceptorDeleteCmd.Flags().StringVar(&vClusterValue, vClusterFlag, "", "vCluster of the "+name) + interceptorDeleteCmd.Flags().StringVar(&groupValue, groupFlag, "", "group of the "+name) + interceptorDeleteCmd.Flags().StringVar(&usernameValue, usernameFlag, "", "username of the "+name) + + return interceptorDeleteCmd +} diff --git a/cmd/gatewayMkKind.go b/cmd/gatewayMkKind.go new file mode 100644 index 0000000..a55a0bf --- /dev/null +++ b/cmd/gatewayMkKind.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "github.com/conduktor/ctl/schema" + "github.com/conduktor/ctl/utils" + "github.com/spf13/cobra" +) + +func initGatewayMkKind() { + var prettyPrint *bool + var nonStrict *bool + + var makeKind = &cobra.Command{ + Use: "gatewayMakeKind [file]", + Short: "Make kind json from openapi file if file not given it will read from api", + Long: ``, + Aliases: []string{"gatewayMkKind", "gwMkKind"}, + Args: cobra.RangeArgs(0, 1), + Hidden: !utils.CdkDebug(), + Run: func(cmd *cobra.Command, args []string) { + runMkKind(cmd, args, *prettyPrint, *nonStrict, func() ([]byte, error) { return gatewayApiClient().GetOpenApi() }, func(schema *schema.Schema, strict bool) (schema.KindCatalog, error) { + return schema.GetGatewayKinds(strict) + }) + }, + } + rootCmd.AddCommand(makeKind) + + prettyPrint = makeKind.Flags().BoolP("pretty", "p", false, "Pretty print the output") + nonStrict = makeKind.Flags().BoolP("non-strict", "n", false, "Don't be strict on the parsing of the schema") +} diff --git a/cmd/gateway_utils.go b/cmd/gateway_utils.go new file mode 100644 index 0000000..3e5a25e --- /dev/null +++ b/cmd/gateway_utils.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "strings" + + "github.com/conduktor/ctl/resource" + "github.com/conduktor/ctl/schema" +) + +func isGatewayKind(kind schema.Kind) bool { + _, ok := kind.GetLatestKindVersion().(*schema.GatewayKindVersion) + return ok +} + +func isGatewayResource(resource resource.Resource, kinds schema.KindCatalog) bool { + kind, ok := kinds[resource.Kind] + return ok && isGatewayKind(kind) +} + +func isResourceIdentifiedByName(resource resource.Resource) bool { + return isIdentifiedByName(resource.Kind) +} + +func isResourceIdentifiedByNameAndVCluster(resource resource.Resource) bool { + return isIdentifiedByNameAndVCluster(resource.Kind) +} + +func isKindIdentifiedByNameAndVCluster(kind schema.Kind) bool { + return isIdentifiedByNameAndVCluster(kind.GetName()) +} + +func isIdentifiedByNameAndVCluster(kind string) bool { + return strings.Contains(strings.ToLower(kind), "aliastopic") || + strings.Contains(strings.ToLower(kind), "serviceaccount") || + strings.Contains(strings.ToLower(kind), "concentrationrule") +} + +func isIdentifiedByName(kind string) bool { + return strings.Contains(strings.ToLower(kind), "vcluster") || + strings.Contains(strings.ToLower(kind), "group") +} + +func isResourceInterceptor(resource resource.Resource) bool { + return strings.Contains(strings.ToLower(resource.Kind), "interceptor") +} + +func isKindInterceptor(kind schema.Kind) bool { + return strings.Contains(strings.ToLower(kind.GetName()), "interceptor") +} diff --git a/cmd/genericMkKind.go b/cmd/genericMkKind.go new file mode 100644 index 0000000..c0c55f8 --- /dev/null +++ b/cmd/genericMkKind.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/conduktor/ctl/schema" + "github.com/spf13/cobra" +) + +func runMkKind(cmd *cobra.Command, args []string, prettyPrint bool, nonStrict bool, getOpenApi func() ([]byte, error), getKinds func(*schema.Schema, bool) (schema.KindCatalog, error)) { + var kinds map[string]schema.Kind + if len(args) == 1 { + data, err := os.ReadFile(args[0]) + if err != nil { + panic(err) + } + schema, err := schema.New(data) + if err != nil { + panic(err) + } + kinds, err = getKinds(schema, !nonStrict) + if err != nil { + panic(err) + } + } else { + data, err := getOpenApi() + if err != nil { + panic(err) + } + schema, err := schema.New(data) + if err != nil { + panic(err) + } + kinds, err = getKinds(schema, !nonStrict) + if err != nil { + panic(err) + } + } + var payload []byte + var err error + if prettyPrint { + payload, err = json.MarshalIndent(kinds, "", " ") + } else { + payload, err = json.Marshal(kinds) + } + if err != nil { + panic(err) + } + fmt.Print(string(payload)) +} diff --git a/cmd/get.go b/cmd/get.go index ff3f5ce..f7d5a16 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -3,10 +3,12 @@ package cmd import ( "fmt" "os" + "strconv" "strings" "github.com/conduktor/ctl/resource" "github.com/conduktor/ctl/schema" + "github.com/conduktor/ctl/utils" "github.com/spf13/cobra" ) @@ -21,34 +23,81 @@ var getCmd = &cobra.Command{ }, } +func buildQueryParams(params map[string]interface{}) map[string]string { + queryParams := make(map[string]string) + for key, value := range params { + if value != nil { + str, strOk := value.(*string) + boolValue, boolOk := value.(*bool) + + if strOk { + if *str != "" { + queryParams[key] = *str + } + } else if boolOk { + queryParams[key] = strconv.FormatBool(*boolValue) + } else { + panic("Unknown query flag type") + } + } + } + return queryParams +} + +func removeTrailingSIfAny(name string) string { + return strings.TrimSuffix(name, "s") +} + +func buildAlias(name string) []string { + return []string{strings.ToLower(name), removeTrailingSIfAny(strings.ToLower(name)), removeTrailingSIfAny(name)} +} + func initGet(kinds schema.KindCatalog) { rootCmd.AddCommand(getCmd) for name, kind := range kinds { - flags := kind.GetFlag() - parentFlagValue := make([]*string, len(flags)) + gatewayKind, isGatewayKind := kind.GetLatestKindVersion().(*schema.GatewayKindVersion) + args := cobra.MaximumNArgs(1) + use := fmt.Sprintf("%s [name]", name) + if isGatewayKind && !gatewayKind.GetAvailable { + args = cobra.NoArgs + use = fmt.Sprintf("%s", name) + } + parentFlags := kind.GetParentFlag() + listFlags := kind.GetListFlag() + parentFlagValue := make([]*string, len(parentFlags)) + listFlagValue := make(map[string]interface{}, len(listFlags)) kindCmd := &cobra.Command{ - Use: fmt.Sprintf("%s [name]", name), + Use: use, Short: "Get resource of kind " + name, - Args: cobra.MatchAll(cobra.MaximumNArgs(1)), + Args: args, Long: `If name not provided it will list all resource`, - Aliases: []string{strings.ToLower(name), strings.ToLower(name) + "s", name + "s"}, + Aliases: buildAlias(name), Run: func(cmd *cobra.Command, args []string) { parentValue := make([]string, len(parentFlagValue)) + queryParams := buildQueryParams(listFlagValue) for i, v := range parentFlagValue { parentValue[i] = *v } var err error if len(args) == 0 { var result []resource.Resource - result, err = apiClient().Get(&kind, parentValue) + if isGatewayKind { + result, err = gatewayApiClient().Get(&kind, parentValue, queryParams) + } else { + result, err = consoleApiClient().Get(&kind, parentValue, queryParams) + } for _, r := range result { r.PrintPreservingOriginalFieldOrder() fmt.Println("---") } } else if len(args) == 1 { var result resource.Resource - result, err = apiClient().Describe(&kind, parentValue, args[0]) + if isGatewayKind { + result, err = gatewayApiClient().Describe(&kind, parentValue, args[0]) + } else { + result, err = consoleApiClient().Describe(&kind, parentValue, args[0]) + } result.PrintPreservingOriginalFieldOrder() } if err != nil { @@ -57,10 +106,27 @@ func initGet(kinds schema.KindCatalog) { } }, } - for i, flag := range kind.GetFlag() { + for i, flag := range parentFlags { parentFlagValue[i] = kindCmd.Flags().String(flag, "", "Parent "+flag) kindCmd.MarkFlagRequired(flag) } + for key, flag := range listFlags { + var flagSetted = false + if flag.Type == "string" { + flagSetted = true + listFlagValue[key] = kindCmd.Flags().String(flag.FlagName, "", "") + } else if flag.Type == "boolean" { + flagSetted = true + listFlagValue[key] = kindCmd.Flags().Bool(flag.FlagName, false, "") + } else { + if *debug || utils.CdkDebug() { + fmt.Fprintf(os.Stderr, "Unknown flag type %s\n", flag.Type) + } + } + if flagSetted && flag.Required { + kindCmd.MarkFlagRequired(flag.FlagName) + } + } getCmd.AddCommand(kindCmd) } } diff --git a/cmd/login.go b/cmd/login.go index 3deca6c..7b3f961 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -3,9 +3,9 @@ package cmd import ( "fmt" "os" - "strings" "github.com/conduktor/ctl/client" + "github.com/conduktor/ctl/utils" "github.com/spf13/cobra" ) @@ -16,7 +16,7 @@ var loginCmd = &cobra.Command{ Long: `Use must use CDK_USER CDK_PASSWORD environment variables to login`, Args: cobra.RangeArgs(0, 0), Run: func(cmd *cobra.Command, args []string) { - specificApiClient, err := client.Make(client.ApiParameter{BaseUrl: os.Getenv("CDK_BASE_URL"), Debug: strings.ToLower(os.Getenv("CDK_DEBUG")) == "true"}) + specificApiClient, err := client.Make(client.ApiParameter{BaseUrl: os.Getenv("CDK_BASE_URL"), Debug: utils.CdkDebug()}) if *debug { specificApiClient.ActivateDebug() } diff --git a/cmd/mkKind.go b/cmd/mkKind.go deleted file mode 100644 index 90052dd..0000000 --- a/cmd/mkKind.go +++ /dev/null @@ -1,67 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "github.com/conduktor/ctl/schema" - "github.com/spf13/cobra" - "os" -) - -func initMkKind() { - var prettyPrint *bool - var nonStrict *bool - - var makeKind = &cobra.Command{ - Use: "makeKind [file]", - Short: "Make kind json from openapi file if file not given it will read from api", - Long: ``, - Args: cobra.RangeArgs(0, 1), - Hidden: true, - Run: func(cmd *cobra.Command, args []string) { - var kinds map[string]schema.Kind - if len(args) == 1 { - data, err := os.ReadFile(args[0]) - if err != nil { - panic(err) - } - schema, err := schema.New(data) - if err != nil { - panic(err) - } - kinds, err = schema.GetKinds(!*nonStrict) - if err != nil { - panic(err) - } - } else { - data, err := apiClient().GetOpenApi() - if err != nil { - panic(err) - } - schema, err := schema.New(data) - if err != nil { - panic(err) - } - kinds, err = schema.GetKinds(!*nonStrict) - if err != nil { - panic(err) - } - } - var payload []byte - var err error - if *prettyPrint { - payload, err = json.MarshalIndent(kinds, "", " ") - } else { - payload, err = json.Marshal(kinds) - } - if err != nil { - panic(err) - } - fmt.Print(string(payload)) - }, - } - rootCmd.AddCommand(makeKind) - - prettyPrint = makeKind.Flags().BoolP("pretty", "p", false, "Pretty print the output") - nonStrict = makeKind.Flags().BoolP("non-strict", "n", false, "Don't be strict on the parsing of the schema") -} diff --git a/cmd/printKind.go b/cmd/printKind.go index c34962c..f866aca 100644 --- a/cmd/printKind.go +++ b/cmd/printKind.go @@ -3,7 +3,9 @@ package cmd import ( "encoding/json" "fmt" + "github.com/conduktor/ctl/schema" + "github.com/conduktor/ctl/utils" "github.com/spf13/cobra" ) @@ -16,7 +18,7 @@ func initPrintKind(kinds schema.KindCatalog) { Short: "Print kind catalog used", Long: ``, Args: cobra.NoArgs, - Hidden: true, + Hidden: !utils.CdkDebug(), Run: func(cmd *cobra.Command, args []string) { var payload []byte var err error diff --git a/cmd/root.go b/cmd/root.go index fbdf0cf..a973475 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,8 +12,10 @@ import ( var debug *bool var apiClient_ *client.Client var apiClientError error +var gatewayApiClient_ *client.GatewayClient +var gatewayApiClientError error -func apiClient() *client.Client { +func consoleApiClient() *client.Client { if apiClientError != nil { fmt.Fprintf(os.Stderr, "Cannot create client: %s", apiClientError) os.Exit(1) @@ -21,6 +23,14 @@ func apiClient() *client.Client { return apiClient_ } +func gatewayApiClient() *client.GatewayClient { + if gatewayApiClientError != nil { + fmt.Fprintf(os.Stderr, "Cannot create gateway client: %s", gatewayApiClientError) + os.Exit(1) + } + return gatewayApiClient_ +} + // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "conduktor", @@ -30,7 +40,8 @@ Additionally, you can configure client TLS authentication by providing your cert For server TLS authentication, you can ignore the certificate by setting CDK_INSECURE=true, or provide a certificate authority using CDK_CACERT.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { if *debug { - apiClient().ActivateDebug() + consoleApiClient().ActivateDebug() + gatewayApiClient().ActivateDebug() } }, Run: func(cmd *cobra.Command, args []string) { @@ -54,12 +65,23 @@ func init() { if apiClientError == nil { kinds = apiClient_.GetKinds() } else { - kinds = schema.DefaultKind() + kinds = schema.ConsoleDefaultKind() + } + gatewayApiClient_, gatewayApiClientError = client.MakeGatewayClientFromEnv() + var gatewayKinds schema.KindCatalog + if gatewayApiClientError == nil { + gatewayKinds = gatewayApiClient().GetKinds() + } else { + gatewayKinds = schema.GatewayDefaultKind() + } + for k, v := range gatewayKinds { + kinds[k] = v } debug = rootCmd.PersistentFlags().BoolP("verbose", "v", false, "show more information for debugging") initGet(kinds) initDelete(kinds) initApply(kinds) - initMkKind() + initConsoleMkKind() + initGatewayMkKind() initPrintKind(kinds) } diff --git a/cmd/token.go b/cmd/token.go index 7e0edb3..4324042 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -30,7 +30,7 @@ var listAdminCmd = &cobra.Command{ Use: "admin", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - result, err := apiClient().ListAdminToken() + result, err := consoleApiClient().ListAdminToken() if err != nil { fmt.Fprintf(os.Stderr, "Could not list admin token: %s\n", err) os.Exit(1) @@ -48,7 +48,7 @@ var listApplicationInstanceTokenCmd = &cobra.Command{ Use: "application-instance", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - result, err := apiClient().ListApplicationInstanceToken(*applicationInstanceNameForList) + result, err := consoleApiClient().ListApplicationInstanceToken(*applicationInstanceNameForList) if err != nil { fmt.Fprintf(os.Stderr, "Could not list application-instance token: %s\n", err) os.Exit(1) @@ -86,18 +86,18 @@ var createAdminTokenCmd = &cobra.Command{ fmt.Fprintln(os.Stderr, "Please set CDK_USER if you set CDK_PASSWORD") os.Exit(3) } else if username != "" && password != "" { - jwtToken, err := apiClient().Login(username, password) + jwtToken, err := consoleApiClient().Login(username, password) if err != nil { fmt.Fprintf(os.Stderr, "Could not login: %s\n", err) os.Exit(4) } - apiClient().SetApiKey(jwtToken.AccessToken) + consoleApiClient().SetApiKey(jwtToken.AccessToken) } else if os.Getenv("CDK_API_KEY") == "" { fmt.Fprintln(os.Stderr, "Please set CDK_API_KEY or CDK_USER and CDK_PASSWORD") os.Exit(5) } - result, err := apiClient().CreateAdminToken(args[0]) + result, err := consoleApiClient().CreateAdminToken(args[0]) if err != nil { fmt.Fprintf(os.Stderr, "Could not create admin token: %s\n", err) os.Exit(4) @@ -110,7 +110,7 @@ var createApplicationInstanceTokenCmd = &cobra.Command{ Use: "application-instance --application-instance=myappinstance ", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - result, err := apiClient().CreateApplicationInstanceToken(*applicationInstanceNameForCreate, args[0]) + result, err := consoleApiClient().CreateApplicationInstanceToken(*applicationInstanceNameForCreate, args[0]) if err != nil { fmt.Fprintf(os.Stderr, "Could not create application-instance token: %s\n", err) os.Exit(1) @@ -123,7 +123,7 @@ var deleteTokenCmd = &cobra.Command{ Use: "delete ", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - err := apiClient().DeleteToken(args[0]) + err := consoleApiClient().DeleteToken(args[0]) if err != nil { fmt.Fprintf(os.Stderr, "Could not delete token: %s\n", err) os.Exit(1) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d8ffc72..596ffb9 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -6,11 +6,24 @@ services: environment: CDK_API_KEY: yo CDK_BASE_URL: http://mock:1080 + CDK_GATEWAY_BASE_URL: http://mockGateway:1090 + CDK_GATEWAY_USER: admin + CDK_GATEWAY_PASSWORD: conduktor volumes: - ./test_resource.yml:/test_resource.yml + - ./test_resource_gw.yml:/test_resource_gw.yml + mock: image: mockserver/mockserver:latest volumes: - ./initializer.json:/config/initializer.json environment: MOCKSERVER_INITIALIZATION_JSON_PATH: /config/initializer.json + + mockGateway: + image: mockserver/mockserver:latest + volumes: + - ./initializerGw.json:/config/initializer.json + environment: + MOCKSERVER_INITIALIZATION_JSON_PATH: /config/initializer.json + MOCKSERVER_SERVER_PORT: 1090 diff --git a/docker/initializerGw.json b/docker/initializerGw.json new file mode 100644 index 0000000..16cc308 --- /dev/null +++ b/docker/initializerGw.json @@ -0,0 +1,404 @@ +[ + { + "httpRequest": { + "method": "Get", + "path": "/gateway/v2/vclusters/vcluster1", + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "{ \"metadata\": { \"name\":\"vcluster1\" }, \"spec\":{\"prefix\":\"vcluster1\"} }", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Get", + "path": "/gateway/v2/gateway-groups", + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "[]", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Get", + "path": "/gateway/v2/gateway-groups/g1", + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "{ \"metadata\": { \"name\":\"g1\" }, \"spec\":{} }", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Get", + "path": "/gateway/v2/vclusters", + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "[]", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Put", + "path": "/gateway/v2/vclusters", + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Created\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Delete", + "path": "/gateway/v2/vclusters/vcluster1", + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Deleted\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Put", + "path": "/gateway/v2/alias-topics", + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Created\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Put", + "path": "/gateway/v2/concentration-rules", + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Created\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Put", + "path": "/gateway/v2/gateway-groups", + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Created\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Put", + "path": "/gateway/v2/service-accounts", + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Created\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Put", + "path": "/gateway/v2/interceptors", + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Created\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "GET", + "path": "/gateway/v2/alias-topics", + "queryStringParameters": { + "vcluster": "mycluster1", + "showDefaults": "true", + "name": "yo" + }, + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": [], + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "GET", + "path": "/gateway/v2/concentration-rules", + "queryStringParameters": { + "vcluster": "mycluster1", + "showDefaults": "true", + "name": "yo" + }, + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": [], + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "GET", + "path": "/gateway/v2/service-accounts", + "queryStringParameters": { + "vcluster": "mycluster1", + "showDefaults": "true", + "name": "yo" + }, + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": [], + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "GET", + "path": "/gateway/v2/interceptors", + "queryStringParameters": { + "vcluster": "mycluster1", + "username": "me", + "name": "yo", + "group": "g1" + }, + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": [], + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Delete", + "path": "/gateway/v2/alias-topics", + "body": { + "name": "aliastopicname", + "vCluster": "v1" + }, + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Deleted\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Delete", + "path": "/gateway/v2/concentration-rules", + "body": { + "name": "cr1", + "vCluster": "v1" + }, + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Deleted\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Delete", + "path": "/gateway/v2/service-accounts", + "body": { + "name": "s1", + "vCluster": "v1" + }, + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Deleted\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Delete", + "path": "/gateway/v2/service-accounts", + "body": { + "name": "s1", + "vCluster": "v1" + }, + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Deleted\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Delete", + "path": "/gateway/v2/interceptors/i1", + "body": { + "username": "me", + "group": "g1", + "vCluster": "c1" + }, + "headers": { + "Authorization": "Basic YWRtaW46Y29uZHVrdG9y" + } + }, + "httpResponse": { + "statusCode": 200, + "body": "\"Deleted\"", + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + } +] \ No newline at end of file diff --git a/docker/test_resource_gw.yml b/docker/test_resource_gw.yml new file mode 100644 index 0000000..3f2a479 --- /dev/null +++ b/docker/test_resource_gw.yml @@ -0,0 +1,137 @@ +--- +apiVersion: gateway/v2 +kind: VClusters +metadata: + name: vcluster1 +spec: + prefix: vcluster1 +--- +apiVersion: gateway/v2 +kind: VClusters +metadata: + name: vcluster2 +spec: + prefix: vcluster2 +--- +apiVersion: gateway/v2 +kind: AliasTopics +metadata: + name: alias1 + vCluster: vcluster1 +spec: + physicalName: vcluster1backtopic +--- +apiVersion: gateway/v2 +kind: AliasTopics +metadata: + name: alias2 + vCluster: vcluster1 +spec: + physicalName: vcluster1backtopic +--- +apiVersion: gateway/v2 +kind: AliasTopics +metadata: + name: alias +spec: + physicalName: vcluster1backtopic +--- +apiVersion: gateway/v2 +kind: ConcentrationRules +metadata: + name: rule1 + vCluster: vcluster1 +spec: + logicalTopicNamePattern: toto.* + physicalTopicName: yolo + autoManaged: true +--- +apiVersion: gateway/v2 +kind: ConcentrationRules +metadata: + name: rule2 + vCluster: vcluster1 +spec: + logicalTopicNamePattern: toto.* + physicalTopicName: yolo +--- +apiVersion: gateway/v2 +kind: GatewayGroups +metadata: + name: group1 +spec: + members: + - vCluster: vcluster1 + name: toto1 + externalGroups: ["ext_g1", "ext_g2"] +--- +apiVersion: gateway/v2 +kind: GatewayGroups +metadata: + name: group2 +spec: + members: + - vCluster: vcluster3 + name: toto1 + externalGroups: ["ext_g1", "ext_g2"] +--- +kind: Interceptors +apiVersion: gateway/v2 +metadata: + name: enforce-partition-limit +spec: + pluginClass: "io.conduktor.gateway.interceptor.safeguard.ReadOnlyTopicPolicyPlugin" + priority: 100 + config: + topic: "client_topic_toto" + action: "BLOCK" +--- +kind: Interceptors +apiVersion: gateway/v2 +metadata: + name: fieldlevelencryption + scope: + vCluster: vcluster1 + username: toto +spec: + pluginClass: "io.conduktor.gateway.interceptor.EncryptPlugin" + priority: 100 + config: { + "schemaRegistryConfig": { + "host": "http://localhost:8081" + }, + "fields": [ + { + "fieldName": "number", + "keySecretId": "number-secret" + }, + { + "fieldName": "shipping_address", + "keySecretId": "shipping_address-secret" + } + ] + } +--- +apiVersion: gateway/v2 +kind: ServiceAccounts +metadata: + name: user1 +spec: + type: Local +--- +apiVersion: gateway/v2 +kind: ServiceAccounts +metadata: + name: user1 + vCluster: vcluster1 +spec: + type: Local +--- +apiVersion: gateway/v2 +kind: ServiceAccounts +metadata: + name: user2 + vCluster: vcluster1 +spec: + type: External + principal: ext_user2 diff --git a/schema/console-default-schema.json b/schema/console-default-schema.json new file mode 100644 index 0000000..f4e0894 --- /dev/null +++ b/schema/console-default-schema.json @@ -0,0 +1 @@ +{"Application":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application","Name":"Application","ParentPathParam":[],"ListQueryParamter":{},"Order":8}}},"ApplicationGroup":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application-group","Name":"ApplicationGroup","ParentPathParam":[],"ListQueryParamter":{},"Order":11}}},"ApplicationInstance":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application-instance","Name":"ApplicationInstance","ParentPathParam":[],"ListQueryParamter":{"application":{"FlagName":"application","Required":false,"Type":"string"}},"Order":9}}},"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"}},"Order":10}}},"Group":{"Versions":{"2":{"ListPath":"/public/iam/v2/group","Name":"Group","ParentPathParam":[],"ListQueryParamter":{},"Order":1}}},"KafkaCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/kafka-cluster","Name":"KafkaCluster","ParentPathParam":[],"ListQueryParamter":{},"Order":2}}},"KafkaConnectCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/cluster/{cluster}/kafka-connect","Name":"KafkaConnectCluster","ParentPathParam":["cluster"],"ListQueryParamter":{},"Order":3}}},"KsqlDBCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/cluster/{cluster}/ksqldb","Name":"KsqlDBCluster","ParentPathParam":["cluster"],"ListQueryParamter":{},"Order":4}}},"Subject":{"Versions":{"2":{"ListPath":"/public/kafka/v2/cluster/{cluster}/subject","Name":"Subject","ParentPathParam":["cluster"],"ListQueryParamter":{},"Order":6}}},"Topic":{"Versions":{"2":{"ListPath":"/public/kafka/v2/cluster/{cluster}/topic","Name":"Topic","ParentPathParam":["cluster"],"ListQueryParamter":{},"Order":5}}},"TopicPolicy":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/topic-policy","Name":"TopicPolicy","ParentPathParam":[],"ListQueryParamter":{"app-instance":{"FlagName":"application-instance","Required":false,"Type":"string"}},"Order":7}}},"User":{"Versions":{"2":{"ListPath":"/public/iam/v2/user","Name":"User","ParentPathParam":[],"ListQueryParamter":{},"Order":0}}}} \ No newline at end of file diff --git a/schema/console_schema_test.go b/schema/console_schema_test.go new file mode 100644 index 0000000..d7f9d29 --- /dev/null +++ b/schema/console_schema_test.go @@ -0,0 +1,360 @@ +package schema + +import ( + "os" + "reflect" + "strings" + "testing" + + "github.com/davecgh/go-spew/spew" +) + +func TestGetKindWithYamlFromOldConsolePlusWithoutOrder(t *testing.T) { + t.Run("gets kinds from schema", func(t *testing.T) { + schemaContent, err := os.ReadFile("docs_without_order.yaml") + if err != nil { + t.Fatalf("failed reading file: %s", err) + } + + schema, err := New(schemaContent) + if err != nil { + t.Fatalf("failed creating new schema: %s", err) + } + + kinds, err := schema.GetConsoleKinds(false) + if err != nil { + t.Fatalf("failed getting kinds: %s", err) + } + + expected := KindCatalog{ + "Application": { + Versions: map[int]KindVersion{ + 1: &ConsoleKindVersion{ + Name: "Application", + ListPath: "/public/self-serve/v1/application", + ParentPathParam: make([]string, 0), + ListQueryParamter: map[string]QueryParameterOption{}, + Order: DefaultPriority, + }, + }, + }, + "ApplicationInstance": { + Versions: map[int]KindVersion{ + 1: &ConsoleKindVersion{ + Name: "ApplicationInstance", + ListPath: "/public/self-serve/v1/application-instance", + ParentPathParam: make([]string, 0), + ListQueryParamter: map[string]QueryParameterOption{ + "application": { + FlagName: "application", + Required: false, + Type: "string", + }, + }, + Order: DefaultPriority, + }, + }, + }, + "ApplicationInstancePermission": { + Versions: map[int]KindVersion{ + 1: &ConsoleKindVersion{ + Name: "ApplicationInstancePermission", + ListPath: "/public/self-serve/v1/application-instance-permission", + ParentPathParam: make([]string, 0), + ListQueryParamter: map[string]QueryParameterOption{ + "filterByApplication": { + FlagName: "application", + Required: false, + Type: "string", + }, + "filterByApplicationInstance": { + FlagName: "application-instance", + Required: false, + Type: "string", + }, + "filterByGrantedTo": { + FlagName: "granted-to", + Required: false, + Type: "string", + }, + }, + Order: DefaultPriority, + }, + }, + }, + "TopicPolicy": { + Versions: map[int]KindVersion{ + 1: &ConsoleKindVersion{ + Name: "TopicPolicy", + ListPath: "/public/self-serve/v1/topic-policy", + ParentPathParam: make([]string, 0), + ListQueryParamter: map[string]QueryParameterOption{ + "app-instance": { + FlagName: "application-instance", + Required: false, + Type: "string", + }, + }, + Order: DefaultPriority, + }, + }, + }, + "Topic": { + Versions: map[int]KindVersion{ + 2: &ConsoleKindVersion{ + Name: "Topic", + ListPath: "/public/kafka/v2/cluster/{cluster}/topic", + ParentPathParam: []string{"cluster"}, + ListQueryParamter: map[string]QueryParameterOption{}, + Order: DefaultPriority, + }, + }, + }, + } + if !reflect.DeepEqual(kinds, expected) { + t.Error(spew.Printf("got kinds %v, want %v", kinds, expected)) + } + }) +} + +func TestGetKindWithYamlFromConsolePlus(t *testing.T) { + t.Run("gets kinds from schema", func(t *testing.T) { + schemaContent, err := os.ReadFile("docs_with_order.yaml") + if err != nil { + t.Fatalf("failed reading file: %s", err) + } + + schema, err := New(schemaContent) + if err != nil { + t.Fatalf("failed creating new schema: %s", err) + } + + kinds, err := schema.GetConsoleKinds(true) + if err != nil { + t.Fatalf("failed getting kinds: %s", err) + } + + expected := KindCatalog{ + "Application": { + Versions: map[int]KindVersion{ + 1: &ConsoleKindVersion{ + Name: "Application", + ListPath: "/public/self-serve/v1/application", + ParentPathParam: []string{}, + ListQueryParamter: map[string]QueryParameterOption{}, + Order: 6, + }, + }, + }, + "ApplicationInstance": { + Versions: map[int]KindVersion{ + 1: &ConsoleKindVersion{ + Name: "ApplicationInstance", + ListPath: "/public/self-serve/v1/application-instance", + ParentPathParam: []string{}, + ListQueryParamter: map[string]QueryParameterOption{ + "application": { + FlagName: "application", + Required: false, + Type: "string", + }, + }, + Order: 7, + }, + }, + }, + "ApplicationInstancePermission": { + Versions: map[int]KindVersion{ + 1: &ConsoleKindVersion{ + Name: "ApplicationInstancePermission", + ListPath: "/public/self-serve/v1/application-instance-permission", + ParentPathParam: []string{}, + ListQueryParamter: map[string]QueryParameterOption{ + "filterByApplication": { + FlagName: "application", + Required: false, + Type: "string", + }, + "filterByApplicationInstance": { + FlagName: "application-instance", + Required: false, + Type: "string", + }, + "filterByGrantedTo": { + FlagName: "granted-to", + Required: false, + Type: "string", + }, + }, + Order: 8, + }, + }, + }, + "ApplicationGroup": { + Versions: map[int]KindVersion{ + 1: &ConsoleKindVersion{ + Name: "ApplicationGroup", + ListPath: "/public/self-serve/v1/application-group", + ParentPathParam: []string{}, + ListQueryParamter: map[string]QueryParameterOption{}, + Order: 9, + }, + }, + }, + "TopicPolicy": { + Versions: map[int]KindVersion{ + 1: &ConsoleKindVersion{ + Name: "TopicPolicy", + ListPath: "/public/self-serve/v1/topic-policy", + ParentPathParam: []string{}, + ListQueryParamter: map[string]QueryParameterOption{ + "app-instance": { + FlagName: "application-instance", + Required: false, + Type: "string", + }, + }, + Order: 5, + }, + }, + }, + "Topic": { + Versions: map[int]KindVersion{ + 2: &ConsoleKindVersion{ + Name: "Topic", + ListPath: "/public/kafka/v2/cluster/{cluster}/topic", + ParentPathParam: []string{"cluster"}, + ListQueryParamter: map[string]QueryParameterOption{}, + Order: 3, + }, + }, + }, + "Subject": { + Versions: map[int]KindVersion{ + 2: &ConsoleKindVersion{ + Name: "Subject", + ListPath: "/public/kafka/v2/cluster/{cluster}/subject", + ParentPathParam: []string{"cluster"}, + ListQueryParamter: map[string]QueryParameterOption{}, + Order: 4, + }, + }, + }, + "User": { + Versions: map[int]KindVersion{ + 2: &ConsoleKindVersion{ + Name: "User", + ListPath: "/public/iam/v2/user", + ParentPathParam: []string{}, + ListQueryParamter: map[string]QueryParameterOption{}, + Order: 0, + }, + }, + }, + "Group": { + Versions: map[int]KindVersion{ + 2: &ConsoleKindVersion{ + Name: "Group", + ListPath: "/public/iam/v2/group", + ParentPathParam: []string{}, + ListQueryParamter: map[string]QueryParameterOption{}, + Order: 1, + }, + }, + }, + "KafkaCluster": { + Versions: map[int]KindVersion{ + 2: &ConsoleKindVersion{ + Name: "KafkaCluster", + ListPath: "/public/console/v2/kafka-cluster", + ParentPathParam: []string{}, + ListQueryParamter: map[string]QueryParameterOption{}, + Order: 2, + }, + }, + }, + } + if !reflect.DeepEqual(kinds, expected) { + t.Error(spew.Printf("got kinds %v, want %v", kinds, expected)) + } + }) +} + +func TestGetKindWithMultipleVersion(t *testing.T) { + t.Run("gets kinds from schema", func(t *testing.T) { + schemaContent, err := os.ReadFile("multiple_version.yaml") + if err != nil { + t.Fatalf("failed reading file: %s", err) + } + + schema, err := New(schemaContent) + if err != nil { + t.Fatalf("failed creating new schema: %s", err) + } + + kinds, err := schema.GetConsoleKinds(false) + if err != nil { + t.Fatalf("failed getting kinds: %s", err) + } + + expected := KindCatalog{ + "Topic": { + Versions: map[int]KindVersion{ + 1: &ConsoleKindVersion{ + Name: "Topic", + ListPath: "/public/v1/cluster/{cluster}/topic", + ParentPathParam: []string{"cluster"}, + ListQueryParamter: map[string]QueryParameterOption{}, + Order: DefaultPriority, + }, + 2: &ConsoleKindVersion{ + Name: "Topic", + ListPath: "/public/v2/cluster/{cluster}/sa/{sa}/topic", + ParentPathParam: []string{"cluster", "sa"}, + ListQueryParamter: map[string]QueryParameterOption{}, + Order: 42, + }, + }, + }, + } + if !reflect.DeepEqual(kinds, expected) { + t.Error(spew.Printf("got kinds %v, want %v", kinds, expected)) + } + }) +} +func TestKindWithMissingMetadataField(t *testing.T) { + t.Run("gets kinds from schema", func(t *testing.T) { + schemaContent, err := os.ReadFile("missing_field_in_metadata.yaml") + if err != nil { + t.Fatalf("failed reading file: %s", err) + } + + schema, err := New(schemaContent) + if err != nil { + t.Fatalf("failed creating new schema: %s", err) + } + + _, err = schema.GetConsoleKinds(true) + if !strings.Contains(err.Error(), "Parent path param sa not found in metadata for kind Topic") { + t.Fatalf("Not expected error: %s", err) + } + }) +} +func TestKindNotRequiredMetadataField(t *testing.T) { + t.Run("gets kinds from schema", func(t *testing.T) { + schemaContent, err := os.ReadFile("not_required_field_in_metadata.yaml") + if err != nil { + t.Fatalf("failed reading file: %s", err) + } + + schema, err := New(schemaContent) + if err != nil { + t.Fatalf("failed creating new schema: %s", err) + } + + _, err = schema.GetConsoleKinds(true) + if !strings.Contains(err.Error(), "Parent path param sa in metadata for kind Topic not required") { + t.Fatalf("Not expected error: %s", err) + } + }) +} diff --git a/schema/default-schema.json b/schema/default-schema.json deleted file mode 100644 index d0a809d..0000000 --- a/schema/default-schema.json +++ /dev/null @@ -1 +0,0 @@ -{"Application":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application","Name":"Application","ParentPathParam":[],"Order":8}}},"ApplicationGroup":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application-group","Name":"ApplicationGroup","ParentPathParam":[],"Order":11}}},"ApplicationInstance":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application-instance","Name":"ApplicationInstance","ParentPathParam":[],"Order":9}}},"ApplicationInstancePermission":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/application-instance-permission","Name":"ApplicationInstancePermission","ParentPathParam":[],"Order":10}}},"Group":{"Versions":{"2":{"ListPath":"/public/iam/v2/group","Name":"Group","ParentPathParam":[],"Order":1}}},"KafkaCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/kafka-cluster","Name":"KafkaCluster","ParentPathParam":[],"Order":2}}},"KafkaConnectCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/cluster/{cluster}/kafka-connect","Name":"KafkaConnectCluster","ParentPathParam":["cluster"],"Order":3}}},"KsqlDBCluster":{"Versions":{"2":{"ListPath":"/public/console/v2/cluster/{cluster}/ksqldb","Name":"KsqlDBCluster","ParentPathParam":["cluster"],"Order":4}}},"Subject":{"Versions":{"2":{"ListPath":"/public/kafka/v2/cluster/{cluster}/subject","Name":"Subject","ParentPathParam":["cluster"],"Order":6}}},"Topic":{"Versions":{"2":{"ListPath":"/public/kafka/v2/cluster/{cluster}/topic","Name":"Topic","ParentPathParam":["cluster"],"Order":5}}},"TopicPolicy":{"Versions":{"1":{"ListPath":"/public/self-serve/v1/topic-policy","Name":"TopicPolicy","ParentPathParam":[],"Order":7}}},"User":{"Versions":{"2":{"ListPath":"/public/iam/v2/user","Name":"User","ParentPathParam":[],"Order":0}}}} \ No newline at end of file diff --git a/schema/gateway-default-schema.json b/schema/gateway-default-schema.json new file mode 100644 index 0000000..1c0b980 --- /dev/null +++ b/schema/gateway-default-schema.json @@ -0,0 +1 @@ +{"AliasTopics":{"Versions":{"2":{"ListPath":"/gateway/v2/alias-topics","Name":"AliasTopics","ParentPathParam":[],"ListQueryParameter":{"name":{"FlagName":"name","Required":false,"Type":"string"},"showDefaults":{"FlagName":"show-defaults","Required":false,"Type":"boolean"},"vcluster":{"FlagName":"vcluster","Required":false,"Type":"string"}},"GetAvailable":false,"Order":8}}},"ConcentrationRules":{"Versions":{"2":{"ListPath":"/gateway/v2/concentration-rules","Name":"ConcentrationRules","ParentPathParam":[],"ListQueryParameter":{"name":{"FlagName":"name","Required":false,"Type":"string"},"showDefaults":{"FlagName":"show-defaults","Required":false,"Type":"boolean"},"vcluster":{"FlagName":"vcluster","Required":false,"Type":"string"}},"GetAvailable":false,"Order":9}}},"GatewayGroups":{"Versions":{"2":{"ListPath":"/gateway/v2/gateway-groups","Name":"GatewayGroups","ParentPathParam":[],"ListQueryParameter":{"showDefaults":{"FlagName":"show-defaults","Required":false,"Type":"boolean"}},"GetAvailable":true,"Order":10}}},"Interceptors":{"Versions":{"2":{"ListPath":"/gateway/v2/interceptors","Name":"Interceptors","ParentPathParam":[],"ListQueryParameter":{"global":{"FlagName":"global","Required":false,"Type":"boolean"},"group":{"FlagName":"group","Required":false,"Type":"string"},"name":{"FlagName":"name","Required":false,"Type":"string"},"username":{"FlagName":"username","Required":false,"Type":"string"},"vcluster":{"FlagName":"vcluster","Required":false,"Type":"string"}},"GetAvailable":false,"Order":12}}},"ServiceAccounts":{"Versions":{"2":{"ListPath":"/gateway/v2/service-accounts","Name":"ServiceAccounts","ParentPathParam":[],"ListQueryParameter":{"name":{"FlagName":"name","Required":false,"Type":"string"},"showDefaults":{"FlagName":"show-defaults","Required":false,"Type":"boolean"},"type":{"FlagName":"type","Required":false,"Type":"string"},"vcluster":{"FlagName":"vcluster","Required":false,"Type":"string"}},"GetAvailable":false,"Order":11}}},"VClusters":{"Versions":{"2":{"ListPath":"/gateway/v2/vclusters","Name":"VClusters","ParentPathParam":[],"ListQueryParameter":{},"GetAvailable":true,"Order":7}}}} \ No newline at end of file diff --git a/schema/gateway.yaml b/schema/gateway.yaml new file mode 100644 index 0000000..08af72f --- /dev/null +++ b/schema/gateway.yaml @@ -0,0 +1,3166 @@ +openapi: 3.1.0 +info: + title: Conduktor API + version: v2 + summary: The API to interact with Conduktor Gateway programmatically + contact: + email: contact@conduktor.io + url: https://docs.conduktor.io + x-logo: + url: https://avatars.githubusercontent.com/u/60062294?s=200&v=4 + backgroundColor: '#FFFFFF' + altText: Conduktor logo +tags: +- name: Introduction + description: | + The Conduktor Gateway REST API 's aim is to help you configure your Gateway. + + Get started with Conduktor Gateway [self-hosted](https://docs.conduktor.io/gateway/installation/) today. Setup takes only a few minutes. +- name: Authentication + description: |- + Authentication to the API requires a basic authentication. + + To get a token and use it you must go through the following steps: + + * Configure the admin password in the Gateway YAML configuration file. + + * Use the password in the **authorization** header of your requests. + + Example: + + ```shell + curl -X GET "https://your.gateway-api.host/gateway/v2/vclusters" \ + -H "accept: application/json" \ + --user "admin:password" + ``` +- name: Kinds + description: | + ### Definition + + Kinds the resource types of the Conduktor gateway. + + ### Conduktor Gateway Kinds + + The following kinds are available in the Conduktor Gateway API: + + * `VCluster` + * `AliasTopic` + * `ConcentratedTopic` + * `ConcentrationRule` + * `Interceptor` + * `Plugin` + * `ServiceAccount` + * `Token` + * `Group` +- name: Api Groups + description: |+ + ### Definition + + API groups a set of resources that share the same API path prefix. They are used to organize the API endpoints and the + resources they manage. + The versioning is set at this level, so all the resources in the same group share the same version. + Kinds of a same group can be nested paths in the API, for example, the `vcluster` kind can have an `alias-topic` kind + nested in it. + + ### Conduktor Api Groups + + The Gateway API consist of a single API group right now (`gateway`), and it manages the following resources: + + | Api Group | Kinds | + |-----------|----------------------------------------------------------------------------------------------------------------------------------------------------| + | `gateway` | `vclusters`, `alias-topics`, `concentrated-topics`, `concentration-rules`,
`interceptors`, `plugins`, `service-accounts`, `groups`, `tokens` | + + + +- name: Versioning + description: |+ + * __The version is set at the api group level__. It is incremented when a breaking change happens in the schema of an endpoint of the group (that has been marked `stable`). The n-1 version is still available for a while to allow users to migrate. The version is part of the endpoint path. + * The API version (v2) is the global version of the Conduktor Gateway API, it should not change unless there is a complete overhaul of the API. + + + Endpoint also have a status to manage their API lifecycle, following the order below: + * __preview__: this is an early-stage feature, really likely to change + * __beta__: early access feature, breaking change + * __stable__: Production-ready endpoint, no breaking change + * __deprecated__: This endpoint isn't supported anymore and the user should migrate + + +- name: Conventions + description: |+ + ### Path conventions + + The API follows as much as possible the endpoints structure below for each kind of resource: + + * `GET /{api-group}/{version}/{kind}/{name}` to read a resource + * `GET /{api-group}/{version}/{kind}` to list resources of a kind + * `PUT /{api-group}/{version}/{kind}` to update or create a resource + * `DELETE /{api-group}/{version}/{kind}/{name}` to delete a resource (returns 204 No Content) + * `POST` is used for specific operations that don't fit this CRUD model. PUT is the default verb for updates and + creations. + * Important principle: the result of a GET can be reused as the body of a PUT to update the resource. + + __Non-unique names__: + When a `name` is not enough to uniquely identify a resource, the GET and DELETE endpoint are different + The GET by name is replaced by query parameters (returning lists or the searched item if the criterias are the elements of the key), and the DELETE by name is replaced by a DELETE with a body. + For example, an `alias-topic` is identified by its `name` and the `vCluster` gives the following endpoints: + + * `GET /gateway/v2/vclusters/{name}/alias-topics?name={name}&vcluster={vcluster}` + * `PUT /gateway/v2/vclusters/{name}/alias-topics` + * `DELETE /gateway/v2/vclusters/{name}/alias-topics` with a body containing the `name` and the `vCluster` + + ### Other conventions + + * All requests and responses are in JSON and should have their `content-type` header set to `application/json` + * Every kind has a pluralized name (e.g. `vclusters` for the `VCluster` kind) that is used in the endpoint path. + * Errors have a standard format: + * The HTTP status code is used to indicate the type of error. + * The response body contains a common JSON object for every error: + * `title`: a unique error code for the error. + * `message`: a human-readable message for the error. + * `cause`: additional information about the error. + * All timestamps are in ISO 8601 format. + +- name: tokens + description: |+ + ### Definition + + The token group contains just a utility endpoint to generate a token for a given Local ServiceAccount (on a given + vCluster, or `passthrough` if omitted). + + This token can be used to authenticate against the Gateway API in the `sasl_plaintext` authentication mode. + More information on this case here : https://docs.conduktor.io/gateway/concepts/Clients/#sasl_plaintext + + ### Available operations + + * Generate a token + + ### Identity + + N/A. + +- name: cli_vclusters_gateway_v2_7 + description: |+ + ### Definition + + https://docs.conduktor.io/gateway/concepts/Virtual%20Cluster/ + + ### Available operations + + * List virtual clusters + * Get a virtual cluster + * Upsert a virtual cluster + * Delete a virtual cluster + + ### Identity + + A virtual cluster is identified by the `name`. + + ### Schema + + + + + x-displayName: vclusters +- name: cli_alias-topics_gateway_v2_8 + description: |+ + ### Definition + + https://docs.conduktor.io/gateway/concepts/Logical_topics/Alias%20topics/ + + ### Available operations + + * List alias topics + * Upsert an alias topic + * Delete an alias topic + + ### Identity + + An alias topic is identified by the `name` inside a `vcluster` (`passthrough` if omitted). + + ### Schema + + + + + x-displayName: alias-topics +- name: cli_concentrated-topics_gateway_v2_0 + description: |+ + ### Definition + + https://docs.conduktor.io/gateway/concepts/Logical_topics/Concentration/ + + ### Available operations + + * List concentrated topics + + Concentrated topics are created when a concentration rule is applied. They are not created or deleted directly by the API. + + ### Identity + + A concentrated topic is identified by the `name` and the `vCluster` (`passthrough` if omitted). + + ### Schema + + + + + x-displayName: concentrated-topics +- name: cli_concentration-rules_gateway_v2_9 + description: |+ + ### Definition + + https://docs.conduktor.io/gateway/concepts/Logical_topics/Concentration/ + + ### Available operations + + * List concentration rules + * Get a concentration rule + * Upsert a concentration rule + * Delete a concentration rule + + + ### Identity + + A concentration rule is identified by its name inside a vCluster. + + ### Schema + + + + + x-displayName: concentration-rules +- name: cli_interceptors_gateway_v2_12 + description: |+ + ### Definition + + https://docs.conduktor.io/gateway/concepts/Interceptors-and-plugins/ + + ### Available operations + + * List the interceptors + * Get an interceptor + * Upsert an interceptor + * Delete an interceptor + + ### Identity + + An interceptor is identified by its `name` and its `scope`. + The `scope` is itself composed of 3 optional fields `vCluster`, `group`, `username`. + A __global__ interceptor is an interceptor whose `scope` has its fields empty. + + ### Schema + + + + + x-displayName: interceptors +- name: plugins + description: |+ + ### Definition + + https://docs.conduktor.io/gateway/concepts/Interceptors-and-plugins/ + + ### Available operations + + * List the available plugins of the Gateway + + ### Identity + + A plugin is identified by its `pluginId`. + + The list of plugins is fixed for a given Gateway instance. + The list is fixed and cannot be modified. + + ### Schema + + + + +- name: cli_service-accounts_gateway_v2_11 + description: |+ + ### Definition + + https://docs.conduktor.io/gateway/concepts/GatewayUser/ + + ### Available operations + + * List service accounts + * Upsert a service account + * Delete a service account + + ### Identity + + The service account is identified by the `name` and the `vClusterName`. + + The vCluster name is not mandatory, but if omitted, the `passthrough` vCluster will be used. + Thus, a service account is always associated with one and only one vCluster. + + ### Local and external service accounts + + A service account can be `Local` or `External`. + + * A `Local` service account is just a local user that allows to generate Gateway tokens for your Kafka client + applications (SASL). + * An `External` service account is a user that is authenticated by an external system (OIDC). In such a + scenario you will only need to create external service accounts in 2 cases : either to rename the OIDC principal + for you Kafka applications OR to gather service accounts into `Groups`. Gateway Tokens are not issued for external + service accounts since the authentication must be done by the external system. + + To create an external service account you must provide have an `principal` that is the name of the user in the + external system. + It can be equal or not to the service account `name` (up to you), but note that __the `principal` must be unique + accross all the vClusters__. + + ### Schema + + + + + x-displayName: service-accounts +- name: cli_gateway-groups_gateway_v2_10 + description: |+ + ### Definition + + Groups a defined by name a allow to regroup Gateway users in order to apply interceptors rules. + + ### Available operations + + * List groups + * Get a group + * Upsert a group + * Delete a group + + ### Identity + + The group is identified by its name (unique across all the vClusters). + + A group can be added external groups (coming from LDAP, OIDC claims etc.) which will allow the Gateway to bind them on the Gateway Group. + + ### Schema + + + + + x-displayName: gateway-groups +paths: + /gateway/v2/tokens: + post: + tags: + - tokens + description: |2+ + + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Create a new token for given service account and a given vcluster. + + If the vcluster is not provided, the token will be created for the passthrough vcluster. + + operationId: Generate a token for a service account on a vcluster + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TokenRequest' + example: + vClusterName: vcluster1 + username: user1 + lifeTimeSeconds: 3600 + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + example: + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMSIsIm5hbWUiOiJ1c2VyMSIsImlhdCI6MTUxNjIzOTAyMn0.1Q2JjNz + '400': + description: 'Invalid value for: body' + content: + text/plain: + schema: + type: string + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The searched entity was not found + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The searched entity was not found + '409': + description: Requesting a JWT token when no user pool service is configured + in the Gateway or requesting a token for an external service account. + content: + application/json: + schema: + $ref: '#/components/schemas/Conflict' + example: + title: Requesting a JWT token when no user pool service is configured + in the Gateway or requesting a token for an external service account. + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request POST \ + --url 'http://localhost:8888/gateway/v2/tokens' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{"vClusterName":"vcluster1","username":"user1","lifeTimeSeconds":3600000}' + /gateway/v2/vclusters: + get: + tags: + - cli_vclusters_gateway_v2_7 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + List the vclusters + + operationId: List the vclusters + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/VCluster' + example: + - kind: VClusters + apiVersion: gateway/v2 + metadata: + name: vcluster1 + spec: + prefix: vcluster1 + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request GET \ + --url 'http://localhost:8888/gateway/v2/vclusters' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' + put: + tags: + - cli_vclusters_gateway_v2_7 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Upsert a vcluster + + operationId: Upsert a vcluster + parameters: + - name: dryMode + in: query + description: If true, the operation will be simulated and no changes will + be made + required: false + schema: + default: false + type: boolean + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/VCluster' + example: + kind: VClusters + apiVersion: gateway/v2 + metadata: + name: vcluster1 + spec: + prefix: vcluster1 + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ApplyResult_VCluster' + example: + resource: + kind: VClusters + apiVersion: gateway/v2 + metadata: + name: vcluster1 + spec: + prefix: vcluster1 + upsertResult: Updated + '400': + description: Wrong format or usage of reserved keywords (e.g. passthrough) + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: Wrong format or usage of reserved keywords (e.g. passthrough) + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '409': + description: The given prefix is already used by another vcluster + content: + application/json: + schema: + $ref: '#/components/schemas/Conflict' + example: + title: The given prefix is already used by another vcluster + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request PUT \ + --url 'http://localhost:8888/gateway/v2/vclusters?dryMode=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "kind" : "VClusters", + "apiVersion" : "gateway/v2", + "metadata" : { + "name" : "vcluster1" + }, + "spec" : { + "prefix" : "vcluster1" + } + }' + /gateway/v2/vclusters/{vClusterName}: + get: + tags: + - cli_vclusters_gateway_v2_7 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Get a vcluster + + operationId: Get a vcluster + parameters: + - name: vClusterName + in: path + description: The name of the vcluster + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/VCluster' + example: + kind: VClusters + apiVersion: gateway/v2 + metadata: + name: vcluster1 + spec: + prefix: vcluster1 + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given vcluster does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given vcluster does not exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request GET \ + --url 'http://localhost:8888/gateway/v2/vclusters/vcluster1' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' + delete: + tags: + - cli_vclusters_gateway_v2_7 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Delete a vcluster + + operationId: Delete a vcluster + parameters: + - name: vClusterName + in: path + description: The name of the vcluster + required: true + schema: + type: string + responses: + '204': + description: '' + '400': + description: Default passthrough vcluster cannot be deleted + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: Default passthrough vcluster cannot be deleted + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given vcluster does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given vcluster does not exist + '409': + description: The given vcluster has references (logical topics, interceptors, + concentration rules, service accounts) and cannot be deleted + content: + application/json: + schema: + $ref: '#/components/schemas/Conflict' + example: + title: The given vcluster has references (logical topics, interceptors, + concentration rules, service accounts) and cannot be deleted + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request DELETE \ + --url 'http://localhost:8888/gateway/v2/vclusters/vcluster1' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' + /gateway/v2/alias-topics: + get: + tags: + - cli_alias-topics_gateway_v2_8 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + List the alias topics of a vcluster + + operationId: List the alias topics + parameters: + - name: vcluster + in: query + description: The vCluster filter + required: false + schema: + type: string + - name: name + in: query + description: The name filter + required: false + schema: + type: string + - name: showDefaults + in: query + description: Whether to show default values or not + required: false + schema: + default: false + type: boolean + example: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AliasTopic' + example: + - kind: AliasTopics + apiVersion: gateway/v2 + metadata: + name: name1 + vCluster: vCluster1 + spec: + physicalName: physicalName1 + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given vCluster does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given vCluster does not exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request GET \ + --url 'http://localhost:8888/gateway/v2/alias-topics?showDefaults=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' + put: + tags: + - cli_alias-topics_gateway_v2_8 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Upsert an alias topic in a vcluster + + operationId: Upsert an alias topic + parameters: + - name: dryMode + in: query + description: Whether to simulate the operation or not + required: false + schema: + default: false + type: boolean + example: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AliasTopic' + example: + kind: AliasTopics + apiVersion: gateway/v2 + metadata: + name: name1 + vCluster: vCluster1 + spec: + physicalName: physicalName1 + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ApplyResult_AliasTopic' + example: + resource: + kind: AliasTopics + apiVersion: gateway/v2 + metadata: + name: name1 + vCluster: vCluster1 + spec: + physicalName: physicalName1 + upsertResult: Updated + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given vCluster does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given vCluster does not exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request PUT \ + --url 'http://localhost:8888/gateway/v2/alias-topics?dryMode=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "kind" : "AliasTopics", + "apiVersion" : "gateway/v2", + "metadata" : { + "name" : "name1", + "vCluster" : "vCluster1" + }, + "spec" : { + "physicalName" : "physicalName1" + } + }' + delete: + tags: + - cli_alias-topics_gateway_v2_8 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Delete an alias topic in a vcluster + + operationId: Delete an alias topic + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AliasTopicId' + required: true + responses: + '204': + description: '' + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given name does not exist in the given vCluster + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given name does not exist in the given vCluster + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request DELETE \ + --url 'http://localhost:8888/gateway/v2/alias-topics' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "name" : "name1", + "vCluster" : "vCluster1" + }' + /gateway/v2/concentrated-topics: + get: + tags: + - cli_concentrated-topics_gateway_v2_0 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + List the concentrated topics of a vcluster + + operationId: List the concentrated topics + parameters: + - name: vcluster + in: query + description: The vCluster filter + required: false + schema: + type: string + - name: name + in: query + description: The name filter + required: false + schema: + type: string + - name: showDefaults + in: query + description: Whether to show default values or not + required: false + schema: + default: false + type: boolean + example: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ConcentratedTopic' + example: + - kind: concentrated-topics + apiVersion: gateway/v2 + metadata: + name: name1 + vCluster: vCluster1 + spec: + physicalName: physicalName1 + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given vCluster does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given vCluster does not exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request GET \ + --url 'http://localhost:8888/gateway/v2/concentrated-topics?showDefaults=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' + /gateway/v2/concentration-rules: + get: + tags: + - cli_concentration-rules_gateway_v2_9 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + List the concentration rules of a vcluster + + operationId: List the concentration rules + parameters: + - name: vcluster + in: query + description: The vCluster filter + required: false + schema: + type: string + - name: name + in: query + description: The name filter + required: false + schema: + type: string + - name: showDefaults + in: query + description: Whether to show default values or not + required: false + schema: + default: false + type: boolean + example: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ConcentrationRule' + example: + - kind: ConcentrationRules + apiVersion: gateway/v2 + metadata: + name: concentrationRule1 + vCluster: vCluster1 + spec: + logicalTopicNamePattern: topic.* + physicalTopicName: physicalTopicName + physicalTopicNameCompacted: physicalTopicNameCompacted + physicalTopicNameCompactedDeleted: physicalTopicNameCompactedDeleted + autoManaged: false + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given vCluster does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given vCluster does not exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request GET \ + --url 'http://localhost:8888/gateway/v2/concentration-rules?showDefaults=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' + put: + tags: + - cli_concentration-rules_gateway_v2_9 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Upsert a concentration rule in a vcluster + + operationId: Upsert a concentration rule + parameters: + - name: dryMode + in: query + description: Whether to simulate the operation or not + required: false + schema: + default: false + type: boolean + example: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ConcentrationRule' + example: + kind: ConcentrationRules + apiVersion: gateway/v2 + metadata: + name: concentrationRule1 + vCluster: vCluster1 + spec: + logicalTopicNamePattern: topic.* + physicalTopicName: physicalTopicName + physicalTopicNameCompacted: physicalTopicNameCompacted + physicalTopicNameCompactedDeleted: physicalTopicNameCompactedDeleted + autoManaged: false + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ApplyResult_ConcentrationRule' + example: + resource: + kind: ConcentrationRules + apiVersion: gateway/v2 + metadata: + name: concentrationRule1 + vCluster: vCluster1 + spec: + logicalTopicNamePattern: topic.* + physicalTopicName: physicalTopicName + physicalTopicNameCompacted: physicalTopicNameCompacted + physicalTopicNameCompactedDeleted: physicalTopicNameCompactedDeleted + autoManaged: false + upsertResult: Created + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given vCluster does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given vCluster does not exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request PUT \ + --url 'http://localhost:8888/gateway/v2/concentration-rules?dryMode=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "kind" : "ConcentrationRules", + "apiVersion" : "gateway/v2", + "metadata" : { + "name" : "concentrationRule1", + "vCluster" : "vCluster1" + }, + "spec" : { + "logicalTopicNamePattern" : "topic.*", + "physicalTopicName" : "physicalTopicName", + "physicalTopicNameCompacted" : "physicalTopicNameCompacted", + "physicalTopicNameCompactedDeleted" : "physicalTopicNameCompactedDeleted", + "autoManaged" : false + } + }' + delete: + tags: + - cli_concentration-rules_gateway_v2_9 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Delete a concentration rule in a vcluster + + operationId: Delete a concentration rule + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ConcentrationRuleId' + example: + name: concentrationRule1 + vCluster: vCluster1 + required: true + responses: + '204': + description: '' + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given vCluster does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given vCluster does not exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request DELETE \ + --url 'http://localhost:8888/gateway/v2/concentration-rules' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "name" : "concentrationRule1", + "vCluster" : "vCluster1" + }' + /gateway/v2/interceptors: + get: + tags: + - cli_interceptors_gateway_v2_12 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + List the interceptors + + operationId: List the interceptors + parameters: + - name: name + in: query + description: Filter by name + required: false + schema: + type: string + - name: global + in: query + description: Keep only global interceptors + required: false + schema: + type: boolean + - name: vcluster + in: query + description: Filter by vCluster + required: false + schema: + type: string + - name: group + in: query + description: Filter by group + required: false + schema: + type: string + - name: username + in: query + description: Filter by service-account + required: false + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/Interceptor' + example: + - kind: interceptors + apiVersion: gateway/v2 + metadata: + name: yellow_cars_filter + scope: + vCluster: vCluster1 + spec: + comment: Filter yellow cars + pluginClass: io.conduktor.gateway.interceptor.VirtualSqlTopicPlugin + priority: 1 + config: + virtualTopic: yellow_cars + statement: SELECT '$.type' as type, '$.price' as price FROM cars + WHERE '$.color' = 'yellow' + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request GET \ + --url 'http://localhost:8888/gateway/v2/interceptors?name=interceptor-name&global=true&vcluster=passthrough&group=group1&username=user1' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' + put: + tags: + - cli_interceptors_gateway_v2_12 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Upsert an interceptor + + operationId: Upsert an interceptor + parameters: + - name: dryMode + in: query + description: Whether to simulate the operation or not + required: false + schema: + default: false + type: boolean + example: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Interceptor' + example: + kind: interceptors + apiVersion: gateway/v2 + metadata: + name: yellow_cars_filter + scope: + vCluster: vCluster1 + spec: + comment: Filter yellow cars + pluginClass: io.conduktor.gateway.interceptor.VirtualSqlTopicPlugin + priority: 1 + config: + virtualTopic: yellow_cars + statement: SELECT '$.type' as type, '$.price' as price FROM cars + WHERE '$.color' = 'yellow' + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ApplyResult_Interceptor' + example: + resource: + kind: interceptors + apiVersion: gateway/v2 + metadata: + name: yellow_cars_filter + scope: + vCluster: vCluster1 + spec: + comment: Filter yellow cars + pluginClass: io.conduktor.gateway.interceptor.VirtualSqlTopicPlugin + priority: 1 + config: + virtualTopic: yellow_cars + statement: SELECT '$.type' as type, '$.price' as price FROM + cars WHERE '$.color' = 'yellow' + upsertResult: Created + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The vCluster or the group specified in the scope does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The vCluster or the group specified in the scope does not exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request PUT \ + --url 'http://localhost:8888/gateway/v2/interceptors?dryMode=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "kind" : "interceptors", + "apiVersion" : "gateway/v2", + "metadata" : { + "name" : "yellow_cars_filter", + "scope" : { + "vCluster" : "vCluster1", + "group" : null, + "username" : null + } + }, + "spec" : { + "comment" : "Filter yellow cars", + "pluginClass" : "io.conduktor.gateway.interceptor.VirtualSqlTopicPlugin", + "priority" : 1, + "config" : { + "virtualTopic" : "yellow_cars", + "statement" : "SELECT \'$.type\' as type, \'$.price\' as price FROM cars WHERE \'$.color\' = \'yellow\'" + } + } + }' + /gateway/v2/interceptors/{name}: + delete: + tags: + - cli_interceptors_gateway_v2_12 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Delete an interceptor + + operationId: Delete an interceptor + parameters: + - name: name + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InterceptorScope' + required: true + responses: + '204': + description: '' + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given interceptor does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given interceptor does not exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request DELETE \ + --url 'http://localhost:8888/gateway/v2/interceptors/yellow_cars_filter' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "vCluster" : "vCluster1", + "group" : null, + "username" : null + }' + /gateway/v2/interceptors/resolve: + post: + tags: + - cli_interceptors_gateway_v2_12 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + A utility endpoint to resolve the interceptors for a given vCluster, groups and username. + Helps to understand which interceptors will be applied for a given request. + + operationId: Resolve interceptors + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InterceptorResolverRequest' + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/Interceptor' + example: + - kind: interceptors + apiVersion: gateway/v2 + metadata: + name: yellow_cars_filter + scope: + vCluster: vCluster1 + spec: + comment: Filter yellow cars + pluginClass: io.conduktor.gateway.interceptor.VirtualSqlTopicPlugin + priority: 1 + config: + virtualTopic: yellow_cars + statement: SELECT '$.type' as type, '$.price' as price FROM cars + WHERE '$.color' = 'yellow' + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request POST \ + --url 'http://localhost:8888/gateway/v2/interceptors/resolve' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "vCluster" : "passthrough", + "groups" : [ + "group1", + "group2" + ], + "username" : "user1" + }' + /gateway/v2/plugins: + get: + tags: + - plugins + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + List the available plugins of the gateway + + operationId: List the Plugins of the Gateway + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/Plugin' + example: + - plugin: io.conduktor.gateway.interceptor.chaos.SimulateSlowProducersConsumersPlugin + pluginId: io.conduktor.gateway.interceptor.chaos.SimulateSlowProducersConsumersPlugin + readme: |2+ + + --- + version: ${project.version} + title: Simulate slow producers and consumers + description: Validate your application behaves correctly when there are delays in responses from the Kafka cluster. + parent: console + license: enterprise + --- + + ## Introduction + + This interceptor slows responses from the brokers. + + It will operate only on a set of topics rather than all traffic. + + This interceptor only works on Produce requests and Fetch requests. + + ## Configuration + + | key | type | default | description | + |:--------------|:--------|:--------|:----------------------------------------------------------------| + | topic | String | `.*` | Topics that match this regex will have the interceptor applied. | + | rateInPercent | int | | The percentage of requests that will apply this interceptor | + | minLatencyMs | int | | Minimum for the random response latency in milliseconds | + | maxLatencyMs | int | | Maximum for the random response latency in milliseconds | + + ## Example + + ```json + { + "name": "mySimulateSlowProducersConsumersInterceptor", + "pluginClass": "io.conduktor.gateway.interceptor.chaos.SimulateSlowProducersConsumersPlugin", + "priority": 100, + "config": { + "rateInPercent": 100, + "minLatencyMs": 50, + "maxLatencyMs": 1200 + } + } + ``` + + parent: Console + license: enterprise + description: Validate your application behaves correctly when broker + errors occur. + title: Broker errors + version: 3.0.1 + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request GET \ + --url 'http://localhost:8888/gateway/v2/plugins' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' + /gateway/v2/service-accounts: + get: + tags: + - cli_service-accounts_gateway_v2_11 + description: |2+ + + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + List the service accounts + + operationId: List the service accounts + parameters: + - name: vcluster + in: query + description: Filter by vCluster + required: false + schema: + type: string + - name: name + in: query + description: Filter by name + required: false + schema: + type: string + - name: type + in: query + description: Filter by type (External or Local) + required: false + schema: + type: string + - name: showDefaults + in: query + description: Whether to show default values or not + required: false + schema: + default: false + type: boolean + example: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ServiceAccount' + example: + - kind: service-accounts + apiVersion: gateway/v2 + metadata: + name: user1 + vCluster: vcluster1 + spec: + type: External + principal: aliasUser1 + - kind: service-accounts + apiVersion: gateway/v2 + metadata: + name: user1 + vCluster: vcluster1 + spec: + type: Local + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request GET \ + --url 'http://localhost:8888/gateway/v2/service-accounts?vcluster=vCluster1&name=user1&type=External&showDefaults=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' + put: + tags: + - cli_service-accounts_gateway_v2_11 + description: |2+ + + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Upsert a service account + + operationId: Upsert a service account + parameters: + - name: dryMode + in: query + description: Whether to simulate the operation or not + required: false + schema: + default: false + type: boolean + example: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ServiceAccount' + example: + kind: service-accounts + apiVersion: gateway/v2 + metadata: + name: user1 + vCluster: vcluster1 + spec: + type: External + principal: aliasUser1 + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ApplyResult_ServiceAccount' + example: + resource: + kind: service-accounts + apiVersion: gateway/v2 + metadata: + name: user1 + vCluster: vcluster1 + spec: + type: External + principal: aliasUser1 + upsertResult: Created + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given service account references a non-existing vCluster + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given service account references a non-existing vCluster + '409': + description: The service account already exist + content: + application/json: + schema: + $ref: '#/components/schemas/Conflict' + example: + title: The service account already exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request PUT \ + --url 'http://localhost:8888/gateway/v2/service-accounts?dryMode=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "kind" : "service-accounts", + "apiVersion" : "gateway/v2", + "metadata" : { + "name" : "user1", + "vCluster" : "vcluster1" + }, + "spec" : { + "type" : "External", + "principal" : "aliasUser1" + } + }' + delete: + tags: + - cli_service-accounts_gateway_v2_11 + description: |2+ + + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Delete a service account + + operationId: Delete a service account + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ServiceAccountId' + required: true + responses: + '204': + description: '' + '400': + description: 'Invalid value for: body' + content: + text/plain: + schema: + type: string + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given service account does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given service account does not exist + '409': + description: The service account is still used by groups + content: + application/json: + schema: + $ref: '#/components/schemas/Conflict' + example: + title: The service account is still used by groups + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request DELETE \ + --url 'http://localhost:8888/gateway/v2/service-accounts' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "name" : "user1" + }' + /gateway/v2/gateway-groups: + get: + tags: + - cli_gateway-groups_gateway_v2_10 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + List the groups + + operationId: List the groups + parameters: + - name: showDefaults + in: query + description: Whether to show default values or not + required: false + schema: + default: false + type: boolean + example: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/GatewayGroup' + example: + - kind: GatewayGroups + apiVersion: gateway/v2 + metadata: + name: group1 + spec: + members: + - vCluster: vCluster1 + name: serviceAccount1 + - vCluster: vCluster2 + name: serviceAccount2 + - vCluster: vCluster3 + name: serviceAccount3 + externalGroups: + - GROUP_READER + - GROUP_WRITER + '400': + description: 'Invalid value for: query parameter showDefaults' + content: + text/plain: + schema: + type: string + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request GET \ + --url 'http://localhost:8888/gateway/v2/gateway-groups?showDefaults=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' + put: + tags: + - cli_gateway-groups_gateway_v2_10 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Upsert a group + + operationId: Upsert a group + parameters: + - name: dryMode + in: query + description: Whether to simulate the operation or not + required: false + schema: + default: false + type: boolean + example: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GatewayGroup' + example: + kind: GatewayGroups + apiVersion: gateway/v2 + metadata: + name: group1 + spec: + members: + - vCluster: vCluster1 + name: serviceAccount1 + - vCluster: vCluster2 + name: serviceAccount2 + - vCluster: vCluster3 + name: serviceAccount3 + externalGroups: + - GROUP_READER + - GROUP_WRITER + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ApplyResult_GatewayGroup' + example: + resource: + kind: GatewayGroups + apiVersion: gateway/v2 + metadata: + name: group1 + spec: + members: + - vCluster: vCluster1 + name: serviceAccount1 + - vCluster: vCluster2 + name: serviceAccount2 + - vCluster: vCluster3 + name: serviceAccount3 + externalGroups: + - GROUP_READER + - GROUP_WRITER + upsertResult: Updated + '400': + description: The request is not valid + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequest' + example: + title: The request is not valid + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The group contains a service account that does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The group contains a service account that does not exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request PUT \ + --url 'http://localhost:8888/gateway/v2/gateway-groups?dryMode=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "kind" : "GatewayGroups", + "apiVersion" : "gateway/v2", + "metadata" : { + "name" : "group1" + }, + "spec" : { + "members" : [ + { + "vCluster" : "vCluster1", + "name" : "serviceAccount1" + }, + { + "vCluster" : "vCluster2", + "name" : "serviceAccount2" + }, + { + "vCluster" : "vCluster3", + "name" : "serviceAccount3" + } + ], + "externalGroups" : [ + "GROUP_READER", + "GROUP_WRITER" + ] + } + }' + /gateway/v2/gateway-groups/{name}: + get: + tags: + - cli_gateway-groups_gateway_v2_10 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Get a group + + operationId: Get a group + parameters: + - name: name + in: path + required: true + schema: + type: string + - name: showDefaults + in: query + description: Whether to show default values or not + required: false + schema: + default: false + type: boolean + example: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/GatewayGroup' + example: + kind: GatewayGroups + apiVersion: gateway/v2 + metadata: + name: group1 + spec: + members: + - vCluster: vCluster1 + name: serviceAccount1 + - vCluster: vCluster2 + name: serviceAccount2 + - vCluster: vCluster3 + name: serviceAccount3 + externalGroups: + - GROUP_READER + - GROUP_WRITER + '400': + description: 'Invalid value for: query parameter showDefaults' + content: + text/plain: + schema: + type: string + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given group does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given group does not exist + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request GET \ + --url 'http://localhost:8888/gateway/v2/gateway-groups/group1?showDefaults=false' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' + delete: + tags: + - cli_gateway-groups_gateway_v2_10 + description: |2+ + + [![Beta](https://img.shields.io/badge/Lifecycle-Beta-orange)](#tag/Versioning) + + Delete a group + + operationId: Delete a group + parameters: + - name: name + in: path + required: true + schema: + type: string + responses: + '204': + description: '' + '401': + description: The given credentials are not valid + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + example: + title: The given credentials are not valid + '404': + description: The given group does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/NotFound' + example: + title: The given group does not exist + '409': + description: The group is still referenced by interceptors + content: + application/json: + schema: + $ref: '#/components/schemas/Conflict' + example: + title: The group is still referenced by interceptors + '500': + description: An unexpected error occurred in the server + content: + application/json: + schema: + $ref: '#/components/schemas/ServerError' + example: + title: An unexpected error occurred in the server + security: + - httpAuth: [] + x-codeSamples: + - lang: Shell + source: |- + curl \ + --request DELETE \ + --url 'http://localhost:8888/gateway/v2/gateway-groups/group1' \ + --header 'Authorization: Basic YWRtaW46YWRtaW4=' +components: + schemas: + AliasTopic: + title: AliasTopic + type: object + required: + - kind + - apiVersion + - metadata + - spec + properties: + kind: + description: The kind of the alias topic + type: string + apiVersion: + description: The api version of the alias topic + type: string + metadata: + $ref: '#/components/schemas/AliasTopicMetadata' + spec: + $ref: '#/components/schemas/AliasTopicSpec' + AliasTopicId: + title: AliasTopicId + type: object + required: + - name + properties: + name: + description: The name of the alias topic + type: string + vCluster: + description: 'The vCluster name of the alias topic (default: passthrough)' + type: string + AliasTopicMetadata: + title: AliasTopicMetadata + description: The metadata of the alias topic + type: object + required: + - name + properties: + name: + description: The name of the alias topic + type: string + format: ^[a-zA-Z0-9._-]+$ + vCluster: + description: 'The vCluster name of the alias topic (default: passthrough)' + type: string + format: ^[a-zA-Z0-9_-]+$ + AliasTopicSpec: + title: AliasTopicSpec + description: The specification of the alias topic + type: object + required: + - physicalName + properties: + physicalName: + description: The physical name of the alias topic + type: string + format: ^[a-zA-Z0-9._-]+$ + ApplyResult_AliasTopic: + title: ApplyResult_AliasTopic + type: object + required: + - resource + - upsertResult + properties: + resource: + $ref: '#/components/schemas/AliasTopic' + description: The resource that was upserted + upsertResult: + $ref: '#/components/schemas/UpsertResult' + ApplyResult_ConcentrationRule: + title: ApplyResult_ConcentrationRule + type: object + required: + - resource + - upsertResult + properties: + resource: + $ref: '#/components/schemas/ConcentrationRule' + description: The resource that was upserted + upsertResult: + $ref: '#/components/schemas/UpsertResult' + ApplyResult_GatewayGroup: + title: ApplyResult_GatewayGroup + type: object + required: + - resource + - upsertResult + properties: + resource: + $ref: '#/components/schemas/GatewayGroup' + description: The resource that was upserted + upsertResult: + $ref: '#/components/schemas/UpsertResult' + ApplyResult_Interceptor: + title: ApplyResult_Interceptor + type: object + required: + - resource + - upsertResult + properties: + resource: + $ref: '#/components/schemas/Interceptor' + description: The resource that was upserted + upsertResult: + $ref: '#/components/schemas/UpsertResult' + ApplyResult_ServiceAccount: + title: ApplyResult_ServiceAccount + type: object + required: + - resource + - upsertResult + properties: + resource: + $ref: '#/components/schemas/ServiceAccount' + description: The resource that was upserted + upsertResult: + $ref: '#/components/schemas/UpsertResult' + ApplyResult_VCluster: + title: ApplyResult_VCluster + type: object + required: + - resource + - upsertResult + properties: + resource: + $ref: '#/components/schemas/VCluster' + description: The resource that was upserted + upsertResult: + $ref: '#/components/schemas/UpsertResult' + BadRequest: + title: BadRequest + type: object + required: + - title + properties: + title: + type: string + msg: + type: string + cause: + type: string + ConcentratedTopic: + title: ConcentratedTopic + type: object + required: + - kind + - apiVersion + - metadata + - spec + properties: + kind: + description: The kind of the concentrated topic + type: string + apiVersion: + description: The api version of the concentrated topic + type: string + metadata: + $ref: '#/components/schemas/ConcentratedTopicMetadata' + spec: + $ref: '#/components/schemas/ConcentratedTopicSpec' + ConcentratedTopicMetadata: + title: ConcentratedTopicMetadata + description: The metadata of the concentrated topic + type: object + required: + - name + properties: + name: + description: The name of the concentrated topic + type: string + vCluster: + description: The vCluster of the concentrated topic. If not provided, defaulted + to passthrough vCluster. + type: string + ConcentratedTopicSpec: + title: ConcentratedTopicSpec + description: The specification of the concentrated topic + type: object + required: + - physicalName + properties: + physicalName: + description: The physical name of the concentrated topic + type: string + ConcentrationRule: + title: ConcentrationRule + type: object + required: + - kind + - apiVersion + - metadata + - spec + properties: + kind: + description: The kind of the concentration rule + type: string + apiVersion: + description: The api version of the concentration rule + type: string + metadata: + $ref: '#/components/schemas/ConcentrationRuleMetadata' + spec: + $ref: '#/components/schemas/ConcentrationRuleSpec' + ConcentrationRuleId: + title: ConcentrationRuleId + type: object + required: + - name + properties: + name: + description: The name of the concentration rule (identifier in a vCluster) + type: string + vCluster: + description: The vCluster of the concentration rule. Default to passthrough + if not provided. + type: string + ConcentrationRuleMetadata: + title: ConcentrationRuleMetadata + description: The metadata of the concentration rule + type: object + required: + - name + properties: + name: + description: The name of the concentration rule (identifier in a vCluster) + type: string + format: ^[a-zA-Z0-9._-]+$ + vCluster: + description: The vCluster of the concentration rule. Default to passthrough + if not provided. + type: string + ConcentrationRuleSpec: + title: ConcentrationRuleSpec + description: The specification of the concentration rule + type: object + required: + - logicalTopicNamePattern + - physicalTopicName + properties: + logicalTopicNamePattern: + description: The pattern of the concentration rule + type: string + physicalTopicName: + description: The physical name of the concentrated topic + type: string + format: ^[a-zA-Z0-9._-]+$ + physicalTopicNameCompacted: + description: The physical name of the topic for compact policy + type: string + format: ^[a-zA-Z0-9._-]+$ + physicalTopicNameCompactedDeleted: + description: The pattern of the topic for delete, compact policy + type: string + format: ^[a-zA-Z0-9._-]+$ + autoManaged: + description: Whether the concentration rule is auto managed + type: boolean + Conflict: + title: Conflict + type: object + required: + - title + properties: + title: + type: string + msg: + type: string + cause: + type: string + Created: + title: Created + type: object + External: + title: External + type: object + required: + - kind + - apiVersion + - metadata + - spec + properties: + kind: + description: The kind of the service account + type: string + apiVersion: + description: The api version of the service account + type: string + metadata: + $ref: '#/components/schemas/ExternalMetadata' + spec: + $ref: '#/components/schemas/ExternalSpec' + ExternalMetadata: + title: ExternalMetadata + description: Metadata of the service account + type: object + required: + - name + properties: + name: + description: The name of the service account (identifier) + type: string + format: ^[a-zA-Z0-9_-]{3,64}$ + vCluster: + description: |2 + + The name of the vcluster the service account belongs to. + + If not provided, the service account will be created in the default `passthrough` vcluster. + type: string + format: ^[a-zA-Z0-9_-]+$ + ExternalSpec: + title: ExternalSpec + description: Spec of the service account + type: object + required: + - principal + properties: + principal: + description: |2 + + An optional field to override the principal of the service account from an external user DB (OIDC) + type: string + format: ^[a-zA-Z0-9_-]{3,64}$ + GatewayGroup: + title: GatewayGroup + type: object + required: + - kind + - apiVersion + - metadata + - spec + properties: + kind: + description: The kind of the group + type: string + apiVersion: + description: The api version of the group + type: string + metadata: + $ref: '#/components/schemas/GroupMetadata' + spec: + $ref: '#/components/schemas/GroupSpec' + GroupMetadata: + title: GroupMetadata + description: The metadata of the group + type: object + required: + - name + properties: + name: + description: The name of the group + type: string + format: ^[a-zA-Z0-9_-]{1,100}$ + GroupSpec: + title: GroupSpec + description: The specification of the group + type: object + properties: + members: + description: The service accounts belonging to the group + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/ServiceAccountId' + externalGroups: + description: The external groups (LDAP, OIDC...) mapped on the group + type: array + uniqueItems: true + items: + type: string + Interceptor: + title: Interceptor + type: object + required: + - kind + - apiVersion + - metadata + - spec + properties: + kind: + description: The kind of the interceptor + type: string + apiVersion: + description: The api version of the interceptor + type: string + metadata: + $ref: '#/components/schemas/InterceptorMetadata' + spec: + $ref: '#/components/schemas/InterceptorSpec' + InterceptorMetadata: + title: InterceptorMetadata + description: Metadata of the interceptor + type: object + required: + - name + properties: + name: + description: The name of the interceptor + type: string + scope: + $ref: '#/components/schemas/InterceptorScope' + description: |2 + + The scope of the interceptor. + It can be applied to a specific vCluster or group or username. + If none of them is set, it will be applied Globally to the gateway. + InterceptorResolverRequest: + title: InterceptorResolverRequest + type: object + properties: + vCluster: + description: The vCluster to test the interceptors resolution + type: string + groups: + description: The groups to test the interceptors resolution + type: array + items: + type: string + username: + description: The username to test the interceptors resolution + type: string + InterceptorScope: + title: InterceptorScope + type: object + properties: + vCluster: + description: An optional vCluster to filter the interceptors + type: string + group: + description: An optional group to filter the interceptors + type: string + username: + description: An optional username to filter the interceptors + type: string + InterceptorSpec: + title: InterceptorSpec + description: Spec of the interceptor + type: object + required: + - pluginClass + - priority + - config + properties: + comment: + description: An optional comment for the interceptor + type: string + pluginClass: + description: The class of the plugin + type: string + priority: + description: The priority of the interceptor + type: integer + format: int32 + config: + $ref: '#/components/schemas/Map_Json' + Local: + title: Local + type: object + required: + - kind + - apiVersion + - metadata + - spec + properties: + kind: + description: The kind of the service account + type: string + apiVersion: + description: The api version of the service account + type: string + metadata: + $ref: '#/components/schemas/LocalMetadata' + spec: + $ref: '#/components/schemas/LocalSpec' + LocalMetadata: + title: LocalMetadata + description: Metadata of the service account + type: object + required: + - name + properties: + name: + description: The name of the service account (identifier) + type: string + format: ^[a-zA-Z0-9_-]{3,64}$ + vCluster: + description: |2 + + The name of the vcluster the service account belongs to. + + If not provided, the service account will be created in the default `passthrough` vcluster. + type: string + format: ^[a-zA-Z0-9_-]+$ + LocalSpec: + title: LocalSpec + description: Spec of the service account + type: object + Map_Json: + title: Map_Json + description: The configuration of the interceptor + type: object + additionalProperties: {} + NotChanged: + title: NotChanged + type: object + NotFound: + title: NotFound + type: object + required: + - title + properties: + title: + type: string + msg: + type: string + cause: + type: string + Plugin: + title: Plugin + type: object + required: + - plugin + - pluginId + - readme + properties: + plugin: + description: The name of the plugin + type: string + pluginId: + description: The id of the plugin + type: string + readme: + description: The readme of the plugin + type: string + parent: + description: The parent of the plugin + type: string + license: + description: The license of the plugin + type: string + description: + description: The description of the plugin + type: string + title: + description: The title of the plugin + type: string + version: + description: The version of the plugin + type: string + ServerError: + title: ServerError + type: object + required: + - title + properties: + title: + type: string + msg: + type: string + cause: + type: string + ServiceAccount: + title: ServiceAccount + oneOf: + - $ref: '#/components/schemas/External' + - $ref: '#/components/schemas/Local' + ServiceAccountId: + title: ServiceAccountId + type: object + required: + - name + properties: + vCluster: + description: |2 + + The name of the vcluster the service account belongs to. + + If not provided, the service account will be created in the default `passthrough` vcluster. + type: string + name: + description: The name of the service account (identifier) + type: string + TokenRequest: + title: TokenRequest + type: object + required: + - username + - lifeTimeSeconds + properties: + vClusterName: + description: 'The name of the vcluster to create the token for. "passthrough + if omitted" ' + type: string + username: + description: The username of the local service account to create the token + for. + type: string + lifeTimeSeconds: + description: The life time of the token in milliseconds. + type: integer + format: int64 + TokenResponse: + title: TokenResponse + type: object + required: + - token + properties: + token: + description: The token created for the given username on the given vcluster. + type: string + Unauthorized: + title: Unauthorized + type: object + required: + - title + properties: + title: + type: string + msg: + type: string + cause: + type: string + Updated: + title: Updated + type: object + UpsertResult: + title: UpsertResult + description: The result of the upsert operation (created, updated, not changed) + oneOf: + - $ref: '#/components/schemas/Created' + - $ref: '#/components/schemas/NotChanged' + - $ref: '#/components/schemas/Updated' + VCluster: + title: VCluster + type: object + required: + - kind + - apiVersion + - metadata + - spec + properties: + kind: + description: The kind of the vCluster + type: string + apiVersion: + description: The api version of the vCluster + type: string + metadata: + $ref: '#/components/schemas/VClusterMetadata' + spec: + $ref: '#/components/schemas/VClusterSpec' + VClusterMetadata: + title: VClusterMetadata + description: Metadata of the vCluster + type: object + required: + - name + properties: + name: + description: The name of the vcluster + type: string + format: ^[a-zA-Z0-9_-]+$ + VClusterSpec: + title: VClusterSpec + description: Spec of the vCluster + type: object + required: + - prefix + properties: + prefix: + description: The prefix that will be used to namespace kafka resources in + the physical cluster + type: string + format: ^[a-zA-Z0-9_-]*$ + securitySchemes: + httpAuth: + type: http + scheme: basic +x-tagGroups: +- name: 🐺 Conduktor Gateway API + tags: + - Introduction + - Authentication + - Kinds + - Api Groups + - Versioning + - Conventions +- name: 🌉 gateway/v2 + tags: + - cli_vclusters_gateway_v2_7 + - cli_alias-topics_gateway_v2_8 + - cli_concentrated-topics_gateway_v2_0 + - cli_concentration-rules_gateway_v2_9 + - cli_interceptors_gateway_v2_12 + - plugins + - cli_service-accounts_gateway_v2_11 + - cli_gateway-groups_gateway_v2_10 + - tokens diff --git a/schema/gateway_schema_test.go b/schema/gateway_schema_test.go new file mode 100644 index 0000000..4109688 --- /dev/null +++ b/schema/gateway_schema_test.go @@ -0,0 +1,191 @@ +package schema + +import ( + "os" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" +) + +func TestGetKindWithYamlFromGateway(t *testing.T) { + t.Run("gets kinds from schema", func(t *testing.T) { + schemaContent, err := os.ReadFile("gateway.yaml") + if err != nil { + t.Fatalf("failed reading file: %s", err) + } + + schema, err := New(schemaContent) + if err != nil { + t.Fatalf("failed creating new schema: %s", err) + } + + kinds, err := schema.GetGatewayKinds(true) + if err != nil { + t.Fatalf("failed getting kinds: %s", err) + } + + expected := KindCatalog{ + "VClusters": { + Versions: map[int]KindVersion{ + 2: &GatewayKindVersion{ + Name: "VClusters", + ListPath: "/gateway/v2/vclusters", + ParentPathParam: []string{}, + ListQueryParameter: map[string]QueryParameterOption{}, + GetAvailable: true, + Order: 7, + }, + }, + }, + "AliasTopics": { + Versions: map[int]KindVersion{ + 2: &GatewayKindVersion{ + Name: "AliasTopics", + ListPath: "/gateway/v2/alias-topics", + ParentPathParam: []string{}, + ListQueryParameter: map[string]QueryParameterOption{ + "name": { + FlagName: "name", + Required: false, + Type: "string", + }, + "vcluster": { + FlagName: "vcluster", + Required: false, + Type: "string", + }, + "showDefaults": { + FlagName: "show-defaults", + Required: false, + Type: "boolean", + }, + }, + GetAvailable: false, + Order: 8, + }, + }, + }, + "ConcentrationRules": { + Versions: map[int]KindVersion{ + 2: &GatewayKindVersion{ + Name: "ConcentrationRules", + ListPath: "/gateway/v2/concentration-rules", + ParentPathParam: []string{}, + ListQueryParameter: map[string]QueryParameterOption{ + "vcluster": { + FlagName: "vcluster", + Required: false, + Type: "string", + }, + "name": { + FlagName: "name", + Required: false, + Type: "string", + }, + "showDefaults": { + FlagName: "show-defaults", + Required: false, + Type: "boolean", + }, + }, + GetAvailable: false, + Order: 9, + }, + }, + }, + "GatewayGroups": { + Versions: map[int]KindVersion{ + 2: &GatewayKindVersion{ + Name: "GatewayGroups", + ListPath: "/gateway/v2/gateway-groups", + ParentPathParam: []string{}, + ListQueryParameter: map[string]QueryParameterOption{ + "showDefaults": { + FlagName: "show-defaults", + Required: false, + Type: "boolean", + }, + }, + GetAvailable: true, + Order: 10, + }, + }, + }, + "ServiceAccounts": { + Versions: map[int]KindVersion{ + 2: &GatewayKindVersion{ + Name: "ServiceAccounts", + ListPath: "/gateway/v2/service-accounts", + ParentPathParam: []string{}, + ListQueryParameter: map[string]QueryParameterOption{ + "name": { + FlagName: "name", + Required: false, + Type: "string", + }, + "type": { + FlagName: "type", + Required: false, + Type: "string", + }, + "vcluster": { + FlagName: "vcluster", + Required: false, + Type: "string", + }, + "showDefaults": { + FlagName: "show-defaults", + Required: false, + Type: "boolean", + }, + }, + GetAvailable: false, + Order: 11, + }, + }, + }, + "Interceptors": { + Versions: map[int]KindVersion{ + 2: &GatewayKindVersion{ + Name: "Interceptors", + ListPath: "/gateway/v2/interceptors", + ParentPathParam: []string{}, + ListQueryParameter: map[string]QueryParameterOption{ + "username": { + FlagName: "username", + Required: false, + Type: "string", + }, + "name": { + FlagName: "name", + Required: false, + Type: "string", + }, + "global": { + FlagName: "global", + Required: false, + Type: "boolean", + }, + "vcluster": { + FlagName: "vcluster", + Required: false, + Type: "string", + }, + "group": { + FlagName: "group", + Required: false, + Type: "string", + }, + }, + GetAvailable: false, + Order: 12, + }, + }, + }, + } + if !reflect.DeepEqual(kinds, expected) { + t.Error(spew.Printf("got kinds %v, want %v", kinds, expected)) + } + }) +} diff --git a/schema/kind.go b/schema/kind.go index c30386e..5a3b301 100644 --- a/schema/kind.go +++ b/schema/kind.go @@ -10,13 +10,89 @@ import ( "strings" "github.com/conduktor/ctl/resource" + "github.com/conduktor/ctl/utils" ) -type KindVersion struct { - ListPath string - Name string - ParentPathParam []string - Order int `json:1000` //same value DefaultPriority +type KindVersion interface { + GetListPath() string + GetName() string + GetParentPathParam() []string + GetOrder() int + GetListQueryParamter() map[string]QueryParameterOption +} + +// two logics: uniformize flag name and kebab case +func ComputeFlagName(name string) string { + kebab := utils.UpperCamelToKebab(name) + kebab = strings.TrimPrefix(kebab, "filter-by-") + return strings.Replace(kebab, "app-instance", "application-instance", 1) +} + +type QueryParameterOption struct { + FlagName string + Required bool + Type string +} +type ConsoleKindVersion struct { + ListPath string + Name string + ParentPathParam []string + ListQueryParamter map[string]QueryParameterOption + Order int `json:1000` //same value DefaultPriority +} + +func (c *ConsoleKindVersion) GetListPath() string { + return c.ListPath +} + +func (c *ConsoleKindVersion) GetName() string { + return c.Name +} + +func (c *ConsoleKindVersion) GetParentPathParam() []string { + return c.ParentPathParam +} + +func (c *ConsoleKindVersion) GetOrder() int { + return c.Order +} + +func (c *ConsoleKindVersion) GetListQueryParamter() map[string]QueryParameterOption { + return c.ListQueryParamter +} + +type GetParameter struct { + Name string + Mandatory bool +} + +type GatewayKindVersion struct { + ListPath string + Name string + ParentPathParam []string + ListQueryParameter map[string]QueryParameterOption + GetAvailable bool + Order int `json:1000` //same value DefaultPriority +} + +func (g *GatewayKindVersion) GetListPath() string { + return g.ListPath +} + +func (g *GatewayKindVersion) GetName() string { + return g.Name +} + +func (g *GatewayKindVersion) GetParentPathParam() []string { + return g.ParentPathParam +} + +func (g *GatewayKindVersion) GetOrder() int { + return g.Order +} + +func (g *GatewayKindVersion) GetListQueryParamter() map[string]QueryParameterOption { + return g.ListQueryParameter } const DefaultPriority = 1000 //update json annotation for Order when changing this value @@ -27,21 +103,46 @@ type Kind struct { type KindCatalog = map[string]Kind -//go:embed default-schema.json -var defaultByteSchema []byte +//go:embed console-default-schema.json +var consoleDefaultByteSchema []byte + +//go:embed gateway-default-schema.json +var gatewayDefaultByteSchema []byte -func DefaultKind() KindCatalog { - var result KindCatalog - err := json.Unmarshal(defaultByteSchema, &result) +type KindGeneric[T KindVersion] struct { + Versions map[int]T +} + +func buildKindCatalogFromByteSchema[T KindVersion](byteSchema []byte) KindCatalog { + var jsonResult map[string]KindGeneric[T] + err := json.Unmarshal(byteSchema, &jsonResult) if err != nil { panic(err) } + var result KindCatalog = make(map[string]Kind) + for kindName, kindGeneric := range jsonResult { + kind := Kind{ + Versions: make(map[int]KindVersion), + } + for version, kindVersion := range kindGeneric.Versions { + kind.Versions[version] = kindVersion + } + result[kindName] = kind + } return result } -func NewKind(version int, kindVersion *KindVersion) Kind { +func ConsoleDefaultKind() KindCatalog { + return buildKindCatalogFromByteSchema[*ConsoleKindVersion](consoleDefaultByteSchema) +} + +func GatewayDefaultKind() KindCatalog { + return buildKindCatalogFromByteSchema[*GatewayKindVersion](gatewayDefaultByteSchema) +} + +func NewKind(version int, kindVersion KindVersion) Kind { return Kind{ - Versions: map[int]KindVersion{version: *kindVersion}, + Versions: map[int]KindVersion{version: kindVersion}, } } @@ -66,20 +167,23 @@ func extractVersionFromApiVersion(apiVersion string) int { return version } -func (kind *Kind) AddVersion(version int, kindVersion *KindVersion) error { +func (kind *Kind) AddVersion(version int, kindVersion KindVersion) error { name := kind.GetName() - if name != kindVersion.Name { - return fmt.Errorf("Adding kind version of kind %s to different kind %s", kindVersion.Name, name) + if name != kindVersion.GetName() { + return fmt.Errorf("Adding kind version of kind %s to different kind %s", kindVersion.GetName(), name) } - kind.Versions[version] = *kindVersion + kind.Versions[version] = kindVersion return nil } -func (kind *Kind) GetFlag() []string { +func (kind *Kind) GetParentFlag() []string { kindVersion := kind.GetLatestKindVersion() - result := make([]string, len(kindVersion.ParentPathParam)) - copy(result, kindVersion.ParentPathParam) - return result + return kindVersion.GetParentPathParam() +} + +func (kind *Kind) GetListFlag() map[string]QueryParameterOption { + kindVersion := kind.GetLatestKindVersion() + return kindVersion.GetListQueryParamter() } func (kind *Kind) MaxVersion() int { @@ -92,29 +196,29 @@ func (kind *Kind) MaxVersion() int { return maxVersion } -func (kind *Kind) GetLatestKindVersion() *KindVersion { +func (kind *Kind) GetLatestKindVersion() KindVersion { kindVersion, ok := kind.Versions[kind.MaxVersion()] if !ok { panic("Max numVersion on kind return a numVersion that does not exist") } - return &kindVersion + return kindVersion } func (Kind *Kind) GetName() string { for _, kindVersion := range Kind.Versions { - return kindVersion.Name + return kindVersion.GetName() } panic("No kindVersion in kind") //should never happen } func (kind *Kind) ListPath(parentPathValues []string) string { kindVersion := kind.GetLatestKindVersion() - if len(parentPathValues) != len(kindVersion.ParentPathParam) { - panic(fmt.Sprintf("For kind %s expected %d parent apiVersion values, got %d", kindVersion.Name, len(kindVersion.ParentPathParam), len(parentPathValues))) + 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))) } - path := kindVersion.ListPath + path := kindVersion.GetListPath() for i, pathValue := range parentPathValues { - path = strings.Replace(path, fmt.Sprintf("{%s}", kindVersion.ParentPathParam[i]), pathValue, 1) + path = strings.Replace(path, fmt.Sprintf("{%s}", kindVersion.GetParentPathParam()[i]), pathValue, 1) } return path } @@ -128,9 +232,9 @@ func (kind *Kind) ApplyPath(resource *resource.Resource) (string, error) { if !ok { return "", fmt.Errorf("Could not find version %s for kind %s", resource.Version, resource.Kind) } - parentPathValues := make([]string, len(kindVersion.ParentPathParam)) + parentPathValues := make([]string, len(kindVersion.GetParentPathParam())) var err error - for i, param := range kindVersion.ParentPathParam { + for i, param := range kindVersion.GetParentPathParam() { parentPathValues[i], err = resource.StringFromMetadata(param) if err != nil { return "", err diff --git a/schema/kind_test.go b/schema/kind_test.go index 19820f2..c972409 100644 --- a/schema/kind_test.go +++ b/schema/kind_test.go @@ -9,13 +9,13 @@ func TestKindGetFlag(t *testing.T) { kind := Kind{ Versions: map[int]KindVersion{ - 1: { + 1: &ConsoleKindVersion{ ParentPathParam: []string{"param-1", "param-2", "param-3"}, }, }, } - got := kind.GetFlag() + got := kind.GetParentFlag() want := []string{"param-1", "param-2", "param-3"} if len(got) != len(want) { @@ -34,13 +34,13 @@ func TestKindGetFlagWhenNoFlag(t *testing.T) { t.Run("converts parent parameters to flags", func(t *testing.T) { kind := Kind{ Versions: map[int]KindVersion{ - 1: { + 1: &ConsoleKindVersion{ ParentPathParam: []string{}, }, }, } - got := kind.GetFlag() + got := kind.GetParentFlag() if len(got) != 0 { t.Fatalf("got %d flags, want %d", len(got), 0) @@ -52,7 +52,7 @@ func TestKindListPath(t *testing.T) { t.Run("replaces parent parameters in ListPath", func(t *testing.T) { kind := Kind{ Versions: map[int]KindVersion{ - 1: { + 1: &ConsoleKindVersion{ ListPath: "/ListPath/{param-1}/{param-2}", ParentPathParam: []string{"param-1", "param-2"}, }, @@ -70,7 +70,7 @@ func TestKindListPath(t *testing.T) { t.Run("panics when parent paths and parameters length mismatch", func(t *testing.T) { kind := Kind{ Versions: map[int]KindVersion{ - 1: { + 1: &ConsoleKindVersion{ ListPath: "/ListPath/{param1}/{param2}", ParentPathParam: []string{"param1", "param2"}, }, diff --git a/schema/schema.go b/schema/schema.go index a079a0d..3c1b157 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -31,18 +31,19 @@ func New(schema []byte) (*Schema, error) { }, nil } -func (s *Schema) GetKinds(strict bool) (map[string]Kind, error) { +func getKinds[T KindVersion](s *Schema, strict bool, buildKindVersion func(s *Schema, path, kind string, order int, put *v3high.Operation, get *v3high.Operation, strict bool) (T, error)) (map[string]Kind, error) { result := make(map[string]Kind, 0) for path := s.doc.Model.Paths.PathItems.First(); path != nil; path = path.Next() { put := path.Value().Put - if put != nil { + get := path.Value().Get + if put != nil && get != nil { cliTag := findCliTag(path.Value().Put.Tags) if cliTag != "" { tagParsed, err := parseTag(cliTag) if err != nil { return nil, err } - newKind, err := buildKindVersion(path.Key(), tagParsed.kind, tagParsed.order, put, strict) + newKind, err := buildKindVersion(s, path.Key(), tagParsed.kind, tagParsed.order, put, get, strict) if err != nil { return nil, err } @@ -61,19 +62,42 @@ func (s *Schema) GetKinds(strict bool) (map[string]Kind, error) { return result, nil } -func buildKindVersion(path, kind string, order int, put *v3high.Operation, strict bool) (*KindVersion, error) { - newKind := &KindVersion{ - Name: kind, - ListPath: path, - ParentPathParam: make([]string, 0, len(put.Parameters)), - Order: order, +func (s *Schema) GetConsoleKinds(strict bool) (map[string]Kind, error) { + return getKinds(s, strict, buildConsoleKindVersion) +} + +func (s *Schema) GetGatewayKinds(strict bool) (map[string]Kind, error) { + return getKinds(s, strict, buildGatewayKindVersion) +} + +func buildConsoleKindVersion(s *Schema, path, kind string, order int, put *v3high.Operation, get *v3high.Operation, strict bool) (*ConsoleKindVersion, error) { + newKind := &ConsoleKindVersion{ + Name: kind, + ListPath: path, + ParentPathParam: make([]string, 0, len(put.Parameters)), + ListQueryParamter: make(map[string]QueryParameterOption, len(get.Parameters)), + Order: order, } - for _, parameter := range put.Parameters { - if parameter.In == "path" && *parameter.Required { - newKind.ParentPathParam = append(newKind.ParentPathParam, parameter.Name) + for _, putParameter := range put.Parameters { + if putParameter.In == "path" && *putParameter.Required { + newKind.ParentPathParam = append(newKind.ParentPathParam, putParameter.Name) } } + for _, getParameter := range get.Parameters { + if getParameter.In == "query" { + schemaTypes := getParameter.Schema.Schema().Type + if len(schemaTypes) == 1 { + schemaType := schemaTypes[0] + name := getParameter.Name + newKind.ListQueryParamter[name] = QueryParameterOption{ + FlagName: ComputeFlagName(name), + Required: *getParameter.Required, + Type: schemaType, + } + } + } + } if strict { err := checkThatPathParamAreInSpec(newKind, put.RequestBody) if err != nil { @@ -88,6 +112,29 @@ func buildKindVersion(path, kind string, order int, put *v3high.Operation, stric return newKind, nil } +func buildGatewayKindVersion(s *Schema, path, kind string, order int, put *v3high.Operation, get *v3high.Operation, strict bool) (*GatewayKindVersion, error) { + //for the moment there is the same but this might evolve latter + consoleKind, err := buildConsoleKindVersion(s, path, kind, order, put, get, strict) + if err != nil { + return nil, err + } + var getAvailable = false + for path := s.doc.Model.Paths.PathItems.First(); path != nil; path = path.Next() { + get := path.Value().Get + if get != nil && strings.HasPrefix(path.Key(), consoleKind.ListPath+"/{") { + getAvailable = true + } + } + return &GatewayKindVersion{ + Name: consoleKind.Name, + ListPath: consoleKind.ListPath, + ParentPathParam: consoleKind.ParentPathParam, + ListQueryParameter: consoleKind.ListQueryParamter, + GetAvailable: getAvailable, + Order: consoleKind.Order, + }, nil +} + type tagParseResult struct { kind string version int @@ -121,10 +168,17 @@ func parseTag(tag string) (tagParseResult, error) { return tagParseResult{}, fmt.Errorf("Invalid order number in tag: %s", orderStr) } - return tagParseResult{kind: utils.KebabToUpperCamel(kind), version: version, order: order}, nil + finalKind := utils.KebabToUpperCamel(kind) + if finalKind == "Vclusters" { + finalKind = "VClusters" + } + return tagParseResult{kind: finalKind, version: version, order: order}, nil } -func checkThatPathParamAreInSpec(kind *KindVersion, requestBody *v3high.RequestBody) error { +func checkThatPathParamAreInSpec(kind *ConsoleKindVersion, requestBody *v3high.RequestBody) error { + if len(kind.ParentPathParam) == 0 { + return nil + } jsonContent, ok := requestBody.Content.Get("application/json") if !ok { return fmt.Errorf("No application/json content for kind %s", kind.Name) @@ -148,7 +202,7 @@ func checkThatPathParamAreInSpec(kind *KindVersion, requestBody *v3high.RequestB return nil } -func checkThatOrderArePresent(kind *KindVersion) error { +func checkThatOrderArePresent(kind *ConsoleKindVersion) error { if kind.Order == DefaultPriority { return fmt.Errorf("No priority set in schema for kind %s", kind.Name) } diff --git a/schema/schema_test.go b/schema/schema_test.go deleted file mode 100644 index 43fd2af..0000000 --- a/schema/schema_test.go +++ /dev/null @@ -1,287 +0,0 @@ -package schema - -import ( - "os" - "reflect" - "strings" - "testing" - - "github.com/davecgh/go-spew/spew" -) - -func TestGetKindWithYamlFromOldConsolePlusWithoutOrder(t *testing.T) { - t.Run("gets kinds from schema", func(t *testing.T) { - schemaContent, err := os.ReadFile("docs_without_order.yaml") - if err != nil { - t.Fatalf("failed reading file: %s", err) - } - - schema, err := New(schemaContent) - if err != nil { - t.Fatalf("failed creating new schema: %s", err) - } - - kinds, err := schema.GetKinds(false) - if err != nil { - t.Fatalf("failed getting kinds: %s", err) - } - - expected := KindCatalog{ - "Application": { - Versions: map[int]KindVersion{ - 1: { - Name: "Application", - ListPath: "/public/self-serve/v1/application", - ParentPathParam: make([]string, 0), - Order: DefaultPriority, - }, - }, - }, - "ApplicationInstance": { - Versions: map[int]KindVersion{ - 1: { - Name: "ApplicationInstance", - ListPath: "/public/self-serve/v1/application-instance", - ParentPathParam: make([]string, 0), - Order: DefaultPriority, - }, - }, - }, - "ApplicationInstancePermission": { - Versions: map[int]KindVersion{ - 1: { - Name: "ApplicationInstancePermission", - ListPath: "/public/self-serve/v1/application-instance-permission", - ParentPathParam: make([]string, 0), - Order: DefaultPriority, - }, - }, - }, - "TopicPolicy": { - Versions: map[int]KindVersion{ - 1: { - Name: "TopicPolicy", - ListPath: "/public/self-serve/v1/topic-policy", - ParentPathParam: make([]string, 0), - Order: DefaultPriority, - }, - }, - }, - "Topic": { - Versions: map[int]KindVersion{ - 2: { - Name: "Topic", - ListPath: "/public/kafka/v2/cluster/{cluster}/topic", - ParentPathParam: []string{"cluster"}, - Order: DefaultPriority, - }, - }, - }, - } - if !reflect.DeepEqual(kinds, expected) { - t.Error(spew.Printf("got kinds %v, want %v", kinds, expected)) - } - }) -} - -func TestGetKindWithYamlFromConsolePlus(t *testing.T) { - t.Run("gets kinds from schema", func(t *testing.T) { - schemaContent, err := os.ReadFile("docs_with_order.yaml") - if err != nil { - t.Fatalf("failed reading file: %s", err) - } - - schema, err := New(schemaContent) - if err != nil { - t.Fatalf("failed creating new schema: %s", err) - } - - kinds, err := schema.GetKinds(true) - if err != nil { - t.Fatalf("failed getting kinds: %s", err) - } - - expected := KindCatalog{ - "Application": { - Versions: map[int]KindVersion{ - 1: { - Name: "Application", - ListPath: "/public/self-serve/v1/application", - ParentPathParam: []string{}, - Order: 6, - }, - }, - }, - "ApplicationInstance": { - Versions: map[int]KindVersion{ - 1: { - Name: "ApplicationInstance", - ListPath: "/public/self-serve/v1/application-instance", - ParentPathParam: []string{}, - Order: 7, - }, - }, - }, - "ApplicationInstancePermission": { - Versions: map[int]KindVersion{ - 1: { - Name: "ApplicationInstancePermission", - ListPath: "/public/self-serve/v1/application-instance-permission", - ParentPathParam: []string{}, - Order: 8, - }, - }, - }, - "ApplicationGroup": { - Versions: map[int]KindVersion{ - 1: { - Name: "ApplicationGroup", - ListPath: "/public/self-serve/v1/application-group", - ParentPathParam: []string{}, - Order: 9, - }, - }, - }, - "TopicPolicy": { - Versions: map[int]KindVersion{ - 1: { - Name: "TopicPolicy", - ListPath: "/public/self-serve/v1/topic-policy", - ParentPathParam: []string{}, - Order: 5, - }, - }, - }, - "Topic": { - Versions: map[int]KindVersion{ - 2: { - Name: "Topic", - ListPath: "/public/kafka/v2/cluster/{cluster}/topic", - ParentPathParam: []string{"cluster"}, - Order: 3, - }, - }, - }, - "Subject": { - Versions: map[int]KindVersion{ - 2: { - Name: "Subject", - ListPath: "/public/kafka/v2/cluster/{cluster}/subject", - ParentPathParam: []string{"cluster"}, - Order: 4, - }, - }, - }, - "User": { - Versions: map[int]KindVersion{ - 2: { - Name: "User", - ListPath: "/public/iam/v2/user", - ParentPathParam: []string{}, - Order: 0, - }, - }, - }, - "Group": { - Versions: map[int]KindVersion{ - 2: { - Name: "Group", - ListPath: "/public/iam/v2/group", - ParentPathParam: []string{}, - Order: 1, - }, - }, - }, - "KafkaCluster": { - Versions: map[int]KindVersion{ - 2: { - Name: "KafkaCluster", - ListPath: "/public/console/v2/kafka-cluster", - ParentPathParam: []string{}, - Order: 2, - }, - }, - }, - } - if !reflect.DeepEqual(kinds, expected) { - t.Error(spew.Printf("got kinds %v, want %v", kinds["Subject"], expected["Subject"])) - } - }) -} - -func TestGetKindWithMultipleVersion(t *testing.T) { - t.Run("gets kinds from schema", func(t *testing.T) { - schemaContent, err := os.ReadFile("multiple_version.yaml") - if err != nil { - t.Fatalf("failed reading file: %s", err) - } - - schema, err := New(schemaContent) - if err != nil { - t.Fatalf("failed creating new schema: %s", err) - } - - kinds, err := schema.GetKinds(false) - if err != nil { - t.Fatalf("failed getting kinds: %s", err) - } - - expected := KindCatalog{ - "Topic": { - Versions: map[int]KindVersion{ - 1: { - Name: "Topic", - ListPath: "/public/v1/cluster/{cluster}/topic", - ParentPathParam: []string{"cluster"}, - Order: DefaultPriority, - }, - 2: { - Name: "Topic", - ListPath: "/public/v2/cluster/{cluster}/sa/{sa}/topic", - ParentPathParam: []string{"cluster", "sa"}, - Order: 42, - }, - }, - }, - } - if !reflect.DeepEqual(kinds, expected) { - t.Error(spew.Printf("got kinds %v, want %v", kinds, expected)) - } - }) -} -func TestKindWithMissingMetadataField(t *testing.T) { - t.Run("gets kinds from schema", func(t *testing.T) { - schemaContent, err := os.ReadFile("missing_field_in_metadata.yaml") - if err != nil { - t.Fatalf("failed reading file: %s", err) - } - - schema, err := New(schemaContent) - if err != nil { - t.Fatalf("failed creating new schema: %s", err) - } - - _, err = schema.GetKinds(true) - if !strings.Contains(err.Error(), "Parent path param sa not found in metadata for kind Topic") { - t.Fatalf("Not expected error: %s", err) - } - }) -} -func TestKindNotRequiredMetadataField(t *testing.T) { - t.Run("gets kinds from schema", func(t *testing.T) { - schemaContent, err := os.ReadFile("not_required_field_in_metadata.yaml") - if err != nil { - t.Fatalf("failed reading file: %s", err) - } - - schema, err := New(schemaContent) - if err != nil { - t.Fatalf("failed creating new schema: %s", err) - } - - _, err = schema.GetKinds(true) - if !strings.Contains(err.Error(), "Parent path param sa in metadata for kind Topic not required") { - t.Fatalf("Not expected error: %s", err) - } - }) -} diff --git a/schema/sort.go b/schema/sort.go index 3cb5a54..48dfa0b 100644 --- a/schema/sort.go +++ b/schema/sort.go @@ -25,16 +25,16 @@ func resourcePriority(catalog KindCatalog, resource resource.Resource, debug, fa } return DefaultPriority } else { - order := kindVersion.Order + order := kindVersion.GetOrder() if order == DefaultPriority && fallbackToDefaultCatalog { - defaultCatalog := DefaultKind() + defaultCatalog := ConsoleDefaultKind() //TODO: handle gateway orderFromDefaultCatalog := resourcePriority(defaultCatalog, resource, false, false) if orderFromDefaultCatalog != DefaultPriority && debug { fmt.Fprintf(os.Stderr, "Could not find version: %d of kind %s in catalog, but find it in default catalog with priority %d\n", version, resource.Kind, orderFromDefaultCatalog) } return orderFromDefaultCatalog } else { - return kindVersion.Order + return kindVersion.GetOrder() } } } diff --git a/schema/sort_test.go b/schema/sort_test.go index ba19f1e..41601b6 100644 --- a/schema/sort_test.go +++ b/schema/sort_test.go @@ -11,18 +11,18 @@ func TestSortResources(t *testing.T) { catalog := KindCatalog{ "kind1": Kind{ Versions: map[int]KindVersion{ - 1: {Order: 1}, + 1: &ConsoleKindVersion{Order: 1}, }, }, "kind2": Kind{ Versions: map[int]KindVersion{ - 1: {Order: 2}, + 1: &ConsoleKindVersion{Order: 2}, }, }, "kind3": Kind{ Versions: map[int]KindVersion{ - 1: {Order: 3}, - 2: {Order: 4}, + 1: &ConsoleKindVersion{Order: 3}, + 2: &ConsoleKindVersion{Order: 4}, }, }, } diff --git a/test_final_exec.sh b/test_final_exec.sh index 51236fe..dbd25ec 100755 --- a/test_final_exec.sh +++ b/test_final_exec.sh @@ -8,26 +8,40 @@ function cleanup { docker compose -f "$SCRIPTDIR/docker/docker-compose.yml" down } - trap cleanup EXIT main() { cd "$SCRIPTDIR" docker compose -f docker/docker-compose.yml build - docker compose -f docker/docker-compose.yml up -d mock + docker compose -f docker/docker-compose.yml up -d mock mockGateway sleep 1 - docker compose -f docker/docker-compose.yml run conduktor apply -f /test_resource.yml - docker compose -f docker/docker-compose.yml run conduktor apply -f / - docker compose -f docker/docker-compose.yml run conduktor delete -f /test_resource.yml - docker compose -f docker/docker-compose.yml run conduktor apply -f / - docker compose -f docker/docker-compose.yml run conduktor get Topic yolo --cluster=my-cluster - docker compose -f docker/docker-compose.yml run conduktor delete Topic yolo -v --cluster=my-cluster - docker compose -f docker/docker-compose.yml run -e CDK_USER=admin -e CDK_PASSWORD=secret conduktor login - docker compose -f docker/docker-compose.yml run -e CDK_USER=admin -e CDK_PASSWORD=secret -e CDK_API_KEY="" conduktor get KafkaCluster my_kafka_cluster - docker compose -f docker/docker-compose.yml run conduktor token list admin - docker compose -f docker/docker-compose.yml run conduktor token list application-instance -i=my_app_instance - docker compose -f docker/docker-compose.yml run conduktor token create admin a_admin_token - docker compose -f docker/docker-compose.yml run conduktor token create application-instance -i=my_app_instance a_admin_token - docker compose -f docker/docker-compose.yml run conduktor token delete 0-0-0-0-0 + docker compose -f docker/docker-compose.yml run --rm conduktor apply -f /test_resource.yml + docker compose -f docker/docker-compose.yml run --rm conduktor apply -f / + docker compose -f docker/docker-compose.yml run --rm conduktor delete -f /test_resource.yml + docker compose -f docker/docker-compose.yml run --rm conduktor apply -f / + docker compose -f docker/docker-compose.yml run --rm conduktor get Topic yolo --cluster=my-cluster + docker compose -f docker/docker-compose.yml run --rm conduktor delete Topic yolo -v --cluster=my-cluster + docker compose -f docker/docker-compose.yml run --rm -e CDK_USER=admin -e CDK_PASSWORD=secret conduktor login + docker compose -f docker/docker-compose.yml run --rm -e CDK_USER=admin -e CDK_PASSWORD=secret -e CDK_API_KEY="" conduktor get KafkaCluster my_kafka_cluster + docker compose -f docker/docker-compose.yml run --rm conduktor token list admin + docker compose -f docker/docker-compose.yml run --rm conduktor token list application-instance -i=my_app_instance + docker compose -f docker/docker-compose.yml run --rm conduktor token create admin a_admin_token + docker compose -f docker/docker-compose.yml run --rm conduktor token create application-instance -i=my_app_instance a_admin_token + docker compose -f docker/docker-compose.yml run --rm conduktor token delete 0-0-0-0-0 + + # Gateway + docker compose -f docker/docker-compose.yml run --rm conduktor apply -f /test_resource_gw.yml + docker compose -f docker/docker-compose.yml run --rm conduktor delete VClusters vcluster1 + docker compose -f docker/docker-compose.yml run --rm conduktor get VClusters + docker compose -f docker/docker-compose.yml run --rm conduktor get VClusters vcluster1 + docker compose -f docker/docker-compose.yml run --rm conduktor get GatewayGroups + docker compose -f docker/docker-compose.yml run --rm conduktor get GatewayGroups g1 + docker compose -f docker/docker-compose.yml run --rm conduktor get AliasTopics --show-defaults --name=yo --vcluster=mycluster1 + docker compose -f docker/docker-compose.yml run --rm conduktor get ConcentrationRules --show-defaults --name=yo --vcluster=mycluster1 + docker compose -f docker/docker-compose.yml run --rm conduktor get ServiceAccounts --show-defaults --name=yo --vcluster=mycluster1 + docker compose -f docker/docker-compose.yml run --rm conduktor get interceptors --group=g1 --name=yo --username=me --vcluster=mycluster1 + docker compose -f docker/docker-compose.yml run --rm conduktor delete aliastopic aliastopicname --vcluster=v1 + docker compose -f docker/docker-compose.yml run --rm conduktor delete concentrationrule cr1 --vcluster=v1 + docker compose -f docker/docker-compose.yml run --rm conduktor delete serviceaccounts s1 --vcluster=v1 } main "$@" diff --git a/utils/cdk_debug.go b/utils/cdk_debug.go new file mode 100644 index 0000000..6b1e881 --- /dev/null +++ b/utils/cdk_debug.go @@ -0,0 +1,10 @@ +package utils + +import ( + "os" + "strings" +) + +func CdkDebug() bool { + return strings.ToLower(os.Getenv("CDK_DEBUG")) == "true" +}