diff --git a/apis/request/v1alpha2/request_types.go b/apis/request/v1alpha2/request_types.go index 37b4386..c41a391 100644 --- a/apis/request/v1alpha2/request_types.go +++ b/apis/request/v1alpha2/request_types.go @@ -30,6 +30,13 @@ const ( ExpectedResponseCheckTypeCustom = "CUSTOM" ) +const ( + ActionCreate = "CREATE" + ActionObserve = "OBSERVE" + ActionUpdate = "UPDATE" + ActionRemove = "REMOVE" +) + // RequestParameters are the configurable fields of a Request. type RequestParameters struct { // Mappings defines the HTTP mappings for different methods. @@ -55,9 +62,14 @@ type RequestParameters struct { } type Mapping struct { + // Either Method or Action must be specified. If both are omitted, the mapping will not be used. // +kubebuilder:validation:Enum=POST;GET;PUT;DELETE // Method specifies the HTTP method for the request. - Method string `json:"method"` + Method string `json:"method,omitempty"` + + // +kubebuilder:validation:Enum=CREATE;OBSERVE;UPDATE;REMOVE + // Action specifies the intended action for the request. + Action string `json:"action,omitempty"` // Body specifies the body of the request. Body string `json:"body,omitempty"` diff --git a/examples/sample/request.yaml b/examples/sample/request.yaml index 67120e4..1bf689b 100644 --- a/examples/sample/request.yaml +++ b/examples/sample/request.yaml @@ -22,7 +22,9 @@ spec: "age": 30 } mappings: - - method: "POST" + # Scenario 1: Action specified, method not specified (defaults to POST for CREATE) + - action: CREATE + # method: "POST" body: | { username: .payload.body.username, @@ -38,8 +40,13 @@ spec: - ("Bearer {{ auth:default:token }}") Extra-Header-For-Post: - extra-value - - method: "GET" + + # Scenario 2: Action specified, method not specified (defaults to GET for OBSERVE) + - action: OBSERVE + # method: "GET" url: (.payload.baseUrl + "/" + (.response.body.id|tostring)) + + # Scenario 3: Method specified, action not specified (PUT implies UPDATE) - method: "PUT" body: | { @@ -47,7 +54,10 @@ spec: age: .payload.body.age } url: (.payload.baseUrl + "/" + (.response.body.id|tostring)) - - method: "DELETE" + + # Scenario 4: Action specified, method not specified (defaults to DELETE for REMOVE) + - action: REMOVE + # method: "DELETE" url: (.payload.baseUrl + "/" + (.response.body.id|tostring)) # expectedResponseCheck is optional. If not specified or if the type is "DEFAULT", diff --git a/internal/controller/request/observe.go b/internal/controller/request/observe.go index c98d241..034cf97 100644 --- a/internal/controller/request/observe.go +++ b/internal/controller/request/observe.go @@ -7,6 +7,7 @@ import ( "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/request/requestmapping" "github.com/crossplane-contrib/provider-http/internal/utils" "github.com/pkg/errors" ) @@ -49,12 +50,17 @@ func (c *external) isUpToDate(ctx context.Context, cr *v1alpha2.Request) (Observ return FailedObserve(), errors.New(errObjectNotFound) } - requestDetails, err := c.requestDetails(ctx, cr, http.MethodGet) + mapping, err := requestmapping.GetMapping(&cr.Spec.ForProvider, v1alpha2.ActionObserve, c.logger) if err != nil { return FailedObserve(), err } - details, responseErr := c.http.SendRequest(ctx, http.MethodGet, requestDetails.Url, requestDetails.Body, requestDetails.Headers, cr.Spec.ForProvider.InsecureSkipTLSVerify) + requestDetails, err := c.generateValidRequestDetails(ctx, cr, mapping) + if err != nil { + return FailedObserve(), err + } + + details, responseErr := c.http.SendRequest(ctx, mapping.Method, requestDetails.Url, requestDetails.Body, requestDetails.Headers, cr.Spec.ForProvider.InsecureSkipTLSVerify) if details.HttpResponse.StatusCode == http.StatusNotFound { return FailedObserve(), errors.New(errObjectNotFound) } @@ -79,11 +85,11 @@ func (c *external) isObjectValidForObservation(cr *v1alpha2.Request) bool { !(cr.Status.RequestDetails.Method == http.MethodPost && utils.IsHTTPError(cr.Status.Response.StatusCode)) } -// requestDetails generates the request details for a given request -func (c *external) requestDetails(ctx context.Context, cr *v1alpha2.Request, method string) (requestgen.RequestDetails, error) { - mapping, ok := getMappingByMethod(&cr.Spec.ForProvider, method) - if !ok { - return requestgen.RequestDetails{}, errors.Errorf(errMappingNotFound, method) +// requestDetails generates the request details for a given method or action. +func (c *external) requestDetails(ctx context.Context, cr *v1alpha2.Request, action string) (requestgen.RequestDetails, error) { + mapping, err := requestmapping.GetMapping(&cr.Spec.ForProvider, action, c.logger) + if err != nil { + return requestgen.RequestDetails{}, err } return c.generateValidRequestDetails(ctx, cr, mapping) diff --git a/internal/controller/request/observe_test.go b/internal/controller/request/observe_test.go index a7f4ce9..456595e 100644 --- a/internal/controller/request/observe_test.go +++ b/internal/controller/request/observe_test.go @@ -8,6 +8,7 @@ import ( "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/request/requestmapping" "github.com/crossplane/crossplane-runtime/pkg/logging" "github.com/crossplane/crossplane-runtime/pkg/test" "github.com/google/go-cmp/cmp" @@ -19,6 +20,30 @@ var ( errNotFound = errors.New(errObjectNotFound) ) +var ( + testPostMapping = v1alpha2.Mapping{ + Method: "POST", + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + } + + testPutMapping = v1alpha2.Mapping{ + Method: "PUT", + Body: "{ username: \"john_doe_new_username\" }", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } + + testGetMapping = v1alpha2.Mapping{ + Method: "GET", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } + + testDeleteMapping = v1alpha2.Mapping{ + Method: "DELETE", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } +) + func Test_isUpToDate(t *testing.T) { type args struct { http httpClient.Client @@ -537,7 +562,7 @@ func Test_requestDetails(t *testing.T) { type args struct { ctx context.Context cr *v1alpha2.Request - method string + action string } type want struct { @@ -565,7 +590,7 @@ func Test_requestDetails(t *testing.T) { }, }, }, - method: "GET", + action: v1alpha2.ActionObserve, }, want: want{ result: requestgen.RequestDetails{ @@ -597,7 +622,7 @@ func Test_requestDetails(t *testing.T) { }, }, }, - method: "POST", + action: v1alpha2.ActionCreate, }, want: want{ result: requestgen.RequestDetails{ @@ -619,15 +644,14 @@ func Test_requestDetails(t *testing.T) { ctx: context.Background(), cr: &v1alpha2.Request{ Spec: v1alpha2.RequestSpec{ - ForProvider: v1alpha2.RequestParameters{}, }, }, - method: "UNKNOWN_METHOD", + action: "UNKNOWN_METHOD", }, want: want{ result: requestgen.RequestDetails{}, - err: errors.Errorf(errMappingNotFound, "UNKNOWN_METHOD"), + err: errors.Errorf(requestmapping.ErrMappingNotFound, "UNKNOWN_METHOD", http.MethodGet), }, }, } @@ -636,9 +660,11 @@ func Test_requestDetails(t *testing.T) { tc := tc t.Run(name, func(t *testing.T) { - e := &external{} + e := &external{ + logger: logging.NewNopLogger(), + } - got, gotErr := e.requestDetails(tc.args.ctx, tc.args.cr, tc.args.method) + got, gotErr := e.requestDetails(tc.args.ctx, tc.args.cr, tc.args.action) if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { t.Fatalf("requestDetails(...): -want error, +got error: %s", diff) } diff --git a/internal/controller/request/request.go b/internal/controller/request/request.go index 4ea885a..3ce79fe 100644 --- a/internal/controller/request/request.go +++ b/internal/controller/request/request.go @@ -19,7 +19,6 @@ package request import ( "context" "fmt" - "net/http" "time" "github.com/crossplane/crossplane-runtime/pkg/logging" @@ -39,6 +38,7 @@ import ( apisv1alpha1 "github.com/crossplane-contrib/provider-http/apis/v1alpha1" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/request/requestmapping" "github.com/crossplane-contrib/provider-http/internal/controller/request/statushandler" datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/utils" @@ -54,7 +54,6 @@ const ( errFailedToCheckIfUpToDate = "failed to check if request is up to date" errFailedToUpdateStatusFailures = "failed to reset status failures counter" errFailedUpdateStatusConditions = "failed updating status conditions" - errMappingNotFound = "%s mapping doesn't exist in request, skipping operation" errPatchDataToSecret = "Warning, couldn't patch data from request to secret %s:%s:%s, error: %s" errGetLatestVersion = "failed to get the latest version of the resource" ) @@ -184,10 +183,10 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex } // deployAction executes the action based on the given Request resource and Mapping configuration. -func (c *external) deployAction(ctx context.Context, cr *v1alpha2.Request, method string) error { - mapping, ok := getMappingByMethod(&cr.Spec.ForProvider, method) - if !ok { - c.logger.Info(fmt.Sprintf(errMappingNotFound, method)) +func (c *external) deployAction(ctx context.Context, cr *v1alpha2.Request, action string) error { + mapping, err := requestmapping.GetMapping(&cr.Spec.ForProvider, action, c.logger) + if err != nil { + c.logger.Info(err.Error()) return nil } @@ -213,7 +212,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalCreation{}, errors.New(errNotRequest) } - return managed.ExternalCreation{}, errors.Wrap(c.deployAction(ctx, cr, http.MethodPost), errFailedToSendHttpRequest) + return managed.ExternalCreation{}, errors.Wrap(c.deployAction(ctx, cr, v1alpha2.ActionCreate), errFailedToSendHttpRequest) } func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { @@ -222,7 +221,7 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalUpdate{}, errors.New(errNotRequest) } - return managed.ExternalUpdate{}, errors.Wrap(c.deployAction(ctx, cr, http.MethodPut), errFailedToSendHttpRequest) + return managed.ExternalUpdate{}, errors.Wrap(c.deployAction(ctx, cr, v1alpha2.ActionUpdate), errFailedToSendHttpRequest) } func (c *external) Delete(ctx context.Context, mg resource.Managed) error { @@ -231,7 +230,7 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { return errors.New(errNotRequest) } - return errors.Wrap(c.deployAction(ctx, cr, http.MethodDelete), errFailedToSendHttpRequest) + return errors.Wrap(c.deployAction(ctx, cr, v1alpha2.ActionRemove), errFailedToSendHttpRequest) } // patchResponseToSecret patches the response data to the secret based on the given Request resource and Mapping configuration. diff --git a/internal/controller/request/requestmapping/mapping.go b/internal/controller/request/requestmapping/mapping.go new file mode 100644 index 0000000..5a4599f --- /dev/null +++ b/internal/controller/request/requestmapping/mapping.go @@ -0,0 +1,73 @@ +package requestmapping + +import ( + "fmt" + "net/http" + + "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/pkg/errors" +) + +const ( + ErrMappingNotFound = "%s or %s mapping doesn't exist in request, skipping operation" +) + +var ( + // actionToMathodFactoryMap maps action to the default corresponding HTTP method. + actionToMathodFactoryMap = map[string]string{ + v1alpha2.ActionCreate: http.MethodPost, + v1alpha2.ActionObserve: http.MethodGet, + v1alpha2.ActionUpdate: http.MethodPut, + v1alpha2.ActionRemove: http.MethodDelete, + } +) + +// getMappingByMethod returns the mapping for the given method from the request parameters. +func getMappingByMethod(requestParams *v1alpha2.RequestParameters, method string) (*v1alpha2.Mapping, bool) { + for _, mapping := range requestParams.Mappings { + if mapping.Method == method { + return &mapping, true + } + } + return nil, false +} + +// getMappingByAction returns the mapping for the given action from the request parameters. +func getMappingByAction(requestParams *v1alpha2.RequestParameters, action string) (*v1alpha2.Mapping, bool) { + for _, mapping := range requestParams.Mappings { + if mapping.Action == action { + return &mapping, true + } + } + return nil, false +} + +// GetMapping retrieves the mapping based on the provided request parameters, method, and action. +// It first attempts to find the mapping by the specified action. If found, it sets the method if it's not defined. +// If no action is specified or the mapping by action is not found, it falls back to finding the mapping by the default method. +func GetMapping(requestParams *v1alpha2.RequestParameters, action string, logger logging.Logger) (*v1alpha2.Mapping, error) { + method := getDefaultMethodByAction(action) + if mapping, found := getMappingByAction(requestParams, action); found { + if mapping.Method == "" { + mapping.Method = method + } + return mapping, nil + } + + logger.Debug(fmt.Sprintf("Mapping not found for action %s, trying to find mapping by method %s", action, method)) + if mapping, found := getMappingByMethod(requestParams, method); found { + return mapping, nil + } + + return nil, errors.Errorf(ErrMappingNotFound, action, method) +} + +// getDefaultMethodByAction returns the default HTTP method for the given action. +func getDefaultMethodByAction(action string) string { + if defaultAction, ok := actionToMathodFactoryMap[action]; ok { + return defaultAction + } + + return http.MethodGet +} diff --git a/internal/controller/request/requestmapping/mapping_test.go b/internal/controller/request/requestmapping/mapping_test.go new file mode 100644 index 0000000..4ff9a9c --- /dev/null +++ b/internal/controller/request/requestmapping/mapping_test.go @@ -0,0 +1,362 @@ +package requestmapping + +import ( + "net/http" + "testing" + + "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" + "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" +) + +var ( + testPostMapping = v1alpha2.Mapping{ + Method: "POST", + Action: v1alpha2.ActionCreate, + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + } + + testPutMapping = v1alpha2.Mapping{ + Method: "PUT", + Action: v1alpha2.ActionUpdate, + Body: "{ username: \"john_doe_new_username\" }", + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } + + testGetMapping = v1alpha2.Mapping{ + Method: "GET", + Action: v1alpha2.ActionObserve, + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } + + testDeleteMapping = v1alpha2.Mapping{ + Method: "DELETE", + Action: v1alpha2.ActionRemove, + URL: "(.payload.baseUrl + \"/\" + .response.body.id)", + } +) + +func Test_getMappingByMethod(t *testing.T) { + type args struct { + requestParams *v1alpha2.RequestParameters + method string + } + type want struct { + mapping *v1alpha2.Mapping + ok bool + } + cases := map[string]struct { + args args + want want + }{ + "Fail": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + method: "POST", + }, + want: want{ + mapping: nil, + ok: false, + }, + }, + "Success": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testPostMapping, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + method: "POST", + }, + want: want{ + mapping: &testPostMapping, + ok: true, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, ok := getMappingByMethod(tc.args.requestParams, tc.args.method) + if diff := cmp.Diff(tc.want.mapping, got); diff != "" { + t.Fatalf("getMappingByMethod(...): -want result, +got result: %s", diff) + } + + if diff := cmp.Diff(tc.want.ok, ok); diff != "" { + t.Fatalf("getMappingByMethod(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_getMappingByAction(t *testing.T) { + type args struct { + requestParams *v1alpha2.RequestParameters + action string + } + type want struct { + mapping *v1alpha2.Mapping + ok bool + } + cases := map[string]struct { + args args + want want + }{ + "Fail": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: nil, + ok: false, + }, + }, + "Success": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testPostMapping, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: &testPostMapping, + ok: true, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, ok := getMappingByAction(tc.args.requestParams, tc.args.action) + if diff := cmp.Diff(tc.want.mapping, got); diff != "" { + t.Fatalf("getMappingByAction(...): -want result, +got result: %s", diff) + } + + if diff := cmp.Diff(tc.want.ok, ok); diff != "" { + t.Fatalf("getMappingByAction(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_GetMapping(t *testing.T) { + type args struct { + requestParams *v1alpha2.RequestParameters + action string + } + type want struct { + mapping *v1alpha2.Mapping + err error + } + cases := map[string]struct { + args args + want want + }{ + "Fail": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: nil, + err: errors.Errorf(ErrMappingNotFound, v1alpha2.ActionCreate, http.MethodPost), + }, + }, + "Success": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + testPostMapping, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: &testPostMapping, + err: nil, + }, + }, + "SuccessWithoutMethod": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + { + Action: v1alpha2.ActionCreate, + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + }, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: &testPostMapping, + err: nil, + }, + }, + "SuccessWithoutAction": { + args: args{ + requestParams: &v1alpha2.RequestParameters{ + Payload: v1alpha2.Payload{ + Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", + BaseUrl: "https://api.example.com/users", + }, + Mappings: []v1alpha2.Mapping{ + { + Method: http.MethodPost, + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + }, + testGetMapping, + testPutMapping, + testDeleteMapping, + }, + }, + action: v1alpha2.ActionCreate, + }, + want: want{ + mapping: &v1alpha2.Mapping{ + Method: http.MethodPost, + Body: "{ username: .payload.body.username, email: .payload.body.email }", + URL: ".payload.baseUrl", + }, + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, gotErr := GetMapping(tc.args.requestParams, tc.args.action, logging.NewNopLogger()) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("isUpToDate(...): -want error, +got error: %s", diff) + } + if diff := cmp.Diff(tc.want.mapping, got); diff != "" { + t.Fatalf("GetMapping(...): -want result, +got result: %s", diff) + } + }) + } +} + +func Test_getDefaultMethodByAction(t *testing.T) { + type args struct { + action string + } + type want struct { + method string + } + cases := map[string]struct { + args args + want want + }{ + "ShouldReturnPostMethod": { + args: args{ + action: v1alpha2.ActionCreate, + }, + want: want{ + method: http.MethodPost, + }, + }, + "ShouldReturnGetMethod": { + args: args{ + action: v1alpha2.ActionObserve, + }, + want: want{ + method: http.MethodGet, + }, + }, + "ShouldReturnPutMethod": { + args: args{ + action: v1alpha2.ActionUpdate, + }, + want: want{ + method: http.MethodPut, + }, + }, + "ShouldReturnDeleteMethod": { + args: args{ + action: v1alpha2.ActionRemove, + }, + want: want{ + method: http.MethodDelete, + }, + }, + "ShouldReturnGetMethodByDefault": { + args: args{ + action: "UNKNOWN", + }, + want: want{ + method: http.MethodGet, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := getDefaultMethodByAction(tc.args.action) + if diff := cmp.Diff(tc.want.method, got); diff != "" { + t.Fatalf("getDefaultMethodByAction(...): -want result, +got result: %s", diff) + } + }) + } +} diff --git a/internal/controller/request/response_check.go b/internal/controller/request/response_check.go index 12897d7..e5294e6 100644 --- a/internal/controller/request/response_check.go +++ b/internal/controller/request/response_check.go @@ -9,6 +9,7 @@ import ( "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane-contrib/provider-http/internal/controller/request/requestgen" + "github.com/crossplane-contrib/provider-http/internal/controller/request/requestmapping" "github.com/crossplane-contrib/provider-http/internal/controller/request/responseconverter" datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/jq" @@ -102,7 +103,7 @@ func (d *DefaultResponseCheck) compareJSON(body, desiredState string, statusCode // desiredState returns the desired state for a given request func (d *DefaultResponseCheck) desiredState(ctx context.Context, cr *v1alpha2.Request) (string, error) { - requestDetails, err := d.client.requestDetails(ctx, cr, http.MethodPut) + requestDetails, err := d.client.requestDetails(ctx, cr, v1alpha2.ActionUpdate) if err != nil { return "", err } @@ -162,5 +163,5 @@ func (c *external) getResponseCheck(cr *v1alpha2.Request) ResponseCheck { // isErrorMappingNotFound checks if the provided error indicates that the // mapping for an HTTP PUT request is not found. func isErrorMappingNotFound(err error) bool { - return errors.Cause(err).Error() == fmt.Sprintf(errMappingNotFound, http.MethodPut) + return errors.Cause(err).Error() == fmt.Sprintf(requestmapping.ErrMappingNotFound, v1alpha2.ActionUpdate, http.MethodPut) } diff --git a/internal/controller/request/response_check_test.go b/internal/controller/request/response_check_test.go index f353857..a5016ef 100644 --- a/internal/controller/request/response_check_test.go +++ b/internal/controller/request/response_check_test.go @@ -139,7 +139,7 @@ func Test_DefaultResponseCheck(t *testing.T) { client: &external{ localKube: nil, http: nil, - logger: nil, + logger: logging.NewNopLogger(), }, } got, gotErr := e.Check(tc.args.ctx, tc.args.cr, tc.args.details, tc.args.responseErr) diff --git a/internal/controller/request/utils.go b/internal/controller/request/utils.go deleted file mode 100644 index 65ba4b8..0000000 --- a/internal/controller/request/utils.go +++ /dev/null @@ -1,14 +0,0 @@ -package request - -import ( - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" -) - -func getMappingByMethod(requestParams *v1alpha2.RequestParameters, method string) (*v1alpha2.Mapping, bool) { - for _, mapping := range requestParams.Mappings { - if mapping.Method == method { - return &mapping, true - } - } - return nil, false -} diff --git a/internal/controller/request/utils_test.go b/internal/controller/request/utils_test.go deleted file mode 100644 index c5d41df..0000000 --- a/internal/controller/request/utils_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package request - -import ( - "testing" - - "github.com/crossplane-contrib/provider-http/apis/request/v1alpha2" - "github.com/google/go-cmp/cmp" -) - -var ( - testPostMapping = v1alpha2.Mapping{ - Method: "POST", - Body: "{ username: .payload.body.username, email: .payload.body.email }", - URL: ".payload.baseUrl", - } - - testPutMapping = v1alpha2.Mapping{ - Method: "PUT", - Body: "{ username: \"john_doe_new_username\" }", - URL: "(.payload.baseUrl + \"/\" + .response.body.id)", - } - - testGetMapping = v1alpha2.Mapping{ - Method: "GET", - URL: "(.payload.baseUrl + \"/\" + .response.body.id)", - } - - testDeleteMapping = v1alpha2.Mapping{ - Method: "DELETE", - URL: "(.payload.baseUrl + \"/\" + .response.body.id)", - } -) - -func Test_getMappingByMethod(t *testing.T) { - type args struct { - requestParams *v1alpha2.RequestParameters - method string - } - type want struct { - mapping *v1alpha2.Mapping - ok bool - } - cases := map[string]struct { - args args - want want - }{ - "Fail": { - args: args{ - requestParams: &v1alpha2.RequestParameters{ - Payload: v1alpha2.Payload{ - Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", - BaseUrl: "https://api.example.com/users", - }, - Mappings: []v1alpha2.Mapping{ - testGetMapping, - testPutMapping, - testDeleteMapping, - }, - }, - method: "POST", - }, - want: want{ - mapping: nil, - ok: false, - }, - }, - "Success": { - args: args{ - requestParams: &v1alpha2.RequestParameters{ - Payload: v1alpha2.Payload{ - Body: "{\"username\": \"john_doe\", \"email\": \"john.doe@example.com\"}", - BaseUrl: "https://api.example.com/users", - }, - Mappings: []v1alpha2.Mapping{ - testPostMapping, - testGetMapping, - testPutMapping, - testDeleteMapping, - }, - }, - method: "POST", - }, - want: want{ - mapping: &testPostMapping, - ok: true, - }, - }, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - got, ok := getMappingByMethod(tc.args.requestParams, tc.args.method) - if diff := cmp.Diff(tc.want.mapping, got); diff != "" { - t.Fatalf("getMappingByMethod(...): -want result, +got result: %s", diff) - } - - if diff := cmp.Diff(tc.want.ok, ok); diff != "" { - t.Fatalf("getMappingByMethod(...): -want result, +got result: %s", diff) - } - }) - } -} diff --git a/package/crds/http.crossplane.io_requests.yaml b/package/crds/http.crossplane.io_requests.yaml index 9c12fdf..8df69cb 100644 --- a/package/crds/http.crossplane.io_requests.yaml +++ b/package/crds/http.crossplane.io_requests.yaml @@ -501,6 +501,15 @@ spec: methods. items: properties: + action: + description: Action specifies the intended action for the + request. + enum: + - CREATE + - OBSERVE + - UPDATE + - REMOVE + type: string body: description: Body specifies the body of the request. type: string @@ -512,7 +521,9 @@ spec: description: Headers specifies the headers for the request. type: object method: - description: Method specifies the HTTP method for the request. + description: |- + Either Method or Action must be specified. If both are omitted, the mapping will not be used. + Method specifies the HTTP method for the request. enum: - POST - GET @@ -523,7 +534,6 @@ spec: description: URL specifies the URL for the request. type: string required: - - method - url type: object type: array @@ -837,6 +847,14 @@ spec: type: integer requestDetails: properties: + action: + description: Action specifies the intended action for the request. + enum: + - CREATE + - OBSERVE + - UPDATE + - REMOVE + type: string body: description: Body specifies the body of the request. type: string @@ -848,7 +866,9 @@ spec: description: Headers specifies the headers for the request. type: object method: - description: Method specifies the HTTP method for the request. + description: |- + Either Method or Action must be specified. If both are omitted, the mapping will not be used. + Method specifies the HTTP method for the request. enum: - POST - GET @@ -859,7 +879,6 @@ spec: description: URL specifies the URL for the request. type: string required: - - method - url type: object response: