Skip to content

Commit

Permalink
feat: add customizable removal check for Request resource
Browse files Browse the repository at this point in the history
Signed-off-by: Ariel Septon <[email protected]>
  • Loading branch information
Ariel Septon authored and Ariel Septon committed Nov 27, 2024
1 parent 4758bde commit 862bef6
Show file tree
Hide file tree
Showing 20 changed files with 816 additions and 266 deletions.
5 changes: 4 additions & 1 deletion apis/request/v1alpha2/request_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@ type RequestParameters struct {
// SecretInjectionConfig specifies the secrets receiving patches for response data.
SecretInjectionConfigs []SecretInjectionConfig `json:"secretInjectionConfigs,omitempty"`

// ExpectedResponseCheck specifies the mechanism to validate the GET response against expected value.
// ExpectedResponseCheck specifies the mechanism to validate the OBSERVE response against expected value.
ExpectedResponseCheck ExpectedResponseCheck `json:"expectedResponseCheck,omitempty"`

// IsRemovedCheck specifies the mechanism to validate the OBSERVE response after removal against expected value.
IsRemovedCheck ExpectedResponseCheck `json:"isRemovedCheck,omitempty"`
}

type Mapping struct {
Expand Down
1 change: 1 addition & 0 deletions apis/request/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cluster/test/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ spec:
spec:
containers:
- name: flask-api
image: arielsepton/flask-api:latest
image: arielsepton/flask-api:v1.0.0
ports:
- containerPort: 5000
---
Expand Down
15 changes: 15 additions & 0 deletions examples/sample/request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ spec:

# Scenario 3: Method specified, action not specified (PUT implies UPDATE)
- method: "PUT"
# action: UPDATE
body: |
{
email: .payload.body.email,
Expand Down Expand Up @@ -76,6 +77,20 @@ spec:
else false
end
# isRemovedCheck is optional. If not specified or if the type is "DEFAULT",
# the resource is considered removed if the OBSERVE response after REMOVE has 404 status code.
# If specified, the JQ logic determines if the resource is removed:
# - If the JQ query is false, a REMOVE request is sent to remove the resource.
# - If true, the resource is considered up to date.
isRemovedCheck:
type: CUSTOM
logic: |
if .response.statusCode == 404
and .response.body.error == "User not found"
then true
else false
end
# Secrets receiving patches from response data
secretInjectionConfigs:
- secretRef:
Expand Down
40 changes: 27 additions & 13 deletions internal/controller/request/observe.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,17 @@ 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/observe"
"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"
)

const (
errObjectNotFound = "object wasn't found"
errNotValidJSON = "%s is not a valid JSON string: %s"
errConvertResToMap = "failed to convert response to map"
ErrExpectedFormat = "expectedResponseCheck.Logic JQ filter should return a boolean, but returned error: %s"
errExpectedResponseCheckType = "expectedResponseCheck.Type should be either DEFAULT, CUSTOM or empty"
errExpectedResponseCheckType = "%s.Type should be either DEFAULT, CUSTOM or empty"
)

type ObserveRequestDetails struct {
Expand Down Expand Up @@ -47,33 +46,48 @@ func FailedObserve() ObserveRequestDetails {
// isUpToDate checks whether desired spec up to date with the observed state for a given request
func (c *external) isUpToDate(ctx context.Context, cr *v1alpha2.Request) (ObserveRequestDetails, error) {
if !c.isObjectValidForObservation(cr) {
return FailedObserve(), errors.New(errObjectNotFound)
return FailedObserve(), errors.New(observe.ErrObjectNotFound)
}

mapping, err := requestmapping.GetMapping(&cr.Spec.ForProvider, v1alpha2.ActionObserve, c.logger)
if err != nil {
return FailedObserve(), err
}

requestDetails, err := c.generateValidRequestDetails(ctx, cr, mapping)
requestDetails, err := requestgen.GenerateValidRequestDetails(ctx, cr, mapping, c.localKube, c.logger)
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)
if err := c.determineIfRemoved(ctx, cr, details, responseErr); err != nil {
return FailedObserve(), err
}

c.patchResponseToSecret(ctx, cr, &details.HttpResponse)
return c.determineResponseCheck(ctx, cr, details, responseErr)
return c.determineIfUpToDate(ctx, cr, details, responseErr)
}

// determineIfUpToDate determines if the object is up to date based on the response check.
func (c *external) determineIfUpToDate(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) (ObserveRequestDetails, error) {
responseChecker := observe.GetIsUpToDateResponseCheck(cr, c.localKube, c.logger, c.http)
if responseChecker == nil {
return FailedObserve(), errors.Errorf(errExpectedResponseCheckType, "expectedResponseCheck")
}

result, err := responseChecker.Check(ctx, cr, details, responseErr)
if err != nil {
return FailedObserve(), err
}

return NewObserve(details, responseErr, result), nil
}

// determineResponseCheck determines the response check based on the expectedResponseCheck.Type
func (c *external) determineResponseCheck(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) (ObserveRequestDetails, error) {
responseChecker := c.getResponseCheck(cr)
// determineIfRemoved determines if the object is removed based on the response check.
func (c *external) determineIfRemoved(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) error {
responseChecker := observe.GetIsRemovedResponseCheck(cr, c.localKube, c.logger, c.http)
if responseChecker == nil {
return FailedObserve(), errors.New(errExpectedResponseCheckType)
return errors.Errorf(errExpectedResponseCheckType, "isRemovedCheck")
}

return responseChecker.Check(ctx, cr, details, responseErr)
Expand All @@ -92,5 +106,5 @@ func (c *external) requestDetails(ctx context.Context, cr *v1alpha2.Request, act
return requestgen.RequestDetails{}, err
}

return c.generateValidRequestDetails(ctx, cr, mapping)
return requestgen.GenerateValidRequestDetails(ctx, cr, mapping, c.localKube, c.logger)
}
77 changes: 77 additions & 0 deletions internal/controller/request/observe/is_deleted_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package observe

import (
"context"
"net/http"

"github.com/crossplane-contrib/provider-http/apis/request/v1alpha2"
httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/pkg/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
ErrObjectNotFound = "object wasn't found"
)

// isDeletedCheck is an interface for performing isDeleted checks.
type isDeletedCheck interface {
Check(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) error
}

// defaultIsRemovedResponseCheck performs a default comparison between the response and desired state.
type defaultIsRemovedResponseCheck struct {
localKube client.Client
logger logging.Logger
http httpClient.Client
}

// Check performs a default comparison between the response and desired state.
func (d *defaultIsRemovedResponseCheck) Check(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) error {
if details.HttpResponse.StatusCode == http.StatusNotFound {
return errors.New(ErrObjectNotFound)
}

return nil
}

// // customIsRemovedResponseCheck performs a custom response check using JQ logic.
type customIsRemovedResponseCheck struct {
localKube client.Client
logger logging.Logger
http httpClient.Client
}

// Check performs a custom response check using JQ logic.
func (c *customIsRemovedResponseCheck) Check(ctx context.Context, cr *v1alpha2.Request, details httpClient.HttpDetails, responseErr error) error {
logic := cr.Spec.ForProvider.IsRemovedCheck.Logic
customCheck := &customCheck{localKube: c.localKube, logger: c.logger, http: c.http}

isRemoved, err := customCheck.check(ctx, cr, details, logic)
if err != nil {
return errors.Errorf(errExpectedFormat, "isRemovedCheck", err.Error())
} else if isRemoved {
return errors.New(ErrObjectNotFound)
}

return nil
}

// isRemovedCheckFactoryMap is a map that associates each check type with its corresponding factory function.
var isRemovedCheckFactoryMap = map[string]func(localKube client.Client, logger logging.Logger, http httpClient.Client) isDeletedCheck{
v1alpha2.ExpectedResponseCheckTypeDefault: func(localKube client.Client, logger logging.Logger, http httpClient.Client) isDeletedCheck {
return &defaultIsRemovedResponseCheck{localKube: localKube, logger: logger, http: http}
},
v1alpha2.ExpectedResponseCheckTypeCustom: func(localKube client.Client, logger logging.Logger, http httpClient.Client) isDeletedCheck {
return &customIsRemovedResponseCheck{localKube: localKube, logger: logger, http: http}
},
}

// GetIsRemovedResponseCheck uses a map to select and return the appropriate ResponseCheck.
func GetIsRemovedResponseCheck(cr *v1alpha2.Request, localKube client.Client, logger logging.Logger, http httpClient.Client) isDeletedCheck {
if factory, ok := isRemovedCheckFactoryMap[cr.Spec.ForProvider.IsRemovedCheck.Type]; ok {
return factory(localKube, logger, http)
}
return isRemovedCheckFactoryMap[v1alpha2.ExpectedResponseCheckTypeDefault](localKube, logger, http)
}
Loading

0 comments on commit 862bef6

Please sign in to comment.