Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Known Issues

## `--rule-transform-name` with an ID or a mistyped name attaches an empty transformation

**Symptom:** `hookdeck gateway connection create`, `update` or `upsert` with `--rule-transform-name`
set to a transformation ID, or to a name that doesn't exist, succeeds, but every event on the
connection then fails its transformation with a `TypeError`.

**Why:** The CLI always sent the value as a transformation name. When no transformation has that
name, the API creates one, even with no code, so the connection got a new, empty transformation
instead of the one you meant.

**Affected:** v3.1.0 and earlier.

**Recommended fix:** Update to **v3.2.0 or later**. The value is then resolved as an ID or an exact
name, and an unknown value without `--rule-transform-code` is an error.

- npm: `npm install -g hookdeck-cli@latest`
- Homebrew: `brew upgrade hookdeck`

**Workaround (until you update):** Pass the rule as JSON with `--rules` or `--rules-file`,
referencing the transformation by `transformation_id`.

**Cleanup:** If you hit this, you have empty transformations named after an ID (`trs_...`) or a
typo. Find them with `hookdeck gateway transformation list`. Point any connection that uses one at
the right transformation, then delete it with `hookdeck gateway transformation delete <id>`.

**Tracking:** #468

## Connection rules built from `--rule-*` flags run in a fixed order, not the order you wrote them

