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

support setting a variable of type Connection using a connection string or cloud workspace handle #591

Merged
merged 2 commits into from
Oct 22, 2024
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
6 changes: 6 additions & 0 deletions connection/duckdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const DuckDbConnectionType = "duckdb"
type DuckDbConnection struct {
ConnectionImpl
FileName *string `json:"file_name,omitempty" cty:"file_name" hcl:"file_name,optional"`
// used only to set the connection string from command line variable value with a connection string
ConnectionString *string `json:"connection_string,omitempty" cty:"connection_string"`
}

func NewDuckDbConnection(shortName string, declRange hcl.Range) PipelingConnection {
Expand Down Expand Up @@ -78,6 +80,10 @@ func (c *DuckDbConnection) CtyValue() (cty.Value, error) {
}

func (c *DuckDbConnection) GetConnectionString() string {
if c.ConnectionString != nil {
return *c.ConnectionString
}

return fmt.Sprintf("duckdb://%s", c.getFileName())
}

Expand Down
6 changes: 6 additions & 0 deletions connection/sqllite.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const SqliteConnectionType = "sqlite"
type SqliteConnection struct {
ConnectionImpl
FileName *string `json:"file_name,omitempty" cty:"file_name" hcl:"file_name,optional"`
// used only to set the connection string from command line variable value with a connection string
ConnectionString *string `json:"connection_string,omitempty" cty:"connection_string"`
}

func NewSqliteConnection(shortName string, declRange hcl.Range) PipelingConnection {
Expand Down Expand Up @@ -79,6 +81,10 @@ func (c *SqliteConnection) CtyValue() (cty.Value, error) {
}

func (c *SqliteConnection) GetConnectionString() string {
if c.ConnectionString != nil {
return *c.ConnectionString
}

return fmt.Sprintf("sqlite://%s", c.getFileName())
}

Expand Down
14 changes: 9 additions & 5 deletions inputvars/collect_variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func CollectVariableValues(workspacePath string, variableFileArgs []string, vari
name := raw[:eq]
rawVal := raw[eq+1:]

ret[name] = unparsedVariableValueString{
ret[name] = UnparsedVariableValueString{
str: rawVal,
name: name,
sourceType: terraform.ValueFromEnvVar,
Expand Down Expand Up @@ -122,7 +122,7 @@ func CollectVariableValues(workspacePath string, variableFileArgs []string, vari

name := raw[:eq]
rawVal := raw[eq+1:]
ret[name] = unparsedVariableValueString{
ret[name] = UnparsedVariableValueString{
str: rawVal,
name: name,
sourceType: terraform.ValueFromCLIArg,
Expand Down Expand Up @@ -300,17 +300,21 @@ func (v unparsedVariableValueExpression) ParseVariableValue(evalCtx *hcl.EvalCon
}, diags
}

// unparsedVariableValueString is a UnparsedVariableValue
// UnparsedVariableValueString is a UnparsedVariableValue
// implementation that parses its value from a string. This can be used
// to deal with values given directly on the command line and via environment
// variables.
type unparsedVariableValueString struct {
type UnparsedVariableValueString struct {
str string
name string
sourceType terraform.ValueSourceType
}

func (v unparsedVariableValueString) ParseVariableValue(evalCtx *hcl.EvalContext, mode modconfig.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
func (v UnparsedVariableValueString) Raw() string {
return v.str
}

func (v UnparsedVariableValueString) ParseVariableValue(evalCtx *hcl.EvalContext, mode modconfig.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

val, hclDiags := mode.Parse(evalCtx, v.name, v.str)
Expand Down
84 changes: 82 additions & 2 deletions inputvars/unparsed_value.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package inputvars

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/hcl/v2"
"github.com/spf13/viper"
"github.com/turbot/pipe-fittings/backend"
"github.com/turbot/pipe-fittings/connection"
"github.com/turbot/pipe-fittings/constants"
"github.com/turbot/pipe-fittings/error_helpers"
"github.com/turbot/pipe-fittings/modconfig"
"github.com/turbot/pipe-fittings/pipes"
"github.com/turbot/pipe-fittings/steampipeconfig"
"github.com/turbot/terraform-components/terraform"
"github.com/turbot/terraform-components/tfdiags"
"github.com/zclconf/go-cty/cty"
Expand Down Expand Up @@ -61,8 +69,15 @@ func ParseVariableValues(evalContext *hcl.EvalContext, inputValueUnparsed map[st
} else {
mode = modconfig.VariableParseLiteral
}

val, valDiags := unparsedVal.ParseVariableValue(evalContext, mode)
var val *terraform.InputValue
var valDiags tfdiags.Diagnostics
// if the variable is a connection, use special case logic to parse the value
// - handling connection strings and pipes workspace handle
if declared && config.IsConnectionType() {
val, valDiags = parseConnectionVariableValue(evalContext, unparsedVal, mode)
} else {
val, valDiags = unparsedVal.ParseVariableValue(evalContext, mode)
}
diags = diags.Append(valDiags)
if valDiags.HasErrors() {
continue
Expand Down Expand Up @@ -161,6 +176,71 @@ func ParseVariableValues(evalContext *hcl.EvalContext, inputValueUnparsed map[st
return ret, diags
}

// this function handles the case that the value for a connection variable is being set with a connection string or cloud workspace identifier
func parseConnectionVariableValue(evalContext *hcl.EvalContext, unparsedVal UnparsedVariableValue, mode modconfig.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
var ctyVal cty.Value
var err error
if unparsedString, ok := unparsedVal.(UnparsedVariableValueString); ok {
str := unparsedString.Raw()
// is the value a workspace handle?
if steampipeconfig.IsPipesWorkspaceIdentifier(str) {
// verify the cloud token was provided
cloudToken := viper.GetString(constants.ArgPipesToken)
if cloudToken == "" {
return nil, tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "Failed to get pipes metadata", error_helpers.MissingCloudTokenError().Error())}
}

pipesMetadata, err := pipes.GetPipesMetadata(context.Background(), str, cloudToken)
if err != nil {
return nil, tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "Failed to get pipes metadata", err.Error())}
}
// create new connection
c := connection.NewSteampipePgConnection(str, hcl.Range{}).(*connection.SteampipePgConnection)
c.ConnectionString = &pipesMetadata.ConnectionString
ctyVal, err = c.CtyValue()
if err != nil {
return nil, tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "Failed to get connection value", err.Error())}
}
return &terraform.InputValue{
Value: ctyVal,
SourceType: terraform.ValueFromCLIArg,
}, nil

} else if backend.HasBackend(str) {
// is the value a connection string?
var c connection.PipelingConnection
switch {
case backend.IsDuckDBConnectionString(str):
c = connection.NewDuckDbConnection("command_arg", hcl.Range{}).(*connection.DuckDbConnection)
c.(*connection.DuckDbConnection).ConnectionString = &str
case backend.IsPostgresConnectionString(str):
c = connection.NewPostgresConnection("command_arg", hcl.Range{})
c.(*connection.PostgresConnection).ConnectionString = &str
case backend.IsMySqlConnectionString(str):
c = connection.NewMysqlConnection("command_arg", hcl.Range{})
c.(*connection.MysqlConnection).ConnectionString = &str
case backend.IsSqliteConnectionString(str):
c = connection.NewSqliteConnection("command_arg", hcl.Range{})
c.(*connection.SqliteConnection).ConnectionString = &str

default:
return nil, tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "Failed to get connection value", fmt.Sprintf("Invalid connection string: %s", str))}
}
ctyVal, err = c.CtyValue()
if err != nil {
return nil, tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "Failed to get connection value", err.Error())}
}

return &terraform.InputValue{
Value: ctyVal,
SourceType: terraform.ValueFromCLIArg,
}, nil
}
}
// fall back to normal parsing
return unparsedVal.ParseVariableValue(evalContext, mode)
}

func getUndeclaredVariableError(name string, variablesMap *modconfig.ModVariableMap) string {
// is this a qualified variable?
if len(strings.Split(name, ".")) == 1 {
Expand Down
17 changes: 17 additions & 0 deletions modconfig/custom_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ func IsCustomType(ty cty.Type) bool {
return isCustomType
}

// IsConnectionType returns true if the given cty.Type is a connection type
func IsConnectionType(ty cty.Type) bool {
// is the value a workspace handle?
encapsulatedGoType, nestedCapsule := hclhelpers.IsNestedCapsuleType(ty)
if !nestedCapsule {
return false
}

encapulatedInstanceNew := reflect.New(encapsulatedGoType)
if _, ok := encapulatedInstanceNew.Interface().(connection.PipelingConnection); ok {
return true
}

var connectionImpl *connection.ConnectionImpl
return encapsulatedGoType.String() == reflect.TypeOf(connectionImpl).String()
}

func ValidateValueMatchesType(ctyVal cty.Value, ty cty.Type, sourceRange *hcl.Range) hcl.Diagnostics {
if ty != cty.DynamicPseudoType {
ctyValType := ctyVal.Type()
Expand Down
14 changes: 1 addition & 13 deletions modconfig/flowpipe_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

"github.com/hashicorp/hcl/v2"
"github.com/turbot/go-kit/helpers"
"github.com/turbot/pipe-fittings/connection"
"github.com/turbot/pipe-fittings/hclhelpers"
"github.com/turbot/pipe-fittings/options"
"github.com/turbot/pipe-fittings/perr"
Expand Down Expand Up @@ -531,18 +530,7 @@ func (p *PipelineParam) PipelineParamCustomValueValidation(setting cty.Value, ev
}

func (p *PipelineParam) IsConnectionType() bool {
encapsulatedGoType, nestedCapsule := hclhelpers.IsNestedCapsuleType(p.Type)
if !nestedCapsule {
return false
}

encapulatedInstanceNew := reflect.New(encapsulatedGoType)
if _, ok := encapulatedInstanceNew.Interface().(connection.PipelingConnection); ok {
return true
}

var connectionImpl *connection.ConnectionImpl
return encapsulatedGoType.String() == reflect.TypeOf(connectionImpl).String()
return IsConnectionType(p.Type)
}

func (p *PipelineParam) IsNotifierType() bool {
Expand Down
7 changes: 4 additions & 3 deletions modconfig/variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package modconfig

import (
"fmt"

"github.com/hashicorp/hcl/v2"
typehelpers "github.com/turbot/go-kit/types"
"github.com/turbot/pipe-fittings/cty_helpers"
Expand All @@ -15,8 +14,6 @@ import (
"github.com/zclconf/go-cty/cty/convert"
)

// TODO check DescriptionSet - still required?

// Variable is a struct representing a Variable resource
type Variable struct {
ResourceWithMetadataImpl
Expand Down Expand Up @@ -182,3 +179,7 @@ func (v *Variable) CtyValue() (cty.Value, error) {
func (v *Variable) IsLateBinding() bool {
return IsLateBindingType(v.Type)
}

func (p *Variable) IsConnectionType() bool {
return IsConnectionType(p.Type)
}
Loading