Skip to content

Commit

Permalink
302 Data app env (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
BSick7 authored Apr 23, 2021
1 parent 34ecea2 commit 3b2e64a
Show file tree
Hide file tree
Showing 6 changed files with 419 additions and 3 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ require (
github.com/hashicorp/terraform-plugin-sdk/v2 v2.3.0
github.com/nullstone-io/module v0.2.3
github.com/stretchr/testify v1.5.1
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20210422145113-1def18026731
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20210423152214-04647c0f0672
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20210422145113-1def18026731 h1:Xvwk1m7Zqjl+pueoSGlh9ylN0fv1xCVMsMQvqRO5DdA=
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20210422145113-1def18026731/go.mod h1:6z3xyBE0tWngWi5equdUPGtEKClza3g5hpSkQKWLid8=
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20210423152214-04647c0f0672 h1:Q2iAQMcf5/hi6pmaVzypa1VKOQRVtK+hKcgExnxURK0=
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20210423152214-04647c0f0672/go.mod h1:6z3xyBE0tWngWi5equdUPGtEKClza3g5hpSkQKWLid8=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
132 changes: 132 additions & 0 deletions internal/provider/data_app_env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package provider

import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tfprotov5/tftypes"
"gopkg.in/nullstone-io/go-api-client.v0"
"gopkg.in/nullstone-io/go-api-client.v0/types"
"strconv"
)

type dataAppEnv struct {
p *provider
}

func newDataAppEnv(p *provider) (*dataAppEnv, error) {
if p == nil {
return nil, fmt.Errorf("a provider is required")
}
return &dataAppEnv{p: p}, nil
}

func (*dataAppEnv) Schema(ctx context.Context) *tfprotov5.Schema {
return &tfprotov5.Schema{
Version: 1,
Block: &tfprotov5.SchemaBlock{
Description: "Data source to configure module based on current nullstone workspace.",
DescriptionKind: tfprotov5.StringKindMarkdown,
Attributes: []*tfprotov5.SchemaAttribute{
deprecatedIDAttribute(),
{
Name: "app",
Type: tftypes.String,
Required: true,
Description: "The name of the application in nullstone.",
DescriptionKind: tfprotov5.StringKindMarkdown,
},
{
Name: "stack",
Type: tftypes.String,
Required: true,
Description: "The owning stack for the application in nullstone.",
DescriptionKind: tfprotov5.StringKindMarkdown,
},
{
Name: "env",
Type: tftypes.String,
Required: true,
Description: "The name of the environment in nullstone.",
DescriptionKind: tfprotov5.StringKindMarkdown,
},
{
Name: "version",
Type: tftypes.String,
Computed: true,
Description: "The version configured for the application in the specific environment.",
DescriptionKind: tfprotov5.StringKindMarkdown,
},
},
},
}
}

func (d *dataAppEnv) Validate(ctx context.Context, config map[string]tftypes.Value) ([]*tfprotov5.Diagnostic, error) {
return nil, nil
}

func (d *dataAppEnv) Read(ctx context.Context, config map[string]tftypes.Value) (map[string]tftypes.Value, []*tfprotov5.Diagnostic, error) {
appName := extractStringFromConfig(config, "app")
stackName := extractStringFromConfig(config, "stack")
envName := extractStringFromConfig(config, "env")
diags := make([]*tfprotov5.Diagnostic, 0)

var appEnvId int
var appEnvVersion string

nsClient := api.Client{Config: d.p.NsConfig}
app, err := d.findApp(appName, stackName)
if err != nil {
diags = append(diags, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: fmt.Sprintf("Unable to list applications."),
Detail: err.Error(),
})
} else if app == nil {
diags = append(diags, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: fmt.Sprintf("The application environment (app=%s, stack=%s, env=%s) is missing.", appName, stackName, envName),
})
} else {
appEnv, err := nsClient.AppEnvs().Get(app.Id, envName)
if err != nil {
diags = append(diags, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: fmt.Sprintf("Unable to retrieve the application environment (app=%s, stack=%s, env=%s).", appName, stackName, envName),
Detail: err.Error(),
})
} else if appEnv == nil {
diags = append(diags, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: fmt.Sprintf("Unable to find the application environment (app=%s, stack=%s, env=%s).", appName, stackName, envName),
Detail: err.Error(),
})
} else {
appEnvId = appEnv.Id
appEnvVersion = appEnv.Version
}
}

