Skip to content

Commit bb8f95c

Browse files
authored
fix: [TKC-3469] use dynamic secret value in webhook (#6315)
* fix: use dynamic secret Signed-off-by: Vladislav Sukhin <[email protected]> * fix: move paramteres check to listener Signed-off-by: Vladislav Sukhin <[email protected]> * fix: get hashed metadata Signed-off-by: Vladislav Sukhin <[email protected]> * fix: remove duplicated code Signed-off-by: Vladislav Sukhin <[email protected]> * fix: get text hashed metadata Signed-off-by: Vladislav Sukhin <[email protected]> * fix: sort arrays Signed-off-by: Vladislav Sukhin <[email protected]> --------- Signed-off-by: Vladislav Sukhin <[email protected]>
1 parent 43789ed commit bb8f95c

File tree

3 files changed

+140
-65
lines changed

3 files changed

+140
-65
lines changed

pkg/event/kind/webhook/listener.go

+124-5
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@ package webhook
33
import (
44
"bytes"
55
"context"
6+
"crypto/sha256"
67
"encoding/json"
78
"fmt"
89
"io"
910
"net/http"
11+
"regexp"
12+
"sort"
1013
"text/template"
1114

1215
"github.com/pkg/errors"
1316
"go.uber.org/zap"
1417

18+
executorv1 "github.com/kubeshop/testkube-operator/api/executor/v1"
1519
"github.com/kubeshop/testkube/cmd/api-server/commons"
1620
v1 "github.com/kubeshop/testkube/internal/app/api/metrics"
1721
"github.com/kubeshop/testkube/internal/config"
@@ -21,6 +25,7 @@ import (
2125
thttp "github.com/kubeshop/testkube/pkg/http"
2226
"github.com/kubeshop/testkube/pkg/log"
2327
"github.com/kubeshop/testkube/pkg/repository/testworkflow"
28+
"github.com/kubeshop/testkube/pkg/secret"
2429
"github.com/kubeshop/testkube/pkg/utils"
2530
"github.com/kubeshop/testkube/pkg/utils/text"
2631
)
@@ -33,9 +38,11 @@ func NewWebhookListener(name, uri, selector string, events []testkube.EventType,
3338
testWorkflowExecutionResults testworkflow.Repository,
3439
metrics v1.Metrics,
3540
webhookRepository cloudwebhook.WebhookRepository,
41+
secretClient secret.Interface,
3642
proContext *config.ProContext,
3743
envs map[string]string,
38-
config map[string]string,
44+
config map[string]executorv1.WebhookConfigValue,
45+
parameters []executorv1.WebhookParameterSchema,
3946
) *WebhookListener {
4047
return &WebhookListener{
4148
name: name,
@@ -52,9 +59,11 @@ func NewWebhookListener(name, uri, selector string, events []testkube.EventType,
5259
testWorkflowExecutionResults: testWorkflowExecutionResults,
5360
metrics: metrics,
5461
webhookRepository: webhookRepository,
62+
secretClient: secretClient,
5563
proContext: proContext,
5664
envs: envs,
5765
config: config,
66+
parameters: parameters,
5867
}
5968
}
6069

@@ -73,9 +82,11 @@ type WebhookListener struct {
7382
testWorkflowExecutionResults testworkflow.Repository
7483
metrics v1.Metrics
7584
webhookRepository cloudwebhook.WebhookRepository
85+
secretClient secret.Interface
7686
proContext *config.ProContext
7787
envs map[string]string
78-
config map[string]string
88+
config map[string]executorv1.WebhookConfigValue
89+
parameters []executorv1.WebhookParameterSchema
7990
}
8091

8192
func (l *WebhookListener) Name() string {
@@ -90,15 +101,32 @@ func (l *WebhookListener) Events() []testkube.EventType {
90101
return l.events
91102
}
92103
func (l *WebhookListener) Metadata() map[string]string {
104+
headers, err := getMapHashedMetadata(l.headers)
105+
if err != nil {
106+
l.Log.Errorw("headers hashing error", "error", err)
107+
}
108+
109+
config, err := getMapHashedMetadata(l.config)
110+
if err != nil {
111+
l.Log.Errorw("config hashing error", "error", err)
112+
}
113+
114+
parameters, err := getSliceHashedMetadata(l.parameters)
115+
if err != nil {
116+
l.Log.Errorw("parameters hashing error", "error", err)
117+
}
118+
93119
return map[string]string{
94120
"name": l.Name(),
95121
"uri": l.Uri,
96122
"selector": l.selector,
97123
"events": fmt.Sprintf("%v", l.events),
98124
"payloadObjectField": l.payloadObjectField,
99-
"payloadTemplate": l.payloadTemplate,
100-
"headers": fmt.Sprintf("%v", l.headers),
125+
"payloadTemplate": getTextHashedMetadata([]byte(l.payloadTemplate)),
126+
"headers": headers,
101127
"disabled": fmt.Sprint(l.disabled),
128+
"config": config,
129+
"parameters": parameters,
102130
}
103131
}
104132

@@ -290,8 +318,62 @@ func (l *WebhookListener) processTemplate(field, body string, event testkube.Eve
290318
return nil, err
291319
}
292320

321+
config := make(map[string]string)
322+
for key, val := range l.config {
323+
var data string
324+
if val.Value != nil {
325+
data = *val.Value
326+
}
327+
328+
if val.Secret != nil {
329+
var ns []string
330+
if val.Secret.Namespace != "" {
331+
ns = append(ns, val.Secret.Namespace)
332+
}
333+
334+
elements, err := l.secretClient.Get(val.Secret.Name, ns...)
335+
if err != nil {
336+
log.Errorw("error secret loading", "error", err, "name", val.Secret.Name)
337+
return nil, err
338+
}
339+
340+
if element, ok := elements[val.Secret.Key]; ok {
341+
data = element
342+
} else {
343+
log.Errorw("error secret key finding loading", "name", val.Secret.Name, "key", val.Secret.Key)
344+
return nil, errors.New("error secret key finding loading")
345+
}
346+
}
347+
348+
config[key] = data
349+
}
350+
351+
for _, parameter := range l.parameters {
352+
if _, ok := config[parameter.Name]; !ok {
353+
if parameter.Default_ != nil {
354+
config[parameter.Name] = *parameter.Default_
355+
} else if parameter.Required {
356+
log.Errorw("error missing required parameter", "name", parameter.Name)
357+
return nil, errors.New("error missing required parameter")
358+
}
359+
}
360+
361+
if parameter.Pattern != "" {
362+
re, err := regexp.Compile(parameter.Pattern)
363+
if err != nil {
364+
log.Errorw("error compiling pattern", "error", err, "name", parameter.Name, "pattern", parameter.Pattern)
365+
return nil, err
366+
}
367+
368+
if data, ok := config[parameter.Name]; ok && !re.MatchString(data) {
369+
log.Errorw("error matching pattern", "error", err, "name", parameter.Name, "pattern", parameter.Pattern)
370+
return nil, errors.New("error matching pattern")
371+
}
372+
}
373+
}
374+
293375
var buffer bytes.Buffer
294-
if err = tmpl.ExecuteTemplate(&buffer, field, NewTemplateVars(event, l.proContext, l.config)); err != nil {
376+
if err = tmpl.ExecuteTemplate(&buffer, field, NewTemplateVars(event, l.proContext, config)); err != nil {
295377
log.Errorw(fmt.Sprintf("executing webhook %s error", field), "error", err)
296378
return nil, err
297379
}
@@ -346,3 +428,40 @@ func (l *WebhookListener) hasBecomeState(event testkube.Event) (bool, error) {
346428

347429
return false, nil
348430
}
431+
432+
type configKeyValue[T any] struct {
433+
Key string
434+
Value T
435+
}
436+
437+
type configKeyValues[T any] []configKeyValue[T]
438+
439+
// getMapHashedMetadata returns map hashed metadata
440+
func getMapHashedMetadata[T any](data map[string]T) (string, error) {
441+
var slice configKeyValues[T]
442+
for key, value := range data {
443+
slice = append(slice, configKeyValue[T]{Key: key, Value: value})
444+
}
445+
446+
sort.Slice(slice, func(i, j int) bool {
447+
return slice[i].Key < slice[j].Key
448+
})
449+
450+
return getSliceHashedMetadata(slice)
451+
}
452+
453+
// getSliceHashedMetadata returns slice hashed metadata
454+
func getSliceHashedMetadata[T any](slice []T) (string, error) {
455+
result, err := json.Marshal(slice)
456+
if err != nil {
457+
return "", err
458+
}
459+
460+
return getTextHashedMetadata(result), nil
461+
}
462+
463+
// getTextHashedMetadata returns text hashed metadata
464+
func getTextHashedMetadata(result []byte) string {
465+
466+
return fmt.Sprintf("%x", sha256.Sum256(result))
467+
}

pkg/event/kind/webhook/listener_test.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func TestWebhookListener_Notify(t *testing.T) {
4040
defer mockCtrl.Finish()
4141
mockWebhooRepository := cloudwebhook.NewMockWebhookRepository(mockCtrl)
4242
mockWebhooRepository.EXPECT().CollectExecutionResult(gomock.Any(), gomock.Any(), "l1", "", http.StatusOK).AnyTimes()
43-
l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil)
43+
l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil, nil, nil)
4444

4545
// when
4646
r := l.Notify(testkube.Event{
@@ -66,7 +66,7 @@ func TestWebhookListener_Notify(t *testing.T) {
6666
defer mockCtrl.Finish()
6767
mockWebhooRepository := cloudwebhook.NewMockWebhookRepository(mockCtrl)
6868
mockWebhooRepository.EXPECT().CollectExecutionResult(gomock.Any(), gomock.Any(), "l1", gomock.Any(), http.StatusBadGateway).AnyTimes()
69-
l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil)
69+
l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil, nil, nil)
7070

7171
// when
7272
r := l.Notify(testkube.Event{
@@ -87,7 +87,7 @@ func TestWebhookListener_Notify(t *testing.T) {
8787
defer mockCtrl.Finish()
8888
mockWebhooRepository := cloudwebhook.NewMockWebhookRepository(mockCtrl)
8989
mockWebhooRepository.EXPECT().CollectExecutionResult(gomock.Any(), gomock.Any(), "l1", gomock.Any(), 0).AnyTimes()
90-
s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, false, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil)
90+
s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, false, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil, nil, nil)
9191

9292
// when
9393
r := s.Notify(testkube.Event{
@@ -124,7 +124,7 @@ func TestWebhookListener_Notify(t *testing.T) {
124124
defer mockCtrl.Finish()
125125
mockWebhooRepository := cloudwebhook.NewMockWebhookRepository(mockCtrl)
126126
mockWebhooRepository.EXPECT().CollectExecutionResult(gomock.Any(), gomock.Any(), "l1", "", http.StatusOK).AnyTimes()
127-
l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "field", "", nil, false, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil)
127+
l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "field", "", nil, false, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil, nil, nil)
128128

129129
// when
130130
r := l.Notify(testkube.Event{
@@ -155,7 +155,7 @@ func TestWebhookListener_Notify(t *testing.T) {
155155
mockWebhooRepository := cloudwebhook.NewMockWebhookRepository(mockCtrl)
156156
mockWebhooRepository.EXPECT().CollectExecutionResult(gomock.Any(), gomock.Any(), "l1", "", http.StatusOK).AnyTimes()
157157
l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "{\"id\": \"{{ .Id }}\"}",
158-
map[string]string{"Content-Type": "application/json"}, false, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil)
158+
map[string]string{"Content-Type": "application/json"}, false, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil, nil, nil)
159159

160160
// when
161161
r := l.Notify(testkube.Event{
@@ -176,7 +176,7 @@ func TestWebhookListener_Notify(t *testing.T) {
176176
defer mockCtrl.Finish()
177177
mockWebhooRepository := cloudwebhook.NewMockWebhookRepository(mockCtrl)
178178
mockWebhooRepository.EXPECT().CollectExecutionResult(gomock.Any(), gomock.Any(), "l1", "", 0).AnyTimes()
179-
s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, true, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil)
179+
s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, true, nil, nil, v1.NewMetrics(), mockWebhooRepository, nil, nil, nil, nil, nil)
180180

181181
// when
182182
r := s.Notify(testkube.Event{

pkg/event/kind/webhook/loader.go

+10-54
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package webhook
22

33
import (
44
"fmt"
5-
"regexp"
5+
"sort"
66

77
"go.uber.org/zap"
88

@@ -68,7 +68,6 @@ func (r WebhooksLoader) Load() (listeners common.Listeners, err error) {
6868
}
6969

7070
// and create listeners for each webhook spec
71-
OuterLoop:
7271
for _, webhook := range webhookList.Items {
7372
if webhook.Spec.WebhookTemplateRef != nil && webhook.Spec.WebhookTemplateRef.Name != "" {
7473
webhookTemplate, err := r.WebhookTemplatesClient.Get(webhook.Spec.WebhookTemplateRef.Name)
@@ -109,65 +108,14 @@ OuterLoop:
109108

110109
types := webhooks.MapEventArrayToCRDEvents(webhook.Spec.Events)
111110
name := fmt.Sprintf("%s.%s", webhook.ObjectMeta.Namespace, webhook.ObjectMeta.Name)
112-
vars := make(map[string]string)
113-
for key, val := range webhook.Spec.Config {
114-
data := ""
115-
if val.Value != nil {
116-
data = *val.Value
117-
}
118-
119-
if val.Secret != nil {
120-
var ns []string
121-
if val.Secret.Namespace != "" {
122-
ns = append(ns, val.Secret.Namespace)
123-
}
124-
125-
elements, err := r.secretClient.Get(val.Secret.Name, ns...)
126-
if err != nil {
127-
r.log.Errorw("error secret loading", "error", err, "name", val.Secret.Name)
128-
continue
129-
}
130-
131-
if element, ok := elements[val.Secret.Key]; ok {
132-
data = element
133-
} else {
134-
r.log.Errorw("error secret key finding loading", "name", val.Secret.Name, "key", val.Secret.Key)
135-
continue
136-
}
137-
}
138-
139-
vars[key] = data
140-
}
141-
142-
for _, parameter := range webhook.Spec.Parameters {
143-
if data, ok := vars[parameter.Name]; !ok {
144-
if parameter.Default_ != nil {
145-
vars[parameter.Name] = *parameter.Default_
146-
} else if parameter.Required {
147-
r.log.Errorw("error missing required parameter", "name", parameter.Name)
148-
continue OuterLoop
149-
}
150-
} else if parameter.Pattern != "" {
151-
re, err := regexp.Compile(parameter.Pattern)
152-
if err != nil {
153-
r.log.Errorw("error compiling pattern", "error", err, "name", parameter.Name, "pattern", parameter.Pattern)
154-
continue OuterLoop
155-
}
156-
157-
if !re.MatchString(data) {
158-
r.log.Errorw("error matching pattern", "error", err, "name", parameter.Name, "pattern", parameter.Pattern)
159-
continue OuterLoop
160-
}
161-
}
162-
}
163111

164112
listeners = append(
165113
listeners,
166114
NewWebhookListener(
167115
name, webhook.Spec.Uri, webhook.Spec.Selector, types,
168116
webhook.Spec.PayloadObjectField, payloadTemplate, webhook.Spec.Headers, webhook.Spec.Disabled,
169117
r.deprecatedRepositories, r.testWorkflowExecutionResults,
170-
r.metrics, r.webhookRepository, r.proContext, r.envs, vars,
118+
r.metrics, r.webhookRepository, r.secretClient, r.proContext, r.envs, webhook.Spec.Config, webhook.Spec.Parameters,
171119
),
172120
)
173121
}
@@ -256,6 +204,10 @@ func mergeWebhooks(dst executorv1.Webhook, src executorv1.WebhookTemplate) execu
256204
}
257205
}
258206

207+
sort.Slice(dst.Spec.Events, func(i, j int) bool {
208+
return dst.Spec.Events[i] < dst.Spec.Events[j]
209+
})
210+
259211
if src.Spec.Config != nil {
260212
if dst.Spec.Config == nil {
261213
dst.Spec.Config = map[string]executorv1.WebhookConfigValue{}
@@ -284,6 +236,10 @@ func mergeWebhooks(dst executorv1.Webhook, src executorv1.WebhookTemplate) execu
284236
dst.Spec.Parameters = append(dst.Spec.Parameters, parameter)
285237
}
286238
}
239+
240+
sort.Slice(dst.Spec.Parameters, func(i, j int) bool {
241+
return dst.Spec.Parameters[i].Name < dst.Spec.Parameters[j].Name
242+
})
287243
}
288244

289245
return dst

0 commit comments

Comments
 (0)