Skip to content

Conversation

@alexluong
Copy link
Collaborator

@alexluong alexluong commented Feb 6, 2025

Description

This PR updates the provider to be in further alignment with the new source and destination type and changes.

Usage

1: Setting auth with hookdeck_source resource only

resource "hookdeck_source" "source" {
  name = "source"

  type = "HTTP"

  config = jsonencode({
    allowed_http_methods = ["POST"]
    auth_type = "API_KEY"
    auth = {
      header_key = "X-API-KEY"
      api_key = "secret"
    }
  })
}

2: Setting auth with hookdeck_source_auth

resource "hookdeck_source" "source" {
  name = "source"

  type = "HTTP"

  config = jsonencode({
    allowed_http_methods = ["POST"]
  })
}

resource "hookdeck_source_auth" "source_auth" {
  source_id = hookdeck_source.source.id
  auth_type = "API_KEY"
  auth = jsonencode({
    header_key = "X-API-KEY"
    api_key = "secret"
  })
}

Breaking changes

This PR introduces a few breaking changes that should be noted.

1: Remove hookdeck_source_verification resources
2: Remove attributes from hookdeck_source: allowed_http_methods and custom_response. Instead, these values can be found in hookdeck_source.config if the user chooses to use them.

Important Notes

As we shift away from static typing and rely on users to handle the config & formatting themselves, it's important we communicate some of our behaviors to users so that they understand and know how to do things.

1: Unset auth from hookdeck_source

Let's say this is the starting point for a source resource

resource "hookdeck_source" "source" {
  name = "source"

  type = "HTTP"

  config = jsonencode({
    allowed_http_methods = ["POST"]
    auth_type = "API_KEY"
    auth = {
      header_key = "X-API-KEY"
      api_key = "secret"
    }
  })
}

If the user wants to remove auth, the intuitive TF thing to do is to remove auth and auth_type fields from TF.

resource "hookdeck_source" "source" {
  name = "source"

  type = "HTTP"

  config = jsonencode({
    allowed_http_methods = ["POST"]
  })
}

During planning, because it's just JSON, it will pass and upon application, we'll make an API request with a config JSON without those fields. However, based on our API behavior, if those fields are not set, Hookdeck won't make changes to the source.

To properly unset auth, this is what the user must do:

resource "hookdeck_source" "source" {
  name = "source"

  type = "HTTP"

  config = jsonencode({
    allowed_http_methods = ["POST"]
    auth_type = null
    auth = null
  })
}

2: hookdeck_source.config is NOT computed

hookdeck_source.config is a JSON string type. This field is NOT computed, meaning if the user does not pass a conifg in, the TF state for config is null.

The reason is that some source type has a default config such as HTTP type having default value for allowed_http_methods, and if the TF user does not specify this field, after creating a source, if we attempt to "compute" and parse the infra data into TF state, it will cause state mismatch.

This behavior may cause problems if the user does meta programming with TF. However, I don't see a better approach to support this better, so it's just something that we have to live with for now. To save our users from potential confusion down the road, it may be helpful for us to call out this behavior in the docs.

3: Most source types can be created without auth; some can't

Example: EBAY source MUST be created with config.auth value. It's how our API behaves. I think there can be other sources like this.

@alexluong alexluong changed the title feat: Implement source auth feat: Source & destination config Mar 2, 2025
@leggetter
Copy link
Collaborator

@alexluong looks like generate-codegen is still in the make file. Should this be removed?

@leggetter
Copy link
Collaborator

leggetter commented Mar 5, 2025

@alexluong - this PR also stops the webhook registration flow working.

Previously:

resource "hookdeck_source" "stripe" {
  name = "stripe"
}

resource "webhook_registration" "stripe" {
  provider = hookdeck

  register = {
    request = {
      method = "POST"
      url    = "https://api.stripe.com/v1/webhook_endpoints"
      headers = jsonencode({
        "content-type" = "application/json"
        authorization  = "Bearer <STRIPE_SECRET_KEY>"
      })
      body = jsonencode({
        url = hookdeck_source.stripe.url
        enabled_events = [
          "charge.failed",
          "charge.succeeded"
        ]
      })
    }
  }
  unregister = {
    request = {
      method = "DELETE"
      url    = "https://api.stripe.com/v1/webhook_endpoints/{{.register.response.body.id}}"
      headers = jsonencode({
        authorization  = "Bearer <STRIPE_SECRET_KEY>"
      })
    }
  }
}

resource "hookdeck_source_verification" "stripe_verification" {
  source_id = hookdeck_source.stripe.id
  verification = {
    stripe = {
      webhook_secret_key = jsondecode(webhook_registration.stripe.register.response).body.secret
    }
  }
}

With this change

Because the Source now includes the config, there's no way of using the webhook registration response to set the webhook secret on the Source.

resource "hookdeck_source" "stripe_source" {
  name = "stripe"
  type = "STRIPE"
}

