forked from hswaw/hscloud
Serge Bazanski
54183ba222
Also skip nix tests on systems without nix. Change-Id: I4c0069a429df10a496b2651c2506b2d4625d5f43 Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1585 Reviewed-by: q3k <q3k@hackerspace.pl>
161 lines
3.8 KiB
Go
161 lines
3.8 KiB
Go
package workspace
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"unicode"
|
|
)
|
|
|
|
// EvalHscloudNix takes a hscloud attribute path (eg.
|
|
// ops.machines.exports.kubeMachineNames) and evaluates its value. The
|
|
// resulting value is then deserialized into the given target, which uses
|
|
// encoding/json under the hood.
|
|
//
|
|
// The given path will be checked for basic mistakes, but is otherwise not
|
|
// sanitized. Do not call this function with untrusted input. Even better, only
|
|
// call it with constant strings.
|
|
func EvalHscloudNix(ctx context.Context, target any, path string) error {
|
|
if err := checkNixPath(path); err != nil {
|
|
return fmt.Errorf("invalid path: %w", err)
|
|
}
|
|
ws, err := Get()
|
|
if err != nil {
|
|
return fmt.Errorf("could not find workspace: %w", err)
|
|
}
|
|
|
|
expression := `
|
|
let
|
|
hscloud = (import %q {});
|
|
in
|
|
hscloud.%s
|
|
|
|
`
|
|
expression = fmt.Sprintf(expression, ws, path)
|
|
|
|
args := []string{
|
|
// Do not attempt to actually instantiate derivation, just do an eval.
|
|
"--eval",
|
|
// Fully evaluate expression.
|
|
"--strict",
|
|
// Serialize evaluated expression into JSON.
|
|
"--json",
|
|
// Use following expression instead of file.
|
|
"-E",
|
|
expression,
|
|
}
|
|
cmd := exec.CommandContext(ctx, "nix-instantiate", args...)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
if eerr, ok := err.(*exec.ExitError); ok {
|
|
return fmt.Errorf("nix-instantiate failed: %w, stderr: %q", err, eerr.Stderr)
|
|
}
|
|
return fmt.Errorf("nix-instantiate failed: %w", err)
|
|
}
|
|
|
|
if err := json.Unmarshal(out, target); err != nil {
|
|
return fmt.Errorf("unmarshaling json into target failed: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkNixPath validates that the given path looks enough like a Nix attribute
|
|
// path. It's not a security measure, it's an anti-footgun measure.
|
|
func checkNixPath(s string) error {
|
|
if len(s) < 1 {
|
|
return fmt.Errorf("empty path")
|
|
}
|
|
|
|
// Split path into parts, supporting quotes.
|
|
var parts []string
|
|
|
|
// State machine: either:
|
|
// !quote && !mustEnd or
|
|
// quote && !mustEnd or
|
|
// !quote && mustEnd.
|
|
quoted := false
|
|
mustEnd := false
|
|
// Current part.
|
|
part := ""
|
|
for _, c := range s {
|
|
if c > unicode.MaxASCII {
|
|
return fmt.Errorf("only ASCII characters supported")
|
|
}
|
|
|
|
// If we must end a part, make sure it actually ends here.
|
|
if mustEnd {
|
|
if c != '.' {
|
|
return fmt.Errorf("quotes can only end at path boundaries")
|
|
}
|
|
mustEnd = false
|
|
parts = append(parts, part)
|
|
part = ""
|
|
continue
|
|
}
|
|
|
|
// Perform quoting logic. Only one a full part may be quoted.
|
|
if c == '"' {
|
|
if !quoted {
|
|
if len(part) > 0 {
|
|
return fmt.Errorf("quotes can only start at path boundaries")
|
|
}
|
|
quoted = true
|
|
continue
|
|
} else {
|
|
quoted = false
|
|
mustEnd = true
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Perform dot/period logic - different if we're in a quoted fragment
|
|
// or not.
|
|
if !quoted {
|
|
// Not in quoted part: finish part.
|
|
if c == '.' {
|
|
parts = append(parts, part)
|
|
part = ""
|
|
continue
|
|
}
|
|
} else {
|
|
// In quoted part: consume dot into part.
|
|
if c == '.' {
|
|
part += string(c)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Consume characters, making sure we only handle what's supported in a
|
|
// nix attrset accessor.
|
|
switch {
|
|
// Letters and underscores can be anywhere in the partq.
|
|
case c >= 'a' && c <= 'z':
|
|
case c >= 'A' && c <= 'Z':
|
|
case c == '_':
|
|
|
|
// Digits/hyphens cannot start a part.
|
|
case c >= '0' && c <= '9', c == '-':
|
|
if len(part) == 0 {
|
|
return fmt.Errorf("part cannot start with a %q", string(c))
|
|
}
|
|
default:
|
|
return fmt.Errorf("unexpected char: %q", string(c))
|
|
}
|
|
part += string(c)
|
|
|
|
}
|
|
if quoted {
|
|
return fmt.Errorf("unterminated quote")
|
|
}
|
|
parts = append(parts, part)
|
|
|
|
// Make sure no parts are empty, ie. there are no double dots.
|
|
for _, part := range parts {
|
|
if len(part) == 0 {
|
|
return fmt.Errorf("empty attrpath part")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|