return map[string]tftypes.Value{
"id": tftypes.NewValue(tftypes.String, strconv.Itoa(appEnvId)),
"app": tftypes.NewValue(tftypes.String, appName),
"stack": tftypes.NewValue(tftypes.String, stackName),
"env": tftypes.NewValue(tftypes.String, envName),
"version": tftypes.NewValue(tftypes.String, appEnvVersion),
}, diags, nil
}

func (d *dataAppEnv) findApp(appName, stackName string) (*types.Application, error) {
nsClient := api.Client{Config: d.p.NsConfig}
apps, err := nsClient.Apps().List()
if err != nil {
return nil, fmt.Errorf("Unable to list applications.")
}
for _, app := range apps {
if app.Name == appName && app.StackName == stackName {
return &app, nil
}
}
return nil, nil
}
243 changes: 243 additions & 0 deletions internal/provider/data_app_env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package provider

import (
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"gopkg.in/nullstone-io/go-api-client.v0/types"
"net/http"
"strconv"
"testing"
)

func TestDataAppEnv(t *testing.T) {
core := &types.Stack{
IdModel: types.IdModel{
Id: 2,
},
Name: "core",
OrgName: "org0",
}
app1 := &types.Application{
IdModel: types.IdModel{
Id: 1,
},
Name: "app1",
OrgName: "org0",
StackName: core.Name,
}
dev := &types.Environment{
IdModel: types.IdModel{
Id: 1,
},
Name: "dev",
OrgName: "org0",
StackName: core.Name,
}
prod := &types.Environment{
IdModel: types.IdModel{
Id: 2,
},
Name: "prod",
OrgName: "org0",
StackName: core.Name,
}

appEnvs := []*types.AppEnv{
{
IdModel: types.IdModel{
Id: 5,
},
AppId: app1.Id,
EnvId: dev.Id,
Version: "1.0.0",
App: app1,
Env: prod,
},
}
apps := []*types.Application{app1}
envs := []*types.Environment{dev,prod}

t.Run("sets up attributes properly with new AppEnv", func(t *testing.T) {
tfconfig := fmt.Sprintf(`
provider "ns" {
organization = "org0"
}
data "ns_workspace" "this" {
stack = "core"
block = "app1"
env = "dev"
}
data "ns_app_env" "this" {
app = data.ns_workspace.this.block
stack = data.ns_workspace.this.stack
env = data.ns_workspace.this.env
}
`)
checks := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.ns_app_env.this", `id`, "10"),
resource.TestCheckResourceAttr("data.ns_app_env.this", `app`, "app1"),
resource.TestCheckResourceAttr("data.ns_app_env.this", `stack`, "core"),
resource.TestCheckResourceAttr("data.ns_app_env.this", `env`, "dev"),
resource.TestCheckResourceAttr("data.ns_app_env.this", `version`, ""),
)

getNsConfig, closeNsFn := mockNs(mockNsHandlerAppEnvs(&appEnvs, apps, envs))
defer closeNsFn()
getTfeConfig, closeTfeFn := mockTfe(nil)
defer closeTfeFn()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(getNsConfig, getTfeConfig),
Steps: []resource.TestStep{
{
Config: tfconfig,
Check: checks,
},
},
})
})

