blob: 8d005a7b020400ef4847da5caddbfc1e82e027ff [file] [log] [blame]
Serge Bazanskia03b60b2023-04-01 14:47:44 +00001package workspace
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os/exec"
8 "unicode"
9)
10
11// EvalHscloudNix takes a hscloud attribute path (eg.
12// ops.machines.exports.kubeMachineNames) and evaluates its value. The
13// resulting value is then deserialized into the given target, which uses
14// encoding/json under the hood.
15//
16// The given path will be checked for basic mistakes, but is otherwise not
17// sanitized. Do not call this function with untrusted input. Even better, only
18// call it with constant strings.
19func EvalHscloudNix(ctx context.Context, target any, path string) error {
20 if err := checkNixPath(path); err != nil {
21 return fmt.Errorf("invalid path: %w", err)
22 }
23 ws, err := Get()
24 if err != nil {
25 return fmt.Errorf("could not find workspace: %w", err)
26 }
27
28 expression := `
29 let
30 hscloud = (import %q {});
31 in
32 hscloud.%s
33
34 `
35 expression = fmt.Sprintf(expression, ws, path)
36
37 args := []string{
38 // Do not attempt to actually instantiate derivation, just do an eval.
39 "--eval",
40 // Fully evaluate expression.
41 "--strict",
42 // Serialize evaluated expression into JSON.
43 "--json",
44 // Use following expression instead of file.
45 "-E",
46 expression,
47 }
48 cmd := exec.CommandContext(ctx, "nix-instantiate", args...)
49 out, err := cmd.Output()
50 if err != nil {
51 eerr := err.(*exec.ExitError)
52 return fmt.Errorf("nix-instantiate failed: %w, stderr: %q", err, eerr.Stderr)
53 }
54
55 if err := json.Unmarshal(out, target); err != nil {
56 return fmt.Errorf("unmarshaling json into target failed: %w", err)
57 }
58 return nil
59}
60
61// checkNixPath validates that the given path looks enough like a Nix attribute
62// path. It's not a security measure, it's an anti-footgun measure.
63func checkNixPath(s string) error {
64 if len(s) < 1 {
65 return fmt.Errorf("empty path")
66 }
67
68 // Split path into parts, supporting quotes.
69 var parts []string
70
71 // State machine: either:
72 // !quote && !mustEnd or
73 // quote && !mustEnd or
74 // !quote && mustEnd.
75 quoted := false
76 mustEnd := false
77 // Current part.
78 part := ""
79 for _, c := range s {
80 if c > unicode.MaxASCII {
81 return fmt.Errorf("only ASCII characters supported")
82 }
83
84 // If we must end a part, make sure it actually ends here.
85 if mustEnd {
86 if c != '.' {
87 return fmt.Errorf("quotes can only end at path boundaries")
88 }
89 mustEnd = false
90 parts = append(parts, part)
91 part = ""
92 continue
93 }
94
95 // Perform quoting logic. Only one a full part may be quoted.
96 if c == '"' {
97 if !quoted {
98 if len(part) > 0 {
99 return fmt.Errorf("quotes can only start at path boundaries")
100 }
101 quoted = true
102 continue
103 } else {
104 quoted = false
105 mustEnd = true
106 }
107 continue
108 }
109
110 // Perform dot/period logic - different if we're in a quoted fragment
111 // or not.
112 if !quoted {
113 // Not in quoted part: finish part.
114 if c == '.' {
115 parts = append(parts, part)
116 part = ""
117 continue
118 }
119 } else {
120 // In quoted part: consume dot into part.
121 if c == '.' {
122 part += string(c)
123 continue
124 }
125 }
126
127 // Consume characters, making sure we only handle what's supported in a
128 // nix attrset accessor.
129 switch {
130 // Letters and underscores can be anywhere in the partq.
131 case c >= 'a' && c <= 'z':
132 case c >= 'A' && c <= 'Z':
133 case c == '_':
134
135 // Digits/hyphens cannot start a part.
136 case c >= '0' && c <= '9', c == '-':
137 if len(part) == 0 {
138 return fmt.Errorf("part cannot start with a %q", string(c))
139 }
140 default:
141 return fmt.Errorf("unexpected char: %q", string(c))
142 }
143 part += string(c)
144
145 }
146 if quoted {
147 return fmt.Errorf("unterminated quote")
148 }
149 parts = append(parts, part)
150
151 // Make sure no parts are empty, ie. there are no double dots.
152 for _, part := range parts {
153 if len(part) == 0 {
154 return fmt.Errorf("empty attrpath part")
155 }
156 }
157
158 return nil
159}