**Symptom:** A connection created, updated or upserted with `--rule-*` flags runs its rules in a
Expand Down
18 changes: 9 additions & 9 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,9 +402,9 @@ hookdeck gateway connection create [flags]
| `--rule-retry-interval` | `int` | Interval between retries in milliseconds (default "0") |
| `--rule-retry-response-status-codes` | `string` | Comma-separated HTTP status codes to retry on |
| `--rule-retry-strategy` | `string` | Retry strategy (linear, exponential) |
| `--rule-transform-code` | `string` | Transformation code (if creating inline) |
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation |
| `--rule-transform-name` | `string` | Name or ID of the transformation to apply |
| `--rule-transform-code` | `string` | Transformation code. Creates the transformation named by `--rule-transform-name`, or replaces its code if it exists |
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation. Replaces the env of an existing transformation |
| `--rule-transform-name` | `string` | Name or ID of an existing transformation to apply. With `--rule-transform-code`, an existing transformation's code is replaced, or a new transformation is created with this name |
| `--rules` | `string` | JSON string representing the entire rules array |
| `--rules-file` | `string` | Path to a JSON file containing the rules array |
| `--source-allowed-http-methods` | `string` | Comma-separated list of allowed HTTP methods (GET, POST, PUT, PATCH, DELETE) |
Expand Down Expand Up @@ -521,9 +521,9 @@ hookdeck gateway connection update <connection-id> [flags]
| `--rule-retry-interval` | `int` | Interval between retries in milliseconds (default "0") |
| `--rule-retry-response-status-codes` | `string` | Comma-separated HTTP status codes to retry on |
| `--rule-retry-strategy` | `string` | Retry strategy (linear, exponential) |
| `--rule-transform-code` | `string` | Transformation code (if creating inline) |
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation |
| `--rule-transform-name` | `string` | Name or ID of the transformation to apply |
| `--rule-transform-code` | `string` | Transformation code. Creates the transformation named by `--rule-transform-name`, or replaces its code if it exists |
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation. Replaces the env of an existing transformation |
| `--rule-transform-name` | `string` | Name or ID of an existing transformation to apply. With `--rule-transform-code`, an existing transformation's code is replaced, or a new transformation is created with this name |
| `--rules` | `string` | JSON string representing the entire rules array |
| `--rules-file` | `string` | Path to a JSON file containing the rules array |
| `--source-id` | `string` | Update source by ID |
Expand Down Expand Up @@ -661,9 +661,9 @@ hookdeck gateway connection upsert <name> [flags]
| `--rule-retry-interval` | `int` | Interval between retries in milliseconds (default "0") |
| `--rule-retry-response-status-codes` | `string` | Comma-separated HTTP status codes to retry on |
| `--rule-retry-strategy` | `string` | Retry strategy (linear, exponential) |
| `--rule-transform-code` | `string` | Transformation code (if creating inline) |
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation |
| `--rule-transform-name` | `string` | Name or ID of the transformation to apply |
| `--rule-transform-code` | `string` | Transformation code. Creates the transformation named by `--rule-transform-name`, or replaces its code if it exists |
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation. Replaces the env of an existing transformation |
| `--rule-transform-name` | `string` | Name or ID of an existing transformation to apply. With `--rule-transform-code`, an existing transformation's code is replaced, or a new transformation is created with this name |
| `--rules` | `string` | JSON string representing the entire rules array |
| `--rules-file` | `string` | Path to a JSON file containing the rules array |
| `--source-allowed-http-methods` | `string` | Comma-separated list of allowed HTTP methods (GET, POST, PUT, PATCH, DELETE) |
Expand Down
6 changes: 3 additions & 3 deletions pkg/cmd/connection_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ func addConnectionRuleFlags(cmd *cobra.Command, f *connectionRuleFlags) {
cmd.Flags().StringVar(&f.RuleFilterQuery, "rule-filter-query", "", "Filter on request query parameters using Hookdeck filter syntax (JSON)")
cmd.Flags().StringVar(&f.RuleFilterPath, "rule-filter-path", "", "Filter on request path using Hookdeck filter syntax (JSON)")

cmd.Flags().StringVar(&f.RuleTransformName, "rule-transform-name", "", "Name or ID of the transformation to apply")
cmd.Flags().StringVar(&f.RuleTransformCode, "rule-transform-code", "", "Transformation code (if creating inline)")
cmd.Flags().StringVar(&f.RuleTransformEnv, "rule-transform-env", "", "JSON string representing environment variables for transformation")
cmd.Flags().StringVar(&f.RuleTransformName, "rule-transform-name", "", "Name or ID of an existing transformation to apply. With --rule-transform-code, an existing transformation's code is replaced, or a new transformation is created with this name")
cmd.Flags().StringVar(&f.RuleTransformCode, "rule-transform-code", "", "Transformation code. Creates the transformation named by --rule-transform-name, or replaces its code if it exists")
cmd.Flags().StringVar(&f.RuleTransformEnv, "rule-transform-env", "", "JSON string representing environment variables for transformation. Replaces the env of an existing transformation")

cmd.Flags().IntVar(&f.RuleDelay, "rule-delay", 0, "Delay in milliseconds")

Expand Down
3 changes: 3 additions & 0 deletions pkg/cmd/connection_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,9 @@ func (cc *connectionCreateCmd) runConnectionCreateCmd(cmd *cobra.Command, args [
if err != nil {
return err
}
if err := resolveRuleTransformation(context.Background(), client, &cc.connectionRuleFlags, rules); err != nil {
return err
}
if len(rules) > 0 {
req.Rules = &rules
}
Expand Down
132 changes: 132 additions & 0 deletions pkg/cmd/connection_transform_rule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package cmd

import (
"context"
"fmt"
"strconv"
"strings"

"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
)

// The lookup doesn't rely on the name filter on GET /transformations being an
// exact match. It compares names exactly and, in case the filter matches more
// than one page, asks for the largest page the API allows and follows
// pagination.next, up to a bound.
const (
transformationLookupPageLimit = 255
transformationLookupMaxPages = 20
)

// transformationLookup is the subset of the API client used to resolve
// --rule-transform-name.
type transformationLookup interface {
GetTransformation(ctx context.Context, id string) (*hookdeck.Transformation, error)
ListTransformations(ctx context.Context, params map[string]string) (*hookdeck.TransformationListResponse, error)
}

// findTransformationID returns the ID of the transformation whose ID or exact
// name is nameOrID, or "" when there is none.
func findTransformationID(ctx context.Context, client transformationLookup, nameOrID string) (string, error) {
if strings.HasPrefix(nameOrID, "trs_") {
trn, err := client.GetTransformation(ctx, nameOrID)
if err == nil {
return trn.ID, nil
}
if !hookdeck.IsNotFoundError(err) {
return "", fmt.Errorf("failed to look up transformation '%s': %w", nameOrID, err)
}
}

params := map[string]string{
"name": nameOrID,
"limit": strconv.Itoa(transformationLookupPageLimit),
}
for page := 0; page < transformationLookupMaxPages; page++ {
result, err := client.ListTransformations(ctx, params)
if err != nil {
return "", fmt.Errorf("failed to look up transformation '%s': %w", nameOrID, err)
}
for _, trn := range result.Models {
if trn.Name == nameOrID {
return trn.ID, nil
}
}
next := result.Pagination.Next
if next == nil || *next == "" {
return "", nil
}
params["next"] = *next
}
return "", fmt.Errorf("failed to look up transformation '%s': more than %d transformations match the name filter; pass its ID instead",
nameOrID, transformationLookupPageLimit*transformationLookupMaxPages)
}

// resolveRuleTransformation makes --rule-transform-name accept a transformation
// name or ID, as its help text says.
//
// The API matches a transform rule's transformation by name and creates one when
// the name is new, even without code. Sent as a name, an ID or a mistyped name
// therefore created an empty transformation that failed every event. So the value
// is resolved first:
// - found: the rule references it by transformation_id; --rule-transform-code and
// --rule-transform-env, if given, update that transformation
// - not found, with --rule-transform-code: a new transformation is created with
// that name, as before (not allowed for an ID)
// - not found, without --rule-transform-code: an error
//
// Rules from --rules or --rules-file are sent as given.
func resolveRuleTransformation(ctx context.Context, client transformationLookup, f *connectionRuleFlags, rules []hookdeck.Rule) error {
if f.Rules != "" || f.RulesFile != "" {
return nil
}
if f.RuleTransformName == "" {
if f.RuleTransformCode != "" || f.RuleTransformEnv != "" {
return fmt.Errorf("--rule-transform-name is required when using transform rule flags")
}
return nil
}

idx := -1
for i, r := range rules {
if r["type"] == "transform" {
idx = i
break
}
}
if idx == -1 {
return nil
}

nameOrID := f.RuleTransformName
id, err := findTransformationID(ctx, client, nameOrID)
if err != nil {
return err
}

if id == "" {
if strings.HasPrefix(nameOrID, "trs_") {
return fmt.Errorf("transformation '%s' not found. Check the ID with 'hookdeck gateway transformation list'", nameOrID)
}
if f.RuleTransformCode == "" {
return fmt.Errorf("transformation '%s' not found. Check the name with 'hookdeck gateway transformation list', or pass --rule-transform-code to create a new transformation with this name", nameOrID)
}
// New transformation: keep name and code so the API creates it
return nil
}

rule := hookdeck.Rule{"type": "transform", "transformation_id": id}
if existing, ok := rules[idx]["transformation"].(map[string]interface{}); ok {
update := make(map[string]interface{})
for _, key := range []string{"code", "env"} {
if v, ok := existing[key]; ok {
update[key] = v
}
}
if len(update) > 0 {
rule["transformation"] = update
}
}
rules[idx] = rule
return nil
}
Loading
Loading