Skip to content

Commit 645c6a5

Browse files
committed
chore: implement coder owner state
Starting with just groups
1 parent a8b31fe commit 645c6a5

File tree

19 files changed

+512
-47
lines changed

19 files changed

+512
-47
lines changed

cli/root.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/coder/preview"
1212
"github.com/coder/preview/cli/clidisplay"
13+
"github.com/coder/preview/types"
1314
"github.com/coder/serpent"
1415
)
1516

@@ -21,6 +22,7 @@ func (r *RootCmd) Root() *serpent.Command {
2122
var (
2223
dir string
2324
vars []string
25+
groups []string
2426
planJSON string
2527
)
2628
cmd := &serpent.Command{
@@ -48,9 +50,17 @@ func (r *RootCmd) Root() *serpent.Command {
4850
Description: "Variables.",
4951
Flag: "vars",
5052
FlagShorthand: "v",
51-
Default: ".",
53+
Default: "",
5254
Value: serpent.StringArrayOf(&vars),
5355
},
56+
{
57+
Name: "groups",
58+
Description: "Groups.",
59+
Flag: "groups",
60+
FlagShorthand: "g",
61+
Default: "",
62+
Value: serpent.StringArrayOf(&groups),
63+
},
5464
},
5565
Handler: func(i *serpent.Invocation) error {
5666
dfs := os.DirFS(dir)
@@ -65,8 +75,11 @@ func (r *RootCmd) Root() *serpent.Command {
6575
}
6676

6777
input := preview.Input{
68-
ParameterValues: rvars,
6978
PlanJSONPath: planJSON,
79+
ParameterValues: rvars,
80+
Owner: types.WorkspaceOwner{
81+
Groups: groups,
82+
},
7083
}
7184

7285
ctx := i.Context()

owner.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package preview
2+
3+
import (
4+
"io/fs"
5+
6+
"github.com/aquasecurity/trivy/pkg/iac/terraform"
7+
tfcontext "github.com/aquasecurity/trivy/pkg/iac/terraform/context"
8+
"github.com/zclconf/go-cty/cty"
9+
"github.com/zclconf/go-cty/cty/gocty"
10+
"golang.org/x/xerrors"
11+
)
12+
13+
func WorkspaceOwnerHook(dfs fs.FS, input Input) (func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value), error) {
14+
if input.Owner.Groups == nil {
15+
input.Owner.Groups = []string{}
16+
}
17+
ownerGroups, err := gocty.ToCtyValue(input.Owner.Groups, cty.List(cty.String))
18+
if err != nil {
19+
return nil, xerrors.Errorf("converting owner groups: %w", err)
20+
}
21+
22+
ownerValue := cty.ObjectVal(map[string]cty.Value{
23+
"groups": ownerGroups,
24+
})
25+
26+
return func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) {
27+
for _, block := range blocks.OfType("data") {
28+
// TODO: Does it have to be me?
29+
if block.TypeLabel() == "coder_workspace_owner" && block.NameLabel() == "me" {
30+
block.Context().Parent().Set(ownerValue,
31+
"data", block.TypeLabel(), block.NameLabel())
32+
}
33+
}
34+
}, nil
35+
}

