-
-
Notifications
You must be signed in to change notification settings - Fork 147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce project event webhook #1113
base: main
Are you sure you want to change the base?
Changes from 7 commits
5887349
9f82db1
84fe575
8f8d142
34fa918
b7add32
a1a0c5d
c77b317
0d287a3
ad260ab
e64106f
481a109
af0a536
74d3fff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/* | ||
* Copyright 2024 The Yorkie Authors. All rights reserved. | ||
* | ||
* 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 types | ||
|
||
// ProjectEvent represents the event of the project. | ||
type ProjectEvent string | ||
|
||
// ProjectEvent represents the event of the project. | ||
const ( | ||
DocumentCreated ProjectEvent = "DocumentCreated" | ||
DocumentRemoved ProjectEvent = "DocumentRemoved" | ||
) | ||
|
||
// WebhookAttribute represents the attribute of the webhook. | ||
type WebhookAttribute struct { | ||
DocumentKey string `json:"documentKey"` | ||
ClientKey string `json:"clientKey"` | ||
IssuedAt string `json:"issuedAt"` | ||
} | ||
|
||
// WebhookRequest represents the request of the webhook. | ||
type WebhookRequest struct { | ||
Type ProjectEvent `json:"type"` | ||
Attributes WebhookAttribute `json:"attributes"` | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/* | ||
* Copyright 2024 The Yorkie Authors. All rights reserved. | ||
* | ||
* 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 webhook provides an webhook utilities. | ||
package webhook | ||
|
||
import ( | ||
"bytes" | ||
"crypto/hmac" | ||
"crypto/sha256" | ||
"encoding/hex" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"time" | ||
) | ||
|
||
// HMACTransport is a http.RoundTripper that adds an X-Signature | ||
// header to each request using an HMAC-SHA256 signature. | ||
type HMACTransport struct { | ||
PrivateKey string | ||
} | ||
|
||
// GenerateHMACSignature computes an HMAC-SHA256 signature for the given data | ||
// using the specified secret key. | ||
func GenerateHMACSignature(secret string, data []byte) string { | ||
h := hmac.New(sha256.New, []byte(secret)) | ||
h.Write(data) | ||
return hex.EncodeToString(h.Sum(nil)) | ||
} | ||
|
||
// RoundTrip implements the http.RoundTripper interface. It reads the request body | ||
// to compute the HMAC signature, sets the "X-Signature" header, and restores | ||
// the body for use by subsequent transports or handlers. | ||
func (t *HMACTransport) RoundTrip(r *http.Request) (*http.Response, error) { | ||
reqCopy := r.Clone(r.Context()) | ||
|
||
rawBody, err := io.ReadAll(r.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to read request: %w", err) | ||
} | ||
reqCopy.Body = io.NopCloser(bytes.NewBuffer(rawBody)) | ||
|
||
signature := GenerateHMACSignature(t.PrivateKey, rawBody) | ||
reqCopy.Header.Set("X-Signature", signature) | ||
|
||
resp, err := http.DefaultTransport.RoundTrip(reqCopy) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to send request: %w", err) | ||
} | ||
|
||
return resp, nil | ||
} | ||
|
||
// NewClient creates an *http.Client configured with a custom HMAC transport | ||
// and a specified timeout. The transport will add an "X-Signature" header to | ||
// every request using the provided private key. | ||
func NewClient(timeout time.Duration, privateKey string) *http.Client { | ||
return &http.Client{ | ||
Timeout: timeout, | ||
Transport: &HMACTransport{ | ||
PrivateKey: privateKey, | ||
}, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/* | ||
* Copyright 2024 The Yorkie Authors. All rights reserved. | ||
* | ||
* 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 webhook | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"math" | ||
"net/http" | ||
"syscall" | ||
gotime "time" | ||
) | ||
|
||
var ( | ||
// ErrUnexpectedStatusCode is returned when the webhook returns an unexpected status code. | ||
ErrUnexpectedStatusCode = errors.New("unexpected status code from webhook") | ||
|
||
// ErrMaxRetriesExceeded indicates we have retried up to the specified max retries | ||
// without a successful outcome. | ||
ErrMaxRetriesExceeded = errors.New("exponential backoff: maximum retries exceeded") | ||
) | ||
|
||
// WithExponentialBackoff retries the given webhookFn with exponential backoff. | ||
func WithExponentialBackoff( | ||
ctx context.Context, | ||
maxRetries uint64, | ||
baseInterval, | ||
maxInterval gotime.Duration, | ||
webhookFn func() (int, error), | ||
) error { | ||
start := gotime.Now() | ||
|
||
var ( | ||
retries uint64 | ||
statusCode int | ||
) | ||
|
||
for retries < maxRetries { | ||
statusCode, err := webhookFn() | ||
if !shouldRetry(statusCode, err) { | ||
if errors.Is(err, ErrUnexpectedStatusCode) { | ||
return fmt.Errorf("%d: %w", statusCode, ErrUnexpectedStatusCode) | ||
} | ||
|
||
return err | ||
} | ||
|
||
waitBeforeRetry := waitInterval(retries, baseInterval, maxInterval) | ||
|
||
select { | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
case <-gotime.After(waitBeforeRetry): | ||
} | ||
|
||
retries++ | ||
} | ||
|
||
return fmt.Errorf( | ||
"maximum retries (%d) exceeded after %s; last status code = %d: %w", | ||
maxRetries, | ||
gotime.Since(start), | ||
statusCode, | ||
ErrMaxRetriesExceeded, | ||
) | ||
} | ||
|
||
// waitInterval returns the interval of given retries. it returns maxWaitInterval | ||
func waitInterval(retries uint64, baseInterval, maxWaitInterval gotime.Duration) gotime.Duration { | ||
interval := gotime.Duration(math.Pow(2, float64(retries))) * baseInterval | ||
if maxWaitInterval < interval { | ||
return maxWaitInterval | ||
} | ||
|
||
return interval | ||
} | ||
|
||
// shouldRetry returns true if the given error should be retried. | ||
// Refer to https://github.com/kubernetes/kubernetes/search?q=DefaultShouldRetry | ||
func shouldRetry(statusCode int, err error) bool { | ||
// If the connection is reset, we should retry. | ||
var errno syscall.Errno | ||
if errors.As(err, &errno) { | ||
return errno == syscall.ECONNRESET | ||
} | ||
|
||
return statusCode == http.StatusInternalServerError || | ||
statusCode == http.StatusServiceUnavailable || | ||
statusCode == http.StatusGatewayTimeout || | ||
statusCode == http.StatusTooManyRequests | ||
} | ||
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -73,6 +73,18 @@ type Config struct { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// AuthWebhookCacheUnauthTTL is the TTL value to set when caching the unauthorized result. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
AuthWebhookCacheUnauthTTL string `yaml:"AuthWebhookCacheUnauthTTL"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// EventWebhookMaxRetries is the max count that retries the project event webhook. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
EventWebhookMaxRetries uint64 `yaml:"EventWebhookMaxRetries"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// EventWebhookBaseWaitInterval is the base of retrying exponential backoff the project event webhook. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
EventWebhookBaseWaitInterval string `yaml:"EventWebhookBaseWaitInterval"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// EventWebhookMaxWaitInterval is the max interval that waits before retrying the project event webhook. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
EventWebhookMaxWaitInterval string `yaml:"EventWebhookMaxWaitInterval"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// EventWebhookRequestTimeout is the time that waits time for response the project webhook. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
EventWebhookRequestTimeout string `yaml:"EventWebhookRequestTimeout"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// ProjectInfoCacheSize is the cache size of the project info. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ProjectInfoCacheSize int `yaml:"ProjectInfoCacheSize"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -175,6 +187,39 @@ func (c *Config) ParseAuthWebhookCacheUnauthTTL() time.Duration { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// ParseProjectWebhookMaxWaitInterval returns max wait interval. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
func (c *Config) ParseProjectWebhookMaxWaitInterval() time.Duration { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
result, err := time.ParseDuration(c.EventWebhookMaxWaitInterval) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fmt.Fprintln(os.Stderr, "parse project webhook max wait interval: %w", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+214
to
+223
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix error message formatting. The error logging statement uses the wrong format:
- fmt.Fprintln(os.Stderr, "parse project webhook max wait interval: %w", err)
+ fmt.Fprintf(os.Stderr, "parse project webhook max wait interval: %v\n", err) 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// ParseProjectWebhookBaseWaitInterval returns base wait interval. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
func (c *Config) ParseProjectWebhookBaseWaitInterval() time.Duration { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
result, err := time.ParseDuration(c.EventWebhookBaseWaitInterval) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fmt.Fprintln(os.Stderr, "parse project webhook max wait interval: %w", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// ParseProjectWebhookTimeout returns timeout for request. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
func (c *Config) ParseProjectWebhookTimeout() time.Duration { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
result, err := time.ParseDuration(c.EventWebhookRequestTimeout) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fmt.Fprintln(os.Stderr, "parse project webhook max wait interval: %w", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+225
to
+245
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix incorrect error messages and formatting. Both methods have incorrect error messages and formatting issues: func (c *Config) ParseProjectWebhookBaseWaitInterval() time.Duration {
result, err := time.ParseDuration(c.EventWebhookBaseWaitInterval)
if err != nil {
- fmt.Fprintln(os.Stderr, "parse project webhook max wait interval: %w", err)
+ fmt.Fprintf(os.Stderr, "parse project webhook base wait interval: %v\n", err)
os.Exit(1)
}
return result
}
func (c *Config) ParseProjectWebhookTimeout() time.Duration {
result, err := time.ParseDuration(c.EventWebhookRequestTimeout)
if err != nil {
- fmt.Fprintln(os.Stderr, "parse project webhook max wait interval: %w", err)
+ fmt.Fprintf(os.Stderr, "parse project webhook timeout: %v\n", err)
os.Exit(1)
}
return result
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// ParseProjectInfoCacheTTL returns TTL for project info cache. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
func (c *Config) ParseProjectInfoCacheTTL() time.Duration { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
result, err := time.ParseDuration(c.ProjectInfoCacheTTL) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add 4xx handling
shouldRetry
currently returnstrue
for only high-level server errors like 500 or 503. In certain scenarios (e.g., 429 Too Many Requests), the function also returnstrue
, which is fine. However, you may need logic for other 4xx status codes if your service wants clients to back off, especially if a rate-limit or temporary ban is in place.