Skip to content
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

feat(resource): Connectors for configuration v2 #141

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
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
Loading