preview.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import (
44
"context"
55
"fmt"
66
"io/fs"
7+
"log/slog"
8+
"os"
79
"path/filepath"
810

911
"github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser"
12+
"github.com/aquasecurity/trivy/pkg/log"
1013
"github.com/hashicorp/hcl/v2"
1114

1215
"github.com/coder/preview/types"
@@ -15,6 +18,7 @@ import (
1518
type Input struct {
1619
PlanJSONPath string
1720
ParameterValues map[string]string
21+
Owner types.WorkspaceOwner
1822
}
1923

2024
type Output struct {
@@ -24,6 +28,10 @@ type Output struct {
2428
}
2529

2630
func Preview(ctx context.Context, input Input, dir fs.FS) (*Output, hcl.Diagnostics) {
31+
// TODO: FIX LOGGING
32+
slog.SetLogLoggerLevel(slog.LevelDebug)
33+
slog.SetDefault(slog.New(log.NewHandler(os.Stderr, nil)))
34+
2735
varFiles, err := tfVarFiles("", dir)
2836
if err != nil {
2937
return nil, hcl.Diagnostics{
@@ -45,12 +53,27 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (*Output, hcl.Diagnost
4553
},
4654
}
4755
}
56+
57+
ownerHook, err := WorkspaceOwnerHook(dir, input)
58+
if err != nil {
59+
return nil, hcl.Diagnostics{
60+
{
61+
Severity: hcl.DiagError,
62+
Summary: "Workspace owner hook",
63+
Detail: err.Error(),
64+
},
65+
}
66+
}
67+
var _ = ownerHook
68+
4869
// moduleSource is "" for a local module
4970
p := parser.New(dir, "",
71+
parser.OptionStopOnHCLError(true),
5072
parser.OptionWithDownloads(false),
5173
parser.OptionWithTFVarsPaths(varFiles...),
5274
parser.OptionWithEvalHook(planHook),
5375
parser.OptionWithEvalHook(ParameterContextsEvalHook(input)),
76+
parser.OptionWithEvalHook(ownerHook),
5477
parser.OptionWithSkipCachedModules(true),
5578
)
5679

preview_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,45 @@ func Test_Extract(t *testing.T) {
192192
name: "not-exists",
193193
dir: "not-existing-directory",
194194
},
195+
{
196+
name: "groups",
197+
dir: "groups",
198+
expTags: map[string]string{},
199+
input: preview.Input{
200+
PlanJSONPath: "",
201+
ParameterValues: map[string]string{},
202+
Owner: types.WorkspaceOwner{
203+
Groups: []string{"developer", "manager", "admin"},
204+
},
205+
},
206+
expUnknowns: []string{},
207+
params: map[string]func(t *testing.T, parameter types.Parameter){
208+
"Groups": ap[cty.Value]().
209+
options("developer", "manager", "admin").
210+
f(),
211+
},
212+
},
213+
{
214+
name: "ambigious",
215+
dir: "ambigious",
216+
expTags: map[string]string{},
217+
input: preview.Input{
218+
PlanJSONPath: "",
219+
ParameterValues: map[string]string{},
220+
Owner: types.WorkspaceOwner{
221+
Groups: []string{"developer", "manager", "admin"},
222+
},
223+
},
224+
expUnknowns: []string{},
225+
params: map[string]func(t *testing.T, parameter types.Parameter){
226+
"IsAdmin": ap[cty.Value]().
227+
value(cty.StringVal("true")).
228+
f(),
229+
"IsAdmin_Root": ap[cty.Value]().
230+
value(cty.StringVal("true")).
231+
f(),
232+
},
233+
},
195234
} {
196235
t.Run(tc.name, func(t *testing.T) {
197236
t.Parallel()

testdata/ambigious/main.tf

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
terraform {
2+
required_providers {
3+
coder = {
4+
source = "coder/coder"
5+
}
6+
}
7+
}
8+
9+
data coder_workspace_owner "me" {}
10+
11+
module "decisions" {
12+
source = "./modules/decisions"
13+
}
14+
15+
data "coder_parameter" "example" {
16+
count = module.decisions.isAdmin ? 1 : 0
17+
name = "IsAdmin"
18+
type = "bool"
19+
default = module.decisions.isAdmin
20+
}
21+
22+
data "coder_parameter" "example_root" {
23+
count = contains(data.coder_workspace_owner.me.groups, "admin") ? 1 : 0
24+
name = "IsAdmin_Root"
25+
type = "bool"
26+
default = contains(data.coder_workspace_owner.me.groups, "admin")
27+
}
28+
29+
output "groups" {
30+
value = module.decisions.groups
31+
}
32+
33+
output "isAdmin" {
34+
value = module.decisions.isAdmin
35+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
terraform {
2+
required_providers {
3+
coder = {
4+
source = "coder/coder"
5+
}
6+
}
7+
}
8+
9+
data coder_workspace_owner "me" {}
10+
11+
output "isAdmin" {
12+
value = contains(data.coder_workspace_owner.me.groups, "admin")
13+
}
14+
15+
output "groups" {
16+
value = data.coder_workspace_owner.me.groups
17+
}

testdata/conditional/main.tf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,5 @@ data "coder_parameter" "favorite" {
8585
error="too high or low"
8686
}
8787
}
88+
89+

testdata/demo/main.tf

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Demo terraform has a complex configuration.
2+
terraform {
3+
required_providers {
4+
coder = {
5+
source = "coder/coder"
6+
}
7+
}
8+
}
9+
10+
11+
locals {
12+
fe_codes = ["PS", "WS"]
13+
be_codes = ["CL", "GO", "IU", "PY"]
14+
teams = {
15+
"frontend" = {
16+
"display_name" = "Frontend",
17+
"codes" = local.fe_codes,
18+
"description" = "The team that works on the frontend.",
19+
"icon" = "/icon/desktop.svg"
20+
},
21+
"backend" = {
22+
"display_name" = "Backend",
23+
"codes" = local.be_codes,
24+
"description" = "The team that works on the backend.",
25+
"icon" = "/emojis/2699.png",
26+
},
27+
"fullstack" = {
28+
"display_name" = "Fullstack",
29+
"codes" = concat(local.be_codes, local.fe_codes),
30+
"description" = "The team that works on both the frontend and backend.",
31+
"icon" = "/emojis/1f916.png",
32+
}
33+
}
34+
}
35+
36+
data "coder_parameter" "team" {
37+
name = "Team"
38+
description = "Which team are you on?"
39+
type = "string"
40+
default = "fullstack"
41+
order = 1
42+
43+
dynamic "option" {
44+
for_each = local.teams
45+
content {
46+
name = option.value.display_name
47+
value = option.key
48+
description = option.value.description
49+
icon = option.value.icon
50+
}
51+
}
52+
53+
validation {
54+
regex = "^frontend|backend|fullstack$"
55+
error = "You must select either frontend, backend, or fullstack."
56+
}
57+
}
58+
59+
module "jetbrains_gateway" {
60+
count = 1
61+
source = "registry.coder.com/modules/jetbrains-gateway/coder"
62+
version = "1.0.28"
63+
agent_id = "random"
64+
folder = "/home/coder/example"
65+
jetbrains_ides = local.teams[data.coder_parameter.team.value].codes
66+
default = local.teams[data.coder_parameter.team.value].codes[0]
67+
}
68+
69+
module "base" {
70+
source = "./modules/base"
71+
}

testdata/demo/modules/base/base.tf

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
terraform {
2+
required_providers {
3+
coder = {
4+
source = "coder/coder"
5+
}
6+
}
7+
}
8+
9+
locals {
10+
// default to the only option if only 1 exists
11+
choose_security = length(keys(module.deploys.security_levels)) > 1
12+
secutity_level = local.choose_security ? data.coder_parameter.security_level[0].value : keys(module.deploys.security_levels)[0]
13+
}
14+
15+
module "deploys" {
16+
// Where we deploy the workspace to
17+
source = "../deploys"
18+
security = local.secutity_level
19+
}
20+
21+
data "coder_parameter" "security_level" {
22+
count = local.choose_security ? 1 : 0
23+
name = "Security Level"
24+
description = "What security level do you need?"
25+
type = "string"
26+
default = "high"
27+
order = 1
28+
29+
30+
dynamic "option" {
31+
for_each = module.deploys.security_levels
32+
content {
33+
name = option.value.display_name
34+
value = option.key
35+
description = option.value.description
36+
}
37+
}
38+
39+
# validation {
40+
# regex = "^high|medium|low$"
41+
# error = "You must select either high, medium, or low."
42+
# }
43+
}
44+
45+
// TODO: REMOVE THIS
46+
data "coder_parameter" "test" {
47+
name = "Test"
48+
description = "Test"
49+
type = "string"
50+
default = tostring(local.choose_security ? 1 : 0)
51+
}
52+

testdata/demo/modules/base/defaults.tfvars

Whitespace-only changes.

0 commit comments

Comments
 (0)