Skip to content

Commit

Permalink
Fixed several issues (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
BSick7 authored Dec 29, 2020
1 parent 528eded commit 00a1500
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 26 deletions.
40 changes: 26 additions & 14 deletions internal/provider/data_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"regexp"

"github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tfprotov5/tftypes"
"github.com/nullstone-io/terraform-provider-ns/ns"
Expand Down Expand Up @@ -105,7 +105,7 @@ func (d *dataConnection) Validate(ctx context.Context, config map[string]tftypes
})
}

workspace := d.p.PlanConfig.GetConnection(name)
workspace := d.p.PlanConfig.GetConnectionWorkspace(name)
if workspace == "" && !optional {
diags = append(diags, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Expand All @@ -126,18 +126,27 @@ func (d *dataConnection) Read(ctx context.Context, config map[string]tftypes.Val
optional := boolFromConfig(config, "optional")
via := stringFromConfig(config, "via")

diags := make([]*tfprotov5.Diagnostic, 0)

outputsValue := tftypes.NewValue(tftypes.Map{AttributeType: tftypes.String}, map[string]tftypes.Value{})
workspace := d.p.PlanConfig.GetConnection(name)
workspace := d.p.PlanConfig.GetConnectionWorkspace(name)
if workspace != "" {
stateFile, err := d.getStateFile(workspace)
if err != nil {
if err != tfe.ErrResourceNotFound {
return nil, nil, fmt.Errorf("error retrieving workspace state file: %w", err)
}
diags = append(diags, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityWarning,
Summary: fmt.Sprintf(`Unable to download workspace outputs for %q. 'outputs' will be empty`, workspace),
Detail: err.Error(),
})
} else {
outputsValue, err = stateFile.Outputs.ToProtov5()
if err != nil {
return nil, nil, err
if ov, err := stateFile.Outputs.ToProtov5(); err != nil {
diags = append(diags, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityWarning,
Summary: fmt.Sprintf(`Unable to read workspace outputs for %q. 'outputs' will be empty`, workspace),
Detail: err.Error(),
})
} else {
outputsValue = ov
}
}
}
Expand All @@ -150,30 +159,33 @@ func (d *dataConnection) Read(ctx context.Context, config map[string]tftypes.Val
"optional": tftypes.NewValue(tftypes.Bool, optional),
"via": tftypes.NewValue(tftypes.String, via),
"outputs": outputsValue,
}, nil, nil
}, diags, nil
}

func (d *dataConnection) getStateFile(workspaceName string) (*ns.StateFile, error) {
tfeClient, orgName := d.p.TfeClient, d.p.PlanConfig.Org
log.Printf("[DEBUG] Retrieving state file (org=%s, workspace=%s)\n", orgName, workspaceName)

workspace, err := tfeClient.Workspaces.Read(context.Background(), orgName, workspaceName)
if err != nil {
return nil, fmt.Errorf(`error reading workspace "%s/%s": %w`, orgName, workspaceName, err)
return nil, fmt.Errorf(`error reading workspace (org=%s, workspace=%s): %w`, orgName, workspaceName, err)
}

sv, err := tfeClient.StateVersions.Current(context.Background(), workspace.ID)
if err != nil {
return nil, fmt.Errorf(`error reading current state version (workspace=%s/%s): %w`, orgName, workspaceName, err)
return nil, fmt.Errorf(`error reading current state version (org=%s, workspace=%s): %w`, orgName, workspaceName, err)
}

state, err := tfeClient.StateVersions.Download(context.Background(), sv.DownloadURL)
if err != nil {
return nil, fmt.Errorf(`error downloading state file (workspace=%s/%s): %w`, orgName, workspaceName, err)
return nil, fmt.Errorf(`error downloading state file (org=%s, workspace=%s): %w`, orgName, workspaceName, err)
}

log.Printf("[DEBUG] Retrieved state file (org=%s, workspace=%s): size=%d\n", orgName, workspaceName, len(state))

var stateFile ns.StateFile
if err := json.Unmarshal(state, &stateFile); err != nil {
return nil, fmt.Errorf(`error parsing state file (workspace=%s/%s): %w`, orgName, workspaceName, err)
return nil, fmt.Errorf(`error parsing state file (org=%s, workspace=%s): %w`, orgName, workspaceName, err)
}
return &stateFile, nil
}
13 changes: 8 additions & 5 deletions internal/provider/data_connection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ data "ns_connection" "service" {
resource.TestCheckResourceAttr("data.ns_connection.service", `outputs.test3.key3`, "value3"),
)

os.Setenv("NULLSTONE_CONNECTION_service", "stack0-env0-lycan")
os.Setenv("NULLSTONE_STACK", "stack0")
os.Setenv("NULLSTONE_ENV", "env0")
os.Setenv("NULLSTONE_BLOCK", "faceless")
os.Setenv("NULLSTONE_CONNECTION_service", "lycan")

