From 5efa60718550088115703a74995ba898274cbdcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 06:20:20 +0000 Subject: [PATCH 01/17] chore(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.1 Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.10.0 to 1.11.1. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.10.0...v1.11.1) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-version: 1.11.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 92d28e0..e0116f8 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( github.com/avast/retry-go/v4 v4.6.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 golang.org/x/tools v0.36.0 ) diff --git a/go.sum b/go.sum index 7fa918d..58b0f63 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= From 1ceb4f52d31885cc90d3bfcea0e75316986d1f1d Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Wed, 3 Sep 2025 14:34:04 +0100 Subject: [PATCH 02/17] feat: adding privatelink service and model --- client.go | 3 + privatelink_test.go | 166 +++++++++++++++++++++++++++++++++ service/privatelink/model.go | 27 ++++++ service/privatelink/service.go | 92 ++++++++++++++++++ 4 files changed, 288 insertions(+) create mode 100644 privatelink_test.go create mode 100644 service/privatelink/model.go create mode 100644 service/privatelink/service.go diff --git a/client.go b/client.go index 92778ec..b0fae90 100644 --- a/client.go +++ b/client.go @@ -3,6 +3,7 @@ package rediscloud_api import ( "bytes" "encoding/json" + "github.com/RedisLabs/rediscloud-go-api/service/privatelink" "log" "net/http" "net/http/httputil" @@ -45,6 +46,7 @@ type Client struct { Pricing *pricing.API TransitGatewayAttachments *attachments.API PrivateServiceConnect *psc.API + PrivateLink *privatelink.API Tags *tags.API // fixed FixedPlans *plans.API @@ -94,6 +96,7 @@ func NewClient(configs ...Option) (*Client, error) { Pricing: pricing.NewAPI(client), TransitGatewayAttachments: attachments.NewAPI(client, t, config.logger), PrivateServiceConnect: psc.NewAPI(client, t, config.logger), + PrivateLink: privatelink.NewAPI(client, t, config.logger), Tags: tags.NewAPI(client), // fixed FixedPlans: plans.NewAPI(client, config.logger), diff --git a/privatelink_test.go b/privatelink_test.go new file mode 100644 index 0000000..0a82aaa --- /dev/null +++ b/privatelink_test.go @@ -0,0 +1,166 @@ +package rediscloud_api + +import ( + "context" + "errors" + "net/http/httptest" + "testing" + + "github.com/RedisLabs/rediscloud-go-api/redis" + pl "github.com/RedisLabs/rediscloud-go-api/service/privatelink" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetPrivateLinkConfig(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedResult *pl.PrivateLinkConfig + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully return a privatelink config", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/private-link", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "privateLinkGetRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2024-07-16T09:26:40.929904847Z", + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "task", + "type": "GET" + } + ] + }`, + ), + getRequest( + t, + "/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "privatelinkGetRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2024-07-16T09:26:49.847808891Z", + "response": { + "resourceId": 114019, + "resource": { + "id": 40, + "connectionHostName": "pl.mc2018-0.us-central1-mz.gcp.sdk-cloud.rlrcp.com", + "serviceAttachmentName": "service-attachment-mc2018-0-us-central1-mz-rlrcp", + "status": "active" + } + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + }, + expectedResult: &pl.PrivateLinkConfig{ + ID: redis.Int(40), + ConnectionHostName: redis.String("pl.mc2018-0.us-central1-mz.gcp.sdk-cloud.rlrcp.com"), + ServiceAttachmentName: redis.String("service-attachment-mc2018-0-us-central1-mz-rlrcp"), + Status: redis.String("active"), + }, + }, + { + description: "should fail when private link is not found", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/private-link", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "privatelinkGetRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2024-07-16T09:26:40.929904847Z", + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "task", + "type": "GET" + } + ] + }`, + ), + getRequest( + t, + "/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "privatelinkGetRequest", + "status": "processing-error", + "description": "Task request failed during processing. See error information for failure details.", + "timestamp": "2025-01-13T11:22:51.204189721Z", + "response": { + "error": { + "type": "PRIVATELINK_SERVICE_NOT_FOUND", + "status": "404 NOT_FOUND", + "description": "Private Service Connect service not found" + } + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + }, + expectedError: errors.New("resource not found - subscription 114019"), + expectedErrorAs: &pl.NotFound{}, + }, + { + description: "should fail when subscription is not found", + mockedResponse: []endpointRequest{ + getRequestWithStatus( + t, + "/subscriptions/114019/private-link", + 404, + `{ + "timestamp" : "2025-01-17T09:34:25.803+00:00", + "status" : 404, + "error" : "Not Found", + "path" : "/v1/subscriptions/114019/private-link" + }`), + }, + expectedError: errors.New("resource not found - subscription 114019"), + expectedErrorAs: &pl.NotFound{}, + }, + } + + for _, testCase := range tc { + t.Run(testCase.description, func(t *testing.T) { + server := httptest.NewServer( + testServer("key", "secret", testCase.mockedResponse...)) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + actual, err := subject.PrivateLink.GetPrivateLink(context.TODO(), 114019) + if testCase.expectedError == nil { + assert.NoError(t, err) + assert.Equal(t, testCase.expectedResult, actual) + } else { + assert.IsType(t, err, testCase.expectedErrorAs) + assert.EqualError(t, err, testCase.expectedError.Error()) + } + }) + } +} diff --git a/service/privatelink/model.go b/service/privatelink/model.go new file mode 100644 index 0000000..2b19da6 --- /dev/null +++ b/service/privatelink/model.go @@ -0,0 +1,27 @@ +package privatelink + +import "fmt" + +type PrivateLinkConfig struct { + ID *int `json:"id,omitempty"` + ConnectionHostName *string `json:"connectionHostName,omitempty"` + ServiceAttachmentName *string `json:"serviceAttachmentName,omitempty"` + Status *string `json:"status,omitempty"` +} + +type NotFound struct { + subscriptionID int +} + +func (f *NotFound) Error() string { + return fmt.Sprintf("resource not found - subscription %d", f.subscriptionID) +} + +type NotFoundActiveActive struct { + subscriptionID int + regionID int +} + +func (f *NotFoundActiveActive) Error() string { + return fmt.Sprintf("resource not found - subscription %d and region %d", f.subscriptionID, f.regionID) +} diff --git a/service/privatelink/service.go b/service/privatelink/service.go new file mode 100644 index 0000000..ea7e5e8 --- /dev/null +++ b/service/privatelink/service.go @@ -0,0 +1,92 @@ +package privatelink + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/RedisLabs/rediscloud-go-api/internal" +) + +type HttpClient interface { + Get(ctx context.Context, name, path string, responseBody interface{}) error + GetWithQuery(ctx context.Context, name, path string, query url.Values, responseBody interface{}) error + Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error + Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error + Delete(ctx context.Context, name, path string, responseBody interface{}) error +} + +type TaskWaiter interface { + WaitForResourceId(ctx context.Context, id string) (int, error) + Wait(ctx context.Context, id string) error + WaitForResource(ctx context.Context, id string, resource interface{}) error +} + +type Log interface { + Printf(format string, args ...interface{}) +} + +type API struct { + client HttpClient + taskWaiter TaskWaiter + logger Log +} + +func NewAPI(client HttpClient, taskWaiter TaskWaiter, logger Log) *API { + return &API{client: client, taskWaiter: taskWaiter, logger: logger} +} + +func (a *API) GetPrivateLink(ctx context.Context, subscription int) (*PrivateLinkConfig, error) { + message := fmt.Sprintf("get private link for subscription %d", subscription) + path := fmt.Sprintf("/subscriptions/%d/private-link", subscription) + task, err := a.getLink(ctx, message, path) + if err != nil { + return nil, wrap404Error(subscription, err) + } + return task, nil +} + +func (a *API) getLink(ctx context.Context, message string, path string) (*PrivateLinkConfig, error) { + var task internal.TaskResponse + err := a.client.Get(ctx, message, path, &task) + if err != nil { + return nil, err + } + + a.logger.Printf("Waiting for privatelink request %d to complete", task.ID) + + var response PrivateLinkConfig + err = a.taskWaiter.WaitForResource(ctx, *task.ID, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func wrap404Error(subId int, err error) error { + var e *internal.HTTPError + if errors.As(err, &e) && e.StatusCode == http.StatusNotFound { + return &NotFound{subscriptionID: subId} + } + var v *internal.Error + if errors.As(err, &v) && v.StatusCode() == strconv.Itoa(http.StatusNotFound) { + return &NotFound{subscriptionID: subId} + } + return err +} + +func wrap404ErrorActiveActive(subId int, regionId int, err error) error { + var e *internal.HTTPError + if errors.As(err, &e) && e.StatusCode == http.StatusNotFound { + return &NotFoundActiveActive{subscriptionID: subId, regionID: regionId} + } + var v *internal.Error + if errors.As(err, &v) && v.StatusCode() == strconv.Itoa(http.StatusNotFound) { + return &NotFoundActiveActive{subscriptionID: subId, regionID: regionId} + } + return err +} From 2e5fa32f3a01353391bdccc2d63f94e0a656ee91 Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Thu, 4 Sep 2025 15:02:03 +0100 Subject: [PATCH 03/17] feat: updating privatelink fields --- privatelink_test.go | 14 ++++--------- service/privatelink/model.go | 37 +++++++++++++++++++++++++++++----- service/privatelink/service.go | 6 +++--- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/privatelink_test.go b/privatelink_test.go index 0a82aaa..a155ff7 100644 --- a/privatelink_test.go +++ b/privatelink_test.go @@ -16,7 +16,7 @@ func TestGetPrivateLinkConfig(t *testing.T) { tc := []struct { description string mockedResponse []endpointRequest - expectedResult *pl.PrivateLinkConfig + expectedResult *pl.PrivateLink expectedError error expectedErrorAs error }{ @@ -53,10 +53,7 @@ func TestGetPrivateLinkConfig(t *testing.T) { "response": { "resourceId": 114019, "resource": { - "id": 40, - "connectionHostName": "pl.mc2018-0.us-central1-mz.gcp.sdk-cloud.rlrcp.com", - "serviceAttachmentName": "service-attachment-mc2018-0-us-central1-mz-rlrcp", - "status": "active" + "subscriptionId": 114019 } }, "links": [ @@ -69,11 +66,8 @@ func TestGetPrivateLinkConfig(t *testing.T) { }`, ), }, - expectedResult: &pl.PrivateLinkConfig{ - ID: redis.Int(40), - ConnectionHostName: redis.String("pl.mc2018-0.us-central1-mz.gcp.sdk-cloud.rlrcp.com"), - ServiceAttachmentName: redis.String("service-attachment-mc2018-0-us-central1-mz-rlrcp"), - Status: redis.String("active"), + expectedResult: &pl.PrivateLink{ + SubscriptionId: redis.Int(114019), }, }, { diff --git a/service/privatelink/model.go b/service/privatelink/model.go index 2b19da6..4ec7c71 100644 --- a/service/privatelink/model.go +++ b/service/privatelink/model.go @@ -2,11 +2,27 @@ package privatelink import "fmt" -type PrivateLinkConfig struct { - ID *int `json:"id,omitempty"` - ConnectionHostName *string `json:"connectionHostName,omitempty"` - ServiceAttachmentName *string `json:"serviceAttachmentName,omitempty"` - Status *string `json:"status,omitempty"` +type CreatePrivateLink struct { + SubscriptionId *int `json:"subscriptionId"` + PrincipalId *int `json:"principal,omitempty"` + PrincipalType *string `json:"type,omitempty"` + PrincipalAlias *string `json:"alias,omitempty"` +} + +type PrivateLink struct { + SubscriptionId *int `json:"subscriptionId"` +} + +type CreatePrivateLinkActiveActive struct { + SubscriptionId *int `json:"subscriptionId"` + PrincipalId *int `json:"principal,omitempty"` + PrincipalType *string `json:"type,omitempty"` + PrincipalAlias *string `json:"alias,omitempty"` +} + +type PrivateLinkActiveActive struct { + SubscriptionId *int `json:"subscriptionId"` + RegionId *string `json:"region_id"` } type NotFound struct { @@ -25,3 +41,14 @@ type NotFoundActiveActive struct { func (f *NotFoundActiveActive) Error() string { return fmt.Sprintf("resource not found - subscription %d and region %d", f.subscriptionID, f.regionID) } + +const ( + // PrivateLinkStatusCreateQueued when PrivateLink creation is queued + PrivateLinkStatusCreateQueued = "create-queued" + // PrivateLinkStatusInitialized when PrivateLink provisioning started + PrivateLinkStatusInitialized = "initialized" + // PrivateLinkStatusCreatePending when PrivateLink provisioning is completed but databases are pending update + PrivateLinkStatusCreatePending = "create-pending" + // PrivateLinkStatusActive when PrivateLink is ready + PrivateLinkStatusActive = "active" +) diff --git a/service/privatelink/service.go b/service/privatelink/service.go index ea7e5e8..5d39a57 100644 --- a/service/privatelink/service.go +++ b/service/privatelink/service.go @@ -39,7 +39,7 @@ func NewAPI(client HttpClient, taskWaiter TaskWaiter, logger Log) *API { return &API{client: client, taskWaiter: taskWaiter, logger: logger} } -func (a *API) GetPrivateLink(ctx context.Context, subscription int) (*PrivateLinkConfig, error) { +func (a *API) GetPrivateLink(ctx context.Context, subscription int) (*PrivateLink, error) { message := fmt.Sprintf("get private link for subscription %d", subscription) path := fmt.Sprintf("/subscriptions/%d/private-link", subscription) task, err := a.getLink(ctx, message, path) @@ -49,7 +49,7 @@ func (a *API) GetPrivateLink(ctx context.Context, subscription int) (*PrivateLin return task, nil } -func (a *API) getLink(ctx context.Context, message string, path string) (*PrivateLinkConfig, error) { +func (a *API) getLink(ctx context.Context, message string, path string) (*PrivateLink, error) { var task internal.TaskResponse err := a.client.Get(ctx, message, path, &task) if err != nil { @@ -58,7 +58,7 @@ func (a *API) getLink(ctx context.Context, message string, path string) (*Privat a.logger.Printf("Waiting for privatelink request %d to complete", task.ID) - var response PrivateLinkConfig + var response PrivateLink err = a.taskWaiter.WaitForResource(ctx, *task.ID, &response) if err != nil { return nil, err From eb8e72e2c0727a5f5197d628b3641bdf77184db1 Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Thu, 4 Sep 2025 17:20:31 +0100 Subject: [PATCH 04/17] feat: updating model for privatelink and corresponding test --- privatelink_test.go | 96 +++++++++++++++++++++++++++++------- service/privatelink/model.go | 43 +++++++++++++--- 2 files changed, 113 insertions(+), 26 deletions(-) diff --git a/privatelink_test.go b/privatelink_test.go index a155ff7..8646e09 100644 --- a/privatelink_test.go +++ b/privatelink_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestGetPrivateLinkConfig(t *testing.T) { +func TestGetPrivateLink(t *testing.T) { tc := []struct { description string mockedResponse []endpointRequest @@ -45,29 +45,87 @@ func TestGetPrivateLinkConfig(t *testing.T) { t, "/tasks/502fc31f-fd44-4cb0-a429-07882309a971", `{ - "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", - "commandType": "privatelinkGetRequest", - "status": "processing-completed", - "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", - "timestamp": "2024-07-16T09:26:49.847808891Z", - "response": { - "resourceId": 114019, - "resource": { - "subscriptionId": 114019 - } - }, - "links": [ - { - "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", - "rel": "self", - "type": "GET" - } - ] + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "privatelinkGetRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2024-07-16T09:26:49.847808891Z", + "response": { + "resourceId": 114019, + "resource": { + "status": "received", + "principals": [ + { + "principal": "arn:aws:iam::123456789012:root", + "status": "ready", + "alias": "some alias", + "type": "aws_account" + } + ], + "resourceConfigurationId": 29291, + "resourceConfigurationArn": "received", + "shareArn": "arn:aws:iam::123456789012:root", + "shareName": "share name", + "connections": [ + { + "associationId": "received", + "connectionId": 144019, + "type": "connection type", + "ownerId": 12312312, + "associationDate": "2024-07-16T09:26:40.929904847Z" + } + ], + "databases": [ + { + "databaseId": 0, + "port": 6379, + "rlEndpoint": "" + } + ], + "subscriptionId": 114019, + "regionId": 12312312, + "errorMessage": "no error" + } + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] }`, ), }, expectedResult: &pl.PrivateLink{ + Status: redis.String("received"), + Principals: []*pl.PrivateLinkPrincipal{ + { + Principal: redis.String("arn:aws:iam::123456789012:root"), + Status: redis.String("ready"), + Alias: redis.String("some alias"), + Type: redis.String("aws_account"), + }, + }, + ResourceConfigurationId: redis.Int(29291), + ResourceConfigurationArn: redis.String("received"), + ShareArn: redis.String("arn:aws:iam::123456789012:root"), + ShareName: redis.String("share name"), + Connections: []*pl.PrivateLinkConnection{{ + AssociationId: redis.String("received"), + ConnectionId: redis.Int(144019), + Type: redis.String("connection type"), + OwnerId: redis.Int(12312312), + AssociationDate: redis.String("2024-07-16T09:26:40.929904847Z"), + }}, + Databases: []*pl.PrivateLinkDatabase{{ + DatabaseId: redis.Int(0), + Port: redis.Int(6379), + ResourceLinkEndpoint: redis.String(""), + }}, SubscriptionId: redis.Int(114019), + RegionId: redis.Int(12312312), + ErrorMessage: redis.String("no error"), }, }, { diff --git a/service/privatelink/model.go b/service/privatelink/model.go index 4ec7c71..8c041d8 100644 --- a/service/privatelink/model.go +++ b/service/privatelink/model.go @@ -10,7 +10,38 @@ type CreatePrivateLink struct { } type PrivateLink struct { - SubscriptionId *int `json:"subscriptionId"` + Status *string `json:"status,omitempty"` + Principals []*PrivateLinkPrincipal `json:"principals,omitempty"` + ResourceConfigurationId *int `json:"resourceConfigurationId,omitempty"` + ResourceConfigurationArn *string `json:"resourceConfigurationArn,omitempty"` + ShareArn *string `json:"shareArn,omitempty"` + ShareName *string `json:"shareName,omitempty"` + Connections []*PrivateLinkConnection `json:"connections,omitempty"` + Databases []*PrivateLinkDatabase `json:"databases,omitempty"` + SubscriptionId *int `json:"subscriptionId,omitempty"` + RegionId *int `json:"regionId,omitempty"` + ErrorMessage *string `json:"errorMessage,omitempty"` +} + +type PrivateLinkPrincipal struct { + Principal *string `json:"principal,omitempty"` + Status *string `json:"status,omitempty"` + Alias *string `json:"alias,omitempty"` + Type *string `json:"type,omitempty"` +} + +type PrivateLinkConnection struct { + AssociationId *string `json:"associationId,omitempty"` + ConnectionId *int `json:"connectionId,omitempty"` + Type *string `json:"type,omitempty"` + OwnerId *int `json:"ownerId,omitempty"` + AssociationDate *string `json:"associationDate,omitempty"` +} + +type PrivateLinkDatabase struct { + DatabaseId *int `json:"databaseId,omitempty"` + Port *int `json:"port,omitempty"` + ResourceLinkEndpoint *string `json:"rlEndpoint,omitempty"` } type CreatePrivateLinkActiveActive struct { @@ -43,12 +74,10 @@ func (f *NotFoundActiveActive) Error() string { } const ( - // PrivateLinkStatusCreateQueued when PrivateLink creation is queued - PrivateLinkStatusCreateQueued = "create-queued" - // PrivateLinkStatusInitialized when PrivateLink provisioning started - PrivateLinkStatusInitialized = "initialized" - // PrivateLinkStatusCreatePending when PrivateLink provisioning is completed but databases are pending update - PrivateLinkStatusCreatePending = "create-pending" + // PrivateLinkStatusInitializing when PrivateLink is initialising + PrivateLinkStatusInitializing = "initializing" + // PrivateLinkStatusDeleted when PrivateLink has been deleted + PrivateLinkStatusDeleted = "deleting" // PrivateLinkStatusActive when PrivateLink is ready PrivateLinkStatusActive = "active" ) From d91debd7e91eaa29a31dc2e69e007ed35341761f Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Thu, 11 Sep 2025 10:57:43 +0100 Subject: [PATCH 05/17] chore: updating DELETE function to support a request body --- internal/http_client.go | 4 +- .../redis_rules/service.go | 4 +- service/access_control_lists/roles/service.go | 4 +- service/access_control_lists/users/service.go | 4 +- service/cloud_accounts/service.go | 4 +- service/databases/service.go | 4 +- service/fixed/databases/service.go | 4 +- service/fixed/subscriptions/service.go | 4 +- service/privatelink/model.go | 8 +- service/privatelink/service.go | 78 +++++++++++++++---- service/psc/service.go | 4 +- service/subscriptions/service.go | 8 +- .../transit_gateway/attachments/service.go | 6 +- 13 files changed, 93 insertions(+), 43 deletions(-) diff --git a/internal/http_client.go b/internal/http_client.go index be441b7..7785837 100644 --- a/internal/http_client.go +++ b/internal/http_client.go @@ -67,8 +67,8 @@ func (c *HttpClient) Post(ctx context.Context, name, path string, requestBody in return c.connectionWithRetries(ctx, http.MethodPost, name, path, nil, requestBody, responseBody) } -func (c *HttpClient) Delete(ctx context.Context, name, path string, responseBody interface{}) error { - return c.connectionWithRetries(ctx, http.MethodDelete, name, path, nil, nil, responseBody) +func (c *HttpClient) Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error { + return c.connectionWithRetries(ctx, http.MethodDelete, name, path, nil, requestBody, responseBody) } func (c *HttpClient) DeleteWithQuery(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error { diff --git a/service/access_control_lists/redis_rules/service.go b/service/access_control_lists/redis_rules/service.go index 1d2823b..74d672e 100644 --- a/service/access_control_lists/redis_rules/service.go +++ b/service/access_control_lists/redis_rules/service.go @@ -16,7 +16,7 @@ type HttpClient interface { Get(ctx context.Context, name, path string, responseBody interface{}) error Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error - Delete(ctx context.Context, name, path string, responseBody interface{}) error + Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error } type TaskWaiter interface { @@ -100,7 +100,7 @@ func (a *API) Update(ctx context.Context, id int, redisRule CreateRedisRuleReque // Delete will destroy an existing redisRule. func (a *API) Delete(ctx context.Context, id int) error { var task internal.TaskResponse - err := a.client.Delete(ctx, fmt.Sprintf("delete redisRule %d", id), fmt.Sprintf("/acl/redisRules/%d", id), &task) + err := a.client.Delete(ctx, fmt.Sprintf("delete redisRule %d", id), fmt.Sprintf("/acl/redisRules/%d", id), nil, &task) if err != nil { return wrap404Error(id, err) } diff --git a/service/access_control_lists/roles/service.go b/service/access_control_lists/roles/service.go index ce0b2f8..86a8b48 100644 --- a/service/access_control_lists/roles/service.go +++ b/service/access_control_lists/roles/service.go @@ -16,7 +16,7 @@ type HttpClient interface { Get(ctx context.Context, name, path string, responseBody interface{}) error Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error - Delete(ctx context.Context, name, path string, responseBody interface{}) error + Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error } type TaskWaiter interface { @@ -100,7 +100,7 @@ func (a *API) Update(ctx context.Context, id int, role CreateRoleRequest) error // Delete will destroy an existing role. func (a *API) Delete(ctx context.Context, id int) error { var task internal.TaskResponse - err := a.client.Delete(ctx, fmt.Sprintf("delete role %d", id), fmt.Sprintf("/acl/roles/%d", id), &task) + err := a.client.Delete(ctx, fmt.Sprintf("delete role %d", id), fmt.Sprintf("/acl/roles/%d", id), nil, &task) if err != nil { return wrap404Error(id, err) } diff --git a/service/access_control_lists/users/service.go b/service/access_control_lists/users/service.go index f0f093d..19fd868 100644 --- a/service/access_control_lists/users/service.go +++ b/service/access_control_lists/users/service.go @@ -16,7 +16,7 @@ type HttpClient interface { Get(ctx context.Context, name, path string, responseBody interface{}) error Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error - Delete(ctx context.Context, name, path string, responseBody interface{}) error + Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error } type TaskWaiter interface { @@ -95,7 +95,7 @@ func (a *API) Update(ctx context.Context, id int, user UpdateUserRequest) error // Delete will destroy an existing user. func (a *API) Delete(ctx context.Context, id int) error { var task internal.TaskResponse - err := a.client.Delete(ctx, fmt.Sprintf("delete user %d", id), fmt.Sprintf("/acl/users/%d", id), &task) + err := a.client.Delete(ctx, fmt.Sprintf("delete user %d", id), fmt.Sprintf("/acl/users/%d", id), nil, &task) if err != nil { return wrap404Error(id, err) } diff --git a/service/cloud_accounts/service.go b/service/cloud_accounts/service.go index 54972c7..06a452f 100644 --- a/service/cloud_accounts/service.go +++ b/service/cloud_accounts/service.go @@ -16,7 +16,7 @@ type HttpClient interface { Get(ctx context.Context, name, path string, responseBody interface{}) error Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error - Delete(ctx context.Context, name, path string, responseBody interface{}) error + Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error } type TaskWaiter interface { @@ -90,7 +90,7 @@ func (a *API) Update(ctx context.Context, id int, account UpdateCloudAccount) er // Delete will delete an existing Cloud Account. func (a *API) Delete(ctx context.Context, id int) error { var response internal.TaskResponse - if err := a.client.Delete(ctx, fmt.Sprintf("delete cloud account %d", id), fmt.Sprintf("/cloud-accounts/%d", id), &response); err != nil { + if err := a.client.Delete(ctx, fmt.Sprintf("delete cloud account %d", id), fmt.Sprintf("/cloud-accounts/%d", id), nil, &response); err != nil { return wrap404Error(id, err) } diff --git a/service/databases/service.go b/service/databases/service.go index 03a2548..9e7acd3 100644 --- a/service/databases/service.go +++ b/service/databases/service.go @@ -20,7 +20,7 @@ type HttpClient interface { GetWithQuery(ctx context.Context, name, path string, query url.Values, responseBody interface{}) error Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error - Delete(ctx context.Context, name, path string, responseBody interface{}) error + Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error } type TaskWaiter interface { @@ -102,7 +102,7 @@ func (a *API) UpgradeRedisVersion(ctx context.Context, subscription int, databas // Delete will destroy an existing database. func (a *API) Delete(ctx context.Context, subscription int, database int) error { var task internal.TaskResponse - err := a.client.Delete(ctx, fmt.Sprintf("delete database %d/%d", subscription, database), fmt.Sprintf("/subscriptions/%d/databases/%d", subscription, database), &task) + err := a.client.Delete(ctx, fmt.Sprintf("delete database %d/%d", subscription, database), fmt.Sprintf("/subscriptions/%d/databases/%d", subscription, database), nil, &task) if err != nil { return err } diff --git a/service/fixed/databases/service.go b/service/fixed/databases/service.go index 72a5e3b..1c4ccaf 100644 --- a/service/fixed/databases/service.go +++ b/service/fixed/databases/service.go @@ -19,7 +19,7 @@ type HttpClient interface { GetWithQuery(ctx context.Context, name, path string, query url.Values, responseBody interface{}) error Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error - Delete(ctx context.Context, name, path string, responseBody interface{}) error + Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error } type TaskWaiter interface { @@ -88,7 +88,7 @@ func (a *API) Update(ctx context.Context, subscription int, database int, update // Delete will destroy an existing fixed database. func (a *API) Delete(ctx context.Context, subscription int, database int) error { var task internal.TaskResponse - err := a.client.Delete(ctx, fmt.Sprintf("delete fixed database %d/%d", subscription, database), fmt.Sprintf("/fixed/subscriptions/%d/databases/%d", subscription, database), &task) + err := a.client.Delete(ctx, fmt.Sprintf("delete fixed database %d/%d", subscription, database), fmt.Sprintf("/fixed/subscriptions/%d/databases/%d", subscription, database), nil, &task) if err != nil { return err } diff --git a/service/fixed/subscriptions/service.go b/service/fixed/subscriptions/service.go index 9a697f4..8bb33ff 100644 --- a/service/fixed/subscriptions/service.go +++ b/service/fixed/subscriptions/service.go @@ -16,7 +16,7 @@ type HttpClient interface { Get(ctx context.Context, name, path string, responseBody interface{}) error Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error - Delete(ctx context.Context, name, path string, responseBody interface{}) error + Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error } type TaskWaiter interface { @@ -96,7 +96,7 @@ func (a *API) Update(ctx context.Context, id int, subscription FixedSubscription // deleted, otherwise this function will fail. func (a *API) Delete(ctx context.Context, id int) error { var task internal.TaskResponse - err := a.client.Delete(ctx, fmt.Sprintf("delete fixed subscription %d", id), fmt.Sprintf("/fixed/subscriptions/%d", id), &task) + err := a.client.Delete(ctx, fmt.Sprintf("delete fixed subscription %d", id), fmt.Sprintf("/fixed/subscriptions/%d", id), nil, &task) if err != nil { return wrap404Error(id, err) } diff --git a/service/privatelink/model.go b/service/privatelink/model.go index 8c041d8..9d187a5 100644 --- a/service/privatelink/model.go +++ b/service/privatelink/model.go @@ -3,8 +3,8 @@ package privatelink import "fmt" type CreatePrivateLink struct { - SubscriptionId *int `json:"subscriptionId"` - PrincipalId *int `json:"principal,omitempty"` + ShareName *string `json:"shareName,omitempty"` + Principal *string `json:"principal,omitempty"` PrincipalType *string `json:"type,omitempty"` PrincipalAlias *string `json:"alias,omitempty"` } @@ -25,9 +25,9 @@ type PrivateLink struct { type PrivateLinkPrincipal struct { Principal *string `json:"principal,omitempty"` - Status *string `json:"status,omitempty"` - Alias *string `json:"alias,omitempty"` Type *string `json:"type,omitempty"` + Alias *string `json:"alias,omitempty"` + Status *string `json:"status,omitempty"` } type PrivateLinkConnection struct { diff --git a/service/privatelink/service.go b/service/privatelink/service.go index 5d39a57..50e1ee8 100644 --- a/service/privatelink/service.go +++ b/service/privatelink/service.go @@ -16,7 +16,7 @@ type HttpClient interface { GetWithQuery(ctx context.Context, name, path string, query url.Values, responseBody interface{}) error Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error - Delete(ctx context.Context, name, path string, responseBody interface{}) error + Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error } type TaskWaiter interface { @@ -39,17 +39,46 @@ func NewAPI(client HttpClient, taskWaiter TaskWaiter, logger Log) *API { return &API{client: client, taskWaiter: taskWaiter, logger: logger} } +// // CreatePrivateLink will create a new PrivateLink. +func (a *API) CreatePrivateLink(ctx context.Context, subscriptionId int, privateLink CreatePrivateLink) error { + message := fmt.Sprintf("create privatelink for subscription %d", subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/private-link", subscriptionId) + err := a.create(ctx, message, path, privateLink) + if err != nil { + return wrap404Error(subscriptionId, err) + } + return nil +} + +func (a *API) create(ctx context.Context, message string, path string, link CreatePrivateLink) error { + var task internal.TaskResponse + err := a.client.Post(ctx, message, path, nil, &task) + if err != nil { + return err + } + + a.logger.Printf("Waiting for task %s to finish creating the PrivateLink", task) + + id, err := a.taskWaiter.WaitForResourceId(ctx, *task.ID) + if err != nil { + return fmt.Errorf("failed when creating PrivateLink %d: %w", id, err) + } + + return nil +} + +// GetPrivateLink will get a new PrivateLink. func (a *API) GetPrivateLink(ctx context.Context, subscription int) (*PrivateLink, error) { message := fmt.Sprintf("get private link for subscription %d", subscription) path := fmt.Sprintf("/subscriptions/%d/private-link", subscription) - task, err := a.getLink(ctx, message, path) + task, err := a.get(ctx, message, path) if err != nil { return nil, wrap404Error(subscription, err) } return task, nil } -func (a *API) getLink(ctx context.Context, message string, path string) (*PrivateLink, error) { +func (a *API) get(ctx context.Context, message string, path string) (*PrivateLink, error) { var task internal.TaskResponse err := a.client.Get(ctx, message, path, &task) if err != nil { @@ -67,26 +96,47 @@ func (a *API) getLink(ctx context.Context, message string, path string) (*Privat return &response, nil } -func wrap404Error(subId int, err error) error { - var e *internal.HTTPError - if errors.As(err, &e) && e.StatusCode == http.StatusNotFound { - return &NotFound{subscriptionID: subId} +// DeletePrincipal will remove a principal from a PrivateLink. +func (a *API) DeletePrincipal(ctx context.Context, subscriptionId int, principal string) error { + message := fmt.Sprintf("delete principal %s for subscription %d", principal, subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/private-link/principals", subscriptionId) + + requestBody := map[string]interface{}{ + "principal": principal, } - var v *internal.Error - if errors.As(err, &v) && v.StatusCode() == strconv.Itoa(http.StatusNotFound) { - return &NotFound{subscriptionID: subId} + + err := a.delete(ctx, message, path, requestBody, nil) + if err != nil { + return wrap404Error(subscriptionId, err) } - return err + return nil } -func wrap404ErrorActiveActive(subId int, regionId int, err error) error { +func (a *API) delete(ctx context.Context, message string, path string, requestBody interface{}, responseBody interface{}) error { + var task internal.TaskResponse + err := a.client.Delete(ctx, message, path, requestBody, &task) + if err != nil { + return err + } + + a.logger.Printf("Waiting for task %s to finish deleting", task) + + err = a.taskWaiter.Wait(ctx, *task.ID) + if err != nil { + return fmt.Errorf("failed when deleting PrivateLink %w", err) + } + + return nil +} + +func wrap404Error(subId int, err error) error { var e *internal.HTTPError if errors.As(err, &e) && e.StatusCode == http.StatusNotFound { - return &NotFoundActiveActive{subscriptionID: subId, regionID: regionId} + return &NotFound{subscriptionID: subId} } var v *internal.Error if errors.As(err, &v) && v.StatusCode() == strconv.Itoa(http.StatusNotFound) { - return &NotFoundActiveActive{subscriptionID: subId, regionID: regionId} + return &NotFound{subscriptionID: subId} } return err } diff --git a/service/psc/service.go b/service/psc/service.go index 53527c1..7e7b9d0 100644 --- a/service/psc/service.go +++ b/service/psc/service.go @@ -16,7 +16,7 @@ type HttpClient interface { GetWithQuery(ctx context.Context, name, path string, query url.Values, responseBody interface{}) error Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error - Delete(ctx context.Context, name, path string, responseBody interface{}) error + Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error } type TaskWaiter interface { @@ -363,7 +363,7 @@ func (a *API) update(ctx context.Context, message string, path string, body any) func (a *API) delete(ctx context.Context, message string, path string) error { var task internal.TaskResponse - err := a.client.Delete(ctx, message, path, &task) + err := a.client.Delete(ctx, message, path, nil, &task) if err != nil { return err } diff --git a/service/subscriptions/service.go b/service/subscriptions/service.go index a5ce00d..4033e73 100644 --- a/service/subscriptions/service.go +++ b/service/subscriptions/service.go @@ -16,7 +16,7 @@ type HttpClient interface { Get(ctx context.Context, name, path string, responseBody interface{}) error Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error - Delete(ctx context.Context, name, path string, responseBody interface{}) error + Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error } type TaskWaiter interface { @@ -115,7 +115,7 @@ func (a *API) UpdateCMKs(ctx context.Context, id int, subscriptionCMKs UpdateSub // deleted, otherwise this function will fail. func (a *API) Delete(ctx context.Context, id int) error { var task internal.TaskResponse - err := a.client.Delete(ctx, fmt.Sprintf("delete subscription %d", id), fmt.Sprintf("/subscriptions/%d", id), &task) + err := a.client.Delete(ctx, fmt.Sprintf("delete subscription %d", id), fmt.Sprintf("/subscriptions/%d", id), nil, &task) if err != nil { return wrap404Error(id, err) } @@ -234,7 +234,7 @@ func (a *API) CreateActiveActiveVPCPeering(ctx context.Context, id int, create C // DeleteVPCPeering destroys an existing VPC peering connection. func (a *API) DeleteVPCPeering(ctx context.Context, subscription int, peering int) error { var task internal.TaskResponse - err := a.client.Delete(ctx, fmt.Sprintf("deleting peering %d for subscription %d", peering, subscription), fmt.Sprintf("/subscriptions/%d/peerings/%d", subscription, peering), &task) + err := a.client.Delete(ctx, fmt.Sprintf("deleting peering %d for subscription %d", peering, subscription), fmt.Sprintf("/subscriptions/%d/peerings/%d", subscription, peering), nil, &task) if err != nil { return err } @@ -246,7 +246,7 @@ func (a *API) DeleteVPCPeering(ctx context.Context, subscription int, peering in func (a *API) DeleteActiveActiveVPCPeering(ctx context.Context, subscription int, peering int) error { var task internal.TaskResponse - err := a.client.Delete(ctx, fmt.Sprintf("deleting peering %d for subscription %d", peering, subscription), fmt.Sprintf("/subscriptions/%d/regions/peerings/%d", subscription, peering), &task) + err := a.client.Delete(ctx, fmt.Sprintf("deleting peering %d for subscription %d", peering, subscription), fmt.Sprintf("/subscriptions/%d/regions/peerings/%d", subscription, peering), nil, &task) if err != nil { return err } diff --git a/service/transit_gateway/attachments/service.go b/service/transit_gateway/attachments/service.go index e05280d..c0bfe60 100644 --- a/service/transit_gateway/attachments/service.go +++ b/service/transit_gateway/attachments/service.go @@ -10,9 +10,9 @@ import ( type HttpClient interface { Get(ctx context.Context, name, path string, responseBody interface{}) error - Post(ctx context.Context, name, path string, requsetBody interface{}, responseBody interface{}) error + Post(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error - Delete(ctx context.Context, name, path string, responseBody interface{}) error + Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error } type TaskWaiter interface { @@ -179,7 +179,7 @@ func (a *API) update(ctx context.Context, message string, address string, cidrs func (a *API) delete(ctx context.Context, message string, address string) error { var task internal.TaskResponse - err := a.client.Delete(ctx, message, address, &task) + err := a.client.Delete(ctx, message, address, nil, &task) if err != nil { return err } From 16893c17ef7246644d54abf85c875187cf026b44 Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Tue, 16 Sep 2025 12:14:53 +0100 Subject: [PATCH 06/17] feat: add create principal endpoint --- client.go | 7 +++---- service/databases/service_test.go | 2 +- service/privatelink/model.go | 8 +++++++- service/privatelink/service.go | 16 ++++++++++++++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/client.go b/client.go index b0fae90..6d9fc1e 100644 --- a/client.go +++ b/client.go @@ -3,7 +3,6 @@ package rediscloud_api import ( "bytes" "encoding/json" - "github.com/RedisLabs/rediscloud-go-api/service/privatelink" "log" "net/http" "net/http/httputil" @@ -11,12 +10,10 @@ import ( "regexp" "strings" + "github.com/RedisLabs/rediscloud-go-api/internal" "github.com/RedisLabs/rediscloud-go-api/service/access_control_lists/redis_rules" "github.com/RedisLabs/rediscloud-go-api/service/access_control_lists/roles" "github.com/RedisLabs/rediscloud-go-api/service/access_control_lists/users" - "github.com/RedisLabs/rediscloud-go-api/service/psc" - - "github.com/RedisLabs/rediscloud-go-api/internal" "github.com/RedisLabs/rediscloud-go-api/service/account" "github.com/RedisLabs/rediscloud-go-api/service/cloud_accounts" "github.com/RedisLabs/rediscloud-go-api/service/databases" @@ -28,6 +25,8 @@ import ( "github.com/RedisLabs/rediscloud-go-api/service/latest_imports" "github.com/RedisLabs/rediscloud-go-api/service/maintenance" "github.com/RedisLabs/rediscloud-go-api/service/pricing" + "github.com/RedisLabs/rediscloud-go-api/service/privatelink" + "github.com/RedisLabs/rediscloud-go-api/service/psc" "github.com/RedisLabs/rediscloud-go-api/service/regions" "github.com/RedisLabs/rediscloud-go-api/service/subscriptions" "github.com/RedisLabs/rediscloud-go-api/service/tags" diff --git a/service/databases/service_test.go b/service/databases/service_test.go index baa5b5d..6a7d08a 100644 --- a/service/databases/service_test.go +++ b/service/databases/service_test.go @@ -142,7 +142,7 @@ func (m *mockHttpClient) Put(ctx context.Context, name, path string, requestBody return args.Error(0) } -func (m *mockHttpClient) Delete(ctx context.Context, name, path string, responseBody interface{}) error { +func (m *mockHttpClient) Delete(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error { args := m.Called(ctx, name, path, responseBody) return args.Error(0) } diff --git a/service/privatelink/model.go b/service/privatelink/model.go index 9d187a5..2d55246 100644 --- a/service/privatelink/model.go +++ b/service/privatelink/model.go @@ -12,7 +12,7 @@ type CreatePrivateLink struct { type PrivateLink struct { Status *string `json:"status,omitempty"` Principals []*PrivateLinkPrincipal `json:"principals,omitempty"` - ResourceConfigurationId *int `json:"resourceConfigurationId,omitempty"` + ResourceConfigurationId *string `json:"resourceConfigurationId,omitempty"` ResourceConfigurationArn *string `json:"resourceConfigurationArn,omitempty"` ShareArn *string `json:"shareArn,omitempty"` ShareName *string `json:"shareName,omitempty"` @@ -44,6 +44,12 @@ type PrivateLinkDatabase struct { ResourceLinkEndpoint *string `json:"rlEndpoint,omitempty"` } +type CreatePrivateLinkPrincipal struct { + Principal *string `json:"principal,omitempty"` + PrincipalType *string `json:"type,omitempty"` + PrincipalAlias *string `json:"alias,omitempty"` +} + type CreatePrivateLinkActiveActive struct { SubscriptionId *int `json:"subscriptionId"` PrincipalId *int `json:"principal,omitempty"` diff --git a/service/privatelink/service.go b/service/privatelink/service.go index 50e1ee8..9806166 100644 --- a/service/privatelink/service.go +++ b/service/privatelink/service.go @@ -50,9 +50,9 @@ func (a *API) CreatePrivateLink(ctx context.Context, subscriptionId int, private return nil } -func (a *API) create(ctx context.Context, message string, path string, link CreatePrivateLink) error { +func (a *API) create(ctx context.Context, message string, path string, requestBody interface{}) error { var task internal.TaskResponse - err := a.client.Post(ctx, message, path, nil, &task) + err := a.client.Post(ctx, message, path, requestBody, &task) if err != nil { return err } @@ -96,6 +96,18 @@ func (a *API) get(ctx context.Context, message string, path string) (*PrivateLin return &response, nil } +// CreatePrincipal will add a principal to a PrivateLink. +func (a *API) CreatePrincipal(ctx context.Context, subscriptionId int, principal CreatePrivateLinkPrincipal) error { + message := fmt.Sprintf("create principal %s for subscription %d", *principal.Principal, subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/private-link/principals", subscriptionId) + + err := a.create(ctx, message, path, principal) + if err != nil { + return wrap404Error(subscriptionId, err) + } + return nil +} + // DeletePrincipal will remove a principal from a PrivateLink. func (a *API) DeletePrincipal(ctx context.Context, subscriptionId int, principal string) error { message := fmt.Sprintf("delete principal %s for subscription %d", principal, subscriptionId) From 2ca42673549dc4c816f3c76c25fda7b2e0b5519a Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Tue, 16 Sep 2025 12:38:39 +0100 Subject: [PATCH 07/17] feat: adding active active endpoints --- service/privatelink/service.go | 114 ++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 32 deletions(-) diff --git a/service/privatelink/service.go b/service/privatelink/service.go index 9806166..dafef06 100644 --- a/service/privatelink/service.go +++ b/service/privatelink/service.go @@ -50,23 +50,6 @@ func (a *API) CreatePrivateLink(ctx context.Context, subscriptionId int, private return nil } -func (a *API) create(ctx context.Context, message string, path string, requestBody interface{}) error { - var task internal.TaskResponse - err := a.client.Post(ctx, message, path, requestBody, &task) - if err != nil { - return err - } - - a.logger.Printf("Waiting for task %s to finish creating the PrivateLink", task) - - id, err := a.taskWaiter.WaitForResourceId(ctx, *task.ID) - if err != nil { - return fmt.Errorf("failed when creating PrivateLink %d: %w", id, err) - } - - return nil -} - // GetPrivateLink will get a new PrivateLink. func (a *API) GetPrivateLink(ctx context.Context, subscription int) (*PrivateLink, error) { message := fmt.Sprintf("get private link for subscription %d", subscription) @@ -78,28 +61,60 @@ func (a *API) GetPrivateLink(ctx context.Context, subscription int) (*PrivateLin return task, nil } -func (a *API) get(ctx context.Context, message string, path string) (*PrivateLink, error) { - var task internal.TaskResponse - err := a.client.Get(ctx, message, path, &task) +// CreatePrincipal will add a principal to a PrivateLink. +func (a *API) CreatePrincipal(ctx context.Context, subscriptionId int, principal CreatePrivateLinkPrincipal) error { + message := fmt.Sprintf("create principal %s for subscription %d", *principal.Principal, subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/private-link/principals", subscriptionId) + + err := a.create(ctx, message, path, principal) if err != nil { - return nil, err + return wrap404Error(subscriptionId, err) } + return nil +} - a.logger.Printf("Waiting for privatelink request %d to complete", task.ID) +// DeletePrincipal will remove a principal from a PrivateLink. +func (a *API) DeletePrincipal(ctx context.Context, subscriptionId int, principal string) error { + message := fmt.Sprintf("delete principal %s for subscription %d", principal, subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/private-link/principals", subscriptionId) - var response PrivateLink - err = a.taskWaiter.WaitForResource(ctx, *task.ID, &response) + requestBody := map[string]interface{}{ + "principal": principal, + } + + err := a.delete(ctx, message, path, requestBody, nil) if err != nil { - return nil, err + return wrap404Error(subscriptionId, err) } + return nil +} - return &response, nil +// CreateActiveActivePrivateLink will create a new active active PrivateLink. +func (a *API) CreateActiveActivePrivateLink(ctx context.Context, subscriptionId int, privateLink CreatePrivateLink) error { + message := fmt.Sprintf("create privatelink for subscription %d", subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link", subscriptionId) + err := a.create(ctx, message, path, privateLink) + if err != nil { + return wrap404Error(subscriptionId, err) + } + return nil } -// CreatePrincipal will add a principal to a PrivateLink. -func (a *API) CreatePrincipal(ctx context.Context, subscriptionId int, principal CreatePrivateLinkPrincipal) error { +// GetActiveActivePrivateLink will get a new active active PrivateLink. +func (a *API) GetActiveActivePrivateLink(ctx context.Context, subscription int) (*PrivateLink, error) { + message := fmt.Sprintf("get private link for subscription %d", subscription) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link", subscription) + task, err := a.get(ctx, message, path) + if err != nil { + return nil, wrap404Error(subscription, err) + } + return task, nil +} + +// CreateActiveActivePrincipal will add a principal to an active active PrivateLink. +func (a *API) CreateActiveActivePrincipal(ctx context.Context, subscriptionId int, principal CreatePrivateLinkPrincipal) error { message := fmt.Sprintf("create principal %s for subscription %d", *principal.Principal, subscriptionId) - path := fmt.Sprintf("/subscriptions/%d/private-link/principals", subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link/principals", subscriptionId) err := a.create(ctx, message, path, principal) if err != nil { @@ -108,10 +123,10 @@ func (a *API) CreatePrincipal(ctx context.Context, subscriptionId int, principal return nil } -// DeletePrincipal will remove a principal from a PrivateLink. -func (a *API) DeletePrincipal(ctx context.Context, subscriptionId int, principal string) error { +// DeleteActiveActivePrincipal will remove a principal from an active active PrivateLink. +func (a *API) DeleteActiveActivePrincipal(ctx context.Context, subscriptionId int, principal string) error { message := fmt.Sprintf("delete principal %s for subscription %d", principal, subscriptionId) - path := fmt.Sprintf("/subscriptions/%d/private-link/principals", subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link/principals", subscriptionId) requestBody := map[string]interface{}{ "principal": principal, @@ -124,6 +139,41 @@ func (a *API) DeletePrincipal(ctx context.Context, subscriptionId int, principal return nil } +func (a *API) create(ctx context.Context, message string, path string, requestBody interface{}) error { + var task internal.TaskResponse + err := a.client.Post(ctx, message, path, requestBody, &task) + if err != nil { + return err + } + + a.logger.Printf("Waiting for task %s to finish creating the PrivateLink", task) + + id, err := a.taskWaiter.WaitForResourceId(ctx, *task.ID) + if err != nil { + return fmt.Errorf("failed when creating PrivateLink %d: %w", id, err) + } + + return nil +} + +func (a *API) get(ctx context.Context, message string, path string) (*PrivateLink, error) { + var task internal.TaskResponse + err := a.client.Get(ctx, message, path, &task) + if err != nil { + return nil, err + } + + a.logger.Printf("Waiting for privatelink request %d to complete", task.ID) + + var response PrivateLink + err = a.taskWaiter.WaitForResource(ctx, *task.ID, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + func (a *API) delete(ctx context.Context, message string, path string, requestBody interface{}, responseBody interface{}) error { var task internal.TaskResponse err := a.client.Delete(ctx, message, path, requestBody, &task) From 3884416cf46853d3dba37a49927296826cb74aad Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Wed, 17 Sep 2025 20:46:22 +0100 Subject: [PATCH 08/17] feat: adding new statuses for private link principals --- service/privatelink/model.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/service/privatelink/model.go b/service/privatelink/model.go index 2d55246..217cb59 100644 --- a/service/privatelink/model.go +++ b/service/privatelink/model.go @@ -12,7 +12,7 @@ type CreatePrivateLink struct { type PrivateLink struct { Status *string `json:"status,omitempty"` Principals []*PrivateLinkPrincipal `json:"principals,omitempty"` - ResourceConfigurationId *string `json:"resourceConfigurationId,omitempty"` + ResourceConfigurationId *string `json:"resourceConfigurationId,omitempty"` ResourceConfigurationArn *string `json:"resourceConfigurationArn,omitempty"` ShareArn *string `json:"shareArn,omitempty"` ShareName *string `json:"shareName,omitempty"` @@ -86,4 +86,20 @@ const ( PrivateLinkStatusDeleted = "deleting" // PrivateLinkStatusActive when PrivateLink is ready PrivateLinkStatusActive = "active" + + // PrivateLinkPrincipalStatusInitializing when PrivateLinkPrincipal is initializing + PrivateLinkPrincipalStatusInitializing = "initializing" + + // PrivateLinkPrincipalStatusDisassociating when PrivateLinkPrincipal is disassociating + PrivateLinkPrincipalStatusDisassociating = "disassociating" + // PrivateLinkPrincipalStatusDisassociated when PrivateLinkPrincipal has disassociated + PrivateLinkPrincipalStatusDisassociated = "disassociated" + + // PrivateLinkPrincipalStatusAssociating when PrivateLinkPrincipal is associating + PrivateLinkPrincipalStatusAssociating = "associating" + // PrivateLinkPrincipalStatusAssociated when PrivateLinkPrincipal has associated + PrivateLinkPrincipalStatusAssociated = "associated" + + // PrivateLinkPrincipalStatusFailed when PrivateLinkPrincipal has failed + PrivateLinkPrincipalStatusFailed = "failed" ) From 8e04bbc74a023073cfb7b1f1c9f5a866c4bf655e Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Wed, 17 Sep 2025 20:50:52 +0100 Subject: [PATCH 09/17] test: fixing tests --- privatelink_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/privatelink_test.go b/privatelink_test.go index 8646e09..88215b2 100644 --- a/privatelink_test.go +++ b/privatelink_test.go @@ -62,8 +62,8 @@ func TestGetPrivateLink(t *testing.T) { "type": "aws_account" } ], - "resourceConfigurationId": 29291, - "resourceConfigurationArn": "received", + "resourceConfigurationId": "123456789012", + "resourceConfigurationArn": "arn:aws:iam::123456789012:root", "shareArn": "arn:aws:iam::123456789012:root", "shareName": "share name", "connections": [ @@ -107,8 +107,8 @@ func TestGetPrivateLink(t *testing.T) { Type: redis.String("aws_account"), }, }, - ResourceConfigurationId: redis.Int(29291), - ResourceConfigurationArn: redis.String("received"), + ResourceConfigurationId: redis.String("123456789012"), + ResourceConfigurationArn: redis.String("arn:aws:iam::123456789012:root"), ShareArn: redis.String("arn:aws:iam::123456789012:root"), ShareName: redis.String("share name"), Connections: []*pl.PrivateLinkConnection{{ From 59ac975559562eeb5c9408cc831a6ea5cea09d25 Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Wed, 17 Sep 2025 21:04:44 +0100 Subject: [PATCH 10/17] feat: adding get privatelink endpoint script and other fixes --- service/privatelink/service.go | 51 +++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/service/privatelink/service.go b/service/privatelink/service.go index dafef06..b5081be 100644 --- a/service/privatelink/service.go +++ b/service/privatelink/service.go @@ -61,6 +61,19 @@ func (a *API) GetPrivateLink(ctx context.Context, subscription int) (*PrivateLin return task, nil } +type PrivateLinkEndpointScript = string + +// GetPrivateLinkEndpointScript will get the script for an endpoint. +func (a *API) GetPrivateLinkEndpointScript(ctx context.Context, subscription int) (*PrivateLinkEndpointScript, error) { + message := fmt.Sprintf("get private link for subscription %d", subscription) + path := fmt.Sprintf("/subscriptions/%d/private-link/endpoint-script/?includeTerraformAwsScript=true", subscription) + task, err := a.getScript(ctx, message, path) + if err != nil { + return nil, wrap404Error(subscription, err) + } + return task, nil +} + // CreatePrincipal will add a principal to a PrivateLink. func (a *API) CreatePrincipal(ctx context.Context, subscriptionId int, principal CreatePrivateLinkPrincipal) error { message := fmt.Sprintf("create principal %s for subscription %d", *principal.Principal, subscriptionId) @@ -90,9 +103,9 @@ func (a *API) DeletePrincipal(ctx context.Context, subscriptionId int, principal } // CreateActiveActivePrivateLink will create a new active active PrivateLink. -func (a *API) CreateActiveActivePrivateLink(ctx context.Context, subscriptionId int, privateLink CreatePrivateLink) error { - message := fmt.Sprintf("create privatelink for subscription %d", subscriptionId) - path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link", subscriptionId) +func (a *API) CreateActiveActivePrivateLink(ctx context.Context, subscriptionId int, regionId int, privateLink CreatePrivateLink) error { + message := fmt.Sprintf("create active active PrivateLink for subscription %d", subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link", subscriptionId, regionId) err := a.create(ctx, message, path, privateLink) if err != nil { return wrap404Error(subscriptionId, err) @@ -101,9 +114,9 @@ func (a *API) CreateActiveActivePrivateLink(ctx context.Context, subscriptionId } // GetActiveActivePrivateLink will get a new active active PrivateLink. -func (a *API) GetActiveActivePrivateLink(ctx context.Context, subscription int) (*PrivateLink, error) { - message := fmt.Sprintf("get private link for subscription %d", subscription) - path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link", subscription) +func (a *API) GetActiveActivePrivateLink(ctx context.Context, subscription int, regionId int) (*PrivateLink, error) { + message := fmt.Sprintf("get active active PrivateLink for subscription %d", subscription) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link", subscription, regionId) task, err := a.get(ctx, message, path) if err != nil { return nil, wrap404Error(subscription, err) @@ -112,9 +125,9 @@ func (a *API) GetActiveActivePrivateLink(ctx context.Context, subscription int) } // CreateActiveActivePrincipal will add a principal to an active active PrivateLink. -func (a *API) CreateActiveActivePrincipal(ctx context.Context, subscriptionId int, principal CreatePrivateLinkPrincipal) error { +func (a *API) CreateActiveActivePrincipal(ctx context.Context, subscriptionId int, regionId int, principal CreatePrivateLinkPrincipal) error { message := fmt.Sprintf("create principal %s for subscription %d", *principal.Principal, subscriptionId) - path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link/principals", subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link/principals", subscriptionId, regionId) err := a.create(ctx, message, path, principal) if err != nil { @@ -124,9 +137,9 @@ func (a *API) CreateActiveActivePrincipal(ctx context.Context, subscriptionId in } // DeleteActiveActivePrincipal will remove a principal from an active active PrivateLink. -func (a *API) DeleteActiveActivePrincipal(ctx context.Context, subscriptionId int, principal string) error { +func (a *API) DeleteActiveActivePrincipal(ctx context.Context, subscriptionId int, regionId int, principal string) error { message := fmt.Sprintf("delete principal %s for subscription %d", principal, subscriptionId) - path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link/principals", subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link/principals", subscriptionId, regionId) requestBody := map[string]interface{}{ "principal": principal, @@ -174,6 +187,24 @@ func (a *API) get(ctx context.Context, message string, path string) (*PrivateLin return &response, nil } +func (a *API) getScript(ctx context.Context, message string, path string) (*PrivateLinkEndpointScript, error) { + var task internal.TaskResponse + err := a.client.Get(ctx, message, path, &task) + if err != nil { + return nil, err + } + + a.logger.Printf("Waiting for privatelink request %d to complete", task.ID) + + var response PrivateLinkEndpointScript + err = a.taskWaiter.WaitForResource(ctx, *task.ID, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + func (a *API) delete(ctx context.Context, message string, path string, requestBody interface{}, responseBody interface{}) error { var task internal.TaskResponse err := a.client.Delete(ctx, message, path, requestBody, &task) From ddcfe64508ed960db68cfe404d6eb905838843ac Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Wed, 17 Sep 2025 21:08:37 +0100 Subject: [PATCH 11/17] feat: adding endpoints for script get endpoints --- service/privatelink/service.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/service/privatelink/service.go b/service/privatelink/service.go index b5081be..9e870d7 100644 --- a/service/privatelink/service.go +++ b/service/privatelink/service.go @@ -124,6 +124,17 @@ func (a *API) GetActiveActivePrivateLink(ctx context.Context, subscription int, return task, nil } +// GetPrivateLinkEndpointScript will get the script for an endpoint. +func (a *API) GetActiveActivePrivateLinkEndpointScript(ctx context.Context, subscription int, regionId int) (*PrivateLinkEndpointScript, error) { + message := fmt.Sprintf("get private link for subscription %d", subscription) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link/endpoint-script/?includeTerraformAwsScript=true", subscription) + task, err := a.getScript(ctx, message, path) + if err != nil { + return nil, wrap404Error(subscription, err) + } + return task, nil +} + // CreateActiveActivePrincipal will add a principal to an active active PrivateLink. func (a *API) CreateActiveActivePrincipal(ctx context.Context, subscriptionId int, regionId int, principal CreatePrivateLinkPrincipal) error { message := fmt.Sprintf("create principal %s for subscription %d", *principal.Principal, subscriptionId) @@ -176,7 +187,7 @@ func (a *API) get(ctx context.Context, message string, path string) (*PrivateLin return nil, err } - a.logger.Printf("Waiting for privatelink request %d to complete", task.ID) + a.logger.Printf("Waiting for PrivateLink get request %d to complete", task.ID) var response PrivateLink err = a.taskWaiter.WaitForResource(ctx, *task.ID, &response) @@ -194,7 +205,7 @@ func (a *API) getScript(ctx context.Context, message string, path string) (*Priv return nil, err } - a.logger.Printf("Waiting for privatelink request %d to complete", task.ID) + a.logger.Printf("Waiting for PrivateLink script get request %d to complete", task.ID) var response PrivateLinkEndpointScript err = a.taskWaiter.WaitForResource(ctx, *task.ID, &response) @@ -212,7 +223,7 @@ func (a *API) delete(ctx context.Context, message string, path string, requestBo return err } - a.logger.Printf("Waiting for task %s to finish deleting", task) + a.logger.Printf("Waiting for task %s to finish deleting the PrivateLink", task) err = a.taskWaiter.Wait(ctx, *task.ID) if err != nil { From 58003be5cd45e6cbb95a7fe2c55f5046c0900e1e Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Thu, 18 Sep 2025 14:29:27 +0100 Subject: [PATCH 12/17] chore: rename subscriptionid --- service/privatelink/service.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/service/privatelink/service.go b/service/privatelink/service.go index 9e870d7..c9c055f 100644 --- a/service/privatelink/service.go +++ b/service/privatelink/service.go @@ -64,12 +64,12 @@ func (a *API) GetPrivateLink(ctx context.Context, subscription int) (*PrivateLin type PrivateLinkEndpointScript = string // GetPrivateLinkEndpointScript will get the script for an endpoint. -func (a *API) GetPrivateLinkEndpointScript(ctx context.Context, subscription int) (*PrivateLinkEndpointScript, error) { - message := fmt.Sprintf("get private link for subscription %d", subscription) - path := fmt.Sprintf("/subscriptions/%d/private-link/endpoint-script/?includeTerraformAwsScript=true", subscription) +func (a *API) GetPrivateLinkEndpointScript(ctx context.Context, subscriptionId int) (*PrivateLinkEndpointScript, error) { + message := fmt.Sprintf("get private link for subscription %d", subscriptionId) + path := fmt.Sprintf("/subscriptions/%d/private-link/endpoint-script/?includeTerraformAwsScript=true", subscriptionId) task, err := a.getScript(ctx, message, path) if err != nil { - return nil, wrap404Error(subscription, err) + return nil, wrap404Error(subscriptionId, err) } return task, nil } From c6edcbe32861400d44d08e6ddf948c6cd4cd57a9 Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Thu, 18 Sep 2025 14:42:49 +0100 Subject: [PATCH 13/17] fix: missing regionId in endpoint --- service/privatelink/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/privatelink/service.go b/service/privatelink/service.go index c9c055f..c857167 100644 --- a/service/privatelink/service.go +++ b/service/privatelink/service.go @@ -127,7 +127,7 @@ func (a *API) GetActiveActivePrivateLink(ctx context.Context, subscription int, // GetPrivateLinkEndpointScript will get the script for an endpoint. func (a *API) GetActiveActivePrivateLinkEndpointScript(ctx context.Context, subscription int, regionId int) (*PrivateLinkEndpointScript, error) { message := fmt.Sprintf("get private link for subscription %d", subscription) - path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link/endpoint-script/?includeTerraformAwsScript=true", subscription) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-link/endpoint-script/?includeTerraformAwsScript=true", subscription, regionId) task, err := a.getScript(ctx, message, path) if err != nil { return nil, wrap404Error(subscription, err) From 0dfbb1cf04c169d747cea4d5d9ed4d6a06580d84 Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Thu, 18 Sep 2025 14:47:19 +0100 Subject: [PATCH 14/17] docs: changelog.md --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db8237..4c3832e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/). +## 0.36.0 + +# Added: +* Adding model and service for new PrivateLink endpoints + +# Changed: +* Modified `delete` in API client so that it takes a `requestBody` parameter. +* Updating Testify to v1.11.1 + ## 0.35.0 ### Added: From 6aba8b67d59c6ca1dc3a4b8c15e5b2f90e0d1c82 Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Thu, 18 Sep 2025 15:18:45 +0100 Subject: [PATCH 15/17] chore: update error message --- service/privatelink/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/privatelink/model.go b/service/privatelink/model.go index 217cb59..05ebaf0 100644 --- a/service/privatelink/model.go +++ b/service/privatelink/model.go @@ -67,7 +67,7 @@ type NotFound struct { } func (f *NotFound) Error() string { - return fmt.Sprintf("resource not found - subscription %d", f.subscriptionID) + return fmt.Sprintf("privatelink resource not found - subscription %d", f.subscriptionID) } type NotFoundActiveActive struct { From cc81b95fb92e8862b510a754b3f89c3d041416f2 Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Thu, 18 Sep 2025 16:11:48 +0100 Subject: [PATCH 16/17] test: adding active active privatelink test --- privatelink_test.go | 279 ++++++++++++++++++++++++++++++++- service/privatelink/model.go | 2 +- service/privatelink/service.go | 6 +- 3 files changed, 279 insertions(+), 8 deletions(-) diff --git a/privatelink_test.go b/privatelink_test.go index 88215b2..34229c9 100644 --- a/privatelink_test.go +++ b/privatelink_test.go @@ -83,7 +83,6 @@ func TestGetPrivateLink(t *testing.T) { } ], "subscriptionId": 114019, - "regionId": 12312312, "errorMessage": "no error" } }, @@ -124,7 +123,6 @@ func TestGetPrivateLink(t *testing.T) { ResourceLinkEndpoint: redis.String(""), }}, SubscriptionId: redis.Int(114019), - RegionId: redis.Int(12312312), ErrorMessage: redis.String("no error"), }, }, @@ -175,7 +173,7 @@ func TestGetPrivateLink(t *testing.T) { }`, ), }, - expectedError: errors.New("resource not found - subscription 114019"), + expectedError: errors.New("privatelink resource not found - subscription 114019"), expectedErrorAs: &pl.NotFound{}, }, { @@ -192,7 +190,7 @@ func TestGetPrivateLink(t *testing.T) { "path" : "/v1/subscriptions/114019/private-link" }`), }, - expectedError: errors.New("resource not found - subscription 114019"), + expectedError: errors.New("privatelink resource not found - subscription 114019"), expectedErrorAs: &pl.NotFound{}, }, } @@ -216,3 +214,276 @@ func TestGetPrivateLink(t *testing.T) { }) } } + +func TestGetActiveActivePrivateLink(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedResult *pl.PrivateLink + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully return an active active privatelink config", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/regions/1/private-link", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePrivateLinkGetRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2024-07-16T09:26:40.929904847Z", + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "task", + "type": "GET" + } + ] + }`, + ), + getRequest( + t, + "/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePrivateLinkGetRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2024-07-16T09:26:49.847808891Z", + "response": { + "resourceId": 114019, + "resource": { + "status": "received", + "principals": [ + { + "principal": "arn:aws:iam::123456789012:root", + "status": "ready", + "alias": "some alias", + "type": "aws_account" + } + ], + "resourceConfigurationId": "123456789012", + "resourceConfigurationArn": "arn:aws:iam::123456789012:root", + "shareArn": "arn:aws:iam::123456789012:root", + "shareName": "share name", + "connections": [ + { + "associationId": "received", + "connectionId": 144019, + "type": "connection type", + "ownerId": 12312312, + "associationDate": "2024-07-16T09:26:40.929904847Z" + } + ], + "databases": [ + { + "databaseId": 0, + "port": 6379, + "rlEndpoint": "" + } + ], + "subscriptionId": 114019, + "regionId": 1, + "errorMessage": "no error" + } + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + }, + expectedResult: &pl.PrivateLink{ + Status: redis.String("received"), + Principals: []*pl.PrivateLinkPrincipal{ + { + Principal: redis.String("arn:aws:iam::123456789012:root"), + Status: redis.String("ready"), + Alias: redis.String("some alias"), + Type: redis.String("aws_account"), + }, + }, + ResourceConfigurationId: redis.String("123456789012"), + ResourceConfigurationArn: redis.String("arn:aws:iam::123456789012:root"), + ShareArn: redis.String("arn:aws:iam::123456789012:root"), + ShareName: redis.String("share name"), + Connections: []*pl.PrivateLinkConnection{{ + AssociationId: redis.String("received"), + ConnectionId: redis.Int(144019), + Type: redis.String("connection type"), + OwnerId: redis.Int(12312312), + AssociationDate: redis.String("2024-07-16T09:26:40.929904847Z"), + }}, + Databases: []*pl.PrivateLinkDatabase{{ + DatabaseId: redis.Int(0), + Port: redis.Int(6379), + ResourceLinkEndpoint: redis.String(""), + }}, + SubscriptionId: redis.Int(114019), + RegionId: redis.Int(1), + ErrorMessage: redis.String("no error"), + }, + }, + { + description: "should fail when private link is not found", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/regions/1/private-link", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePrivateLinkGetRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2024-07-16T09:26:40.929904847Z", + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "task", + "type": "GET" + } + ] + }`, + ), + getRequest( + t, + "/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePrivateLinkGetRequest", + "status": "processing-error", + "description": "Task request failed during processing. See error information for failure details.", + "timestamp": "2025-01-13T11:22:51.204189721Z", + "response": { + "error": { + "type": "PRIVATELINK_SERVICE_NOT_FOUND", + "status": "404 NOT_FOUND", + "description": "Private Service Connect service not found" + } + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + }, + expectedError: errors.New("privatelink resource not found - subscription 114019"), + expectedErrorAs: &pl.NotFound{}, + }, + { + description: "should fail when subscription is not found", + mockedResponse: []endpointRequest{ + getRequestWithStatus( + t, + "/subscriptions/114019/regions/1/private-link", + 404, + `{ + "timestamp" : "2025-01-17T09:34:25.803+00:00", + "status" : 404, + "error" : "Not Found", + "path" : "/v1/subscriptions/114019/regions/1/private-link" + }`), + }, + expectedError: errors.New("privatelink resource not found - subscription 114019"), + expectedErrorAs: &pl.NotFound{}, + }, + } + + for _, testCase := range tc { + t.Run(testCase.description, func(t *testing.T) { + server := httptest.NewServer( + testServer("key", "secret", testCase.mockedResponse...)) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + actual, err := subject.PrivateLink.GetActiveActivePrivateLink(context.TODO(), 114019, 1) + if testCase.expectedError == nil { + assert.NoError(t, err) + assert.Equal(t, testCase.expectedResult, actual) + } else { + assert.IsType(t, err, testCase.expectedErrorAs) + assert.EqualError(t, err, testCase.expectedError.Error()) + } + }) + } +} + +//func TestCreatePrivateLink(t *testing.T) { +// expected := 114019 +// server := httptest.NewServer( +// testServer( +// "key", +// "secret", +// postRequest( +// t, +// "/subscriptions/114019/private-link", +// `{ +// "alias": "test", +// "principal": "123456789012" +// "shareName": "testshare" +// "type": "aws-account" +// }`, +// `{ +// "taskId": "abcd-efgh-ijkl-mnop", +// "commandType": "privateLinkCreateRequest", +// "status": "received", +// "description": "Task request received and is being queued for processing.", +// "timestamp": "2025-09-18T15:56:00Z", +// "links": [ +// { +// "rel": "task", +// "href": "https://api-staging.qa.redislabs.com/v1/tasks/abcd-efgh-ijkl-mnop", +// "title": "getTaskStatusUpdates", +// "type": "GET" +// } +// ] +// }`, +// ), +// getRequest( +// t, +// "/tasks/abcd-efgh-ijkl-mnop", +// fmt.Sprintf(`{ +// "taskId": "abcd-efgh-ijkl-mnop", +// "commandType": "privateLinkCreateRequest", +// "status": "processing-completed", +// "description": "Request processing completed successfully.", +// "timestamp": "2025-09-18T15:56:10Z", +// "response": { +// "resourceId": %[1]d +// }, +// "links": [ +// { +// "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", +// "rel": "self", +// "type": "GET" +// } +// ] +// }`, expected), +// ), +// ), +// ) +// +// subject, err := clientFromTestServer(server, "key", "secret") +// require.NoError(t, err) +// +// actual, err := subject.PrivateLink.CreatePrivateLink(context.TODO(), 114019, pl.CreatePrivateLink{ +// ShareName: redis.String("testshare"), +// Principal: redis.String("12345679012"), +// PrincipalType: redis.String("aws-account"), +// PrincipalAlias: redis.String("test"), +// }) +// require.NoError(t, err) +// assert.Equal(t, expected, actual) +//} diff --git a/service/privatelink/model.go b/service/privatelink/model.go index 05ebaf0..eafb258 100644 --- a/service/privatelink/model.go +++ b/service/privatelink/model.go @@ -76,7 +76,7 @@ type NotFoundActiveActive struct { } func (f *NotFoundActiveActive) Error() string { - return fmt.Sprintf("resource not found - subscription %d and region %d", f.subscriptionID, f.regionID) + return fmt.Sprintf("privatelink resource not found - subscription %d, region %d", f.subscriptionID, f.regionID) } const ( diff --git a/service/privatelink/service.go b/service/privatelink/service.go index c857167..0365bf1 100644 --- a/service/privatelink/service.go +++ b/service/privatelink/service.go @@ -40,14 +40,14 @@ func NewAPI(client HttpClient, taskWaiter TaskWaiter, logger Log) *API { } // // CreatePrivateLink will create a new PrivateLink. -func (a *API) CreatePrivateLink(ctx context.Context, subscriptionId int, privateLink CreatePrivateLink) error { +func (a *API) CreatePrivateLink(ctx context.Context, subscriptionId int, privateLink CreatePrivateLink) (error, error) { message := fmt.Sprintf("create privatelink for subscription %d", subscriptionId) path := fmt.Sprintf("/subscriptions/%d/private-link", subscriptionId) err := a.create(ctx, message, path, privateLink) if err != nil { - return wrap404Error(subscriptionId, err) + return wrap404Error(subscriptionId, err), nil } - return nil + return nil, nil } // GetPrivateLink will get a new PrivateLink. From 55f9e6794ac85999df5666a023ff79aa4f7dc08c Mon Sep 17 00:00:00 2001 From: Matthew Long Date: Thu, 18 Sep 2025 16:12:22 +0100 Subject: [PATCH 17/17] test: removing commented out test --- privatelink_test.go | 68 --------------------------------------------- 1 file changed, 68 deletions(-) diff --git a/privatelink_test.go b/privatelink_test.go index 34229c9..424ecfc 100644 --- a/privatelink_test.go +++ b/privatelink_test.go @@ -419,71 +419,3 @@ func TestGetActiveActivePrivateLink(t *testing.T) { }) } } - -//func TestCreatePrivateLink(t *testing.T) { -// expected := 114019 -// server := httptest.NewServer( -// testServer( -// "key", -// "secret", -// postRequest( -// t, -// "/subscriptions/114019/private-link", -// `{ -// "alias": "test", -// "principal": "123456789012" -// "shareName": "testshare" -// "type": "aws-account" -// }`, -// `{ -// "taskId": "abcd-efgh-ijkl-mnop", -// "commandType": "privateLinkCreateRequest", -// "status": "received", -// "description": "Task request received and is being queued for processing.", -// "timestamp": "2025-09-18T15:56:00Z", -// "links": [ -// { -// "rel": "task", -// "href": "https://api-staging.qa.redislabs.com/v1/tasks/abcd-efgh-ijkl-mnop", -// "title": "getTaskStatusUpdates", -// "type": "GET" -// } -// ] -// }`, -// ), -// getRequest( -// t, -// "/tasks/abcd-efgh-ijkl-mnop", -// fmt.Sprintf(`{ -// "taskId": "abcd-efgh-ijkl-mnop", -// "commandType": "privateLinkCreateRequest", -// "status": "processing-completed", -// "description": "Request processing completed successfully.", -// "timestamp": "2025-09-18T15:56:10Z", -// "response": { -// "resourceId": %[1]d -// }, -// "links": [ -// { -// "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", -// "rel": "self", -// "type": "GET" -// } -// ] -// }`, expected), -// ), -// ), -// ) -// -// subject, err := clientFromTestServer(server, "key", "secret") -// require.NoError(t, err) -// -// actual, err := subject.PrivateLink.CreatePrivateLink(context.TODO(), 114019, pl.CreatePrivateLink{ -// ShareName: redis.String("testshare"), -// Principal: redis.String("12345679012"), -// PrincipalType: redis.String("aws-account"), -// PrincipalAlias: redis.String("test"), -// }) -// require.NoError(t, err) -// assert.Equal(t, expected, actual) -//}