diff --git a/api/clients/grailfiltersegements/filtersegments.go b/api/clients/grailfiltersegements/filtersegments.go new file mode 100644 index 0000000..bcef195 --- /dev/null +++ b/api/clients/grailfiltersegements/filtersegments.go @@ -0,0 +1,87 @@ +// @license +// Copyright 2024 Dynatrace LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package grailfiltersegements + +import ( + "bytes" + "context" + "fmt" + "net/http" + "net/url" + + "github.com/dynatrace/dynatrace-configuration-as-code-core/api/rest" +) + +const endpointPath = "platform/storage/filter-segments/v1/filter-segments" + +type Client struct { + client *rest.Client +} + +func NewClient(client *rest.Client) *Client { + c := &Client{ + client: client, + } + return c +} + +// Get filter-segment by UID. If querry parameter "add-fields" is not specified, it will be set with next values: "INCLUDES", "VARIABLES", "EXTERNALID", "RESOURCECONTEXT" +func (c Client) Get(ctx context.Context, id string, ro rest.RequestOptions) (*http.Response, error) { + path, err := url.JoinPath(endpointPath, id) + if err != nil { + return nil, fmt.Errorf("failed to create URL: %w", err) + } + + // set default behavior to pick all information by default + if ro.QueryParams == nil && ro.QueryParams.Has("add-fields") { + ro.QueryParams = url.Values{ + "add-fields": []string{"INCLUDES", "VARIABLES", "EXTERNALID", "RESOURCECONTEXT"}, + } + } + + return c.client.GET(ctx, path, ro) +} + +func (c Client) List(ctx context.Context) (*http.Response, error) { + path := endpointPath + ":lean" // minimal set of information is enough + return c.client.GET(ctx, path, rest.RequestOptions{CustomShouldRetryFunc: rest.RetryIfTooManyRequests}) +} + +func (c Client) Create(ctx context.Context, data []byte) (*http.Response, error) { + r, err := c.client.POST(ctx, endpointPath, bytes.NewReader(data), rest.RequestOptions{CustomShouldRetryFunc: rest.RetryIfTooManyRequests}) + if err != nil { + return nil, fmt.Errorf("failed to create new filter segment: %w", err) + } + return r, nil +} + +func (c Client) Update(ctx context.Context, id string, data []byte, ro rest.RequestOptions) (*http.Response, error) { + path, err := url.JoinPath(endpointPath, id) + if err != nil { + return nil, fmt.Errorf("failed to join URL: %w", err) + } + return c.client.PUT(ctx, path, bytes.NewReader(data), ro) +} + +func (c Client) Delete(ctx context.Context, id string) (*http.Response, error) { + if id == "" { + return nil, fmt.Errorf("id must be non-empty") + } + path, err := url.JoinPath(endpointPath, id) + if err != nil { + return nil, fmt.Errorf("failed to create URL: %w", err) + } + return c.client.DELETE(ctx, path, rest.RequestOptions{CustomShouldRetryFunc: rest.RetryIfTooManyRequests}) +} diff --git a/clients/factory.go b/clients/factory.go index eec92fa..8b7b39d 100644 --- a/clients/factory.go +++ b/clients/factory.go @@ -28,6 +28,7 @@ import ( "github.com/dynatrace/dynatrace-configuration-as-code-core/clients/automation" "github.com/dynatrace/dynatrace-configuration-as-code-core/clients/buckets" "github.com/dynatrace/dynatrace-configuration-as-code-core/clients/documents" + "github.com/dynatrace/dynatrace-configuration-as-code-core/clients/grailfiltersegments" "github.com/dynatrace/dynatrace-configuration-as-code-core/clients/openpipeline" "golang.org/x/oauth2/clientcredentials" ) @@ -172,6 +173,15 @@ func (f factory) DocumentClient() (*documents.Client, error) { return documents.NewClient(restClient), nil } +// FilterSegmentsClient creates and returns a new instance of grailfiltersegments.Client for interacting with the grail filter segments API. +func (f factory) FilterSegmentsClient() (*grailfiltersegments.Client, error) { + restClient, err := f.CreatePlatformClient() + if err != nil { + return nil, err + } + return grailfiltersegments.NewClient(restClient), nil +} + // BucketClientWithRetrySettings creates and returns a new instance of buckets.Client with non-default retry settings. // For details about how retry settings are used, see buckets.WithRetrySettings. func (f factory) BucketClientWithRetrySettings(maxRetries int, durationBetweenTries time.Duration, maxWaitDuration time.Duration) (*buckets.Client, error) { diff --git a/clients/grailfiltersegments/export_test.go b/clients/grailfiltersegments/export_test.go new file mode 100644 index 0000000..c45ed85 --- /dev/null +++ b/clients/grailfiltersegments/export_test.go @@ -0,0 +1,19 @@ +// @license +// Copyright 2024 Dynatrace LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package grailfiltersegments + +func NewTestClient(client client) *Client { + return &Client{client: client} +} diff --git a/clients/grailfiltersegments/filtersegment_test.go b/clients/grailfiltersegments/filtersegment_test.go new file mode 100644 index 0000000..570e236 --- /dev/null +++ b/clients/grailfiltersegments/filtersegment_test.go @@ -0,0 +1,474 @@ +// @license +// Copyright 2024 Dynatrace LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package grailfiltersegments_test + +import ( + "context" + "errors" + "io" + "net/http" + "strings" + "testing" + + "github.com/dynatrace/dynatrace-configuration-as-code-core/api" + "github.com/dynatrace/dynatrace-configuration-as-code-core/api/rest" + "github.com/dynatrace/dynatrace-configuration-as-code-core/clients/grailfiltersegments" + "github.com/dynatrace/dynatrace-configuration-as-code-core/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestList(t *testing.T) { + apiResponse := `{ + "filterSegments": [ + { + "uid": "QElQbQcjq3S", + "name": "filter_name", + "isPublic": false, + "owner": "userUUID", + "version": 1 + } + ] +}` + expected := `[ + { + "uid": "QElQbQcjq3S", + "name": "filter_name", + "isPublic": false, + "owner": "userUUID", + "version": 1 + } + ]` + + mockClient := grailfiltersegments.NewMockclient(gomock.NewController(t)) + mockClient.EXPECT(). + List(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(apiResponse)), + }, nil) + + fsClient := grailfiltersegments.NewTestClient(mockClient) + actual, err := fsClient.List(context.Background()) + + require.NoError(t, err) + require.JSONEq(t, expected, string(actual.Data)) +} + +func TestGet(t *testing.T) { + t.Run("no ID given", func(t *testing.T) { + ctx := testutils.ContextWithLogger(t) + fsClient := grailfiltersegments.NewTestClient(grailfiltersegments.NewMockclient(gomock.NewController(t))) + resp, err := fsClient.Get(ctx, "") + + assert.Empty(t, resp) + assert.Error(t, err) + assert.ErrorContains(t, err, "missing required id") + }) + + t.Run("ID not found", func(t *testing.T) { + apiResponse := `{ + "error": { + "code": 404, + "message": "Filter-segment not found", + "errorDetails": [] + } +}` + ctx := testutils.ContextWithLogger(t) + mockClient := grailfiltersegments.NewMockclient(gomock.NewController(t)) + mockClient.EXPECT(). + Get(ctx, "uid", gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(strings.NewReader(apiResponse)), + }, nil) + + fsClient := grailfiltersegments.NewTestClient(mockClient) + resp, err := fsClient.Get(ctx, "uid") + + assert.Empty(t, resp) + assert.ErrorAs(t, err, &api.APIError{}) + + var apiErr api.APIError + errors.As(err, &apiErr) + assert.Equal(t, http.StatusNotFound, apiErr.StatusCode) + assert.Equal(t, apiResponse, string(apiErr.Body)) + }) + + t.Run("found OK", func(t *testing.T) { + apiResponse := `{ + "uid": "D82a1jdA23a", + "name": "dev_environment", + "description": "only includes data of the dev environment", + "variables": { + "type": "query", + "value": "fetch logs | limit 1" + }, + "isPublic": false, + "owner": "john.doe", + "includes": [ + { + "filter": "here goes the filter", + "dataObject": "logs" + }, + { + "filter": "here goes another filter", + "dataObject": "events" + } + ], + "version": 1 +}` + ctx := testutils.ContextWithLogger(t) + mockClient := grailfiltersegments.NewMockclient(gomock.NewController(t)) + mockClient.EXPECT(). + Get(ctx, "uid", gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(apiResponse)), + }, nil) + + fsClient := grailfiltersegments.NewTestClient(mockClient) + resp, err := fsClient.Get(ctx, "uid") + + assert.NotEmpty(t, resp) + assert.NoError(t, err) + assert.Equal(t, apiResponse, string(resp.Data)) + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) +} + +func TestGetAll(t *testing.T) { + + t.Run("fails", func(t *testing.T) { + apiResponse := `{ "err" : "something went wrong" }` + ctx := testutils.ContextWithLogger(t) + mockClient := grailfiltersegments.NewMockclient(gomock.NewController(t)) + mockClient.EXPECT(). + List(ctx). + Return(&http.Response{ + StatusCode: http.StatusBadRequest, + Body: io.NopCloser(strings.NewReader(apiResponse)), + }, nil) + + fsClient := grailfiltersegments.NewTestClient(mockClient) + resp, err := fsClient.GetAll(ctx) + + assert.Empty(t, resp) + assert.ErrorAs(t, err, &api.APIError{}) + + var apiErr api.APIError + errors.As(err, &apiErr) + assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode) + assert.Equal(t, apiResponse, string(apiErr.Body)) + }) + + t.Run("getting individual object fails", func(t *testing.T) { + apiResponse := `{ + "filterSegments": [ + {"uid": "pC7j2sEDzAQ"} + ] +} +` + apiResponse2 := `{ "err" : "something went wrong" }` + ctx := testutils.ContextWithLogger(t) + mockClient := grailfiltersegments.NewMockclient(gomock.NewController(t)) + mockClient.EXPECT(). + List(ctx). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(apiResponse)), + }, nil) + mockClient.EXPECT(). + Get(ctx, "pC7j2sEDzAQ", gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusInternalServerError, + Body: io.NopCloser(strings.NewReader(apiResponse2)), + }, nil) + + fsClient := grailfiltersegments.NewTestClient(mockClient) + resp, err := fsClient.GetAll(ctx) + + assert.Empty(t, resp) + assert.ErrorAs(t, err, &api.APIError{}) + + var apiErr api.APIError + errors.As(err, &apiErr) + assert.Equal(t, http.StatusInternalServerError, apiErr.StatusCode) + assert.Equal(t, apiResponse2, string(apiErr.Body)) + }) + + t.Run("OK", func(t *testing.T) { + apiResponse := `{ + "filterSegments": [ + {"uid": "qW5qn449RsG"}, + {"uid": "pC7j2sEDzAQ"} + ] +} +` + apiResponse2 := ` { + "uid": "qW5qn449RsG", + "name": "dev_environment", + "description": "only includes data of the dev environment", + "variables": {"type": "query", "value": "fetch logs | limit 1"}, + "isPublic": false, + "owner": "2f321c04-566e-4779-b576-3c033b8cd9e9", + "allowedOperations": ["READ", "WRITE", "DELETE"], + "version": 2 + }` + apiResponse3 := ` { + "uid": "pC7j2sEDzAQ", + "name": "dev_environment", + "description": "only includes data of the dev environment", + "variables": {"type": "query", "value": "fetch logs | limit 1"}, + "isPublic": false, + "owner": "2f321c04-566e-4779-b576-3c033b8cd9e9", + "allowedOperations": ["READ", "WRITE", "DELETE"], + "version": 1 + }` + + ctx := testutils.ContextWithLogger(t) + mockClient := grailfiltersegments.NewMockclient(gomock.NewController(t)) + mockClient.EXPECT(). + List(ctx). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(apiResponse)), + }, nil) + mockClient.EXPECT(). + Get(ctx, "qW5qn449RsG", gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(apiResponse2)), + }, nil) + mockClient.EXPECT(). + Get(ctx, "pC7j2sEDzAQ", gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(apiResponse3)), + }, nil) + + fsClient := grailfiltersegments.NewTestClient(mockClient) + resp, err := fsClient.GetAll(ctx) + + assert.NotEmpty(t, resp) + assert.NoError(t, err) + assert.Len(t, resp, 2) + assert.Equal(t, apiResponse2, string(resp[0].Data)) + assert.Equal(t, apiResponse3, string(resp[1].Data)) + + }) +} + +func TestUpsert(t *testing.T) { + + t.Run("Create - Create OK", func(t *testing.T) { + payload := `{ + "uid": "qW5qn449RsG", + "name": "dev_environment", + "description": "only includes data of the dev environment", + "variables": { + "type": "query", + "value": "fetch logs | limit 1" + }, + "isPublic": false, + "owner": "2f321c04-566e-4779-b576-3c033b8cd9e9", + "allowedOperations": [ + "READ", + "WRITE", + "DELETE" + ], + "includes": [] +}` + apiResponse := `{ + "uid": "oKZQWWV0FpR", + "name": "dev_environment", + "description": "only includes data of the dev environment", + "variables": { + "type": "query", + "value": "fetch logs | limit 1" + }, + "isPublic": false, + "owner": "2f321c04-566e-4779-b576-3c033b8cd9e9", + "includes": [], + "version": 1 +}` + + ctx := testutils.ContextWithLogger(t) + mockClient := grailfiltersegments.NewMockclient(gomock.NewController(t)) + mockClient.EXPECT(). + Get(ctx, "uid", gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(strings.NewReader(apiResponse)), + }, nil) + mockClient.EXPECT(). + Create(ctx, []byte(payload)). + Return(&http.Response{ + StatusCode: http.StatusCreated, + Body: io.NopCloser(strings.NewReader(apiResponse)), + }, nil) + + fsClient := grailfiltersegments.NewTestClient(mockClient) + resp, err := fsClient.Upsert(ctx, "uid", []byte(payload)) + + assert.NotEmpty(t, resp) + assert.NoError(t, err) + assert.Equal(t, apiResponse, string(resp.Data)) + assert.Equal(t, http.StatusCreated, resp.StatusCode) + }) + + t.Run("Create - Update OK", func(t *testing.T) { + payload := `{ + "uid": "qW5qn449RsG", + "name": "dev_environment", + "description": "only includes data of the dev environment", + "variables": { + "type": "query", + "value": "fetch logs | limit 1" + }, + "isPublic": false, + "owner": "2f321c04-566e-4779-b576-3c033b8cd9e9", + "allowedOperations": [ + "READ", + "WRITE", + "DELETE" + ], + "includes": [] +}` + + apiExistingResource := `{ + "uid": "D82a1jdA23a", + "name": "dev_environment", + "description": "only includes data of the dev environment", + "variables": { + "type": "query", + "value": "fetch logs | limit 1" + }, + "isPublic": false, + "owner": "john.doe", + "includes": [ + { + "filter": "here goes the filter", + "dataObject": "logs" + }, + { + "filter": "here goes another filter", + "dataObject": "events" + } + ], + "version": 2 +}` + apiResponse := `{ + "uid": "oKZQWWV0FpR", + "name": "dev_environment", + "description": "only includes data of the dev environment", + "variables": { + "type": "query", + "value": "fetch logs | limit 1" + }, + "isPublic": false, + "owner": "2f321c04-566e-4779-b576-3c033b8cd9e9", + "includes": [], + "version": 2 +}` + + ctx := testutils.ContextWithLogger(t) + mockClient := grailfiltersegments.NewMockclient(gomock.NewController(t)) + mockClient.EXPECT(). + Get(ctx, "uid", gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(apiExistingResource)), + }, nil) + mockClient.EXPECT(). + Update(ctx, "uid", []byte(payload), rest.RequestOptions{ + QueryParams: map[string][]string{ + "optimistic-locking-version": {"2"}}, + }). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(apiResponse)), + }, nil) + + fsClient := grailfiltersegments.NewTestClient(mockClient) + resp, err := fsClient.Upsert(ctx, "uid", []byte(payload)) + + assert.NotEmpty(t, resp) + assert.NoError(t, err) + assert.Equal(t, apiResponse, string(resp.Data)) + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + +} + +func TestDelete(t *testing.T) { + t.Run("Delete - no ID given", func(t *testing.T) { + ctx := testutils.ContextWithLogger(t) + fsClient := grailfiltersegments.NewTestClient(grailfiltersegments.NewMockclient(gomock.NewController(t))) + resp, err := fsClient.Delete(ctx, "") + + assert.Empty(t, resp) + assert.Error(t, err) + assert.ErrorContains(t, err, "missing required id") + }) + + t.Run("Delete - ID not found", func(t *testing.T) { + apiResponse := `{ + "error": { + "code": 404, + "message": "Filter-segment not found", + "errorDetails": [] + } + }` + ctx := testutils.ContextWithLogger(t) + mockClient := grailfiltersegments.NewMockclient(gomock.NewController(t)) + mockClient.EXPECT(). + Delete(ctx, "uid"). + Return(&http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(strings.NewReader(apiResponse)), + }, nil) + + fsClient := grailfiltersegments.NewTestClient(mockClient) + resp, err := fsClient.Delete(ctx, "uid") + + assert.Empty(t, resp) + assert.ErrorAs(t, err, &api.APIError{}) + + var apiErr api.APIError + errors.As(err, &apiErr) + assert.Equal(t, http.StatusNotFound, apiErr.StatusCode) + assert.Equal(t, apiResponse, string(apiErr.Body)) + }) + + t.Run("Delete - OK", func(t *testing.T) { + + ctx := testutils.ContextWithLogger(t) + mockClient := grailfiltersegments.NewMockclient(gomock.NewController(t)) + mockClient.EXPECT(). + Delete(ctx, "uid"). + Return(&http.Response{ + StatusCode: http.StatusNoContent, + }, nil) + + fsClient := grailfiltersegments.NewTestClient(mockClient) + resp, err := fsClient.Delete(ctx, "uid") + + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusNoContent) + }) + +} diff --git a/clients/grailfiltersegments/filtersegments.go b/clients/grailfiltersegments/filtersegments.go new file mode 100644 index 0000000..f2295d0 --- /dev/null +++ b/clients/grailfiltersegments/filtersegments.go @@ -0,0 +1,219 @@ +// @license +// Copyright 2024 Dynatrace LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package grailfiltersegments + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/dynatrace/dynatrace-configuration-as-code-core/api" + "github.com/dynatrace/dynatrace-configuration-as-code-core/api/clients/grailfiltersegements" + "github.com/dynatrace/dynatrace-configuration-as-code-core/api/rest" + "github.com/go-logr/logr" +) + +const bodyReadErrMsg = "unable to read API response body" + +type Response = api.Response + +func NewClient(client *rest.Client) *Client { + c := &Client{ + client: grailfiltersegements.NewClient(client), + } + return c +} + +// Client can be used to interact with the Automation API +type Client struct { + client client +} + +//go:generate mockgen -source filtersegments.go -package=grailfiltersegments -destination=client_mock.go +type client interface { + Get(ctx context.Context, id string, ro rest.RequestOptions) (*http.Response, error) + List(ctx context.Context) (*http.Response, error) + Create(ctx context.Context, data []byte) (*http.Response, error) + Update(ctx context.Context, id string, data []byte, ro rest.RequestOptions) (*http.Response, error) + Delete(ctx context.Context, id string) (*http.Response, error) +} + +var _ client = (*grailfiltersegements.Client)(nil) + +func (c Client) Get(ctx context.Context, id string) (Response, error) { + if id == "" { + return Response{}, errors.New("missing required id") + } + + resp, err := c.client.Get(ctx, id, rest.RequestOptions{CustomShouldRetryFunc: rest.RetryIfTooManyRequests}) + if err != nil { + return Response{}, fmt.Errorf("failed to get filtersegment resource with id %s: %w", id, err) + } + return processResponse(resp) +} + +// List gets a complete set of available configs. The Data filed in response is normalized to json list of entries. +func (c Client) List(ctx context.Context) (Response, error) { + resp, err := c.client.List(ctx) + if err != nil { + return Response{}, fmt.Errorf("failed to list filtersegments resources: %w", err) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + logr.FromContextOrDiscard(ctx).Error(err, bodyReadErrMsg) + return Response{}, api.NewAPIErrorFromResponseAndBody(resp, body) + } + if !rest.IsSuccess(resp) { + return Response{}, api.NewAPIErrorFromResponseAndBody(resp, body) + } + { + var tmp map[string]any + if err = json.Unmarshal(body, &tmp); err != nil { + return Response{}, api.NewAPIErrorFromResponseAndBody(resp, body) + } + if body, err = json.Marshal(tmp["filterSegments"]); err != nil { + return Response{}, api.NewAPIErrorFromResponseAndBody(resp, body) + } + } + + return api.NewResponseFromHTTPResponseAndBody(resp, body), nil +} + +func (c Client) GetAll(ctx context.Context) ([]Response, error) { + listResp, err := c.List(ctx) + if err != nil { + return nil, err + } + + type filterSegment struct { + Uid string `json:"uid"` + } + + var fsegments []filterSegment + if err = json.Unmarshal(listResp.Data, &fsegments); err != nil { + return nil, err + } + + var result []Response + for _, f := range fsegments { + resp, err := c.client.Get(ctx, f.Uid, getRequestOptions) + if err != nil { + return nil, fmt.Errorf("failed to get filter segments resource with id %s: %w", f.Uid, err) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + logr.FromContextOrDiscard(ctx).Error(err, bodyReadErrMsg) + return nil, api.NewAPIErrorFromResponseAndBody(resp, body) + } + if !rest.IsSuccess(resp) { + return nil, api.NewAPIErrorFromResponseAndBody(resp, body) + } + + result = append(result, api.NewResponseFromHTTPResponseAndBody(resp, body)) + } + + return result, nil + +} + +func (c Client) Upsert(ctx context.Context, uid string, data []byte) (Response, error) { + existing, err := c.client.Get(ctx, uid, rest.RequestOptions{}) + if err != nil { + return Response{}, fmt.Errorf("failed to get filter segments resource with id %s: %w", uid, err) + } + + if existing.StatusCode == http.StatusNotFound { + resp, err := c.client.Create(ctx, data) + if err != nil { + return Response{}, fmt.Errorf("failed to create filter segments resource: %w", err) + } + return processResponse(resp) + } + + existingResourceBody, err := io.ReadAll(existing.Body) + if err != nil { + logr.FromContextOrDiscard(ctx).Error(err, bodyReadErrMsg) + return Response{}, api.NewAPIErrorFromResponseAndBody(existing, existingResourceBody) + } + + type respWithVersion struct { + Version int `json:"version"` + } + + var currentVersion respWithVersion + err = json.Unmarshal(existingResourceBody, ¤tVersion) + if err != nil { + return Response{}, fmt.Errorf("unable to unmarshal data: %w", err) + } + if currentVersion.Version == 0 { + return Response{}, fmt.Errorf("missing version field in API response") + } + + updateResourceResp, err := c.client.Update(ctx, uid, data, rest.RequestOptions{QueryParams: map[string][]string{ + "optimistic-locking-version": {fmt.Sprint(currentVersion.Version)}, + }}) + if err != nil { + return Response{}, fmt.Errorf("failed to update filter segments resource with id %s and version %d: %w", uid, currentVersion.Version, err) + } + + return processResponse(updateResourceResp) +} + +func processResponse(httpResponse *http.Response) (Response, error) { + if httpResponse != nil && httpResponse.Body != nil { + defer httpResponse.Body.Close() + } + + body, err := io.ReadAll(httpResponse.Body) + if err != nil { + return Response{}, api.NewAPIErrorFromResponseAndBody(httpResponse, body) + } + + if !rest.IsSuccess(httpResponse) { + return Response{}, api.NewAPIErrorFromResponseAndBody(httpResponse, body) + } + + return api.NewResponseFromHTTPResponseAndBody(httpResponse, body), nil +} + +func (c Client) Delete(ctx context.Context, id string) (Response, error) { + if id == "" { + return Response{}, errors.New("missing required id") + } + resp, err := c.client.Delete(ctx, id) + if err != nil { + return Response{}, fmt.Errorf("failed to get filtersegment resource with id %s: %w", id, err) + } + if resp != nil && resp.Body != nil { + defer resp.Body.Close() + } + + if !rest.IsSuccess(resp) { + return Response{}, api.NewAPIErrorFromResponse(resp) + } + return api.NewResponseFromHTTPResponse(resp), nil +} + +var getRequestOptions = rest.RequestOptions{ + CustomShouldRetryFunc: rest.RetryIfTooManyRequests, + QueryParams: map[string][]string{ + "add-fields": {"INCLUDES", "VARIABLES", "RESOURCECONTEXT"}, + }, +}