mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 19:07:30 +00:00
feat: add action v2 execution on requests and responses (#7637)
* feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: split request and response logic to handle the different context information * feat: split request and response logic to handle the different context information * fix: integration test * fix: import alias * fix: refactor execution package * fix: refactor execution interceptor integration and unit tests * fix: refactor execution interceptor integration and unit tests * fix: refactor execution interceptor integration and unit tests * fix: refactor execution interceptor integration and unit tests * fix: refactor execution interceptor integration and unit tests * docs: basic documentation for executions and targets * fix: change order for interceptors * fix: merge back origin/main * fix: change target definition command and query side (#7735) * fix: change target definition command and query side * fix: correct refactoring name changes * fix: correct refactoring name changes * fix: changing execution defintion with target list and type * fix: changing execution definition with target list and type * fix: add back search queries for target and include * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * docs: add example to actions v2 * docs: add example to actions v2 * fix: correct integration tests on query for executions * fix: add separate event for execution v2 as content changed * fix: add separate event for execution v2 as content changed * fix: added review comment changes * fix: added review comment changes * fix: added review comment changes --------- Co-authored-by: adlerhurst <silvan.reusser@gmail.com> * fix: added review comment changes * fix: added review comment changes * Update internal/api/grpc/server/middleware/execution_interceptor.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * fix: added review comment changes * fix: added review comment changes * fix: added review comment changes * fix: added review comment changes * fix: added review comment changes * fix: added review comment changes --------- Co-authored-by: adlerhurst <silvan.reusser@gmail.com> Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
122
internal/execution/execution.go
Normal file
122
internal/execution/execution.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type ContextInfo interface {
|
||||
GetHTTPRequestBody() []byte
|
||||
GetContent() interface{}
|
||||
SetHTTPResponseBody([]byte) error
|
||||
}
|
||||
|
||||
type Target interface {
|
||||
GetTargetID() string
|
||||
IsInterruptOnError() bool
|
||||
GetEndpoint() string
|
||||
GetTargetType() domain.TargetType
|
||||
GetTimeout() time.Duration
|
||||
}
|
||||
|
||||
// CallTargets call a list of targets in order with handling of error and responses
|
||||
func CallTargets(
|
||||
ctx context.Context,
|
||||
targets []Target,
|
||||
info ContextInfo,
|
||||
) (_ interface{}, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer span.EndWithError(err)
|
||||
|
||||
for _, target := range targets {
|
||||
// call the type of target
|
||||
resp, err := CallTarget(ctx, target, info)
|
||||
// handle error if interrupt is set
|
||||
if err != nil && target.IsInterruptOnError() {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp) > 0 {
|
||||
// error in unmarshalling
|
||||
if err := info.SetHTTPResponseBody(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return info.GetContent(), nil
|
||||
}
|
||||
|
||||
type ContextInfoRequest interface {
|
||||
GetHTTPRequestBody() []byte
|
||||
}
|
||||
|
||||
// CallTarget call the desired type of target with handling of responses
|
||||
func CallTarget(
|
||||
ctx context.Context,
|
||||
target Target,
|
||||
info ContextInfoRequest,
|
||||
) (res []byte, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer span.EndWithError(err)
|
||||
|
||||
switch target.GetTargetType() {
|
||||
// get request, ignore response and return request and error for handling in list of targets
|
||||
case domain.TargetTypeWebhook:
|
||||
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody())
|
||||
// get request, return response and error
|
||||
case domain.TargetTypeCall:
|
||||
return call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody())
|
||||
case domain.TargetTypeAsync:
|
||||
go func(target Target, info ContextInfoRequest) {
|
||||
if _, err := call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()); err != nil {
|
||||
logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err)
|
||||
}
|
||||
}(target, info)
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, zerrors.ThrowInternal(nil, "EXEC-auqnansr2m", "Errors.Execution.Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
// webhook call a webhook, ignore the response but return the errror
|
||||
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte) error {
|
||||
_, err := call(ctx, url, timeout, body)
|
||||
return err
|
||||
}
|
||||
|
||||
// call function to do a post HTTP request to a desired url with timeout
|
||||
func call(ctx context.Context, url string, timeout time.Duration, body []byte) (_ []byte, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() {
|
||||
cancel()
|
||||
span.EndWithError(err)
|
||||
}()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := http.DefaultClient
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check for success between 200 and 299, redirect 300 to 399 is handled by the client, return error with statusCode >= 400
|
||||
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
return nil, zerrors.ThrowUnknown(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed")
|
||||
}
|
347
internal/execution/execution_test.go
Normal file
347
internal/execution/execution_test.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
var _ Target = &mockTarget{}
|
||||
|
||||
type mockTarget struct {
|
||||
InstanceID string
|
||||
ExecutionID string
|
||||
TargetID string
|
||||
TargetType domain.TargetType
|
||||
Endpoint string
|
||||
Timeout time.Duration
|
||||
InterruptOnError bool
|
||||
}
|
||||
|
||||
func (e *mockTarget) GetTargetID() string {
|
||||
return e.TargetID
|
||||
}
|
||||
func (e *mockTarget) IsInterruptOnError() bool {
|
||||
return e.InterruptOnError
|
||||
}
|
||||
func (e *mockTarget) GetEndpoint() string {
|
||||
return e.Endpoint
|
||||
}
|
||||
func (e *mockTarget) GetTargetType() domain.TargetType {
|
||||
return e.TargetType
|
||||
}
|
||||
func (e *mockTarget) GetTimeout() time.Duration {
|
||||
return e.Timeout
|
||||
}
|
||||
|
||||
func Test_Call(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
timeout time.Duration
|
||||
sleep time.Duration
|
||||
method string
|
||||
body []byte
|
||||
respBody []byte
|
||||
statusCode int
|
||||
}
|
||||
type res struct {
|
||||
body []byte
|
||||
wantErr bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"not ok status",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
timeout: time.Minute,
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
body: []byte("{\"request\": \"values\"}"),
|
||||
respBody: []byte("{\"response\": \"values\"}"),
|
||||
statusCode: http.StatusBadRequest,
|
||||
},
|
||||
res{
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"timeout",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
timeout: time.Second,
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
body: []byte("{\"request\": \"values\"}"),
|
||||
respBody: []byte("{\"response\": \"values\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
res{
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
timeout: time.Minute,
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
body: []byte("{\"request\": \"values\"}"),
|
||||
respBody: []byte("{\"response\": \"values\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
res{
|
||||
body: []byte("{\"response\": \"values\"}"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
respBody, err := testServerCall(t,
|
||||
tt.args.method,
|
||||
tt.args.body,
|
||||
tt.args.sleep,
|
||||
tt.args.statusCode,
|
||||
tt.args.respBody,
|
||||
testCall(tt.args.ctx, tt.args.timeout, tt.args.body),
|
||||
)
|
||||
if tt.res.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, respBody)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.res.body, respBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) {
|
||||
return func(url string) ([]byte, error) {
|
||||
return call(ctx, url, timeout, body)
|
||||
}
|
||||
}
|
||||
|
||||
func testCallTarget(ctx context.Context,
|
||||
target *mockTarget,
|
||||
info ContextInfoRequest,
|
||||
) func(string) ([]byte, error) {
|
||||
return func(url string) (r []byte, err error) {
|
||||
target.Endpoint = url
|
||||
return CallTarget(ctx, target, info)
|
||||
}
|
||||
}
|
||||
|
||||
func testServerCall(
|
||||
t *testing.T,
|
||||
method string,
|
||||
body []byte,
|
||||
timeout time.Duration,
|
||||
statusCode int,
|
||||
respBody []byte,
|
||||
call func(string) ([]byte, error),
|
||||
) ([]byte, error) {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
checkRequest(t, r, method, body)
|
||||
|
||||
if statusCode != http.StatusOK {
|
||||
http.Error(w, "error", statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(timeout)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err := io.WriteString(w, string(respBody)); err != nil {
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(handler))
|
||||
defer server.Close()
|
||||
|
||||
return call(server.URL)
|
||||
}
|
||||
|
||||
func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) {
|
||||
sentBody, err := io.ReadAll(sent.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedBody, sentBody)
|
||||
require.Equal(t, method, sent.Method)
|
||||
}
|
||||
|
||||
var _ ContextInfoRequest = &mockContextInfoRequest{}
|
||||
|
||||
type request struct {
|
||||
Request string `json:"request"`
|
||||
}
|
||||
|
||||
type mockContextInfoRequest struct {
|
||||
Request *request `json:"request"`
|
||||
}
|
||||
|
||||
func newMockContextInfoRequest(s string) *mockContextInfoRequest {
|
||||
return &mockContextInfoRequest{&request{s}}
|
||||
}
|
||||
|
||||
func (c *mockContextInfoRequest) GetHTTPRequestBody() []byte {
|
||||
data, _ := json.Marshal(c)
|
||||
return data
|
||||
}
|
||||
|
||||
func (c *mockContextInfoRequest) GetContent() []byte {
|
||||
data, _ := json.Marshal(c.Request)
|
||||
return data
|
||||
}
|
||||
|
||||
func Test_CallTarget(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
target *mockTarget
|
||||
sleep time.Duration
|
||||
|
||||
info ContextInfoRequest
|
||||
|
||||
method string
|
||||
body []byte
|
||||
|
||||
respBody []byte
|
||||
statusCode int
|
||||
}
|
||||
type res struct {
|
||||
body []byte
|
||||
wantErr bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"unknown targettype, error",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
info: newMockContextInfoRequest("content1"),
|
||||
target: &mockTarget{
|
||||
TargetType: 4,
|
||||
},
|
||||
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
res{
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"webhook, error",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
info: newMockContextInfoRequest("content1"),
|
||||
target: &mockTarget{
|
||||
TargetType: domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
res{
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"webhook, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
info: newMockContextInfoRequest("content1"),
|
||||
target: &mockTarget{
|
||||
TargetType: domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
res{
|
||||
body: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"request response, error",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
info: newMockContextInfoRequest("content1"),
|
||||
target: &mockTarget{
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
res{
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"request response, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
info: newMockContextInfoRequest("content1"),
|
||||
target: &mockTarget{
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
res{
|
||||
body: []byte("{\"request\":\"content2\"}"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
respBody, err := testServerCall(t,
|
||||
tt.args.method,
|
||||
tt.args.body,
|
||||
tt.args.sleep,
|
||||
tt.args.statusCode,
|
||||
tt.args.respBody,
|
||||
testCallTarget(tt.args.ctx, tt.args.target, tt.args.info),
|
||||
)
|
||||
if tt.res.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.res.body, respBody)
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user