resource "hookdeck_webhook_registration" "stripe_registration" {
  provider = hookdeck

  register = {
    request = {
      method = "POST"
      url    = "https://api.stripe.com/v1/webhook_endpoints"
      headers = jsonencode({
        "content-type" = "application/json"
        authorization  = "Bearer ${var.STRIPE_SECRET_KEY}"
      })
      body = jsonencode({
        url = hookdeck_source.stripe_source.url
        enabled_events = [
          "charge.failed",
          "charge.succeeded"
        ]
      })
    }
  }
  unregister = {
    request = {
      method = "DELETE"
      url    = "https://api.stripe.com/v1/webhook_endpoints/{{.register.response.body.id}}"
      headers = jsonencode({
        authorization  = "Bearer <STRIPE_SECRET_KEY>"
      })
    }
  }
}

# You can't do this
# resource "hookdeck_source" "stripe_source" {
#   name = "stripe"
#   type = "STRIPE"
#   config = jsonencode({
#     webhook_secret_key = jsondecode(webhook_registration.stripe_registration.register.response).body.secret
#   })
# }

Should we consider moving Config out as it's own resource type?

So, something like this:

resource "hookdeck_source" "stripe_source" {
  name = "stripe"
  type = "STRIPE"
}

resource "hookdeck_webhook_registration" "stripe_registration" {
  provider = hookdeck

  register = {
    request = {
      method = "POST"
      url    = "https://api.stripe.com/v1/webhook_endpoints"
      headers = jsonencode({
        "content-type" = "application/json"
        authorization  = "Bearer ${var.STRIPE_SECRET_KEY}"
      })
      body = jsonencode({
        url = hookdeck_source.stripe_source.url
        enabled_events = [
          "charge.failed",
          "charge.succeeded"
        ]
      })
    }
  }
  unregister = {
    request = {
      method = "DELETE"
      url    = "https://api.stripe.com/v1/webhook_endpoints/{{.register.response.body.id}}"
      headers = jsonencode({
        authorization  = "Bearer <STRIPE_SECRET_KEY>"
      })
    }
  }
}

resource "hookdeck_source_config" "stripe_source_config" {
  source_id = hookdeck_source.stripe.id
  # This results in a double "config". So maybe the property should be called "value"
  config = jsonencode({
    webhook_secret_key = jsondecode(webhook_registration.stripe_registration.register.response).body.secret
  })
}

@leggetter
Copy link
Collaborator

@alexluong without the api_base value set I get errors such as:

│ Error: Error reading connection
│ 
│   with data.hookdeck_connection.manually_created_connection,
│   on main.tf line 116, in data "hookdeck_connection" "manually_created_connection":
│  116: data "hookdeck_connection" "manually_created_connection" {
│ 
│ Get "https://2025-01-01/connections/web_xDRnu9yq9GMl": dial tcp: lookup 2025-01-01: no such
│ host

It looks like it assumes an api_base will be set and there is no default.

@leggetter
Copy link
Collaborator

@alexluong - all config is now going to be shown, including secrets within config.

  + resource "hookdeck_source" "second_source" {
      + config      = jsonencode(
            {
              + auth      = {
                  + password = "blah-blah-blah"
                  + username = "some-username"
                }
              + auth_type = "BASIC_AUTH"
            }
        )

@alexluong
Copy link
Collaborator Author

Should we consider moving Config out as it's own resource type?

That should already work actually, not exactly the syntax you provided but similar. Here's an example:

resource "hookdeck_source" "source" {
  name = "source"

  type = "HTTP"

  config = jsonencode({
    allowed_http_methods = ["POST"]
  })
}

resource "hookdeck_source_auth" "source_auth" {
  source_id = hookdeck_source.source.id
  auth_type = "API_KEY"
  auth = jsonencode({
    header_key = "X-API-KEY"
    api_key = "secret"
  })
}

Noted on the other issues, I'll get push a few commits to address them soon.

* chore: update terraform.tfvars to terraform.tfvars.example

* chore(docs): updates for source and destination types

* chore: make pre-commit hook executable

* chore: formatting and docs generation

* chore: update webhook registration to use x-www-form-urlencoded

* chore: add example source_auth example with basic auth

* chore(docs): add v0.x to v1.x migration guide

* fix: Support default API base

* fix: Make config & auth sensitive fields

* chore: Remove unused codegen-related code

* docs: Generate

* chore: remove unnecessary api_base config

* chore(docs): regen docs and format

* chore(docs): add notes on config/auth validation and sensitivity

* chore(docs): docs gen

* chore(docs): update migration guide

---------

Co-authored-by: Alex Luong <[email protected]>
@leggetter leggetter merged commit a0a6fa4 into main Mar 6, 2025
14 checks passed
@leggetter leggetter deleted the source-types branch March 6, 2025 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants