Skip to content

Commit

Permalink
feat(resource): Connectors for configuration v2 (#141)
Browse files Browse the repository at this point in the history
* add connectors

* support delete

* move readRolloutOptions()

* WIP: We need component ID to be generated before apply

* generate connector id

* Remove test file
  • Loading branch information
jsirianni authored Feb 8, 2025
1 parent ce7b51b commit 4cdb3e6
Show file tree
Hide file tree
Showing 11 changed files with 1,162 additions and 44 deletions.
48 changes: 48 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,38 @@ func (i *BindPlane) Rollout(name string) error {
return err
}

// Connector takes a name and returns the matching connector
func (i *BindPlane) Connector(name string) (*model.Connector, error) {
r, err := i.Client.Resource(context.Background(), model.KindConnector, name)
if err != nil {
// Do not return an error if the resource is not found. Terraform
// will understand that the resource does not exist when it receives
// a nil value, and will instead offer to create the resource.
if isNotFoundError(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to get connector with name %s: %w", name, err)
}

// Bindplane should always return a connector but we should handle it
// anyway considering we need to type assert it.
switch c := r.(type) {
case *model.Connector:
return c, nil
default:
return nil, fmt.Errorf("unexpected response from bindplane, expected connector, got %T, this is a bug that should be reported", c)
}
}

// DeleteConnector will delete a BindPlane connector
func (i *BindPlane) DeleteConnector(name string) error {
err := i.Client.DeleteResource(context.Background(), model.KindConnector, name)
if err != nil {
return fmt.Errorf("error while deleting connector with name %s: %w", name, err)
}
return nil
}

// Configuration takes a name and returns the matching configuration
func (i *BindPlane) Configuration(name string) (*model.Configuration, error) {
c, err := i.Client.Configuration(context.Background(), name)
Expand Down Expand Up @@ -231,6 +263,8 @@ func (i *BindPlane) Delete(k model.Kind, name string) error {
return i.DeleteProcessor(name)
case model.KindExtension:
return i.DeleteExtension(name)
case model.KindConnector:
return i.DeleteConnector(name)
default:
return fmt.Errorf("Delete does not support bindplane kind '%s'", k)
}
Expand Down Expand Up @@ -304,6 +338,20 @@ func (i *BindPlane) GenericResource(k model.Kind, name string) (*GenericResource
return nil, nil
}

g.ID = r.ID()
g.Name = r.Name()
g.Version = r.Version()
g.Spec = r.Spec
case model.KindConnector:
r, err := i.Connector(name)
if err != nil {
return nil, err
}

if r == nil {
return nil, nil
}

g.ID = r.ID()
g.Name = r.Name()
g.Version = r.Version()
Expand Down
183 changes: 183 additions & 0 deletions docs/resources/bindplane_connector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
---
subcategory: "Pipeline"
description: |-
A Connector creates a BindPlane OP connector that can be attached
to a Configuration's sources or destinations.
---

# bindplane_connector

The `bindplane_connector` resource creates a BindPlane connector from a BindPlane
connector-type. The connector can be used by multiple [configurations](./bindplane_configuration.md).

## Options

| Option | Type | Default | Description |
| ------------------- | ----- | -------- | ---------------------------- |
| `name` | string | required | The connector name. |
| `type` | string | required | The connector type. |
| `parameters_json` | string | optional | The serialized JSON representation of the connector type's parameters. |
| `rollout` | bool | required | Whether or not updates to the connector should trigger an automatic rollout of any configuration that uses it. |

## Examples

### Routing Connector

This example shows the Routing connector type with two routes.

```hcl
resource "bindplane_connector" "routing" {
rollout = true
name = "my-routing"
type = "routing"
parameters_json = jsonencode(
[
{
"name": "telemetry_types",
"value": [
"Logs"
]
},
{
"name": "routes",
"value": [
{
"condition": {
"ottl": "(attributes[\"env\"] == \"prod\")",
"ottlContext": "resource",
"ui": {
"operator": "",
"statements": [
{
"key": "env",
"match": "resource",
"operator": "Equals",
"value": "prod"
}
]
}
},
"id": "route-1"
},
{
"condition": {
"ottl": "(attributes[\"env\"] == \"dev\")",
"ottlContext": "resource",
"ui": {
"operator": "",
"statements": [
{
"key": "env",
"match": "resource",
"operator": "Equals",
"value": "dev"
}
]
}
},
"id": "route-2"
}
]
}
]
)
}
```

## Usage

You can find available connector types with the `bindplane get connector-type` command:
```bash
NAME DISPLAY VERSION
count Count 1
routing Routing 1
```

You can view an individual connector type's options with the `bindplane get connector-type <name> -o yaml` command:
```yaml
# bindplane get connector-type routing -o yaml
apiVersion: bindplane.observiq.com/v1
kind: ConnectorType
metadata:
id: 01JKGZE5JPHEN60VHQ2VTFDBFM
name: routing
displayName: Routing
description: Route telemetry based on conditions
icon: /icons/connectors/routing.svg
hash: 69487953ba8144a063f4d855f95f129789fcfcf368570f47a713625083b7abc7
version: 1
dateModified: 2025-02-07T14:50:54.294599087-05:00
stability: beta
spec:
parameters:
- name: telemetry_types
label: Choose Telemetry Type
description: Telemetry Type for the routes
required: true
type: telemetrySelector
validValues:
- Logs
- Metrics
- Traces
default: []
options:
gridColumns: 12
- name: routes
label: Routes
...
```

You can view the json representation of the connector type's options with the `-o json` flag combined with `jq`.
For example, `bindplane get connector-type routing -o json | jq .spec.parameters` produces the following:
```json
[
{
"name": "telemetry_types",
"label": "Choose Telemetry Type",
"description": "Telemetry Type for the routes",
"required": true,
"type": "telemetrySelector",
"validValues": [
"Logs",
"Metrics",
"Traces"
],
"default": [],
"options": {
"gridColumns": 12
}
},
{
"name": "routes",
"label": "Routes",
"description": "Telemetry will be sent to the first route it matches based on the condition. If\nthere is no condition specified for a route, all remaining telemetry will be sent\nto that route.\n",
"required": true,
"type": "routes",
"default": [
{
"id": "route-1"
},
{
"id": "route-2"
}
],
"options": {
"gridColumns": 12
},
"properties": {
"addButtonText": "Add Route",
"condition": true,
"routeBase": "route"
}
}
]
```

## Import

When using the [terraform import command](https://developer.hashicorp.com/terraform/cli/commands/import),
connector can be imported. For example:

```bash
terraform import bindplane_connector.connector {{name}}
```
35 changes: 35 additions & 0 deletions internal/configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type ResourceConfig struct {

// A list of processor names to attach to the resource
Processors []string

// Routes to attach to the resource
Routes *model.Routes
}

// Option is a function that configures a
Expand Down Expand Up @@ -147,6 +150,37 @@ func NewV1(options ...Option) (*model.Configuration, error) {
return c, nil
}

// NewV2Beta takes a configuration options and returns a BindPlane configuration
// with API version bindplane.observiq.com/v2beta
func NewV2Beta(options ...Option) (*model.Configuration, error) {
const (
version = "bindplane.observiq.com/v2beta"
kind = model.KindConfiguration
contentType = "text/yaml" // TODO(jsirianni): Is this required and does it make sense?
)

c := &model.Configuration{
ResourceMeta: model.ResourceMeta{
APIVersion: version,
Kind: kind,
},
Spec: model.ConfigurationSpec{
ContentType: contentType,
},
}

for _, option := range options {
if option == nil {
continue
}
if err := option(c); err != nil {
return nil, err
}
}

return c, nil
}

// WithResourcesByName takes a list of resource configurations
// and returns a list of bindplane model.ResourceConfigurations.
func withResourcesByName(r []ResourceConfig) []model.ResourceConfiguration {
Expand All @@ -169,6 +203,7 @@ func withResourcesByName(r []ResourceConfig) []model.ResourceConfiguration {
ParameterizedSpec: model.ParameterizedSpec{
Processors: processorResources,
},
Routes: r.Routes,
}
resources = append(resources, r)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func AnyResourceV1(id, rName, rType string, rKind model.Kind, rParameters []mode
}

switch rKind {
case model.KindSource, model.KindDestination, model.KindProcessor, model.KindExtension:
case model.KindSource, model.KindDestination, model.KindProcessor, model.KindExtension, model.KindConnector:
return model.AnyResource{
ResourceMeta: model.ResourceMeta{
APIVersion: "bindplane.observiq.com/v1",
Expand Down
2 changes: 2 additions & 0 deletions provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ func Configure() *schema.Provider {
},
},
ResourcesMap: map[string]*schema.Resource{
"bindplane_connector": resourceConnector(),
"bindplane_configuration": resourceConfiguration(),
"bindplane_configuration_v2": resourceConfigurationV2(),
"bindplane_destination": resourceDestination(),
"bindplane_source": resourceSource(),
"bindplane_processor": resourceProcessor(),
Expand Down
43 changes: 0 additions & 43 deletions provider/resource_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,49 +333,6 @@ func resourceConfigurationCreate(d *schema.ResourceData, meta any) error {
return resourceConfigurationRead(d, meta)
}

// readRolloutOptions safely reads "rollout_options" from the resource data.
func readRolloutOptions(d *schema.ResourceData) (model.ResourceConfiguration, error) {
rolloutOptionsRaw, ok := d.GetOk("rollout_options")
if !ok || len(rolloutOptionsRaw.([]interface{})) == 0 {
return model.ResourceConfiguration{}, nil
}

// Because d.GetOk returned a non nil value, we can assume that the
// rollout_options list has at least one element due to the Terraform
// framework's schema validation. Type assertion is safe in this case.

rolloutOptions := rolloutOptionsRaw.([]interface{})[0].(map[string]interface{})
resourceConfig := model.ResourceConfiguration{}

if t, ok := rolloutOptions["type"].(string); ok {
resourceConfig.Type = t
}

if parametersRaw, ok := rolloutOptions["parameters"]; ok {
parametersList := parametersRaw.([]interface{})
parameters := make([]model.Parameter, len(parametersList))
for i, p := range parametersList {
paramMap := p.(map[string]interface{})
param := model.Parameter{}
if name, ok := paramMap["name"].(string); ok {
param.Name = name
}
if valueRaw, ok := paramMap["value"]; ok {
valueList := valueRaw.([]interface{})
values := make([]interface{}, len(valueList))
for j, v := range valueList {
values[j] = v.(map[string]interface{})
}
param.Value = values
}
parameters[i] = param
}
resourceConfig.Parameters = parameters
}

return resourceConfig, nil
}

func resourceConfigurationRead(d *schema.ResourceData, meta any) error {
bindplane := meta.(*client.BindPlane)

Expand Down
Loading

0 comments on commit 4cdb3e6

Please sign in to comment.