diff --git a/CHANGELOG.md b/CHANGELOG.md index 513ba2bc..21328fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,14 @@ - [0.2.0](#020) - [0.1.0](#010) +## [v0.62.0] + +> Release date: 2024/12/11 + +- Added Client APIs (Create, List, ListALL, Delete) to interact + with the Konnect Application Auth resources. + [#490](https://github.com/Kong/go-kong/pull/490) + ## [v0.61.0] > Release date: 2024/12/06 diff --git a/go.mod b/go.mod index dceebc4f..8f23917d 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/samber/lo v1.47.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect diff --git a/go.sum b/go.sum index d2306b44..2b200d7d 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,8 @@ github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/kong/client.go b/kong/client.go index df98a232..6bde3fa2 100644 --- a/kong/client.go +++ b/kong/client.go @@ -67,6 +67,7 @@ type Client struct { Vaults AbstractVaultService Keys AbstractKeyService KeySets AbstractKeySetService + KonnectApplication AbstractKonnectApplicationService Licenses AbstractLicenseService FilterChains AbstractFilterChainService @@ -165,6 +166,7 @@ func NewClient(baseURL *string, client *http.Client) (*Client, error) { kong.Vaults = (*VaultService)(&kong.common) kong.Keys = (*KeyService)(&kong.common) kong.KeySets = (*KeySetService)(&kong.common) + kong.KonnectApplication = (*KonnectApplicationService)(&kong.common) kong.Licenses = (*LicenseService)(&kong.common) kong.FilterChains = (*FilterChainService)(&kong.common) diff --git a/kong/konnect_application.go b/kong/konnect_application.go new file mode 100644 index 00000000..07359b00 --- /dev/null +++ b/kong/konnect_application.go @@ -0,0 +1,27 @@ +package kong + +// KonnectApplication represents Konnect-Application-Auth +// in Kong. +// Read https://docs.konghq.com/konnect/dev-portal/applications/application-overview/ +// +k8s:deepcopy-gen=true +type KonnectApplication struct { + ID *string `json:"id"` + CreatedAt int64 `json:"created_at"` + ClientID string `json:"client_id"` + ConsumerGroups []string `json:"consumer_groups"` + Scopes []string `json:"scopes"` + AuthStrategyID *string `json:"auth_strategy_id"` + ApplicationContext *ApplicationContext `json:"application_context"` + ExhaustedScopes []string `json:"exhausted_scopes"` + Tags *[]string `json:"tags"` +} + +// ApplicationContext represents the application context inside the +// Konnenct-Application-Auth. +// +k8s:deepcopy-gen=true +type ApplicationContext struct { + PortalID *string `json:"portal_id"` + ApplicationID *string `json:"application_id"` + DeveloperID *string `json:"developer_id"` + OrganizationID *string `json:"organization_id"` +} diff --git a/kong/konnect_application_service.go b/kong/konnect_application_service.go new file mode 100644 index 00000000..469ea5b0 --- /dev/null +++ b/kong/konnect_application_service.go @@ -0,0 +1,105 @@ +package kong + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +var _ AbstractKonnectApplicationService = &KonnectApplicationService{} + +// AbstractKonnectApplicationService handles Konnect applications in Kong. +type AbstractKonnectApplicationService interface { + // Create creates a Konnect Application in Kong. + Create(ctx context.Context, key *KonnectApplication) (*KonnectApplication, error) + // List fetches list of Konnect Applications in Kong. + List(ctx context.Context, opt *ListOpt) ([]*KonnectApplication, *ListOpt, error) + // ListAll fetches all Konnect Applications in Kong. + ListAll(ctx context.Context) ([]*KonnectApplication, error) + // Delete deletes a Konnect Application in Kong by ID. + Delete(ctx context.Context, ID *string) error +} + +type KonnectApplicationService service + +// Create creates a Konnect Application in Kong. +func (k *KonnectApplicationService) Create(ctx context.Context, key *KonnectApplication) (*KonnectApplication, error) { + queryPath := "/konnect_applications" + method := "POST" + if key.ID != nil { + queryPath = queryPath + "/" + *key.ID + method = "PUT" + } + req, err := k.client.NewRequest(method, queryPath, nil, key) + if err != nil { + return nil, err + } + + var createdKey KonnectApplication + _, err = k.client.Do(ctx, req, &createdKey) + if err != nil { + return nil, err + } + return &createdKey, nil +} + +// List fetches list of Konnect Applications in Kong. +func (k *KonnectApplicationService) List(ctx context.Context, opt *ListOpt) ([]*KonnectApplication, *ListOpt, error) { + data, next, err := k.client.list(ctx, "/konnect_applications", opt) + if err != nil { + return nil, nil, err + } + var kaas []*KonnectApplication + + for _, object := range data { + b, err := object.MarshalJSON() + if err != nil { + return nil, nil, err + } + var kaa KonnectApplication + err = json.Unmarshal(b, &kaa) + if err != nil { + return nil, nil, err + } + kaas = append(kaas, &kaa) + } + + return kaas, next, nil +} + +// ListAll fetches all Konnect Applications in Kong. +func (k *KonnectApplicationService) ListAll(ctx context.Context) ([]*KonnectApplication, error) { + var kaa, data []*KonnectApplication + var err error + opt := &ListOpt{Size: pageSize} + + for opt != nil { + data, opt, err = k.List(ctx, opt) + if err != nil { + return nil, err + } + kaa = append(kaa, data...) + } + return kaa, nil +} + +// Delete deletes a Konnect Application in Kong by ID. +func (k *KonnectApplicationService) Delete(ctx context.Context, ID *string) error { + if isEmptyString(ID) { + return fmt.Errorf("ID cannot be nil for Delete operation") + } + + req, err := k.client.NewRequest("DELETE", fmt.Sprintf("/konnect_applications/%s", *ID), nil, nil) + if err != nil { + return err + } + + resp, err := k.client.Do(ctx, req, nil) + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("failed to delete Konnect Application: %s: "+ + "expected status %v, but received %v", *ID, http.StatusNoContent, resp.Status) + } + + return err +} diff --git a/kong/konnect_application_service_test.go b/kong/konnect_application_service_test.go new file mode 100644 index 00000000..238ea2da --- /dev/null +++ b/kong/konnect_application_service_test.go @@ -0,0 +1,203 @@ +package kong + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestKonnectApplicationService_Create(T *testing.T) { + RunWhenDBMode(T, "postgres") + RunWhenEnterprise(T, ">=3.6.0", RequiredFeatures{}) + + assert := assert.New(T) + require := require.New(T) + + client, err := NewTestClient(nil, nil) + require.NoError(err) + require.NotNil(client) + + var ( + clientID = uuid.NewString() + consumerGroup = []string{uuid.NewString()} + scopes = []string{"/auth"} + authStrategyID = uuid.NewString() + exhaustedScopes = []string{"/eauth"} + orgID = uuid.NewString() + developerID = uuid.NewString() + createdAt = time.Now().Unix() + ) + + kaa := &KonnectApplication{ + ID: &clientID, + ClientID: clientID, + ConsumerGroups: consumerGroup, + Scopes: scopes, + AuthStrategyID: &authStrategyID, + ExhaustedScopes: exhaustedScopes, + ApplicationContext: &ApplicationContext{ + OrganizationID: &orgID, + DeveloperID: &developerID, + }, + CreatedAt: createdAt, + } + createResponse, err := client.KonnectApplication.Create(defaultCtx, kaa) + require.NoError(err) + require.NotNil(createResponse) + + T.Cleanup(func() { + assert.NoError(client.KonnectApplication.Delete(context.Background(), createResponse.ID)) + }) + + require.Equal(createResponse.ClientID, clientID) + require.Equal(createResponse.CreatedAt, createdAt) + require.Equal(createResponse.ConsumerGroups, consumerGroup) + require.Equal(createResponse.Scopes, scopes) + require.Equal(createResponse.ExhaustedScopes, exhaustedScopes) +} + +func TestKonnectApplicationService_ListAll(T *testing.T) { + RunWhenDBMode(T, "postgres") + RunWhenEnterprise(T, ">=3.6.0", RequiredFeatures{}) + + assert := assert.New(T) + require := require.New(T) + + client, err := NewTestClient(nil, nil) + require.NoError(err) + require.NotNil(client) + + var ( + expectedKonnectApplications = 10 + consumerGroup = []string{uuid.NewString()} + scopes = []string{"/auth"} + authStrategyID = uuid.NewString() + exhaustedScopes = []string{"/eauth"} + orgID = uuid.NewString() + developerID = uuid.NewString() + createdAt = time.Now().Unix() + ) + + kaa := &KonnectApplication{ + ConsumerGroups: consumerGroup, + Scopes: scopes, + AuthStrategyID: &authStrategyID, + ExhaustedScopes: exhaustedScopes, + ApplicationContext: &ApplicationContext{ + OrganizationID: &orgID, + DeveloperID: &developerID, + }, + CreatedAt: createdAt, + } + + for i := 0; i < expectedKonnectApplications; i++ { + clientID := uuid.NewString() + kaa.ID = &clientID + kaa.ClientID = clientID + createResponse, err := client.KonnectApplication.Create(defaultCtx, kaa) + require.NoError(err) + require.NotNil(createResponse) + + T.Cleanup(func() { + assert.NoError(client.KonnectApplication.Delete(context.Background(), createResponse.ID)) + }) + } + + listKonnectApplicationResponse, err := client.KonnectApplication.ListAll(defaultCtx) + require.NoError(err) + require.Len(listKonnectApplicationResponse, expectedKonnectApplications) +} + +func TestKonnectApplicationService_List(T *testing.T) { + RunWhenDBMode(T, "postgres") + RunWhenEnterprise(T, ">=3.6.0", RequiredFeatures{}) + + assert := assert.New(T) + require := require.New(T) + + client, err := NewTestClient(nil, nil) + require.NoError(err) + require.NotNil(client) + + var ( + expectedKonnectApplications = 10 + consumerGroup = []string{uuid.NewString()} + scopes = []string{"/auth"} + authStrategyID = uuid.NewString() + exhaustedScopes = []string{"/eauth"} + orgID = uuid.NewString() + developerID = uuid.NewString() + createdAt = time.Now().Unix() + tagA = "list1" + tagB = "list2" + ) + + kaa := &KonnectApplication{ + ConsumerGroups: consumerGroup, + Scopes: scopes, + AuthStrategyID: &authStrategyID, + ExhaustedScopes: exhaustedScopes, + ApplicationContext: &ApplicationContext{ + OrganizationID: &orgID, + DeveloperID: &developerID, + }, + CreatedAt: createdAt, + } + + for i := 0; i < expectedKonnectApplications/2; i++ { + clientID := uuid.NewString() + kaa.ID = &clientID + kaa.ClientID = clientID + kaa.Tags = lo.ToPtr([]string{tagA}) + createResponse, err := client.KonnectApplication.Create(defaultCtx, kaa) + require.NoError(err) + require.NotNil(createResponse) + + T.Cleanup(func() { + assert.NoError(client.KonnectApplication.Delete(context.Background(), createResponse.ID)) + }) + } + + for i := 0; i < expectedKonnectApplications/2; i++ { + clientID := uuid.NewString() + kaa.ID = &clientID + kaa.ClientID = clientID + kaa.Tags = lo.ToPtr([]string{tagB}) + createResponse, err := client.KonnectApplication.Create(defaultCtx, kaa) + require.NoError(err) + require.NotNil(createResponse) + + T.Cleanup(func() { + assert.NoError(client.KonnectApplication.Delete(context.Background(), createResponse.ID)) + }) + } + + // Filter by tag listA + listKonnectApplicationResponseByTagA, _, err := client.KonnectApplication.List(defaultCtx, &ListOpt{ + Size: 10, + Tags: []*string{lo.ToPtr(tagA)}, + }) + require.NoError(err) + require.Len(listKonnectApplicationResponseByTagA, 5) + + // Filter by tag listB + listKonnectApplicationResponseByTagB, _, err := client.KonnectApplication.List(defaultCtx, &ListOpt{ + Size: 10, + Tags: []*string{lo.ToPtr(tagB)}, + }) + require.NoError(err) + require.Len(listKonnectApplicationResponseByTagB, 5) + + size := 2 + // Filter by size + listKonnectApplicationResponseBySize, _, err := client.KonnectApplication.List(defaultCtx, &ListOpt{ + Size: size, + }) + require.NoError(err) + require.Len(listKonnectApplicationResponseBySize, size) +} diff --git a/kong/zz_generated.deepcopy.go b/kong/zz_generated.deepcopy.go index 9552d783..5aff0f4f 100644 --- a/kong/zz_generated.deepcopy.go +++ b/kong/zz_generated.deepcopy.go @@ -204,6 +204,42 @@ func (in *Admin) DeepCopy() *Admin { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationContext) DeepCopyInto(out *ApplicationContext) { + *out = *in + if in.PortalID != nil { + in, out := &in.PortalID, &out.PortalID + *out = new(string) + **out = **in + } + if in.ApplicationID != nil { + in, out := &in.ApplicationID, &out.ApplicationID + *out = new(string) + **out = **in + } + if in.DeveloperID != nil { + in, out := &in.DeveloperID, &out.DeveloperID + *out = new(string) + **out = **in + } + if in.OrganizationID != nil { + in, out := &in.OrganizationID, &out.OrganizationID + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationContext. +func (in *ApplicationContext) DeepCopy() *ApplicationContext { + if in == nil { + return nil + } + out := new(ApplicationContext) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BasicAuth) DeepCopyInto(out *BasicAuth) { *out = *in @@ -1422,6 +1458,61 @@ func (in *KeySet) DeepCopy() *KeySet { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KonnectApplication) DeepCopyInto(out *KonnectApplication) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.ConsumerGroups != nil { + in, out := &in.ConsumerGroups, &out.ConsumerGroups + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AuthStrategyID != nil { + in, out := &in.AuthStrategyID, &out.AuthStrategyID + *out = new(string) + **out = **in + } + if in.ApplicationContext != nil { + in, out := &in.ApplicationContext, &out.ApplicationContext + *out = new(ApplicationContext) + (*in).DeepCopyInto(*out) + } + if in.ExhaustedScopes != nil { + in, out := &in.ExhaustedScopes, &out.ExhaustedScopes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = new([]string) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KonnectApplication. +func (in *KonnectApplication) DeepCopy() *KonnectApplication { + if in == nil { + return nil + } + out := new(KonnectApplication) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *License) DeepCopyInto(out *License) { *out = *in