getTfeConfig, closeFn := mockTfe(mockServerWithLycan())
defer closeFn()
Expand Down Expand Up @@ -132,7 +135,7 @@ func mockServerWithLycan() http.Handler {
"name": "stack0-env0-lycan",
"serial": 1,
"lineage": "64aef234-2ff9-9d8e-25ae-22fb30b62860",
"hosted-state-download-url": "/state/terraform/v2/state-versions/53516a9e-ffd7-4834-8234-63fd070d064f/download"
"hosted-state-download-url": "/terraform/v2/state-versions/53516a9e-ffd7-4834-8234-63fd070d064f/download"
},
"relationships": {}
}
Expand Down Expand Up @@ -179,7 +182,7 @@ func mockTfeStatePull(workspaces map[string]json.RawMessage, currentStateVersion
router := mux.NewRouter()
router.
Methods(http.MethodGet).
Path("/state/terraform/v2/organizations/{orgName}/workspaces/{workspaceName}").
Path("/terraform/v2/organizations/{orgName}/workspaces/{workspaceName}").
HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_, workspaceName := vars["orgName"], vars["workspaceName"]
Expand All @@ -191,7 +194,7 @@ func mockTfeStatePull(workspaces map[string]json.RawMessage, currentStateVersion
})
router.
Methods(http.MethodGet).
Path("/state/terraform/v2/workspaces/{workspaceId}/current-state-version").
Path("/terraform/v2/workspaces/{workspaceId}/current-state-version").
HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
workspaceId := mux.Vars(r)["workspaceId"]
if msg, ok := currentStateVersions[workspaceId]; ok {
Expand All @@ -202,7 +205,7 @@ func mockTfeStatePull(workspaces map[string]json.RawMessage, currentStateVersion
})
router.
Methods(http.MethodGet).
Path("/state/terraform/v2/state-versions/{stateVersionId}/download").
Path("/terraform/v2/state-versions/{stateVersionId}/download").
HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
stateVersionId := mux.Vars(r)["stateVersionId"]
if msg, ok := stateFiles[stateVersionId]; ok {
Expand Down
26 changes: 23 additions & 3 deletions internal/provider/plan_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"os"
"strings"
)

type PlanConfig struct {
Expand All @@ -15,11 +16,30 @@ type PlanConfig struct {
Connections map[string]string `json:"connections"`
}

func (c PlanConfig) GetConnection(name string) string {
func (c PlanConfig) GetConnectionWorkspace(name string) string {
connValue := os.Getenv(fmt.Sprintf(`NULLSTONE_CONNECTION_%s`, name))
if value, ok := c.Connections[name]; ok {
return value
connValue = value
}
return os.Getenv(fmt.Sprintf(`NULLSTONE_CONNECTION_%s`, name))
if connValue == "" {
return ""
}
return c.FullyQualifiedConnection(connValue)
}

func (c PlanConfig) FullyQualifiedConnection(name string) string {
destStack, destEnv, destBlock := c.Stack, c.Env, ""

tokens := strings.Split(name, ".")
if len(tokens) > 2 {
destStack = tokens[len(tokens)-3]
}
if len(tokens) > 1 {
destEnv = tokens[len(tokens)-2]
}
destBlock = tokens[len(tokens)-1]

return fmt.Sprintf("%s-%s-%s", destStack, destEnv, destBlock)
}

func DefaultPlanConfig() PlanConfig {
Expand Down
3 changes: 3 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package provider

import (
"context"
"log"

"github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
Expand Down Expand Up @@ -91,5 +92,7 @@ func (p *provider) Configure(ctx context.Context, config map[string]tftypes.Valu
return nil, err
}

log.Printf("[DEBUG] Configured TFE client (Address=%s, BasePath=%s)\n", p.TfeConfig.Address, p.TfeConfig.BasePath)

return nil, nil
}
9 changes: 6 additions & 3 deletions ns/tfe_config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ns

import (
"os"

"github.com/hashicorp/go-tfe"
)

Expand All @@ -10,9 +12,10 @@ var (

func NewTfeConfig() *tfe.Config {
cfg := tfe.DefaultConfig()
if cfg.Address == "" {
cfg.Address = DefaultAddress
cfg.Address = DefaultAddress
if val := os.Getenv("TFE_ADDRESS"); val != "" {
cfg.Address = val
}
cfg.BasePath = "/state/terraform/v2/"
cfg.BasePath = "/terraform/v2/"
return cfg
}
8 changes: 7 additions & 1 deletion website/docs/d/connection.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ data "ns_connection" "network" {
* `name` - Name of nullstone connection.
* `type` - Type of nullstone module to make connection.
* `optional` - By default, if this connection has not been configured, this causes an error. Set to true to disable. (Default: `false`)
* `workspace` - Name of workspace for connection. (Environment variable: `NULLSTONE_CONNECTION_{name}`)
* `workspace` - This refers to the exact workspace used for state files in nullstone.
This value will always be of the form `{stack}-{env}-{block}`.
Utilizes environment variable `NULLSTONE_CONNECTION_{name}` to resolve.
This value can be one of the following formats:
* `{stack}.{env}.{block}`
* `{env}.{block}` - (`stack` is pulled from the current workspace)
* `{block}` - (`stack` and `env` are pulled from the current workspace)
* `via` - Name of workspace to satisfy this connection through. Typically, this is set to `data.ns_connection.other.workspace`.
- `outputs` - An object containing every root-level output in the remote state. This attribute is interchangeable for `data.terraform_remote_state.outputs`.

0 comments on commit 00a1500

Please sign in to comment.