t.Run("sets up attributes properly with existing AppEnv", func(t *testing.T) {
tfconfig := fmt.Sprintf(`
provider "ns" {
organization = "org0"
}
data "ns_workspace" "this" {
stack = "core"
block = "app1"
env = "prod"
}
data "ns_app_env" "this" {
app = data.ns_workspace.this.block
stack = data.ns_workspace.this.stack
env = data.ns_workspace.this.env
}
`)
checks := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.ns_app_env.this", `id`, "5"),
resource.TestCheckResourceAttr("data.ns_app_env.this", `app`, "app1"),
resource.TestCheckResourceAttr("data.ns_app_env.this", `stack`, "core"),
resource.TestCheckResourceAttr("data.ns_app_env.this", `env`, "prod"),
resource.TestCheckResourceAttr("data.ns_app_env.this", `version`, "1.0.0"),
)

getNsConfig, closeNsFn := mockNs(mockNsHandlerAppEnvs(&appEnvs, apps, envs))
defer closeNsFn()
getTfeConfig, closeTfeFn := mockTfe(nil)
defer closeTfeFn()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(getNsConfig, getTfeConfig),
Steps: []resource.TestStep{
{
Config: tfconfig,
Check: checks,
},
},
})
})
}

func mockNsHandlerAppEnvs(appEnvs *[]*types.AppEnv, apps []*types.Application, envs []*types.Environment) http.Handler {
findApp := func(orgName, appIdStr string) *types.Application {
for _, app := range apps {
if app.OrgName == orgName && strconv.Itoa(app.Id) == appIdStr {
return app
}
}
return nil
}
findEnv := func(orgName, envName string) *types.Environment {
for _, env := range envs {
if env.OrgName == orgName && env.Name == envName {
return env
}
}
return nil
}
getAppEnv := func(orgName, appIdStr, envName string) *types.AppEnv {
for _, existing := range *appEnvs {
if existing.App.OrgName == orgName && strconv.Itoa(existing.App.Id) == appIdStr &&
existing.Env.OrgName == orgName && existing.Env.Name == envName {
return existing
}
}
return nil
}
addAppEnv := func(orgName, appIdStr, envName string) *types.AppEnv {
app := findApp(orgName, appIdStr)
env := findEnv(orgName, envName)
if app == nil || env == nil {
return nil
}

appEnv := &types.AppEnv{
IdModel: types.IdModel{
Id: 10,
},
AppId: app.Id,
EnvId: env.Id,
Version: "",
App: app,
Env: env,
}
*appEnvs = append(*appEnvs, appEnv)
return appEnv
}
findOrCreateEnv := func(r *http.Request) *types.AppEnv {
vars := mux.Vars(r)
orgName, appId, envName := vars["orgName"], vars["appId"], vars["envName"]

appEnv := getAppEnv(orgName, appId, envName)
if appEnv == nil {
appEnv = addAppEnv(orgName, appId, envName)
}
return appEnv
}

router := mux.NewRouter()
router.
Methods(http.MethodGet).
Path("/orgs/{orgName}/apps").
HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
raw, _ := json.Marshal(apps)
w.Write(raw)
})
router.
Methods(http.MethodGet).
Path("/orgs/{orgName}/apps/{appId}/envs/{envName}").
HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if appEnv := findOrCreateEnv(r); appEnv == nil {
http.NotFound(w, r)
} else {
raw, _ := json.Marshal(appEnv)
w.Write(raw)
}
})
router.
Methods(http.MethodPut).
Path("/orgs/{orgName}/apps/{appId}/envs/{envName}").
HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
payload := struct{
Version string `json:"version"`
}{}
decoder := json.NewDecoder(r.Body)
if appEnv := findOrCreateEnv(r); appEnv == nil {
http.NotFound(w, r)
} else if err := decoder.Decode(&payload); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
} else {
appEnv.Version = payload.Version
raw, _ := json.Marshal(appEnv)
w.Write(raw)
}
})
return router
}
Loading

0 comments on commit 3b2e64a

Please sign in to comment.