diff --git a/.ci/_common.sh b/.ci/_common.sh index 6f7fabc24..ce0af36f4 100644 --- a/.ci/_common.sh +++ b/.ci/_common.sh @@ -1,5 +1,7 @@ #!/bin/bash +readonly DOCKER_LABEL=com.konghq.deck.ci=1 + # Usage: waitContainer "PostgreSQL" 5432 0.2 function waitContainer() { @@ -9,7 +11,7 @@ function waitContainer() for try in {1..100}; do echo "waiting for ${container}.." - nc localhost ${port} && break; + nc localhost ${port} = 3.5 + consumers = cg.Data + } else { + consumers = make([]*Consumer, 0) + } + + return consumers, nil } diff --git a/kong/consumer_group_consumer_test.go b/kong/consumer_group_consumer_test.go index 1659d3e5a..7f35f3630 100644 --- a/kong/consumer_group_consumer_test.go +++ b/kong/consumer_group_consumer_test.go @@ -98,12 +98,12 @@ func TestConsumerGroupConsumersListEndpoint(t *testing.T) { consumerGroupConsumersFromKong, err := client.ConsumerGroupConsumers.ListAll(defaultCtx, cg.Name) require.NoError(t, err) assert.NotNil(consumerGroupConsumersFromKong) - assert.Equal(3, len(consumerGroupConsumersFromKong.Consumers)) + assert.Equal(3, len(consumerGroupConsumersFromKong)) // check if we see all consumer groups - assert.True(compareConsumers(consumers, consumerGroupConsumersFromKong.Consumers)) + assert.True(compareConsumers(consumers, consumerGroupConsumersFromKong)) - for i := 0; i < len(consumerGroupConsumersFromKong.Consumers); i++ { + for i := 0; i < len(consumerGroupConsumersFromKong); i++ { assert.NoError(client.Consumers.Delete(defaultCtx, consumers[i].ID)) } diff --git a/kong/developer_role_service_test.go b/kong/developer_role_service_test.go index c908afdbc..91bf00f2d 100644 --- a/kong/developer_role_service_test.go +++ b/kong/developer_role_service_test.go @@ -9,6 +9,7 @@ import ( func TestDeveloperRoleService(T *testing.T) { RunWhenEnterprise(T, ">=0.33.0", RequiredFeatures{Portal: true}) + RunWhenEnterprise(T, "<3.7.0", RequiredFeatures{Portal: true}) assert := assert.New(T) client, err := NewTestClient(nil, nil) @@ -60,6 +61,7 @@ func TestDeveloperRoleService(T *testing.T) { func TestDeveloperRoleServiceList(T *testing.T) { RunWhenEnterprise(T, ">=0.33.0", RequiredFeatures{Portal: true}) + RunWhenEnterprise(T, "<3.7.0", RequiredFeatures{Portal: true}) assert := assert.New(T) client, err := NewTestClient(nil, nil) @@ -98,6 +100,7 @@ func TestDeveloperRoleServiceList(T *testing.T) { func TestDeveloperRoleListEndpoint(T *testing.T) { RunWhenEnterprise(T, ">=0.33.0", RequiredFeatures{Portal: true}) + RunWhenEnterprise(T, "<3.7.0", RequiredFeatures{Portal: true}) assert := assert.New(T) client, err := NewTestClient(nil, nil) diff --git a/kong/developer_service_test.go b/kong/developer_service_test.go index 853ebe0cd..51d175dfc 100644 --- a/kong/developer_service_test.go +++ b/kong/developer_service_test.go @@ -9,6 +9,7 @@ import ( func TestDevelopersService(T *testing.T) { RunWhenEnterprise(T, ">=0.33.0", RequiredFeatures{Portal: true}) + RunWhenEnterprise(T, "<3.7.0", RequiredFeatures{Portal: true}) assert := assert.New(T) client, err := NewTestClient(nil, nil) @@ -79,6 +80,7 @@ func TestDevelopersService(T *testing.T) { func TestDeveloperListEndpoint(T *testing.T) { RunWhenEnterprise(T, ">=0.33.0", RequiredFeatures{Portal: true}) + RunWhenEnterprise(T, "<3.7.0", RequiredFeatures{Portal: true}) assert := assert.New(T) client, err := NewTestClient(nil, nil) diff --git a/kong/filter_chain.go b/kong/filter_chain.go new file mode 100644 index 000000000..b24915e98 --- /dev/null +++ b/kong/filter_chain.go @@ -0,0 +1,37 @@ +package kong + +import "encoding/json" + +// FilterChain represents a FilterChain in Kong. +// Read https://docs.konghq.com/gateway/latest/admin-api/#filter-chain-object +// +k8s:deepcopy-gen=true +type FilterChain struct { + ID *string `json:"id,omitempty" yaml:"id,omitempty"` + Name *string `json:"name,omitempty" yaml:"name,omitempty"` + Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + Route *Route `json:"route,omitempty" yaml:"route,omitempty"` + Service *Service `json:"service,omitempty" yaml:"service,omitempty"` + Filters []*Filter `json:"filters,omitempty" yaml:"filters,omitempty"` + CreatedAt *int `json:"created_at,omitempty" yaml:"created_at,omitempty"` + UpdatedAt *int `json:"updated_at,omitempty" yaml:"updated_at,omitempty"` + Tags []*string `json:"tags,omitempty" yaml:"tags,omitempty"` +} + +// Filter contains information about each filter in the chain +// +k8s:deepcopy-gen=true +type Filter struct { + Name *string `json:"name,omitempty" yaml:"name,omitempty"` + Config *json.RawMessage `json:"config,omitempty" yaml:"config,omitempty"` + Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` +} + +// FriendlyName returns the endpoint key name or ID. +func (f *FilterChain) FriendlyName() string { + if f.Name != nil { + return *f.Name + } + if f.ID != nil { + return *f.ID + } + return "" +} diff --git a/kong/filter_chain_service.go b/kong/filter_chain_service.go new file mode 100644 index 000000000..77e8ee97c --- /dev/null +++ b/kong/filter_chain_service.go @@ -0,0 +1,333 @@ +package kong + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +// AbstractFilterChainService handles FilterChains in Kong. +type AbstractFilterChainService interface { + // Create creates a FilterChain in Kong. + Create(ctx context.Context, filterChain *FilterChain) (*FilterChain, error) + // CreateForService creates a FilterChain in Kong. + CreateForService(ctx context.Context, serviceIDorName *string, filterChain *FilterChain) (*FilterChain, error) + // CreateForRoute creates a FilterChain in Kong. + CreateForRoute(ctx context.Context, routeIDorName *string, filterChain *FilterChain) (*FilterChain, error) + // Get fetches a FilterChain in Kong. + Get(ctx context.Context, nameOrID *string) (*FilterChain, error) + // Update updates a FilterChain in Kong + Update(ctx context.Context, filterChain *FilterChain) (*FilterChain, error) + // UpdateForService updates a FilterChain in Kong for a service + UpdateForService(ctx context.Context, serviceIDorName *string, filterChain *FilterChain) (*FilterChain, error) + // UpdateForRoute updates a FilterChain in Kong for a service + UpdateForRoute(ctx context.Context, routeIDorName *string, filterChain *FilterChain) (*FilterChain, error) + // Delete deletes a FilterChain in Kong + Delete(ctx context.Context, nameOrID *string) error + // DeleteForService deletes a FilterChain in Kong + DeleteForService(ctx context.Context, serviceIDorName *string, filterChainID *string) error + // DeleteForRoute deletes a FilterChain in Kong + DeleteForRoute(ctx context.Context, routeIDorName *string, filterChainID *string) error + // List fetches a list of FilterChains in Kong. + List(ctx context.Context, opt *ListOpt) ([]*FilterChain, *ListOpt, error) + // ListAll fetches all FilterChains in Kong. + ListAll(ctx context.Context) ([]*FilterChain, error) + // ListAllForService fetches all FilterChains in Kong enabled for a service. + ListAllForService(ctx context.Context, serviceIDorName *string) ([]*FilterChain, error) + // ListAllForRoute fetches all FilterChains in Kong enabled for a service. + ListAllForRoute(ctx context.Context, routeID *string) ([]*FilterChain, error) +} + +// FilterChainService handles FilterChains in Kong. +type FilterChainService service + +// Create creates a FilterChain in Kong. +// If an ID is specified, it will be used to +// create a filter chain in Kong, otherwise an ID +// is auto-generated. +func (s *FilterChainService) Create(ctx context.Context, + filterChain *FilterChain, +) (*FilterChain, error) { + queryPath := "/filter-chains" + method := "POST" + if filterChain.ID != nil { + queryPath = queryPath + "/" + *filterChain.ID + method = "PUT" + } + return s.sendRequest(ctx, filterChain, queryPath, method) +} + +// CreateForService creates a FilterChain in Kong at Service level. +// If an ID is specified, it will be used to +// create a filter chain in Kong, otherwise an ID +// is auto-generated. +func (s *FilterChainService) CreateForService(ctx context.Context, + serviceIDorName *string, filterChain *FilterChain, +) (*FilterChain, error) { + queryPath := "/filter-chains" + method := "POST" + if filterChain.ID != nil { + queryPath = queryPath + "/" + *filterChain.ID + method = "PUT" + } + if isEmptyString(serviceIDorName) { + return nil, fmt.Errorf("serviceIDorName cannot be nil") + } + + return s.sendRequest(ctx, filterChain, fmt.Sprintf("/services/%v"+queryPath, *serviceIDorName), method) +} + +// CreateForRoute creates a FilterChain in Kong at Route level. +// If an ID is specified, it will be used to +// create a filter chain in Kong, otherwise an ID +// is auto-generated. +func (s *FilterChainService) CreateForRoute(ctx context.Context, + routeIDorName *string, filterChain *FilterChain, +) (*FilterChain, error) { + queryPath := "/filter-chains" + method := "POST" + + if filterChain.ID != nil { + queryPath = queryPath + "/" + *filterChain.ID + method = "PUT" + } + if isEmptyString(routeIDorName) { + return nil, fmt.Errorf("routeIDorName cannot be nil") + } + + return s.sendRequest(ctx, filterChain, fmt.Sprintf("/routes/%v"+queryPath, *routeIDorName), method) +} + +// Get fetches a FilterChain in Kong. +func (s *FilterChainService) Get(ctx context.Context, + nameOrID *string, +) (*FilterChain, error) { + if isEmptyString(nameOrID) { + return nil, fmt.Errorf("nameOrID cannot be nil for Get operation") + } + + endpoint := fmt.Sprintf("/filter-chains/%v", *nameOrID) + req, err := s.client.NewRequest("GET", endpoint, nil, nil) + if err != nil { + return nil, err + } + + var filterChain FilterChain + _, err = s.client.Do(ctx, req, &filterChain) + if err != nil { + return nil, err + } + return &filterChain, nil +} + +// Update updates a FilterChain in Kong +func (s *FilterChainService) Update(ctx context.Context, + filterChain *FilterChain, +) (*FilterChain, error) { + if isEmptyString(filterChain.ID) { + return nil, fmt.Errorf("ID cannot be nil for Update operation") + } + + endpoint := fmt.Sprintf("/filter-chains/%v", *filterChain.ID) + return s.sendRequest(ctx, filterChain, endpoint, "PATCH") +} + +// UpdateForService updates a FilterChain in Kong at Service level. +func (s *FilterChainService) UpdateForService(ctx context.Context, + serviceIDorName *string, filterChain *FilterChain, +) (*FilterChain, error) { + if isEmptyString(filterChain.ID) { + return nil, fmt.Errorf("ID cannot be nil for Update operation") + } + if isEmptyString(serviceIDorName) { + return nil, fmt.Errorf("serviceIDorName cannot be nil") + } + + endpoint := fmt.Sprintf("/services/%v/filter-chains/%v", *serviceIDorName, *filterChain.ID) + return s.sendRequest(ctx, filterChain, endpoint, "PATCH") +} + +// UpdateForRoute updates a FilterChain in Kong at Route level. +func (s *FilterChainService) UpdateForRoute(ctx context.Context, + routeIDorName *string, filterChain *FilterChain, +) (*FilterChain, error) { + if isEmptyString(filterChain.ID) { + return nil, fmt.Errorf("ID cannot be nil for Update operation") + } + if isEmptyString(routeIDorName) { + return nil, fmt.Errorf("routeIDorName cannot be nil") + } + + endpoint := fmt.Sprintf("/routes/%v/filter-chains/%v", *routeIDorName, *filterChain.ID) + return s.sendRequest(ctx, filterChain, endpoint, "PATCH") +} + +// Delete deletes a FilterChain in Kong +func (s *FilterChainService) Delete(ctx context.Context, + filterChainID *string, +) error { + if isEmptyString(filterChainID) { + return fmt.Errorf("filterChainID cannot be nil for Delete operation") + } + + endpoint := fmt.Sprintf("/filter-chains/%v", *filterChainID) + _, err := s.sendRequest(ctx, nil, endpoint, "DELETE") + if err != nil { + return err + } + return err +} + +// DeleteForService deletes a FilterChain in Kong at Service level. +func (s *FilterChainService) DeleteForService(ctx context.Context, + serviceIDorName *string, filterChainID *string, +) error { + if isEmptyString(filterChainID) { + return fmt.Errorf("filterChain ID cannot be nil for Delete operation") + } + if isEmptyString(serviceIDorName) { + return fmt.Errorf("serviceIDorName cannot be nil") + } + + endpoint := fmt.Sprintf("/services/%v/filter-chains/%v", *serviceIDorName, *filterChainID) + _, err := s.sendRequest(ctx, nil, endpoint, "DELETE") + if err != nil { + return err + } + return err +} + +// DeleteForRoute deletes a FilterChain in Kong at Route level. +func (s *FilterChainService) DeleteForRoute(ctx context.Context, + routeIDorName *string, filterChainID *string, +) error { + if isEmptyString(filterChainID) { + return fmt.Errorf("filterChain ID cannot be nil for Delete operation") + } + if isEmptyString(routeIDorName) { + return fmt.Errorf("routeIDorName cannot be nil") + } + + endpoint := fmt.Sprintf("/routes/%v/filter-chains/%v", *routeIDorName, *filterChainID) + _, err := s.sendRequest(ctx, nil, endpoint, "DELETE") + if err != nil { + return err + } + return nil +} + +// listByPath fetches a list of FilterChains in Kong +// on a specific path. +// This is a helper method for listing all filter chains +// or filter chains for specific entities. +func (s *FilterChainService) listByPath(ctx context.Context, + path string, opt *ListOpt, +) ([]*FilterChain, *ListOpt, error) { + data, next, err := s.client.list(ctx, path, opt) + if err != nil { + return nil, nil, err + } + var filterChains []*FilterChain + + for _, object := range data { + b, err := object.MarshalJSON() + if err != nil { + return nil, nil, err + } + var filterChain FilterChain + err = json.Unmarshal(b, &filterChain) + if err != nil { + return nil, nil, err + } + filterChains = append(filterChains, &filterChain) + } + + return filterChains, next, nil +} + +// ListAll fetches all FilterChains in Kong. +// This method can take a while if there +// a lot of FilterChains present. +func (s *FilterChainService) listAllByPath(ctx context.Context, + path string, +) ([]*FilterChain, error) { + var filterChains, data []*FilterChain + var err error + opt := &ListOpt{Size: pageSize} + + for opt != nil { + data, opt, err = s.listByPath(ctx, path, opt) + if err != nil { + return nil, err + } + filterChains = append(filterChains, data...) + } + return filterChains, nil +} + +// List fetches a list of FilterChains in Kong. +// opt can be used to control pagination. +func (s *FilterChainService) List(ctx context.Context, + opt *ListOpt, +) ([]*FilterChain, *ListOpt, error) { + return s.listByPath(ctx, "/filter-chains", opt) +} + +// ListAll fetches all FilterChains in Kong. +// This method can take a while if there +// a lot of FilterChains present. +func (s *FilterChainService) ListAll(ctx context.Context) ([]*FilterChain, error) { + return s.listAllByPath(ctx, "/filter-chains") +} + +// ListAllForService fetches all FilterChains in Kong enabled for a service. +func (s *FilterChainService) ListAllForService(ctx context.Context, + serviceIDorName *string, +) ([]*FilterChain, error) { + if isEmptyString(serviceIDorName) { + return nil, fmt.Errorf("serviceIDorName cannot be nil") + } + return s.listAllByPath(ctx, "/services/"+*serviceIDorName+"/filter-chains") +} + +// ListAllForRoute fetches all FilterChains in Kong enabled for a service. +func (s *FilterChainService) ListAllForRoute(ctx context.Context, + routeID *string, +) ([]*FilterChain, error) { + if isEmptyString(routeID) { + return nil, fmt.Errorf("routeID cannot be nil") + } + return s.listAllByPath(ctx, "/routes/"+*routeID+"/filter-chains") +} + +func (s *FilterChainService) sendRequest(ctx context.Context, + filterChain *FilterChain, endpoint, method string, +) (*FilterChain, error) { + var req *http.Request + var err error + if method == "DELETE" { + req, err = s.client.NewRequest(method, endpoint, nil, nil) + if err != nil { + return nil, err + } + } else { + req, err = s.client.NewRequest(method, endpoint, nil, filterChain) + if err != nil { + return nil, err + } + } + var createdFilterChain FilterChain + if method == "DELETE" { + _, err = s.client.Do(ctx, req, nil) + if err != nil { + return nil, err + } + } else { + _, err = s.client.Do(ctx, req, &createdFilterChain) + if err != nil { + return nil, err + } + } + return &createdFilterChain, nil +} diff --git a/kong/filter_chain_service_test.go b/kong/filter_chain_service_test.go new file mode 100644 index 000000000..75990d4d0 --- /dev/null +++ b/kong/filter_chain_service_test.go @@ -0,0 +1,508 @@ +package kong + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFilterChainsService(T *testing.T) { + RunWhenDBMode(T, "postgres") + RunWhenKong(T, ">=3.4.0") + SkipWhenKongRouterFlavor(T, Expressions) + + assert := assert.New(T) + require := require.New(T) + + client, err := NewTestClient(nil, nil) + assert.NoError(err) + assert.NotNil(client) + + service := &Service{ + Name: String("fooWithFilterChain1"), + Host: String("example.com"), + Port: Int(42), + Path: String("/"), + } + err = client.Services.Delete(defaultCtx, service.Name) + assert.NoError(err) + + _, err = client.Services.Create(defaultCtx, service) + assert.NoError(err) + + filterChain := &FilterChain{ + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Howdy\" }"`), + }, + }, + Service: service, + } + assert.NotNil(filterChain) + + createdFilterChain, err := client.FilterChains.Create(defaultCtx, filterChain) + assert.NoError(err) + require.NotNil(createdFilterChain) + require.Nil(createdFilterChain.Name) + + filterChain, err = client.FilterChains.Get(defaultCtx, createdFilterChain.ID) + assert.NoError(err) + assert.NotNil(filterChain) + + filterChain.Name = String("my-chain") + filterChain, err = client.FilterChains.Update(defaultCtx, filterChain) + assert.NoError(err) + assert.NotNil(filterChain) + assert.Equal(String("my-chain"), filterChain.Name) + + err = client.FilterChains.Delete(defaultCtx, createdFilterChain.ID) + assert.NoError(err) + + // ID can be specified + id := uuid.NewString() + filterChain = &FilterChain{ + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Howdy\" }"`), + }, + }, + Service: service, + ID: String(id), + } + + createdFilterChain, err = client.FilterChains.Create(defaultCtx, filterChain) + assert.NoError(err) + assert.NotNil(createdFilterChain) + assert.Equal(id, *createdFilterChain.ID) + + err = client.FilterChains.Delete(defaultCtx, createdFilterChain.ID) + assert.NoError(err) + + service = &Service{ + Name: String("fooWithFilterChain2"), + Host: String("upstream"), + Port: Int(42), + Path: String("/path"), + } + // Clean Data + err = client.Services.Delete(defaultCtx, service.Name) + assert.NoError(err) + // Test to create filter chain from service endpoint + createdService, err := client.Services.Create(defaultCtx, service) + assert.NoError(err) + + id = uuid.NewString() + FilterChainForService := &FilterChain{ + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Howdy\" }"`), + Enabled: Bool(true), + }, + }, + ID: String(id), + } + + createdFilterChain, err = client.FilterChains.CreateForService(defaultCtx, createdService.Name, FilterChainForService) + assert.NoError(err) + assert.NotNil(createdFilterChain) + assert.Equal(id, *createdFilterChain.ID) + assert.Equal(Bool(true), createdFilterChain.Filters[0].Enabled) + + createdFilterChain.Filters[0].Enabled = Bool(false) + updatedFilterChain, err := client.FilterChains.UpdateForService(defaultCtx, createdService.Name, createdFilterChain) + assert.NoError(err) + assert.NotNil(updatedFilterChain) + assert.Equal(id, *updatedFilterChain.ID) + assert.Equal(Bool(false), createdFilterChain.Filters[0].Enabled) + + err = client.FilterChains.DeleteForService(defaultCtx, createdService.Name, updatedFilterChain.ID) + assert.NoError(err) + + // Create filter chain without ID + createdFilterChain, err = client.FilterChains.CreateForService(defaultCtx, createdService.Name, &FilterChain{ + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Howdy\" }"`), + Enabled: Bool(true), + }, + }, + }) + assert.NoError(err) + assert.NotNil(createdFilterChain) + assert.NotNil(createdFilterChain.ID) + + assert.NoError(client.Services.Delete(defaultCtx, createdService.ID)) + + route := &Route{ + Name: String("route_filter_chain"), + Paths: []*string{String("/route_filter_chain")}, + } + // Clean Data + err = client.Routes.Delete(defaultCtx, route.Name) + assert.NoError(err) + // Test to create filter chain from route endpoint + createdRoute, err := client.Routes.Create(defaultCtx, route) + assert.NoError(err) + + id = uuid.NewString() + FilterChainForRoute := &FilterChain{ + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Howdy\" }"`), + Enabled: Bool(true), + }, + }, + ID: String(id), + } + + createdFilterChain, err = client.FilterChains.CreateForRoute(defaultCtx, createdRoute.Name, FilterChainForRoute) + assert.NoError(err) + assert.NotNil(createdFilterChain) + assert.Equal(id, *createdFilterChain.ID) + assert.Equal(Bool(true), createdFilterChain.Filters[0].Enabled) + + createdFilterChain.Filters[0].Enabled = Bool(false) + updatedFilterChain, err = client.FilterChains.UpdateForRoute(defaultCtx, createdRoute.Name, createdFilterChain) + assert.NoError(err) + assert.NotNil(updatedFilterChain) + assert.Equal(id, *updatedFilterChain.ID) + assert.Equal(Bool(false), createdFilterChain.Filters[0].Enabled) + + err = client.FilterChains.DeleteForRoute(defaultCtx, createdRoute.Name, updatedFilterChain.ID) + assert.NoError(err) + + // Create filter chain without ID + createdFilterChain, err = client.FilterChains.CreateForRoute(defaultCtx, createdRoute.Name, &FilterChain{ + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Howdy\" }"`), + Enabled: Bool(true), + }, + }, + }) + assert.NoError(err) + assert.NotNil(createdFilterChain) + assert.NotNil(createdFilterChain.ID) + + assert.NoError(client.Routes.Delete(defaultCtx, createdRoute.ID)) +} + +func TestFilterChainWithTags(T *testing.T) { + RunWhenDBMode(T, "postgres") + RunWhenKong(T, ">=3.4.0") + + assert := assert.New(T) + require := require.New(T) + + client, err := NewTestClient(nil, nil) + require.NoError(err) + require.NotNil(client) + + service := &Service{ + Name: String("fooWithFilterChain1"), + Host: String("example.com"), + Port: Int(42), + Path: String("/"), + } + err = client.Services.Delete(defaultCtx, service.Name) + assert.NoError(err) + + createdService, err := client.Services.Create(defaultCtx, service) + assert.NoError(err) + + filterChain := &FilterChain{ + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Howdy\" }"`), + }, + }, + Service: createdService, + Tags: StringSlice("tag1", "tag2"), + } + + createdFilterChain, err := client.FilterChains.Create(defaultCtx, filterChain) + assert.NoError(err) + require.NotNil(createdFilterChain) + require.Equal(StringSlice("tag1", "tag2"), createdFilterChain.Tags) + + err = client.FilterChains.Delete(defaultCtx, createdFilterChain.ID) + require.NoError(err) + + err = client.Services.Delete(defaultCtx, createdService.ID) + require.NoError(err) +} + +func TestUnknownFilterChain(T *testing.T) { + RunWhenDBMode(T, "postgres") + RunWhenKong(T, ">=3.4.0") + + assert := assert.New(T) + + client, err := NewTestClient(nil, nil) + assert.NoError(err) + assert.NotNil(client) + + service := &Service{ + Name: String("fooWithFilterChain1"), + Host: String("example.com"), + Port: Int(42), + Path: String("/"), + } + err = client.Services.Delete(defaultCtx, service.Name) + assert.NoError(err) + + createdService, err := client.Services.Create(defaultCtx, service) + assert.NoError(err) + + filterChain := &FilterChain{ + Filters: []*Filter{ + { + Name: String("filter-chain-not-present"), + Config: JSONRawMessage(`"{ \"option\": true }"`), + }, + }, + Service: createdService, + Tags: StringSlice("tag1", "tag2"), + } + + createdFilterChain, err := client.FilterChains.Create(defaultCtx, filterChain) + require.Error(T, err) + require.Nil(T, createdFilterChain) + + err = client.Services.Delete(defaultCtx, createdService.ID) + assert.NoError(err) +} + +func TestFilterChainListEndpoint(T *testing.T) { + RunWhenDBMode(T, "postgres") + RunWhenKong(T, ">=3.4.0") + + assert := assert.New(T) + + client, err := NewTestClient(nil, nil) + assert.NoError(err) + assert.NotNil(client) + + // fixtures + filterChains := []*FilterChain{ + { + Name: String("chain-1"), + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Hi\" }"`), + }, + }, + }, + { + Name: String("chain-2"), + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Hey\" }"`), + }, + }, + }, + { + Name: String("chain-3"), + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Howdy\" }"`), + }, + }, + }, + } + + // create fixtures + for i := 0; i < len(filterChains); i++ { + service, err := client.Services.Create(defaultCtx, &Service{ + Name: String("service-for-" + *filterChains[i].Name), + Host: String("example.com"), + Port: Int(42), + Path: String("/"), + }) + + assert.NoError(err) + assert.NotNil(service) + filterChain, err := client.FilterChains.CreateForService(defaultCtx, service.Name, filterChains[i]) + assert.NoError(err) + assert.NotNil(filterChain) + filterChains[i] = filterChain + } + + filterChainsFromKong, next, err := client.FilterChains.List(defaultCtx, nil) + assert.NoError(err) + assert.Nil(next) + assert.NotNil(filterChainsFromKong) + assert.Equal(3, len(filterChainsFromKong)) + + // check if we see all filterChains + assert.True(compareFilterChains(T, filterChains, filterChainsFromKong)) + + // Test pagination + filterChainsFromKong = []*FilterChain{} + + // first page + page1, next, err := client.FilterChains.List(defaultCtx, &ListOpt{Size: 1}) + assert.NoError(err) + assert.NotNil(next) + assert.NotNil(page1) + assert.Equal(1, len(page1)) + filterChainsFromKong = append(filterChainsFromKong, page1...) + + // second page + page2, next, err := client.FilterChains.List(defaultCtx, next) + assert.NoError(err) + assert.NotNil(next) + assert.NotNil(page2) + assert.Equal(1, len(page2)) + filterChainsFromKong = append(filterChainsFromKong, page2...) + + // last page + page3, next, err := client.FilterChains.List(defaultCtx, next) + assert.NoError(err) + assert.Nil(next) + assert.NotNil(page3) + assert.Equal(1, len(page3)) + filterChainsFromKong = append(filterChainsFromKong, page3...) + + assert.True(compareFilterChains(T, filterChains, filterChainsFromKong)) + + filterChains, err = client.FilterChains.ListAll(defaultCtx) + assert.NoError(err) + assert.NotNil(filterChains) + assert.Equal(3, len(filterChains)) + + for i := 0; i < len(filterChains); i++ { + assert.NoError(client.Services.Delete(defaultCtx, filterChains[i].Service.ID)) + } +} + +func TestFilterChainListAllForEntityEndpoint(T *testing.T) { + RunWhenDBMode(T, "postgres") + RunWhenKong(T, ">=3.4.0") + SkipWhenKongRouterFlavor(T, Expressions) + + assert := assert.New(T) + + client, err := NewTestClient(nil, nil) + assert.NoError(err) + assert.NotNil(client) + + // fixtures + + createdService, err := client.Services.Create(defaultCtx, &Service{ + Name: String("foo"), + Host: String("upstream"), + Port: Int(42), + Path: String("/path"), + }) + assert.NoError(err) + assert.NotNil(createdService) + + createdRoute, err := client.Routes.Create(defaultCtx, &Route{ + Hosts: StringSlice("example.com", "example.test"), + Service: createdService, + }) + assert.NoError(err) + assert.NotNil(createdRoute) + + filterChains := []*FilterChain{ + // specific to route + { + Name: String("route-chain"), + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Hello, route\" }"`), + }, + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"option\": false }"`), + }, + }, + Route: createdRoute, + }, + // specific to service + { + Name: String("service-chain"), + Filters: []*Filter{ + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"option\": false }"`), + }, + { + Name: String("example-filter"), + Config: JSONRawMessage(`"{ \"my_greeting\": \"Hello, service\" }"`), + }, + }, + Service: createdService, + }, + } + + // create fixtures + for i := 0; i < len(filterChains); i++ { + filterChain, err := client.FilterChains.Create(defaultCtx, filterChains[i]) + assert.NoError(err) + assert.NotNil(filterChain) + filterChains[i] = filterChain + } + + filterChainsFromKong, err := client.FilterChains.ListAll(defaultCtx) + assert.NoError(err) + assert.NotNil(filterChainsFromKong) + assert.Equal(len(filterChains), len(filterChainsFromKong)) + + // check if we see all filterChains + assert.True(compareFilterChains(T, filterChains, filterChainsFromKong)) + + filterChainsFromKong, err = client.FilterChains.ListAll(defaultCtx) + assert.NoError(err) + assert.NotNil(filterChainsFromKong) + assert.Equal(2, len(filterChainsFromKong)) + + filterChainsFromKong, err = client.FilterChains.ListAllForService(defaultCtx, + createdService.ID) + assert.NoError(err) + assert.NotNil(filterChainsFromKong) + assert.Equal(1, len(filterChainsFromKong)) + + filterChainsFromKong, err = client.FilterChains.ListAllForRoute(defaultCtx, + createdRoute.ID) + assert.NoError(err) + assert.NotNil(filterChainsFromKong) + assert.Equal(1, len(filterChainsFromKong)) + + for i := 0; i < len(filterChains); i++ { + assert.NoError(client.FilterChains.Delete(defaultCtx, filterChains[i].ID)) + } + + assert.NoError(client.Routes.Delete(defaultCtx, createdRoute.ID)) + assert.NoError(client.Services.Delete(defaultCtx, createdService.ID)) +} + +func compareFilterChains(T *testing.T, expected, actual []*FilterChain) bool { + var expectedNames, actualNames []string + for _, filterChain := range expected { + if !assert.NotNil(T, filterChain) { + continue + } + expectedNames = append(expectedNames, *filterChain.Name) + } + + for _, filterChain := range actual { + actualNames = append(actualNames, *filterChain.Name) + } + + return (compareSlices(expectedNames, actualNames)) +} diff --git a/kong/plugin_service_test.go b/kong/plugin_service_test.go index 9c45c4a39..ae1ff3bbf 100644 --- a/kong/plugin_service_test.go +++ b/kong/plugin_service_test.go @@ -1,6 +1,7 @@ package kong import ( + "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -119,8 +120,8 @@ func TestPluginsService(T *testing.T) { createdPlugin.Config["anonymous"] = "false" updatedPlugin, err := client.Plugins.UpdateForService(defaultCtx, createdService.Name, createdPlugin) assert.NoError(err) - assert.NotNil(createdPlugin) - assert.Equal(id, *createdPlugin.ID) + assert.NotNil(updatedPlugin) + assert.Equal(id, *updatedPlugin.ID) assert.Equal("false", updatedPlugin.Config["anonymous"]) err = client.Plugins.DeleteForService(defaultCtx, createdService.Name, updatedPlugin.ID) @@ -343,7 +344,7 @@ func TestPluginListEndpoint(T *testing.T) { }, } - // create fixturs + // create fixtures for i := 0; i < len(plugins); i++ { schema, err := client.Plugins.GetFullSchema(defaultCtx, plugins[i].Name) assert.NoError(err) @@ -495,8 +496,6 @@ func TestPluginListAllForEntityEndpoint(T *testing.T) { // check if we see all plugins assert.True(comparePlugins(T, plugins, pluginsFromKong)) - assert.True(comparePlugins(T, plugins, pluginsFromKong)) - pluginsFromKong, err = client.Plugins.ListAll(defaultCtx) assert.NoError(err) assert.NotNil(pluginsFromKong) @@ -561,9 +560,11 @@ func TestFillPluginDefaults(T *testing.T) { name string plugin *Plugin expected *Plugin + version string }{ { - name: "no config no protocols", + name: "no config no protocols", + version: ">=3.7.0", plugin: &Plugin{ Name: String("basic-auth"), RunOn: String("test"), @@ -574,13 +575,59 @@ func TestFillPluginDefaults(T *testing.T) { Config: Configuration{ "anonymous": nil, "hide_credentials": false, + "realm": "service", + }, + Protocols: []*string{String("grpc"), String("grpcs"), String("http"), String("https")}, + Enabled: Bool(true), + }, + }, + { + name: "no config no protocols", + version: "<3.7.0", + plugin: &Plugin{ + Name: String("basic-auth"), + RunOn: String("test"), + }, + expected: &Plugin{ + Name: String("basic-auth"), + RunOn: String("test"), + Config: Configuration{ + "anonymous": nil, + "hide_credentials": false, + }, + Protocols: []*string{String("grpc"), String("grpcs"), String("http"), String("https")}, + Enabled: Bool(true), + }, + }, + { + name: "partial config no protocols", + version: ">=3.7.0", + plugin: &Plugin{ + Name: String("basic-auth"), + Consumer: &Consumer{ + ID: String("3bb9a73c-a467-11ec-b909-0242ac120002"), + }, + Config: Configuration{ + "hide_credentials": true, + }, + }, + expected: &Plugin{ + Name: String("basic-auth"), + Consumer: &Consumer{ + ID: String("3bb9a73c-a467-11ec-b909-0242ac120002"), + }, + Config: Configuration{ + "anonymous": nil, + "hide_credentials": true, + "realm": "service", }, Protocols: []*string{String("grpc"), String("grpcs"), String("http"), String("https")}, Enabled: Bool(true), }, }, { - name: "partial config no protocols", + name: "partial config no protocols", + version: "<3.7.0", plugin: &Plugin{ Name: String("basic-auth"), Consumer: &Consumer{ @@ -662,7 +709,16 @@ func TestFillPluginDefaults(T *testing.T) { } for _, tc := range tests { - T.Run(tc.name, func(t *testing.T) { + name := tc.name + if tc.version != "" { + name = fmt.Sprintf("%s (kong %s)", name, tc.version) + } + + T.Run(name, func(t *testing.T) { + if tc.version != "" { + RunWhenKong(t, tc.version) + } + p := tc.plugin fullSchema, err := client.Plugins.GetFullSchema(defaultCtx, p.Name) require.NoError(t, err) @@ -842,7 +898,7 @@ func TestPluginsWithConsumerGroup(T *testing.T) { }, } - // create fixturs + // create fixtures for i := 0; i < len(plugins); i++ { plugin, err := client.Plugins.Create(defaultCtx, plugins[i]) assert.NoError(err) diff --git a/kong/test_utils.go b/kong/test_utils.go index c3c10a197..e3362a60c 100644 --- a/kong/test_utils.go +++ b/kong/test_utils.go @@ -105,7 +105,7 @@ func SkipWhenEnterprise(t *testing.T) { } if currentVersion.IsKongGatewayEnterprise() { - t.Skip("non-Enterprise test Kong instance, skipping") + t.Skip("Enterprise test Kong instance, skipping") } } diff --git a/kong/utils.go b/kong/utils.go index c6902f20f..c57eb9acf 100644 --- a/kong/utils.go +++ b/kong/utils.go @@ -42,6 +42,12 @@ func Float64(f float64) *float64 { return &f } +// JSONRawMessage returns a pointer to a json.RawMessage +func JSONRawMessage(s string) *json.RawMessage { + j := json.RawMessage(s) + return &j +} + func isEmptyString(s *string) bool { return s == nil || strings.TrimSpace(*s) == "" } diff --git a/kong/zz_generated.deepcopy.go b/kong/zz_generated.deepcopy.go index d3da6c8b4..b8cf3de57 100644 --- a/kong/zz_generated.deepcopy.go +++ b/kong/zz_generated.deepcopy.go @@ -21,6 +21,10 @@ limitations under the License. package kong +import ( + json "encoding/json" +) + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ACLGroup) DeepCopyInto(out *ACLGroup) { *out = *in @@ -780,6 +784,114 @@ func (in *DeveloperRole) DeepCopy() *DeveloperRole { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Filter) DeepCopyInto(out *Filter) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = new(json.RawMessage) + if **in != nil { + in, out := *in, *out + *out = make([]byte, len(*in)) + copy(*out, *in) + } + } + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Filter. +func (in *Filter) DeepCopy() *Filter { + if in == nil { + return nil + } + out := new(Filter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FilterChain) DeepCopyInto(out *FilterChain) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Route != nil { + in, out := &in.Route, &out.Route + *out = new(Route) + (*in).DeepCopyInto(*out) + } + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(Service) + (*in).DeepCopyInto(*out) + } + if in.Filters != nil { + in, out := &in.Filters, &out.Filters + *out = make([]*Filter, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Filter) + (*in).DeepCopyInto(*out) + } + } + } + if in.CreatedAt != nil { + in, out := &in.CreatedAt, &out.CreatedAt + *out = new(int) + **out = **in + } + if in.UpdatedAt != nil { + in, out := &in.UpdatedAt, &out.UpdatedAt + *out = new(int) + **out = **in + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]*string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(string) + **out = **in + } + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FilterChain. +func (in *FilterChain) DeepCopy() *FilterChain { + if in == nil { + return nil + } + out := new(FilterChain) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GraphqlRateLimitingCostDecoration) DeepCopyInto(out *GraphqlRateLimitingCostDecoration) { *out = *in