Files
terraform-provider-threshold/internal/provider/resource_target.go
T

711 lines
22 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"math"
"net/http"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
targetHostSourceIdsKey = "host_source_ids"
targetBrokeredCredentialSourceIdsKey = "brokered_credential_source_ids"
targetInjectedAppCredentialSourceIdsKey = "injected_application_credential_source_ids"
targetDefaultPortKey = "default_port"
targetDefaultClientPortKey = "default_client_port"
targetSessionMaxSecondsKey = "session_max_seconds"
targetSessionConnectionLimitKey = "session_connection_limit"
targetWorkerFilterKey = "worker_filter"
targetWorkerEgressFilterKey = "egress_worker_filter"
targetWorkerIngressFilterKey = "ingress_worker_filter"
targetAddressKey = "address"
targetTypeTcp = "tcp"
targetTypeSsh = "ssh"
)
func resourceTarget() *schema.Resource {
return &schema.Resource{
Description: "The target resource allows you to configure a Boundary target.",
CreateContext: resourceTargetCreate,
ReadContext: resourceTargetRead,
UpdateContext: resourceTargetUpdate,
DeleteContext: resourceTargetDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
IDKey: {
Description: "The ID of the target.",
Type: schema.TypeString,
Computed: true,
},
NameKey: {
Description: "The target name. Defaults to the resource name.",
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Description: "The target description.",
Type: schema.TypeString,
Optional: true,
},
TypeKey: {
Description: "The target resource type.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
ScopeIdKey: {
Description: "The scope ID in which the resource is created. Defaults to the provider's `default_scope` if unset.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
targetDefaultPortKey: {
Description: "The default port for this target.",
Type: schema.TypeInt,
Optional: true,
},
targetDefaultClientPortKey: {
Description: "The default client port for this target.",
Type: schema.TypeInt,
Optional: true,
},
targetHostSourceIdsKey: {
Description: "A list of host source ID's. Cannot be used alongside address.",
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
ConflictsWith: []string{targetAddressKey},
},
targetBrokeredCredentialSourceIdsKey: {
Description: "A list of brokered credential source ID's.",
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
targetInjectedAppCredentialSourceIdsKey: {
Description: "A list of injected application credential source ID's.",
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
targetSessionMaxSecondsKey: {
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
targetSessionConnectionLimitKey: {
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
targetWorkerFilterKey: {
Description: "Boolean expression to filter the workers for this target",
Type: schema.TypeString,
Optional: true,
Deprecated: "Deprecated. Use `egress_worker_filter` and `ingress_worker_filter` instead",
},
targetWorkerEgressFilterKey: {
Description: "Boolean expression to filter the workers used to access this target",
Type: schema.TypeString,
Optional: true,
},
targetWorkerIngressFilterKey: {
Description: "HCP Only. Boolean expression to filter the workers a user will connect to when initiating a session against this target",
Type: schema.TypeString,
Optional: true,
},
targetAddressKey: {
Description: "Optionally, a valid network address to connect to for this target. Cannot be used alongside host_source_ids.",
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{targetHostSourceIdsKey},
},
},
}
}
func setFromTargetResponseMap(d *schema.ResourceData, raw map[string]interface{}) error {
if err := d.Set(NameKey, raw["name"]); err != nil {
return err
}
if err := d.Set(DescriptionKey, raw["description"]); err != nil {
return err
}
if err := d.Set(ScopeIdKey, raw["scope_id"]); err != nil {
return err
}
if err := d.Set(TypeKey, raw["type"]); err != nil {
return err
}
if err := d.Set(targetHostSourceIdsKey, raw["host_source_ids"]); err != nil {
return err
}
if err := d.Set(targetBrokeredCredentialSourceIdsKey, raw["brokered_credential_source_ids"]); err != nil {
return err
}
if err := d.Set(targetInjectedAppCredentialSourceIdsKey, raw["injected_application_credential_source_ids"]); err != nil {
return err
}
if err := d.Set(targetSessionMaxSecondsKey, raw["session_max_seconds"]); err != nil {
return err
}
if err := d.Set(targetSessionConnectionLimitKey, raw["session_connection_limit"]); err != nil {
return err
}
if err := d.Set(targetWorkerFilterKey, raw["worker_filter"]); err != nil {
return err
}
if err := d.Set(targetWorkerEgressFilterKey, raw["egress_worker_filter"]); err != nil {
return err
}
if err := d.Set(targetWorkerIngressFilterKey, raw["ingress_worker_filter"]); err != nil {
return err
}
if err := d.Set(targetAddressKey, raw["address"]); err != nil {
return err
}
switch raw["type"].(string) {
case targetTypeTcp, targetTypeSsh:
if attrsVal, ok := raw["attributes"]; ok {
attrs := attrsVal.(map[string]interface{})
if defPort, ok := attrs[targetDefaultPortKey].(json.Number); ok {
defPortInt, _ := defPort.Int64()
if err := d.Set(targetDefaultPortKey, int(defPortInt)); err != nil {
return err
}
}
if defCliPort, ok := attrs[targetDefaultClientPortKey].(json.Number); ok {
defCliPortInt, _ := defCliPort.Int64()
if err := d.Set(targetDefaultClientPortKey, int(defCliPortInt)); err != nil {
return err
}
}
}
}
d.SetId(raw["id"].(string))
return nil
}
func resourceTargetCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) (errs diag.Diagnostics) {
md := meta.(*metaData)
var scopeId string
if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok {
scopeId = scopeIdVal.(string)
} else {
return diag.Errorf("no scope ID provided")
}
var typeStr string
if typeVal, ok := d.GetOk(TypeKey); ok {
typeStr = typeVal.(string)
} else {
return diag.Errorf("no type provided")
}
switch typeStr {
case targetTypeTcp, targetTypeSsh:
default:
return diag.Errorf("invalid type provided")
}
var opts []targets.Option
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, targets.WithName(nameStr))
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, targets.WithDescription(descStr))
}
defaultPortVal, ok := d.GetOk(targetDefaultPortKey)
if ok {
defaultPortInt := defaultPortVal.(int)
if defaultPortInt <= 0 || defaultPortInt > math.MaxUint16 {
return diag.Errorf(`"default_port" must be a valid tcp port`)
}
switch typeStr {
case targetTypeTcp:
opts = append(opts, targets.WithTcpTargetDefaultPort(uint32(defaultPortInt)))
case targetTypeSsh:
opts = append(opts, targets.WithSshTargetDefaultPort(uint32(defaultPortInt)))
}
}
defaultClientPortVal, ok := d.GetOk(targetDefaultClientPortKey)
if ok {
defaultClientPortInt := defaultClientPortVal.(int)
if defaultClientPortInt <= 0 || defaultClientPortInt > math.MaxUint16 {
return diag.Errorf(`"default_client_port" must be a valid tcp port`)
}
switch typeStr {
case targetTypeTcp:
opts = append(opts, targets.WithTcpTargetDefaultClientPort(uint32(defaultClientPortInt)))
case targetTypeSsh:
opts = append(opts, targets.WithSshTargetDefaultClientPort(uint32(defaultClientPortInt)))
}
}
sessionMaxSecondsVal, ok := d.GetOk(targetSessionMaxSecondsKey)
if ok {
sessionMaxSecondsInt := sessionMaxSecondsVal.(int)
if sessionMaxSecondsInt <= 0 {
return diag.Errorf(`"session_max_seconds" must be greater than zero`)
}
opts = append(opts, targets.WithSessionMaxSeconds(uint32(sessionMaxSecondsInt)))
}
sessionConnectionLimitVal, ok := d.GetOk(targetSessionConnectionLimitKey)
if ok {
sessionConnectionLimitInt := sessionConnectionLimitVal.(int)
if sessionConnectionLimitInt != -1 && sessionConnectionLimitInt <= 0 {
return diag.Errorf(`"session_connection_limit" must be -1 or greater than zero`)
}
opts = append(opts, targets.WithSessionConnectionLimit(int32(sessionConnectionLimitInt)))
}
var hostSourceIds []string
if hostSourceIdsVal, ok := d.GetOk(targetHostSourceIdsKey); ok {
list := hostSourceIdsVal.(*schema.Set).List()
hostSourceIds = make([]string, 0, len(list))
for _, i := range list {
hostSourceIds = append(hostSourceIds, i.(string))
}
}
var brokeredCreds []string
if credentialSourceIdsVal, ok := d.GetOk(targetBrokeredCredentialSourceIdsKey); ok {
list := credentialSourceIdsVal.(*schema.Set).List()
brokeredCreds = make([]string, 0, len(list))
for _, i := range list {
brokeredCreds = append(brokeredCreds, i.(string))
}
}
var injectedCreds []string
if credentialSourceIdsVal, ok := d.GetOk(targetInjectedAppCredentialSourceIdsKey); ok {
list := credentialSourceIdsVal.(*schema.Set).List()
injectedCreds = make([]string, 0, len(list))
for _, i := range list {
injectedCreds = append(injectedCreds, i.(string))
}
}
workerFilterVal, ok := d.GetOk(targetWorkerFilterKey)
if ok {
workerFilterStr := workerFilterVal.(string)
opts = append(opts, targets.WithWorkerFilter(workerFilterStr))
}
workerEgressFilterVal, ok := d.GetOk(targetWorkerEgressFilterKey)
if ok {
workerEgressFilterStr := workerEgressFilterVal.(string)
opts = append(opts, targets.WithEgressWorkerFilter(workerEgressFilterStr))
}
workerIngressFilterVal, ok := d.GetOk(targetWorkerIngressFilterKey)
if ok {
workerIngressFilterStr := workerIngressFilterVal.(string)
opts = append(opts, targets.WithIngressWorkerFilter(workerIngressFilterStr))
}
addrVal, ok := d.GetOk(targetAddressKey)
if ok {
addrStr := addrVal.(string)
opts = append(opts, targets.WithAddress(addrStr))
}
tc := targets.NewClient(md.client)
tcr, err := tc.Create(ctx, typeStr, scopeId, opts...)
if err != nil {
return diag.Errorf("error creating target: %v", err)
}
if tcr == nil {
return diag.Errorf("target nil after create")
}
apiResponse := tcr.GetResponse().Map
defer func() {
if err := setFromTargetResponseMap(d, apiResponse); err != nil {
errs = append(errs, diag.FromErr(err)...)
}
}()
version := tcr.Item.Version
if hostSourceIds != nil {
tur, err := tc.SetHostSources(ctx, tcr.Item.Id, version, hostSourceIds)
if err != nil {
return diag.Errorf("error setting host sources on target: %v", err)
}
if tur == nil {
return diag.Errorf("nil target after setting host sources")
}
apiResponse = tur.GetResponse().Map
version = tur.Item.Version
}
var credOpts []targets.Option
if brokeredCreds != nil {
credOpts = append(credOpts, targets.WithBrokeredCredentialSourceIds(brokeredCreds))
}
if injectedCreds != nil {
credOpts = append(credOpts, targets.WithInjectedApplicationCredentialSourceIds(injectedCreds))
}
if len(credOpts) > 0 {
tur, err := tc.SetCredentialSources(ctx, tcr.Item.Id, version, credOpts...)
if err != nil {
return diag.Errorf("error setting credential sources on target: %v", err)
}
if tur == nil {
return diag.Errorf("nil target after setting credential sources")
}
apiResponse = tur.GetResponse().Map
}
return nil
}
func resourceTargetRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
tc := targets.NewClient(md.client)
trr, err := tc.Read(ctx, d.Id())
if err != nil {
if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound {
d.SetId("")
return nil
}
return diag.Errorf("error reading target: %v", err)
}
if trr == nil {
return diag.Errorf("target nil after read")
}
if err := setFromTargetResponseMap(d, trr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
return nil
}
func resourceTargetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
tc := targets.NewClient(md.client)
var opts []targets.Option
var typeStr string
if typeVal, ok := d.GetOk(TypeKey); ok {
typeStr = typeVal.(string)
}
switch typeStr {
case targetTypeTcp, targetTypeSsh:
default:
return diag.Errorf("invalid type provided")
}
var name *string
if d.HasChange(NameKey) {
opts = append(opts, targets.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
name = &nameStr
opts = append(opts, targets.WithName(nameStr))
}
}
var desc *string
if d.HasChange(DescriptionKey) {
opts = append(opts, targets.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
desc = &descStr
opts = append(opts, targets.WithDescription(descStr))
}
}
var defaultPort *int
if d.HasChange(targetDefaultPortKey) {
switch typeStr {
case targetTypeTcp:
opts = append(opts, targets.DefaultTcpTargetDefaultPort())
defaultPortVal, ok := d.GetOk(targetDefaultPortKey)
if ok {
defaultPortInt := defaultPortVal.(int)
if defaultPortInt <= 0 || defaultPortInt > math.MaxUint16 {
return diag.Errorf(`"default_port" must be a valid tcp port`)
}
defaultPort = &defaultPortInt
opts = append(opts, targets.WithTcpTargetDefaultPort(uint32(defaultPortInt)))
}
case targetTypeSsh:
opts = append(opts, targets.DefaultSshTargetDefaultPort())
defaultPortVal, ok := d.GetOk(targetDefaultPortKey)
if ok {
defaultPortInt := defaultPortVal.(int)
if defaultPortInt <= 0 || defaultPortInt > math.MaxUint16 {
return diag.Errorf(`"default_port" must be a valid tcp port`)
}
defaultPort = &defaultPortInt
opts = append(opts, targets.WithSshTargetDefaultPort(uint32(defaultPortInt)))
}
}
}
var defaultClientPort *int
if d.HasChange(targetDefaultClientPortKey) {
switch typeStr {
case targetTypeTcp:
opts = append(opts, targets.DefaultTcpTargetDefaultClientPort())
defaultClientPortVal, ok := d.GetOk(targetDefaultClientPortKey)
if ok {
defaultClientPortInt := defaultClientPortVal.(int)
if defaultClientPortInt <= 0 || defaultClientPortInt > math.MaxUint16 {
return diag.Errorf(`"default_client_port" must be a valid tcp port`)
}
defaultClientPort = &defaultClientPortInt
opts = append(opts, targets.WithTcpTargetDefaultClientPort(uint32(defaultClientPortInt)))
}
case targetTypeSsh:
opts = append(opts, targets.DefaultSshTargetDefaultClientPort())
defaultClientPortVal, ok := d.GetOk(targetDefaultClientPortKey)
if ok {
defaultClientPortInt := defaultClientPortVal.(int)
if defaultClientPortInt <= 0 || defaultClientPortInt > math.MaxUint16 {
return diag.Errorf(`"default_client_port" must be a valid tcp port`)
}
defaultClientPort = &defaultClientPortInt
opts = append(opts, targets.WithSshTargetDefaultClientPort(uint32(defaultClientPortInt)))
}
}
}
var sessionMaxSeconds *int
if d.HasChange(targetSessionMaxSecondsKey) {
opts = append(opts, targets.DefaultSessionMaxSeconds())
sessionMaxSecondsVal, ok := d.GetOk(targetSessionMaxSecondsKey)
if ok {
sessionMaxSecondsInt := sessionMaxSecondsVal.(int)
if sessionMaxSecondsInt <= 0 {
return diag.Errorf(`"session_max_seconds" must be greater than zero`)
}
sessionMaxSeconds = &sessionMaxSecondsInt
opts = append(opts, targets.WithSessionMaxSeconds(uint32(sessionMaxSecondsInt)))
}
}
var sessionConnectionLimit *int
if d.HasChange(targetSessionConnectionLimitKey) {
opts = append(opts, targets.DefaultSessionConnectionLimit())
sessionConnectionLimitVal, ok := d.GetOk(targetSessionConnectionLimitKey)
if ok {
sessionConnectionLimitInt := sessionConnectionLimitVal.(int)
if sessionConnectionLimitInt != -1 && sessionConnectionLimitInt <= 0 {
return diag.Errorf(`"session_connection_limit" must be -1 or greater than zero`)
}
sessionConnectionLimit = &sessionConnectionLimitInt
opts = append(opts, targets.WithSessionConnectionLimit(int32(sessionConnectionLimitInt)))
}
}
var workerFilter *string
if d.HasChange(targetWorkerFilterKey) {
opts = append(opts, targets.DefaultWorkerFilter())
workerFilterVal, ok := d.GetOk(targetWorkerFilterKey)
if ok {
workerFilterStr := workerFilterVal.(string)
workerFilter = &workerFilterStr
opts = append(opts, targets.WithWorkerFilter(workerFilterStr))
}
}
var workerEgressFilter *string
if d.HasChange(targetWorkerEgressFilterKey) {
opts = append(opts, targets.DefaultEgressWorkerFilter())
workerEgressFilterVal, ok := d.GetOk(targetWorkerEgressFilterKey)
if ok {
workerEgressFilterStr := workerEgressFilterVal.(string)
workerEgressFilter = &workerEgressFilterStr
opts = append(opts, targets.WithEgressWorkerFilter(workerEgressFilterStr))
}
}
var workerIngressFilter *string
if d.HasChange(targetWorkerIngressFilterKey) {
opts = append(opts, targets.DefaultIngressWorkerFilter())
workerIngressFilterVal, ok := d.GetOk(targetWorkerIngressFilterKey)
if ok {
workerIngressFilterStr := workerIngressFilterVal.(string)
workerIngressFilter = &workerIngressFilterStr
opts = append(opts, targets.WithIngressWorkerFilter(workerIngressFilterStr))
}
}
var addr *string
if d.HasChange(targetAddressKey) {
opts = append(opts, targets.DefaultAddress())
addrVal, ok := d.GetOk(targetAddressKey)
if ok {
addrStr := addrVal.(string)
addr = &addrStr
opts = append(opts, targets.WithAddress(addrStr))
}
}
if len(opts) > 0 {
opts = append(opts, targets.WithAutomaticVersioning(true))
_, err := tc.Update(ctx, d.Id(), 0, opts...)
if err != nil {
return diag.Errorf("error updating target: %v", err)
}
}
if d.HasChange(NameKey) {
if err := d.Set(NameKey, name); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(DescriptionKey) {
if err := d.Set(DescriptionKey, desc); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(targetDefaultPortKey) {
if err := d.Set(targetDefaultPortKey, defaultPort); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(targetDefaultClientPortKey) {
if err := d.Set(targetDefaultClientPortKey, defaultClientPort); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(targetSessionMaxSecondsKey) {
if err := d.Set(targetSessionMaxSecondsKey, sessionMaxSeconds); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(targetSessionConnectionLimitKey) {
if err := d.Set(targetSessionConnectionLimitKey, sessionConnectionLimit); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(targetWorkerFilterKey) {
if err := d.Set(targetWorkerFilterKey, workerFilter); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(targetWorkerEgressFilterKey) {
if err := d.Set(targetWorkerEgressFilterKey, workerEgressFilter); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(targetWorkerIngressFilterKey) {
if err := d.Set(targetWorkerIngressFilterKey, workerIngressFilter); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(targetAddressKey) {
if err := d.Set(targetAddressKey, addr); err != nil {
return diag.FromErr(err)
}
}
// The above call may not actually happen, so we use d.Id() and automatic
// versioning here
if d.HasChange(targetHostSourceIdsKey) {
var hostSourceIds []string
if hostSourceIdsVal, ok := d.GetOk(targetHostSourceIdsKey); ok {
hostSources := hostSourceIdsVal.(*schema.Set).List()
for _, hostSource := range hostSources {
hostSourceIds = append(hostSourceIds, hostSource.(string))
}
}
_, err := tc.SetHostSources(ctx, d.Id(), 0, hostSourceIds, targets.WithAutomaticVersioning(true))
if err != nil {
return diag.Errorf("error updating host sources in target: %v", err)
}
if err := d.Set(targetHostSourceIdsKey, hostSourceIds); err != nil {
return diag.FromErr(err)
}
}
// if any of the credential types are changed, then all credential ids must be gathered
// because the SetCredentialSources function will remove ids that are not present.
if d.HasChange(targetBrokeredCredentialSourceIdsKey) || d.HasChange(targetInjectedAppCredentialSourceIdsKey) {
credOpts := []targets.Option{
targets.WithAutomaticVersioning(true),
}
if credentialSourceIdsVal, ok := d.GetOk(targetBrokeredCredentialSourceIdsKey); ok {
var credentialSourceIds []string
credSourceIds := credentialSourceIdsVal.(*schema.Set).List()
for _, credSourceId := range credSourceIds {
credentialSourceIds = append(credentialSourceIds, credSourceId.(string))
}
credOpts = append(credOpts, targets.WithBrokeredCredentialSourceIds(credentialSourceIds))
}
if credentialSourceIdsVal, ok := d.GetOk(targetInjectedAppCredentialSourceIdsKey); ok {
var credentialSourceIds []string
credSourceIds := credentialSourceIdsVal.(*schema.Set).List()
for _, credSourceId := range credSourceIds {
credentialSourceIds = append(credentialSourceIds, credSourceId.(string))
}
credOpts = append(credOpts, targets.WithInjectedApplicationCredentialSourceIds(credentialSourceIds))
}
result, err := tc.SetCredentialSources(ctx, d.Id(), 0, credOpts...)
if err != nil {
return diag.Errorf("error updating credential sources in target: %v", err)
}
if d.HasChange(targetBrokeredCredentialSourceIdsKey) {
if err := d.Set(targetBrokeredCredentialSourceIdsKey, result.Item.BrokeredCredentialSourceIds); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(targetInjectedAppCredentialSourceIdsKey) {
if err := d.Set(targetInjectedAppCredentialSourceIdsKey, result.Item.InjectedApplicationCredentialSourceIds); err != nil {
return diag.FromErr(err)
}
}
}
return nil
}
func resourceTargetDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
tc := targets.NewClient(md.client)
_, err := tc.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error deleting target: %s", err.Error())
}
return nil
}