From 329adcef82e87c3e74c68dcf4d9ba40e46f5426f Mon Sep 17 00:00:00 2001 From: Mateus Pimenta <1920261+matpimenta@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:50:35 +0000 Subject: [PATCH] Add new private service connect API (#195) --- client.go | 3 + psc_test.go | 2102 ++++++++++++++++++++++++++++++++++++++++ service/psc/model.go | 133 +++ service/psc/service.go | 403 ++++++++ util_test.go | 10 + 5 files changed, 2651 insertions(+) create mode 100644 psc_test.go create mode 100644 service/psc/model.go create mode 100644 service/psc/service.go diff --git a/client.go b/client.go index c1ae0f1..92778ec 100644 --- a/client.go +++ b/client.go @@ -13,6 +13,7 @@ import ( "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" @@ -43,6 +44,7 @@ type Client struct { Maintenance *maintenance.API Pricing *pricing.API TransitGatewayAttachments *attachments.API + PrivateServiceConnect *psc.API Tags *tags.API // fixed FixedPlans *plans.API @@ -91,6 +93,7 @@ func NewClient(configs ...Option) (*Client, error) { Maintenance: maintenance.NewAPI(client, t, config.logger), Pricing: pricing.NewAPI(client), TransitGatewayAttachments: attachments.NewAPI(client, t, config.logger), + PrivateServiceConnect: psc.NewAPI(client, t, config.logger), Tags: tags.NewAPI(client), // fixed FixedPlans: plans.NewAPI(client, config.logger), diff --git a/psc_test.go b/psc_test.go new file mode 100644 index 0000000..83d4a2d --- /dev/null +++ b/psc_test.go @@ -0,0 +1,2102 @@ +package rediscloud_api + +import ( + "context" + "errors" + "net/http/httptest" + "net/url" + "testing" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/psc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetPrivateServiceConnectService(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedResult *psc.PrivateServiceConnectService + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully return a private service connect service", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/private-service-connect", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "pscServiceGetRequest", + "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": "pscServiceGetRequest", + "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": "psc.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: &psc.PrivateServiceConnectService{ + ID: redis.Int(40), + ConnectionHostName: redis.String("psc.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 service connect is not found", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/private-service-connect", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "pscServiceGetRequest", + "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": "pscServiceGetRequest", + "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": "PSC_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: &psc.NotFound{}, + }, + { + description: "should fail when subscription is not found", + mockedResponse: []endpointRequest{ + getRequestWithStatus( + t, + "/subscriptions/114019/private-service-connect", + 404, + `{ + "timestamp" : "2025-01-17T09:34:25.803+00:00", + "status" : 404, + "error" : "Not Found", + "path" : "/v1/subscriptions/114019/private-service-connect" + }`), + }, + expectedError: errors.New("resource not found - subscription 114019"), + expectedErrorAs: &psc.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.PrivateServiceConnect.GetService(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()) + } + }) + } +} + +func TestGetPrivateServiceConnectServiceActiveActive(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedResult *psc.PrivateServiceConnectService + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully return a private service connect service", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/regions/1/private-service-connect", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePscServiceGetRequest", + "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": "activeActivePscServiceGetRequest", + "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": "psc.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: &psc.PrivateServiceConnectService{ + ID: redis.Int(40), + ConnectionHostName: redis.String("psc.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 service connect is not found", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/regions/1/private-service-connect", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePscServiceGetRequest", + "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": "activeActivePscServiceGetRequest", + "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": "PSC_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 and region 1"), + expectedErrorAs: &psc.NotFoundActiveActive{}, + }, + { + description: "should fail when subscription is not found", + mockedResponse: []endpointRequest{ + getRequestWithStatus( + t, + "/subscriptions/114019/regions/1/private-service-connect", + 404, + `{ + "timestamp" : "2025-01-17T09:34:25.803+00:00", + "status" : 404, + "error" : "Not Found", + "path" : "/v1/subscriptions/114019/regions/1/private-service-connect" + }`), + }, + expectedError: errors.New("resource not found - subscription 114019 and region 1"), + expectedErrorAs: &psc.NotFoundActiveActive{}, + }, + } + + 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.PrivateServiceConnect.GetActiveActiveService(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 TestCreatePrivateServiceConnectService(t *testing.T) { + server := httptest.NewServer( + testServer( + "key", + "secret", + postRequestWithNoRequest( + t, + "/subscriptions/114019/private-service-connect", + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "pscServiceCreateRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2025-01-13T11:58:49.673306547Z", + "links": [ + { + "rel": "task", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515" + } + ] + }`, + ), + getRequest( + t, + "/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515", + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "pscServiceCreateRequest", + "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": 40 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + )) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + actual, err := subject.PrivateServiceConnect.CreateService(context.TODO(), 114019) + assert.NoError(t, err) + assert.Equal(t, 40, actual) +} + +func TestCreatePrivateServiceConnectEndpoint(t *testing.T) { + server := httptest.NewServer( + testServer( + "key", + "secret", + postRequest( + t, + "/subscriptions/114019/private-service-connect/40", + `{ + "gcpProjectId": "my-gcp-project", + "gcpVpcName": "my-vpc", + "gcpVpcSubnetName": "my-vpc-subnet", + "endpointConnectionName": "my-endpoint-connection" + }`, + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "pscEndpointCreateRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2025-01-13T11:58:49.673306547Z", + "links": [ + { + "rel": "task", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515" + } + ] + }`, + ), + getRequest( + t, + "/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515", + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "pscEndpointCreateRequest", + "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": 39 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + )) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + actual, err := subject.PrivateServiceConnect.CreateEndpoint(context.TODO(), 114019, 40, psc.CreatePrivateServiceConnectEndpoint{ + GCPProjectID: redis.String("my-gcp-project"), + GCPVPCName: redis.String("my-vpc"), + GCPVPCSubnetName: redis.String("my-vpc-subnet"), + EndpointConnectionName: redis.String("my-endpoint-connection"), + }) + assert.NoError(t, err) + assert.Equal(t, 39, actual) +} + +func TestCreatePrivateServiceConnectEndpointActiveActive(t *testing.T) { + server := httptest.NewServer( + testServer( + "key", + "secret", + postRequest( + t, + "/subscriptions/114019/regions/1/private-service-connect/40", + `{ + "gcpProjectId": "my-gcp-project", + "gcpVpcName": "my-vpc", + "gcpVpcSubnetName": "my-vpc-subnet", + "endpointConnectionName": "my-endpoint-connection" + }`, + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "activeActivePscEndpointCreateRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2025-01-13T11:58:49.673306547Z", + "links": [ + { + "rel": "task", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515" + } + ] + }`, + ), + getRequest( + t, + "/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515", + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "activeActivePscEndpointCreateRequest", + "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": 39 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + )) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + actual, err := subject.PrivateServiceConnect.CreateActiveActiveEndpoint(context.TODO(), 114019, 1, 40, psc.CreatePrivateServiceConnectEndpoint{ + GCPProjectID: redis.String("my-gcp-project"), + GCPVPCName: redis.String("my-vpc"), + GCPVPCSubnetName: redis.String("my-vpc-subnet"), + EndpointConnectionName: redis.String("my-endpoint-connection"), + }) + assert.NoError(t, err) + assert.Equal(t, 39, actual) +} + +func TestGetPrivateServiceConnectEndpoints(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedResult *psc.PrivateServiceConnectEndpoints + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully return a private service connect service endpoints", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/private-service-connect/40", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "pscServiceEndpointsGetRequest", + "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": "pscServiceEndpointsGetRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2025-01-14T16:18:42.048633704Z", + "response": { + "resourceId": 114019, + "resource": { + "pscServiceId": 40, + "endpoints": [ + { + "id": 39, + "gcpProjectId": "my-gcp-project", + "gcpVpcName": "my-vpc", + "gcpVpcSubnetName": "my-vpc-subnet", + "endpointConnectionName": "my-endpoint-connection", + "status": "initialized" + } + ] + } + }, + "links": [ + { + "rel": "self", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971" + } + ] + }`), + }, + expectedResult: &psc.PrivateServiceConnectEndpoints{ + PSCServiceID: redis.Int(40), + Endpoints: []*psc.PrivateServiceConnectEndpoint{ + { + ID: redis.Int(39), + GCPProjectID: redis.String("my-gcp-project"), + GCPVPCName: redis.String("my-vpc"), + GCPVPCSubnetName: redis.String("my-vpc-subnet"), + EndpointConnectionName: redis.String("my-endpoint-connection"), + Status: redis.String("initialized"), + }, + }, + }, + }, + { + description: "should fail when private service connect is not found", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/private-service-connect/40", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "pscServiceEndpointsGetRequest", + "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": "pscServiceEndpointsGetRequest", + "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": "PSC_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: &psc.NotFound{}, + }, + { + description: "should fail when subscription is not found", + mockedResponse: []endpointRequest{ + getRequestWithStatus( + t, + "/subscriptions/114019/private-service-connect/40", + 404, + `{ + "timestamp" : "2025-01-17T09:34:25.803+00:00", + "status" : 404, + "error" : "Not Found", + "path" : "/v1/subscriptions/114019/private-service-connect/40" + }`, + ), + }, + expectedError: errors.New("resource not found - subscription 114019"), + expectedErrorAs: &psc.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.PrivateServiceConnect.GetEndpoints(context.TODO(), 114019, 40) + 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 TestGetPrivateServiceConnectEndpointsActiveActive(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedResult *psc.PrivateServiceConnectEndpoints + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully return a private service connect service endpoints", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/regions/1/private-service-connect/40", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePscServiceEndpointsGetRequest", + "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": "activeActivePscServiceEndpointsGetRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2025-01-14T16:18:42.048633704Z", + "response": { + "resourceId": 114019, + "resource": { + "pscServiceId": 40, + "endpoints": [ + { + "id": 39, + "gcpProjectId": "my-gcp-project", + "gcpVpcName": "my-vpc", + "gcpVpcSubnetName": "my-vpc-subnet", + "endpointConnectionName": "my-endpoint-connection", + "status": "initialized" + } + ] + } + }, + "links": [ + { + "rel": "self", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971" + } + ] + }`), + }, + expectedResult: &psc.PrivateServiceConnectEndpoints{ + PSCServiceID: redis.Int(40), + Endpoints: []*psc.PrivateServiceConnectEndpoint{ + { + ID: redis.Int(39), + GCPProjectID: redis.String("my-gcp-project"), + GCPVPCName: redis.String("my-vpc"), + GCPVPCSubnetName: redis.String("my-vpc-subnet"), + EndpointConnectionName: redis.String("my-endpoint-connection"), + Status: redis.String("initialized"), + }, + }, + }, + }, + { + description: "should fail when private service connect is not found", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/regions/1/private-service-connect/40", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePscServiceEndpointsGetRequest", + "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": "activeActivePscServiceEndpointsGetRequest", + "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": "PSC_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 and region 1"), + expectedErrorAs: &psc.NotFoundActiveActive{}, + }, + { + description: "should fail when subscription is not found", + mockedResponse: []endpointRequest{ + getRequestWithStatus( + t, + "/subscriptions/114019/regions/1/private-service-connect/40", + 404, + `{ + "timestamp" : "2025-01-17T09:34:25.803+00:00", + "status" : 404, + "error" : "Not Found", + "path" : "/v1/subscriptions/114019/regions/1/private-service-connect/40" + }`, + ), + }, + expectedError: errors.New("resource not found - subscription 114019 and region 1"), + expectedErrorAs: &psc.NotFoundActiveActive{}, + }, + } + + 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.PrivateServiceConnect.GetActiveActiveEndpoints(context.TODO(), 114019, 1, 40) + 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 TestGetPrivateServiceConnectEndpointCreationScript(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedResult *psc.CreationScript + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully return a private service connect service endpoint creation script", + mockedResponse: []endpointRequest{ + getRequestWithQuery( + t, + "/subscriptions/114019/private-service-connect/40/endpoints/52/creationScripts", + url.Values{ + "includeTerraformGcpScript": []string{"true"}, + }, + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "pscServiceEndpointScriptGetRequest", + "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": "pscServiceEndpointScriptGetRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2025-01-14T16:18:42.048633704Z", + "response": { + "resourceId": 42, + "resource": { + "script": { + "bash": "bash script", + "powershell": "powershell script", + "terraformGcp": { + "serviceAttachments": [ + { + "name": "projects/s169ae38adc5a3c69/regions/us-central1/serviceAttachments/service-attachment1-mc2025-0-us-central1-mz-rlrcp", + "dnsRecord": "psc1.mc2025-0.us-central1-mz.gcp.sdk-cloud.rlrcp.com.", + "ipAddressName": "redis-1111-psc-static-ip1", + "forwardingRuleName": "redis-1111" + } + ] + } + } + } + }, + "links": [ + { + "rel": "self", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971" + } + ] + }`, + ), + }, + expectedResult: &psc.CreationScript{ + Script: &psc.GCPCreationScript{ + Bash: redis.String("bash script"), + Powershell: redis.String("powershell script"), + TerraformGcp: &psc.TerraformGCP{ + ServiceAttachments: []psc.TerraformGCPServiceAttachment{ + { + Name: redis.String("projects/s169ae38adc5a3c69/regions/us-central1/serviceAttachments/service-attachment1-mc2025-0-us-central1-mz-rlrcp"), + DNSRecord: redis.String("psc1.mc2025-0.us-central1-mz.gcp.sdk-cloud.rlrcp.com."), + IPAddressName: redis.String("redis-1111-psc-static-ip1"), + ForwardingRuleName: redis.String("redis-1111"), + }, + }, + }, + }, + }, + }, + { + description: "should fail when subscription is not found", + mockedResponse: []endpointRequest{ + getRequestWithQueryAndStatus( + t, + "/subscriptions/114019/private-service-connect/40/endpoints/52/creationScripts", + url.Values{ + "includeTerraformGcpScript": []string{"true"}, + }, + 404, + `{ + "timestamp" : "2025-01-17T09:34:25.803+00:00", + "status" : 404, + "error" : "Not Found", + "path" : "/v1/subscriptions/114019/private-service-connect/40/endpoints/52/creationScripts" + }`, + ), + }, + expectedError: errors.New("resource not found - subscription 114019"), + expectedErrorAs: &psc.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.PrivateServiceConnect.GetEndpointCreationScripts(context.TODO(), + 114019, 40, 52, true) + 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 TestGetPrivateServiceConnectEndpointCreationScriptActiveActive(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedResult *psc.CreationScript + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully return a private service connect service endpoint creation script", + mockedResponse: []endpointRequest{ + getRequestWithQuery( + t, + "/subscriptions/114019/regions/1/private-service-connect/40/endpoints/52/creationScripts", + url.Values{ + "includeTerraformGcpScript": []string{"true"}, + }, + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePscServiceEndpointScriptGetRequest", + "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": "activeActivePscServiceEndpointScriptGetRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2025-01-14T16:18:42.048633704Z", + "response": { + "resourceId": 42, + "resource": { + "script": { + "bash": "bash script", + "powershell": "powershell script", + "terraformGcp": { + "serviceAttachments": [ + { + "name": "projects/s169ae38adc5a3c69/regions/us-central1/serviceAttachments/service-attachment1-mc2025-0-us-central1-mz-rlrcp", + "dnsRecord": "psc1.mc2025-0.us-central1-mz.gcp.sdk-cloud.rlrcp.com.", + "ipAddressName": "redis-1111-psc-static-ip1", + "forwardingRuleName": "redis-1111" + } + ] + } + } + } + }, + "links": [ + { + "rel": "self", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971" + } + ] + }`, + ), + }, + expectedResult: &psc.CreationScript{ + Script: &psc.GCPCreationScript{ + Bash: redis.String("bash script"), + Powershell: redis.String("powershell script"), + TerraformGcp: &psc.TerraformGCP{ + ServiceAttachments: []psc.TerraformGCPServiceAttachment{ + { + Name: redis.String("projects/s169ae38adc5a3c69/regions/us-central1/serviceAttachments/service-attachment1-mc2025-0-us-central1-mz-rlrcp"), + DNSRecord: redis.String("psc1.mc2025-0.us-central1-mz.gcp.sdk-cloud.rlrcp.com."), + IPAddressName: redis.String("redis-1111-psc-static-ip1"), + ForwardingRuleName: redis.String("redis-1111"), + }, + }, + }, + }, + }, + }, + { + description: "should fail when subscription is not found", + mockedResponse: []endpointRequest{ + getRequestWithQueryAndStatus( + t, + "/subscriptions/114019/regions/1/private-service-connect/40/endpoints/52/creationScripts", + url.Values{ + "includeTerraformGcpScript": []string{"true"}, + }, + 404, + `{ + "timestamp" : "2025-01-17T09:34:25.803+00:00", + "status" : 404, + "error" : "Not Found", + "path" : "/v1/subscriptions/114019/regions/1/private-service-connect/40/endpoints/52/creationScripts" + }`, + ), + }, + expectedError: errors.New("resource not found - subscription 114019 and region 1"), + expectedErrorAs: &psc.NotFoundActiveActive{}, + }, + } + + 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.PrivateServiceConnect.GetActiveActiveEndpointCreationScripts(context.TODO(), + 114019, 1, 40, 52, true) + 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 TestGetPrivateServiceConnectEndpointDeletionScript(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedResult *psc.DeletionScript + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully return a private service connect service endpoint deletion script", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/private-service-connect/40/endpoints/52/deletionScripts", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "pscServiceEndpointScriptGetRequest", + "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": "pscServiceEndpointScriptGetRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2025-01-14T16:18:42.048633704Z", + "response": { + "resourceId": 42, + "resource": { + "script": { + "bash": "bash script", + "powershell": "powershell script" + } + } + }, + "links": [ + { + "rel": "self", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971" + } + ] + }`, + ), + }, + expectedResult: &psc.DeletionScript{ + Script: &psc.GCPDeletionScript{ + Bash: redis.String("bash script"), + Powershell: redis.String("powershell script"), + }, + }, + }, + { + description: "should fail when subscription is not found", + mockedResponse: []endpointRequest{ + getRequestWithStatus( + t, + "/subscriptions/114019/private-service-connect/40/endpoints/52/deletionScripts", + 404, + `{ + "timestamp" : "2025-01-17T09:34:25.803+00:00", + "status" : 404, + "error" : "Not Found", + "path" : "/v1/subscriptions/114019/private-service-connect/40/endpoints/52/deletionScripts" + }`, + ), + }, + expectedError: errors.New("resource not found - subscription 114019"), + expectedErrorAs: &psc.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.PrivateServiceConnect.GetEndpointDeletionScripts(context.TODO(), + 114019, 40, 52) + 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 TestGetPrivateServiceConnectEndpointDeletionScriptActiveActive(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedResult *psc.DeletionScript + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully return a private service connect service endpoint deletion script", + mockedResponse: []endpointRequest{ + getRequest( + t, + "/subscriptions/114019/regions/1/private-service-connect/40/endpoints/52/deletionScripts", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePscServiceEndpointScriptGetRequest", + "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": "activeActivePscServiceEndpointScriptGetRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2025-01-14T16:18:42.048633704Z", + "response": { + "resourceId": 42, + "resource": { + "script": { + "bash": "bash script", + "powershell": "powershell script" + } + } + }, + "links": [ + { + "rel": "self", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971" + } + ] + }`, + ), + }, + expectedResult: &psc.DeletionScript{ + Script: &psc.GCPDeletionScript{ + Bash: redis.String("bash script"), + Powershell: redis.String("powershell script"), + }, + }, + }, + { + description: "should fail when subscription is not found", + mockedResponse: []endpointRequest{ + getRequestWithStatus( + t, + "/subscriptions/114019/regions/1/private-service-connect/40/endpoints/52/deletionScripts", + 404, + `{ + "timestamp" : "2025-01-17T09:34:25.803+00:00", + "status" : 404, + "error" : "Not Found", + "path" : "/v1/subscriptions/114019/regions/1/private-service-connect/40/endpoints/52/deletionScripts" + }`, + ), + }, + expectedError: errors.New("resource not found - subscription 114019 and region 1"), + expectedErrorAs: &psc.NotFoundActiveActive{}, + }, + } + + 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.PrivateServiceConnect.GetActiveActiveEndpointDeletionScripts(context.TODO(), + 114019, 1, 40, 52) + 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 TestCreatePrivateServiceConnectServiceActiveActive(t *testing.T) { + server := httptest.NewServer( + testServer( + "key", + "secret", + postRequestWithNoRequest( + t, + "/subscriptions/114019/regions/1/private-service-connect", + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "activeActivePscServiceCreateRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2025-01-13T11:58:49.673306547Z", + "links": [ + { + "rel": "task", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515" + } + ] + }`, + ), + getRequest( + t, + "/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515", + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "activeActivePscServiceGetRequest", + "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": 40 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + )) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + actual, err := subject.PrivateServiceConnect.CreateActiveActiveService(context.TODO(), 114019, 1) + assert.NoError(t, err) + assert.Equal(t, 40, actual) +} + +func TestUpdatePrivateServiceConnectEndpoint(t *testing.T) { + server := httptest.NewServer( + testServer( + "key", + "secret", + putRequest( + t, + "/subscriptions/114019/private-service-connect/40/endpoints/50", + `{ + "gcpProjectId": "my-gcp-project", + "gcpVpcName": "my-vpc", + "gcpVpcSubnetName": "my-vpc-subnet", + "endpointConnectionName": "my-endpoint-connection", + "action":"accept" + }`, + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "pscEndpointUpdateRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2025-01-13T11:58:49.673306547Z", + "links": [ + { + "rel": "task", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515" + } + ] + }`, + ), + getRequest( + t, + "/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515", + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "pscEndpointUpdateRequest", + "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": 39 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + )) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + err = subject.PrivateServiceConnect.UpdateEndpoint(context.TODO(), 114019, 40, 50, &psc.UpdatePrivateServiceConnectEndpoint{ + GCPProjectID: redis.String("my-gcp-project"), + GCPVPCName: redis.String("my-vpc"), + GCPVPCSubnetName: redis.String("my-vpc-subnet"), + EndpointConnectionName: redis.String("my-endpoint-connection"), + Action: redis.String(psc.EndpointActionAccept), + }) + assert.NoError(t, err) +} + +func TestUpdatePrivateServiceConnectEndpointActiveActive(t *testing.T) { + server := httptest.NewServer( + testServer( + "key", + "secret", + putRequest( + t, + "/subscriptions/114019/regions/1/private-service-connect/40/endpoints/50", + `{ + "gcpProjectId": "my-gcp-project", + "gcpVpcName": "my-vpc", + "gcpVpcSubnetName": "my-vpc-subnet", + "endpointConnectionName": "my-endpoint-connection", + "action":"accept" + }`, + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "activeActivePscEndpointUpdateRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2025-01-13T11:58:49.673306547Z", + "links": [ + { + "rel": "task", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515" + } + ] + }`, + ), + getRequest( + t, + "/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515", + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "activeActivePscEndpointUpdateRequest", + "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": 39 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + )) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + err = subject.PrivateServiceConnect.UpdateActiveActiveEndpoint(context.TODO(), 114019, 1, 40, 50, &psc.UpdatePrivateServiceConnectEndpoint{ + GCPProjectID: redis.String("my-gcp-project"), + GCPVPCName: redis.String("my-vpc"), + GCPVPCSubnetName: redis.String("my-vpc-subnet"), + EndpointConnectionName: redis.String("my-endpoint-connection"), + Action: redis.String(psc.EndpointActionAccept), + }) + assert.NoError(t, err) +} + +func TestUpdatePrivateServiceConnectEndpointAccept(t *testing.T) { + server := httptest.NewServer( + testServer( + "key", + "secret", + putRequest( + t, + "/subscriptions/114019/private-service-connect/40/endpoints/50", + `{ + "action":"accept" + }`, + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "pscEndpointUpdateRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2025-01-13T11:58:49.673306547Z", + "links": [ + { + "rel": "task", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515" + } + ] + }`, + ), + getRequest( + t, + "/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515", + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "pscEndpointUpdateRequest", + "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": 39 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + )) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + err = subject.PrivateServiceConnect.UpdateEndpoint(context.TODO(), 114019, 40, 50, &psc.UpdatePrivateServiceConnectEndpoint{ + Action: redis.String(psc.EndpointActionAccept), + }) + assert.NoError(t, err) +} + +func TestUpdatePrivateServiceConnectEndpointActiveActiveAccept(t *testing.T) { + server := httptest.NewServer( + testServer( + "key", + "secret", + putRequest( + t, + "/subscriptions/114019/regions/1/private-service-connect/40/endpoints/50", + `{ + "action":"accept" + }`, + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "activeActivePscEndpointUpdateRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2025-01-13T11:58:49.673306547Z", + "links": [ + { + "rel": "task", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515" + } + ] + }`, + ), + getRequest( + t, + "/tasks/41a37bac-91dc-4468-bb4b-45dfd61df515", + `{ + "taskId": "41a37bac-91dc-4468-bb4b-45dfd61df515", + "commandType": "activeActivePscEndpointUpdateRequest", + "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": 39 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + )) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + err = subject.PrivateServiceConnect.UpdateActiveActiveEndpoint(context.TODO(), 114019, 1, 40, 50, &psc.UpdatePrivateServiceConnectEndpoint{ + Action: redis.String(psc.EndpointActionAccept), + }) + assert.NoError(t, err) +} + +func TestDeletePrivateServiceConnectService(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully delete a private service connect service", + mockedResponse: []endpointRequest{ + deleteRequest( + t, + "/subscriptions/114019/private-service-connect", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "pscServiceDeleteRequest", + "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": "pscServiceDeleteRequest", + "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 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + }, + }, + { + description: "should fail when private service connect is not found", + mockedResponse: []endpointRequest{ + deleteRequestWithStatus( + t, + "/subscriptions/114019/private-service-connect", + 404, + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "pscServiceDeleteRequest", + "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": "pscServiceDeleteRequest", + "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": "PSC_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: &psc.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) + + err = subject.PrivateServiceConnect.DeleteService(context.TODO(), 114019) + if testCase.expectedError == nil { + assert.NoError(t, err) + } else { + assert.IsType(t, err, testCase.expectedErrorAs) + assert.EqualError(t, err, testCase.expectedError.Error()) + } + }) + } +} + +func TestDeletePrivateServiceConnectServiceActiveActive(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully delete a private service connect service", + mockedResponse: []endpointRequest{ + deleteRequest( + t, + "/subscriptions/114019/regions/1/private-service-connect", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePscServiceDeleteRequest", + "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": "activeActivePscServiceDeleteRequest", + "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 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + }, + }, + { + description: "should fail when private service connect is not found", + mockedResponse: []endpointRequest{ + deleteRequestWithStatus( + t, + "/subscriptions/114019/regions/1/private-service-connect", + 404, + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePscServiceDeleteRequest", + "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": "activeActivePscServiceDeleteRequest", + "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": "PSC_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 and region 1"), + expectedErrorAs: &psc.NotFoundActiveActive{}, + }, + } + + 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) + + err = subject.PrivateServiceConnect.DeleteActiveActiveService(context.TODO(), 114019, 1) + if testCase.expectedError == nil { + assert.NoError(t, err) + } else { + assert.IsType(t, err, testCase.expectedErrorAs) + assert.EqualError(t, err, testCase.expectedError.Error()) + } + }) + } +} + +func TestDeletePrivateServiceConnectEndpoint(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully delete a private service connect endpoint", + mockedResponse: []endpointRequest{ + deleteRequest( + t, + "/subscriptions/114019/private-service-connect/40/endpoints/50", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "pscEndpointDeleteRequest", + "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": "pscEndpointDeleteRequest", + "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 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + }, + }, + { + description: "should fail when private service connect endpoint is not found", + mockedResponse: []endpointRequest{ + deleteRequestWithStatus( + t, + "/subscriptions/114019/private-service-connect/40/endpoints/50", + 404, + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "pscEndpointDeleteRequest", + "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": "pscEndpointDeleteRequest", + "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": "PSC_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: &psc.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) + + err = subject.PrivateServiceConnect.DeleteEndpoint(context.TODO(), 114019, 40, 50) + if testCase.expectedError == nil { + assert.NoError(t, err) + } else { + assert.IsType(t, err, testCase.expectedErrorAs) + assert.EqualError(t, err, testCase.expectedError.Error()) + } + }) + } +} + +func TestDeletePrivateServiceConnectEndpointActiveActive(t *testing.T) { + tc := []struct { + description string + mockedResponse []endpointRequest + expectedError error + expectedErrorAs error + }{ + { + description: "should successfully delete a private service connect endpoint", + mockedResponse: []endpointRequest{ + deleteRequest( + t, + "/subscriptions/114019/regions/1/private-service-connect/40/endpoints/50", + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePscEndpointDeleteRequest", + "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": "activeActivePscEndpointDeleteRequest", + "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 + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/502fc31f-fd44-4cb0-a429-07882309a971", + "rel": "self", + "type": "GET" + } + ] + }`, + ), + }, + }, + { + description: "should fail when private service connect endpoint is not found", + mockedResponse: []endpointRequest{ + deleteRequestWithStatus( + t, + "/subscriptions/114019/regions/1/private-service-connect/40/endpoints/50", + 404, + `{ + "taskId": "502fc31f-fd44-4cb0-a429-07882309a971", + "commandType": "activeActivePscEndpointDeleteRequest", + "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": "activeActivePscEndpointDeleteRequest", + "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": "PSC_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 and region 1"), + expectedErrorAs: &psc.NotFoundActiveActive{}, + }, + } + + 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) + + err = subject.PrivateServiceConnect.DeleteActiveActiveEndpoint(context.TODO(), 114019, 1, 40, 50) + if testCase.expectedError == nil { + assert.NoError(t, err) + } else { + assert.IsType(t, err, testCase.expectedErrorAs) + assert.EqualError(t, err, testCase.expectedError.Error()) + } + }) + } +} diff --git a/service/psc/model.go b/service/psc/model.go new file mode 100644 index 0000000..0f08cd5 --- /dev/null +++ b/service/psc/model.go @@ -0,0 +1,133 @@ +package psc + +import ( + "fmt" +) + +type PrivateServiceConnectService struct { + ID *int `json:"id,omitempty"` + ConnectionHostName *string `json:"connectionHostName,omitempty"` + ServiceAttachmentName *string `json:"serviceAttachmentName,omitempty"` + Status *string `json:"status,omitempty"` +} + +type CreatePrivateServiceConnectEndpoint struct { + GCPProjectID *string `json:"gcpProjectId,omitempty"` + GCPVPCName *string `json:"gcpVpcName,omitempty"` + GCPVPCSubnetName *string `json:"gcpVpcSubnetName,omitempty"` + EndpointConnectionName *string `json:"endpointConnectionName,omitempty"` +} + +type UpdatePrivateServiceConnectEndpoint struct { + GCPProjectID *string `json:"gcpProjectId,omitempty"` + GCPVPCName *string `json:"gcpVpcName,omitempty"` + GCPVPCSubnetName *string `json:"gcpVpcSubnetName,omitempty"` + EndpointConnectionName *string `json:"endpointConnectionName,omitempty"` + Action *string `json:"action,omitempty"` +} + +type PrivateServiceConnectEndpoints struct { + PSCServiceID *int `json:"pscServiceId,omitempty"` + Endpoints []*PrivateServiceConnectEndpoint `json:"endpoints,omitempty"` +} +type PrivateServiceConnectEndpoint struct { + ID *int `json:"id,omitempty"` + GCPProjectID *string `json:"gcpProjectId,omitempty"` + GCPVPCName *string `json:"gcpVpcName,omitempty"` + GCPVPCSubnetName *string `json:"gcpVpcSubnetName,omitempty"` + EndpointConnectionName *string `json:"endpointConnectionName,omitempty"` + Status *string `json:"status,omitempty"` +} + +type CreationScript struct { + Script *GCPCreationScript `json:"script,omitempty"` +} + +type DeletionScript struct { + Script *GCPDeletionScript `json:"script,omitempty"` +} + +type GCPCreationScript struct { + Bash *string `json:"bash,omitempty"` + Powershell *string `json:"powershell,omitempty"` + TerraformGcp *TerraformGCP `json:"terraformGcp,omitempty"` +} + +type GCPDeletionScript struct { + Bash *string `json:"bash,omitempty"` + Powershell *string `json:"powershell,omitempty"` +} + +type TerraformGCP struct { + ServiceAttachments []TerraformGCPServiceAttachment `json:"serviceAttachments,omitempty"` +} + +type TerraformGCPServiceAttachment struct { + Name *string `json:"name,omitempty"` + DNSRecord *string `json:"dnsRecord,omitempty"` + IPAddressName *string `json:"ipAddressName,omitempty"` + ForwardingRuleName *string `json:"forwardingRuleName,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) +} + +const ( + + // ServiceStatusCreateQueued when PSC service creation is queued + ServiceStatusCreateQueued = "create-queued" + // ServiceStatusDeleteQueued when PSC service deletion is queued + ServiceStatusDeleteQueued = "delete-queued" + // ServiceStatusInitialized when PSC service provisioning started + ServiceStatusInitialized = "initialized" + // ServiceStatusCreatePending when PSC service provisioning completed but databases are pending update + ServiceStatusCreatePending = "create-pending" + // ServiceStatusActive when PSC service is ready + ServiceStatusActive = "active" + // ServiceStatusDeletePending when infrastructure deletion is completed but databases are pending update + ServiceStatusDeletePending = "delete-pending" + // ServiceStatusDeleted when PSC service is deleted + ServiceStatusDeleted = "deleted" + // ServiceStatusProvisionFailed when PSC service has failed while creation/deletion + ServiceStatusProvisionFailed = "provision-failed" + // ServiceStatusFailed when PSC service failed after it's been reported as active + ServiceStatusFailed = "failed" + + // EndpointStatusInitialized the endpoint was created in the SM but the creation script wasn't yet run + EndpointStatusInitialized = "initialized" + // EndpointStatusProcessing Processing the status during deletion or creation of 40 endpoints in cloud provider + EndpointStatusProcessing = "processing" + // EndpointStatusPending the endpoint is waiting for the user to accept or reject it + EndpointStatusPending = "pending" + // EndpointStatusAcceptPending the user accepted. the endpoint is not yet fully accepted + EndpointStatusAcceptPending = "accept-pending" + // EndpointStatusActive the endpoint is ready for use + EndpointStatusActive = "active" + // EndpointStatusDeleted the endpoint was successfully deleted + EndpointStatusDeleted = "deleted" + // EndpointStatusRejected the endpoint was successfully rejected + EndpointStatusRejected = "rejected" + // EndpointStatusRejectPending the user rejected. the endpoint is not yet fully rejected + EndpointStatusRejectPending = "reject-pending" + // EndpointStatusFailed endpoint is in error status + EndpointStatusFailed = "failed" + + // EndpointActionAccept accepts the endpoint + EndpointActionAccept = "accept" + // EndpointActionReject rejects the endpoint + EndpointActionReject = "reject" +) diff --git a/service/psc/service.go b/service/psc/service.go new file mode 100644 index 0000000..53527c1 --- /dev/null +++ b/service/psc/service.go @@ -0,0 +1,403 @@ +package psc + +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) GetService(ctx context.Context, subscription int) (*PrivateServiceConnectService, error) { + message := fmt.Sprintf("get private service connect for subscription %d", subscription) + path := fmt.Sprintf("/subscriptions/%d/private-service-connect", subscription) + task, err := a.getService(ctx, message, path) + if err != nil { + return nil, wrap404Error(subscription, err) + } + return task, nil +} + +func (a *API) GetActiveActiveService(ctx context.Context, subscription int, regionId int) (*PrivateServiceConnectService, error) { + message := fmt.Sprintf("get private service connect for subscription %d in region %d", subscription, regionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-service-connect", subscription, regionId) + task, err := a.getService(ctx, message, path) + if err != nil { + return nil, wrap404ErrorActiveActive(subscription, regionId, err) + } + return task, nil +} + +func (a *API) CreateService(ctx context.Context, subscription int) (int, error) { + message := fmt.Sprintf("create private service connect for subscription %d", subscription) + path := fmt.Sprintf("/subscriptions/%d/private-service-connect", subscription) + resourceId, err := a.create(ctx, message, path) + if err != nil { + return 0, wrap404Error(subscription, err) + } + return resourceId, nil +} + +func (a *API) CreateActiveActiveService(ctx context.Context, subscription int, regionId int) (int, error) { + message := fmt.Sprintf("create private service connect for subscription %d in region %d", subscription, regionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-service-connect", subscription, regionId) + resourceId, err := a.create(ctx, message, path) + if err != nil { + return 0, wrap404ErrorActiveActive(subscription, regionId, err) + } + return resourceId, nil +} + +func (a *API) GetEndpointCreationScripts(ctx context.Context, subscription int, pscServiceId int, endpointId int, includeTerraformGcpScript bool) (*CreationScript, error) { + message := fmt.Sprintf("get private service connect creation script for subscription %d, service %d and endpoint %d", + subscription, pscServiceId, endpointId) + path := fmt.Sprintf("/subscriptions/%d/private-service-connect/%d/endpoints/%d/creationScripts", + subscription, pscServiceId, endpointId) + creationScript, err := a.getCreationScript(ctx, message, path, url.Values{ + "includeTerraformGcpScript": []string{strconv.FormatBool(includeTerraformGcpScript)}, + }) + if err != nil { + return nil, wrap404Error(subscription, err) + } + return creationScript, nil +} + +func (a *API) GetActiveActiveEndpointCreationScripts(ctx context.Context, subscription int, regionId int, pscServiceId int, endpointId int, includeTerraformGcpScript bool) (*CreationScript, error) { + message := fmt.Sprintf("get private service connect creation script for subscription %d, service %d and endpoint %d in region %d", + subscription, pscServiceId, endpointId, regionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-service-connect/%d/endpoints/%d/creationScripts", + subscription, regionId, pscServiceId, endpointId) + creationScript, err := a.getCreationScript(ctx, message, path, url.Values{ + "includeTerraformGcpScript": []string{strconv.FormatBool(includeTerraformGcpScript)}, + }) + if err != nil { + return nil, wrap404ErrorActiveActive(subscription, regionId, err) + } + return creationScript, nil +} + +func (a *API) GetEndpointDeletionScripts(ctx context.Context, subscription int, pscServiceId int, endpointId int) (*DeletionScript, error) { + message := fmt.Sprintf("get private service connect deletion script for subscription %d, service %d and endpoint %d", + subscription, pscServiceId, endpointId) + path := fmt.Sprintf("/subscriptions/%d/private-service-connect/%d/endpoints/%d/deletionScripts", + subscription, pscServiceId, endpointId) + deletionScript, err := a.getDeletionScript(ctx, message, path) + if err != nil { + return nil, wrap404Error(subscription, err) + } + return deletionScript, nil +} + +func (a *API) GetActiveActiveEndpointDeletionScripts(ctx context.Context, subscription int, regionId int, pscServiceId int, endpointId int) (*DeletionScript, error) { + message := fmt.Sprintf("get private service connect deletion script for subscription %d, service %d and endpoint %d in region %d", + subscription, pscServiceId, endpointId, regionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-service-connect/%d/endpoints/%d/deletionScripts", + subscription, regionId, pscServiceId, endpointId) + deletionScript, err := a.getDeletionScript(ctx, message, path) + if err != nil { + return nil, wrap404ErrorActiveActive(subscription, regionId, err) + } + return deletionScript, nil +} + +func (a *API) GetEndpoints(ctx context.Context, subscription int, pscServiceId int) (*PrivateServiceConnectEndpoints, error) { + message := fmt.Sprintf("get private service connect for subscription %d and service %d", subscription, pscServiceId) + path := fmt.Sprintf("/subscriptions/%d/private-service-connect/%d", subscription, pscServiceId) + endpoints, err := a.getEndpoints(ctx, message, path) + if err != nil { + return nil, wrap404Error(subscription, err) + } + return endpoints, nil +} + +func (a *API) GetActiveActiveEndpoints(ctx context.Context, subscription int, regionId int, pscServiceId int) (*PrivateServiceConnectEndpoints, error) { + message := fmt.Sprintf("get private service connect for subscription %d and service %d in region %d", subscription, pscServiceId, regionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-service-connect/%d", subscription, regionId, pscServiceId) + endpoints, err := a.getEndpoints(ctx, message, path) + if err != nil { + return nil, wrap404ErrorActiveActive(subscription, regionId, err) + } + return endpoints, nil +} + +func (a *API) CreateEndpoint(ctx context.Context, subscription int, pscServiceId int, endpoint CreatePrivateServiceConnectEndpoint) (int, error) { + message := fmt.Sprintf("create private service connect endpoint for subscription %d and service %d", subscription, pscServiceId) + path := fmt.Sprintf("/subscriptions/%d/private-service-connect/%d", subscription, pscServiceId) + + var task internal.TaskResponse + err := a.client.Post(ctx, message, path, endpoint, &task) + if err != nil { + return 0, err + } + + a.logger.Printf("Waiting for private service connect endpoint for subscription %d and service %d to finish being created", subscription, pscServiceId) + + id, err := a.taskWaiter.WaitForResourceId(ctx, *task.ID) + if err != nil { + return 0, err + } + + return id, nil +} + +func (a *API) CreateActiveActiveEndpoint(ctx context.Context, subscription int, regionId int, pscServiceId int, endpoint CreatePrivateServiceConnectEndpoint) (int, error) { + message := fmt.Sprintf("create private service connect endpoint for subscription %d and service %d in region %d", subscription, pscServiceId, regionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-service-connect/%d", subscription, regionId, pscServiceId) + + var task internal.TaskResponse + err := a.client.Post(ctx, message, path, endpoint, &task) + if err != nil { + return 0, err + } + + a.logger.Printf("Waiting for private service connect endpoint for subscription %d and service %d in region %d to finish being created", subscription, pscServiceId, regionId) + + id, err := a.taskWaiter.WaitForResourceId(ctx, *task.ID) + if err != nil { + return 0, err + } + + return id, nil +} + +func (a *API) UpdateEndpoint(ctx context.Context, subscription int, pscServiceId int, endpointId int, + endpoint *UpdatePrivateServiceConnectEndpoint) error { + message := fmt.Sprintf("update private service connect endpoint %d/%d for subscription %d", pscServiceId, endpointId, subscription) + path := fmt.Sprintf("/subscriptions/%d/private-service-connect/%d/endpoints/%d", subscription, pscServiceId, endpointId) + err := a.update(ctx, message, path, endpoint) + if err != nil { + return wrap404Error(subscription, err) + } + return nil +} + +func (a *API) UpdateActiveActiveEndpoint(ctx context.Context, subscription int, regionId int, pscServiceId int, + endpointId int, endpoint *UpdatePrivateServiceConnectEndpoint) error { + message := fmt.Sprintf("update private service connect endpoint %d/%d for subscription %d in region %d", pscServiceId, endpointId, subscription, regionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-service-connect/%d/endpoints/%d", subscription, regionId, pscServiceId, endpointId) + err := a.update(ctx, message, path, endpoint) + if err != nil { + return wrap404ErrorActiveActive(subscription, regionId, err) + } + return nil +} + +func (a *API) DeleteService(ctx context.Context, subscription int) error { + message := fmt.Sprintf("delete private service connect for subscription %d", subscription) + path := fmt.Sprintf("/subscriptions/%d/private-service-connect", subscription) + err := a.delete(ctx, message, path) + if err != nil { + return wrap404Error(subscription, err) + } + return nil +} + +func (a *API) DeleteActiveActiveService(ctx context.Context, subscription int, regionId int) error { + message := fmt.Sprintf("delete private service connect for subscription %d in region %d", subscription, regionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-service-connect", subscription, regionId) + err := a.delete(ctx, message, path) + if err != nil { + return wrap404ErrorActiveActive(subscription, regionId, err) + } + return nil +} + +func (a *API) DeleteEndpoint(ctx context.Context, subscription int, pscServiceId int, + endpointId int) error { + message := fmt.Sprintf("delete private service connect endpoint %d/%d for subscription %d", pscServiceId, endpointId, subscription) + path := fmt.Sprintf("/subscriptions/%d/private-service-connect/%d/endpoints/%d", subscription, pscServiceId, endpointId) + err := a.delete(ctx, message, path) + if err != nil { + return wrap404Error(subscription, err) + } + return nil +} + +func (a *API) DeleteActiveActiveEndpoint(ctx context.Context, subscription int, regionId int, pscServiceId int, + endpointId int) error { + message := fmt.Sprintf("delete private service connect endpoint %d/%d for subscription %d in region %d", pscServiceId, endpointId, subscription, regionId) + path := fmt.Sprintf("/subscriptions/%d/regions/%d/private-service-connect/%d/endpoints/%d", subscription, regionId, pscServiceId, endpointId) + err := a.delete(ctx, message, path) + if err != nil { + return wrap404ErrorActiveActive(subscription, regionId, err) + } + return nil +} + +func (a *API) getService(ctx context.Context, message string, path string) (*PrivateServiceConnectService, error) { + var task internal.TaskResponse + err := a.client.Get(ctx, message, path, &task) + if err != nil { + return nil, err + } + + a.logger.Printf("Waiting for private service connect request %d to complete", task.ID) + + var response PrivateServiceConnectService + err = a.taskWaiter.WaitForResource(ctx, *task.ID, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (a *API) getEndpoints(ctx context.Context, message string, path string) (*PrivateServiceConnectEndpoints, error) { + var task internal.TaskResponse + err := a.client.Get(ctx, message, path, &task) + if err != nil { + return nil, err + } + + a.logger.Printf("Waiting for private service connect request %d to complete", task.ID) + + var response PrivateServiceConnectEndpoints + err = a.taskWaiter.WaitForResource(ctx, *task.ID, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (a *API) getCreationScript(ctx context.Context, message string, path string, values url.Values) (*CreationScript, error) { + var task internal.TaskResponse + err := a.client.GetWithQuery(ctx, message, path, values, &task) + if err != nil { + return nil, err + } + + a.logger.Printf("Waiting for private service connect creation script request %d to complete", task.ID) + + var response CreationScript + err = a.taskWaiter.WaitForResource(ctx, *task.ID, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (a *API) getDeletionScript(ctx context.Context, message string, path string) (*DeletionScript, error) { + var task internal.TaskResponse + err := a.client.Get(ctx, message, path, &task) + if err != nil { + return nil, err + } + + a.logger.Printf("Waiting for private service connect deletion script request %d to complete", task.ID) + + var response DeletionScript + err = a.taskWaiter.WaitForResource(ctx, *task.ID, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (a *API) create(ctx context.Context, message string, path string) (int, error) { + var task internal.TaskResponse + err := a.client.Post(ctx, message, path, nil, &task) + if err != nil { + return 0, err + } + + a.logger.Printf("Waiting for task %s to finish creating the Private Service Connect", task) + + id, err := a.taskWaiter.WaitForResourceId(ctx, *task.ID) + if err != nil { + return 0, fmt.Errorf("failed when creating Private Service Connect %d: %w", id, err) + } + + return id, nil +} + +func (a *API) update(ctx context.Context, message string, path string, body any) error { + var task internal.TaskResponse + err := a.client.Put(ctx, message, path, body, &task) + if err != nil { + return err + } + + a.logger.Printf("Waiting for task %s to finish updating the Private Service Connect", task) + + err = a.taskWaiter.Wait(ctx, *task.ID) + if err != nil { + return fmt.Errorf("failed when updating Private Service Connect %w", err) + } + + return nil +} + +func (a *API) delete(ctx context.Context, message string, path string) error { + var task internal.TaskResponse + err := a.client.Delete(ctx, message, path, &task) + if err != nil { + return err + } + + a.logger.Printf("Waiting for task %s to finish deleting the Private Service Connect", task) + + err = a.taskWaiter.Wait(ctx, *task.ID) + if err != nil { + return fmt.Errorf("failed when deleting Private Service Connect %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 &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 +} diff --git a/util_test.go b/util_test.go index f78735e..7d0df5a 100644 --- a/util_test.go +++ b/util_test.go @@ -142,6 +142,16 @@ func deleteRequest(t *testing.T, path string, body string) endpointRequest { } } +func deleteRequestWithStatus(t *testing.T, path string, status int, body string) endpointRequest { + return endpointRequest{ + method: http.MethodDelete, + path: path, + body: body, + status: status, + t: t, + } +} + func postRequest(t *testing.T, path string, request string, body string) endpointRequest { return endpointRequest{ method: http.MethodPost,