Skip to content

Commit c49ec0d

Browse files
committed
chore: add initial websocket preview server
1 parent e068f68 commit c49ec0d

File tree

17 files changed

+603
-26
lines changed

17 files changed

+603
-26
lines changed

cli/clidisplay/resources.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77

88
"github.com/hashicorp/hcl/v2"
99
"github.com/jedib0t/go-pretty/v6/table"
10-
"github.com/zclconf/go-cty/cty"
1110

1211
"github.com/coder/preview/types"
1312
)
@@ -58,18 +57,18 @@ func Parameters(writer io.Writer, params []types.Parameter) {
5857
row := table.Row{"Parameter"}
5958
tableWriter.AppendHeader(row)
6059
for _, p := range params {
61-
strVal := ""
62-
value := p.Value.Value
63-
64-
if value.IsNull() {
65-
strVal = "null"
66-
} else if !p.Value.Value.IsKnown() {
67-
strVal = "unknown"
68-
} else if value.Type().Equals(cty.String) {
69-
strVal = value.AsString()
70-
} else {
71-
strVal = value.GoString()
72-
}
60+
strVal := p.Value.Value
61+
//value := p.Value.Value
62+
//
63+
//if value.IsNull() {
64+
// strVal = "null"
65+
//} else if !p.Value.Value.IsKnown() {
66+
// strVal = "unknown"
67+
//} else if value.Type().Equals(cty.String) {
68+
// strVal = value.AsString()
69+
//} else {
70+
// strVal = value.GoString()
71+
//}
7372

7473
tableWriter.AppendRow(table.Row{
7574
fmt.Sprintf("%s (%s): %s\n%s", p.Name, p.BlockName, p.Description, formatOptions(strVal, p.Options)),

cli/root.go

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

88
"github.com/hashicorp/hcl/v2"
99
"github.com/hashicorp/hcl/v2/hclsyntax"
10-
"github.com/zclconf/go-cty/cty"
1110

1211
"github.com/coder/preview"
1312
"github.com/coder/preview/cli/clidisplay"
@@ -64,7 +63,7 @@ func (r *RootCmd) Root() *serpent.Command {
6463
continue
6564
}
6665
rvars[parts[0]] = types.ParameterValue{
67-
Value: cty.StringVal(parts[1]),
66+
Value: parts[1],
6867
}
6968
}
7069

@@ -97,6 +96,7 @@ func (r *RootCmd) Root() *serpent.Command {
9796
},
9897
}
9998
cmd.AddSubcommands(r.TerraformPlan())
99+
cmd.AddSubcommands(r.WebsocketServer())
100100
return cmd
101101
}
102102

cli/static/index.html

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>WebSocket JSON Preview</title>
7+
<script defer>
8+
let socket;
9+
10+
function connectWebSocket() {
11+
const dir = document.getElementById('directory').value;
12+
const path = document.getElementById('path').value;
13+
const input = document.getElementById('input').value;
14+
const output = document.getElementById('output');
15+
16+
if (socket) {
17+
socket.close();
18+
}
19+
20+
const url = `ws://localhost:8100/ws/${encodeURIComponent(dir)}?plan=${encodeURIComponent(path)}`;
21+
socket = new WebSocket(url);
22+
23+
socket.onopen = () => {
24+
socket.send(input);
25+
};
26+
27+
socket.onmessage = (event) => {
28+
output.textContent = event.data;
29+
};
30+
}
31+
32+
function handleInputChange() {
33+
if (socket && socket.readyState === WebSocket.OPEN) {
34+
const input = document.getElementById('input').value;
35+
socket.send(input);
36+
}
37+
}
38+
</script>
39+
<style>
40+
body { font-family: Arial, sans-serif; padding: 20px; }
41+
textarea { width: 100%; height: 150px; font-family: monospace; }
42+
pre { background: #f4f4f4; padding: 10px; border: 1px solid #ccc; white-space: pre-wrap; word-wrap: break-word; }
43+
</style>
44+
</head>
45+
<body>
46+
<h2>WebSocket JSON Preview</h2>
47+
<label>Directory: <input type="text" id="directory"></label><br>
48+
<label>Path: <input type="text" id="path"></label><br>
49+
<label>Input JSON:</label><br>
50+
<textarea id="input" oninput="handleInputChange()">{}</textarea><br>
51+
<button onclick="connectWebSocket()">Connect</button>
52+
<h3>Output:</h3>
53+
<pre id="output"></pre>
54+
</body>
55+
</html>

cli/web.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"embed"
6+
"io/fs"
7+
"net"
8+
"net/http"
9+
"os"
10+
11+
"github.com/go-chi/chi"
12+
13+
"cdr.dev/slog"
14+
"cdr.dev/slog/sloggers/sloghuman"
15+
"github.com/coder/preview/web"
16+
"github.com/coder/serpent"
17+
"github.com/coder/websocket"
18+
)
19+
20+
//go:embed static/*
21+
var static embed.FS
22+
23+
func (r *RootCmd) WebsocketServer() *serpent.Command {
24+
var (
25+
address string
26+
)
27+
28+
cmd := &serpent.Command{
29+
Use: "web",
30+
Short: "Runs a websocket for interactive form inputs.",
31+
Options: serpent.OptionSet{
32+
{
33+
Name: "Address",
34+
Description: "Address to listen on.",
35+
Required: false,
36+
Flag: "addr",
37+
Default: "0.0.0.0:8100",
38+
Value: serpent.StringOf(&address),
39+
},
40+
},
41+
// This command is mainly for developing the preview tool.
42+
Hidden: true,
43+
Handler: func(i *serpent.Invocation) error {
44+
ctx := i.Context()
45+
logger := slog.Make(sloghuman.Sink(i.Stderr))
46+
47+
mux := chi.NewMux()
48+
mux.HandleFunc("/ws/{dir}", websocketHandler(logger))
49+
50+
staticDir, err := fs.Sub(static, "static")
51+
if err != nil {
52+
return err
53+
}
54+
mux.NotFound(http.FileServer(http.FS(staticDir)).ServeHTTP)
55+
56+
srv := &http.Server{
57+
Addr: address,
58+
Handler: mux,
59+
BaseContext: func(listener net.Listener) context.Context {
60+
return ctx
61+
},
62+
}
63+
64+
logger.Info(ctx, "Starting server", slog.F("address", address))
65+
return srv.ListenAndServe()
66+
},
67+
}
68+
69+
return cmd
70+
}
71+
72+
func websocketHandler(logger slog.Logger) func(rw http.ResponseWriter, r *http.Request) {
73+
return func(rw http.ResponseWriter, r *http.Request) {
74+
conn, err := websocket.Accept(rw, r, nil)
75+
if err != nil {
76+
http.Error(rw, "Could not accept websocket connection", http.StatusInternalServerError)
77+
return
78+
}
79+
80+
dir := chi.URLParam(r, "dir")
81+
dinfo, err := os.Stat(dir)
82+
if err != nil {
83+
_ = conn.Close(websocket.StatusInternalError, "Could not stat directory")
84+
return
85+
}
86+
87+
if !dinfo.IsDir() {
88+
_ = conn.Close(websocket.StatusInternalError, "Not a directory")
89+
return
90+
}
91+
92+
dirFS := os.DirFS(dir)
93+
planPath := r.URL.Query().Get("plan")
94+
95+
session := web.NewSession(logger, dirFS, planPath)
96+
session.Listen(r.Context(), conn)
97+
}
98+
}

extract/parameter.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,11 +243,25 @@ func required(block *terraform.Block, keys ...string) hcl.Diagnostics {
243243
return diags
244244
}
245245

246-
func richParameterValue(block *terraform.Block) (cty.Value, hcl.Diagnostics) {
246+
func richParameterValue(block *terraform.Block) (string, hcl.Diagnostics) {
247247
// Find the value of the parameter from the context.
248248
paramPath := append([]string{"data"}, block.Labels()...)
249249
valueRef := hclext.ScopeTraversalExpr(append(paramPath, "value")...)
250-
return valueRef.Value(block.Context().Inner())
250+
val, diags := valueRef.Value(block.Context().Inner())
251+
if diags.HasErrors() {
252+
return "", diags
253+
}
254+
255+
if !val.Type().Equals(cty.String) {
256+
return "", hcl.Diagnostics{
257+
{
258+
Severity: hcl.DiagError,
259+
Summary: "Invalid parameter value",
260+
Detail: fmt.Sprintf("Expected a string, got %q", val.Type().FriendlyName()),
261+
},
262+
}
263+
}
264+
return val.AsString(), nil
251265
}
252266

253267
func ParameterCtyType(typ string) (cty.Type, error) {

extract/state.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66

77
tfjson "github.com/hashicorp/terraform-json"
8-
"github.com/zclconf/go-cty/cty"
98

109
"github.com/coder/preview/types"
1110
)
@@ -54,7 +53,7 @@ func ParameterFromState(block *tfjson.StateResource) (types.Parameter, error) {
5453

5554
param := types.Parameter{
5655
Value: types.ParameterValue{
57-
Value: cty.StringVal(st.string("value")),
56+
Value: st.string("value"),
5857
},
5958
RichParameter: types.RichParameter{
6059
Name: st.string("name"),

go.mod

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ module github.com/coder/preview
33
go 1.23.5
44

55
require (
6+
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6
67
github.com/aquasecurity/trivy v0.58.2
78
github.com/coder/serpent v0.10.0
9+
github.com/coder/websocket v1.8.12
10+
github.com/go-chi/chi v4.1.2+incompatible
811
github.com/hashicorp/go-version v1.7.0
912
github.com/hashicorp/hc-install v0.9.1
1013
github.com/hashicorp/hcl/v2 v2.23.0
@@ -16,7 +19,6 @@ require (
1619
)
1720

1821
require (
19-
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6 // indirect
2022
cel.dev/expr v0.19.0 // indirect
2123
cloud.google.com/go v0.116.0 // indirect
2224
cloud.google.com/go/auth v0.13.0 // indirect
@@ -39,17 +41,24 @@ require (
3941
github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect
4042
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
4143
github.com/cespare/xxhash/v2 v2.3.0 // indirect
44+
github.com/charmbracelet/lipgloss v0.8.0 // indirect
4245
github.com/cloudflare/circl v1.5.0 // indirect
4346
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
47+
github.com/coder/guts v1.0.1 // indirect
4448
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 // indirect
4549
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
50+
github.com/dlclark/regexp2 v1.11.4 // indirect
51+
github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd // indirect
4652
github.com/envoyproxy/go-control-plane v0.13.1 // indirect
4753
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
4854
github.com/fatih/color v1.18.0 // indirect
55+
github.com/fatih/structtag v1.2.0 // indirect
4956
github.com/felixge/httpsnoop v1.0.4 // indirect
5057
github.com/go-logr/logr v1.4.2 // indirect
5158
github.com/go-logr/stdr v1.2.2 // indirect
59+
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
5260
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
61+
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
5362
github.com/google/s2a-go v0.1.8 // indirect
5463
github.com/google/uuid v1.6.0 // indirect
5564
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
@@ -71,6 +80,7 @@ require (
7180
github.com/mitchellh/go-homedir v1.1.0 // indirect
7281
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
7382
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
83+
github.com/muesli/reflow v0.3.0 // indirect
7484
github.com/muesli/termenv v0.15.2 // indirect
7585
github.com/pion/transport/v2 v2.0.0 // indirect
7686
github.com/pion/udp v0.1.4 // indirect
@@ -102,7 +112,7 @@ require (
102112
golang.org/x/text v0.22.0 // indirect
103113
golang.org/x/time v0.9.0 // indirect
104114
golang.org/x/tools v0.29.0 // indirect
105-
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
115+
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
106116
google.golang.org/api v0.216.0 // indirect
107117
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
108118
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect

0 commit comments

Comments
 (0)