mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix(agent/agentcontainers/dcspec): generate unmarshalers and add tests (#17330)
This change allows proper unmarshaling of `devcontainer.json` and will no longer break EnvInfoer or the Web Terminal. Fixes coder/internal#556
This commit is contained in:
committed by
GitHub
parent
c1816e3674
commit
33b9487899
+246
@@ -1,6 +1,30 @@
|
||||
// Code generated by dcspec/gen.sh. DO NOT EDIT.
|
||||
//
|
||||
// This file was generated from JSON Schema using quicktype, do not modify it directly.
|
||||
// To parse and unparse this JSON data, add this code to your project and do:
|
||||
//
|
||||
// devContainer, err := UnmarshalDevContainer(bytes)
|
||||
// bytes, err = devContainer.Marshal()
|
||||
|
||||
package dcspec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func UnmarshalDevContainer(data []byte) (DevContainer, error) {
|
||||
var r DevContainer
|
||||
err := json.Unmarshal(data, &r)
|
||||
return r, err
|
||||
}
|
||||
|
||||
func (r *DevContainer) Marshal() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
// Defines a dev container
|
||||
type DevContainer struct {
|
||||
// Docker build-related options.
|
||||
@@ -284,6 +308,21 @@ type DevContainerAppPort struct {
|
||||
UnionArray []AppPortElement
|
||||
}
|
||||
|
||||
func (x *DevContainerAppPort) UnmarshalJSON(data []byte) error {
|
||||
x.UnionArray = nil
|
||||
object, err := unmarshalUnion(data, &x.Integer, nil, nil, &x.String, true, &x.UnionArray, false, nil, false, nil, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *DevContainerAppPort) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(x.Integer, nil, nil, x.String, x.UnionArray != nil, x.UnionArray, false, nil, false, nil, false, nil, false)
|
||||
}
|
||||
|
||||
// Application ports that are exposed by the container. This can be a single port or an
|
||||
// array of ports. Each port can be a number or a string. A number is mapped to the same
|
||||
// port on the host. A string is passed to Docker unchanged and can be used to map ports
|
||||
@@ -293,6 +332,20 @@ type AppPortElement struct {
|
||||
String *string
|
||||
}
|
||||
|
||||
func (x *AppPortElement) UnmarshalJSON(data []byte) error {
|
||||
object, err := unmarshalUnion(data, &x.Integer, nil, nil, &x.String, false, nil, false, nil, false, nil, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AppPortElement) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(x.Integer, nil, nil, x.String, false, nil, false, nil, false, nil, false, nil, false)
|
||||
}
|
||||
|
||||
// The image to consider as a cache. Use an array to specify multiple images.
|
||||
//
|
||||
// The name of the docker-compose file(s) used to start the services.
|
||||
@@ -301,17 +354,64 @@ type CacheFrom struct {
|
||||
StringArray []string
|
||||
}
|
||||
|
||||
func (x *CacheFrom) UnmarshalJSON(data []byte) error {
|
||||
x.StringArray = nil
|
||||
object, err := unmarshalUnion(data, nil, nil, nil, &x.String, true, &x.StringArray, false, nil, false, nil, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CacheFrom) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(nil, nil, nil, x.String, x.StringArray != nil, x.StringArray, false, nil, false, nil, false, nil, false)
|
||||
}
|
||||
|
||||
type ForwardPort struct {
|
||||
Integer *int64
|
||||
String *string
|
||||
}
|
||||
|
||||
func (x *ForwardPort) UnmarshalJSON(data []byte) error {
|
||||
object, err := unmarshalUnion(data, &x.Integer, nil, nil, &x.String, false, nil, false, nil, false, nil, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ForwardPort) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(x.Integer, nil, nil, x.String, false, nil, false, nil, false, nil, false, nil, false)
|
||||
}
|
||||
|
||||
type GPUUnion struct {
|
||||
Bool *bool
|
||||
Enum *GPUEnum
|
||||
GPUClass *GPUClass
|
||||
}
|
||||
|
||||
func (x *GPUUnion) UnmarshalJSON(data []byte) error {
|
||||
x.GPUClass = nil
|
||||
x.Enum = nil
|
||||
var c GPUClass
|
||||
object, err := unmarshalUnion(data, nil, nil, &x.Bool, nil, false, nil, true, &c, false, nil, true, &x.Enum, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
x.GPUClass = &c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GPUUnion) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(nil, nil, x.Bool, nil, false, nil, x.GPUClass != nil, x.GPUClass, false, nil, x.Enum != nil, x.Enum, false)
|
||||
}
|
||||
|
||||
// A command to run locally (i.e Your host machine, cloud VM) before anything else. This
|
||||
// command is run before "onCreateCommand". If this is a single string, it will be run in a
|
||||
// shell. If this is an array of strings, it will be run as a single command without shell.
|
||||
@@ -349,7 +449,153 @@ type Command struct {
|
||||
UnionMap map[string]*CacheFrom
|
||||
}
|
||||
|
||||
func (x *Command) UnmarshalJSON(data []byte) error {
|
||||
x.StringArray = nil
|
||||
x.UnionMap = nil
|
||||
object, err := unmarshalUnion(data, nil, nil, nil, &x.String, true, &x.StringArray, false, nil, true, &x.UnionMap, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Command) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(nil, nil, nil, x.String, x.StringArray != nil, x.StringArray, false, nil, x.UnionMap != nil, x.UnionMap, false, nil, false)
|
||||
}
|
||||
|
||||
type MountElement struct {
|
||||
Mount *Mount
|
||||
String *string
|
||||
}
|
||||
|
||||
func (x *MountElement) UnmarshalJSON(data []byte) error {
|
||||
x.Mount = nil
|
||||
var c Mount
|
||||
object, err := unmarshalUnion(data, nil, nil, nil, &x.String, false, nil, true, &c, false, nil, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
x.Mount = &c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MountElement) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(nil, nil, nil, x.String, false, nil, x.Mount != nil, x.Mount, false, nil, false, nil, false)
|
||||
}
|
||||
|
||||
func unmarshalUnion(data []byte, pi **int64, pf **float64, pb **bool, ps **string, haveArray bool, pa interface{}, haveObject bool, pc interface{}, haveMap bool, pm interface{}, haveEnum bool, pe interface{}, nullable bool) (bool, error) {
|
||||
if pi != nil {
|
||||
*pi = nil
|
||||
}
|
||||
if pf != nil {
|
||||
*pf = nil
|
||||
}
|
||||
if pb != nil {
|
||||
*pb = nil
|
||||
}
|
||||
if ps != nil {
|
||||
*ps = nil
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.UseNumber()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch v := tok.(type) {
|
||||
case json.Number:
|
||||
if pi != nil {
|
||||
i, err := v.Int64()
|
||||
if err == nil {
|
||||
*pi = &i
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if pf != nil {
|
||||
f, err := v.Float64()
|
||||
if err == nil {
|
||||
*pf = &f
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.New("Unparsable number")
|
||||
}
|
||||
return false, errors.New("Union does not contain number")
|
||||
case float64:
|
||||
return false, errors.New("Decoder should not return float64")
|
||||
case bool:
|
||||
if pb != nil {
|
||||
*pb = &v
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.New("Union does not contain bool")
|
||||
case string:
|
||||
if haveEnum {
|
||||
return false, json.Unmarshal(data, pe)
|
||||
}
|
||||
if ps != nil {
|
||||
*ps = &v
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.New("Union does not contain string")
|
||||
case nil:
|
||||
if nullable {
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.New("Union does not contain null")
|
||||
case json.Delim:
|
||||
if v == '{' {
|
||||
if haveObject {
|
||||
return true, json.Unmarshal(data, pc)
|
||||
}
|
||||
if haveMap {
|
||||
return false, json.Unmarshal(data, pm)
|
||||
}
|
||||
return false, errors.New("Union does not contain object")
|
||||
}
|
||||
if v == '[' {
|
||||
if haveArray {
|
||||
return false, json.Unmarshal(data, pa)
|
||||
}
|
||||
return false, errors.New("Union does not contain array")
|
||||
}
|
||||
return false, errors.New("Cannot handle delimiter")
|
||||
}
|
||||
return false, errors.New("Cannot unmarshal union")
|
||||
}
|
||||
|
||||
func marshalUnion(pi *int64, pf *float64, pb *bool, ps *string, haveArray bool, pa interface{}, haveObject bool, pc interface{}, haveMap bool, pm interface{}, haveEnum bool, pe interface{}, nullable bool) ([]byte, error) {
|
||||
if pi != nil {
|
||||
return json.Marshal(*pi)
|
||||
}
|
||||
if pf != nil {
|
||||
return json.Marshal(*pf)
|
||||
}
|
||||
if pb != nil {
|
||||
return json.Marshal(*pb)
|
||||
}
|
||||
if ps != nil {
|
||||
return json.Marshal(*ps)
|
||||
}
|
||||
if haveArray {
|
||||
return json.Marshal(pa)
|
||||
}
|
||||
if haveObject {
|
||||
return json.Marshal(pc)
|
||||
}
|
||||
if haveMap {
|
||||
return json.Marshal(pm)
|
||||
}
|
||||
if haveEnum {
|
||||
return json.Marshal(pe)
|
||||
}
|
||||
if nullable {
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
return nil, errors.New("Union must not be null")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package dcspec_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentcontainers/dcspec"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
)
|
||||
|
||||
func TestUnmarshalDevContainer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
file string
|
||||
wantErr bool
|
||||
want dcspec.DevContainer
|
||||
}
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "minimal",
|
||||
file: filepath.Join("testdata", "minimal.json"),
|
||||
want: dcspec.DevContainer{
|
||||
Image: ptr.Ref("test-image"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "arrays",
|
||||
file: filepath.Join("testdata", "arrays.json"),
|
||||
want: dcspec.DevContainer{
|
||||
Image: ptr.Ref("test-image"),
|
||||
RunArgs: []string{"--network=host", "--privileged"},
|
||||
ForwardPorts: []dcspec.ForwardPort{
|
||||
{
|
||||
Integer: ptr.Ref[int64](8080),
|
||||
},
|
||||
{
|
||||
String: ptr.Ref("3000:3000"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "devcontainers/template-starter",
|
||||
file: filepath.Join("testdata", "devcontainers-template-starter.json"),
|
||||
wantErr: false,
|
||||
want: dcspec.DevContainer{
|
||||
Image: ptr.Ref("mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye"),
|
||||
Features: &dcspec.Features{},
|
||||
Customizations: map[string]interface{}{
|
||||
"vscode": map[string]interface{}{
|
||||
"extensions": []interface{}{
|
||||
"mads-hartmann.bash-ide-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
},
|
||||
},
|
||||
},
|
||||
PostCreateCommand: &dcspec.Command{
|
||||
String: ptr.Ref("npm install -g @devcontainers/cli"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var missingTests []string
|
||||
files, err := filepath.Glob("testdata/*.json")
|
||||
require.NoError(t, err, "glob test files failed")
|
||||
for _, file := range files {
|
||||
if !slices.ContainsFunc(tests, func(tt testCase) bool {
|
||||
return tt.file == file
|
||||
}) {
|
||||
missingTests = append(missingTests, file)
|
||||
}
|
||||
}
|
||||
require.Empty(t, missingTests, "missing tests case for files: %v", missingTests)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data, err := os.ReadFile(tt.file)
|
||||
require.NoError(t, err, "read test file failed")
|
||||
|
||||
got, err := dcspec.UnmarshalDevContainer(data)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err, "want error but got nil")
|
||||
return
|
||||
}
|
||||
require.NoError(t, err, "unmarshal DevContainer failed")
|
||||
|
||||
// Compare the unmarshaled data with the expected data.
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
require.Empty(t, diff, "UnmarshalDevContainer() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// Test that marshaling works (without comparing to original).
|
||||
marshaled, err := got.Marshal()
|
||||
require.NoError(t, err, "marshal DevContainer back to JSON failed")
|
||||
require.NotEmpty(t, marshaled, "marshaled JSON should not be empty")
|
||||
|
||||
// Verify the marshaled JSON can be unmarshaled back.
|
||||
var unmarshaled interface{}
|
||||
err = json.Unmarshal(marshaled, &unmarshaled)
|
||||
require.NoError(t, err, "unmarshal marshaled JSON failed")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalDevContainer_EdgeCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty JSON",
|
||||
json: "{}",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
json: "{not valid json",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := dcspec.UnmarshalDevContainer([]byte(tt.json))
|
||||
if tt.wantErr {
|
||||
require.Error(t, err, "want error but got nil")
|
||||
return
|
||||
}
|
||||
require.NoError(t, err, "unmarshal DevContainer failed")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,6 @@ fi
|
||||
if ! pnpm exec quicktype \
|
||||
--src-lang schema \
|
||||
--lang go \
|
||||
--just-types-and-package \
|
||||
--top-level "DevContainer" \
|
||||
--out "${TMPDIR}/${DEST_FILENAME}" \
|
||||
--package "dcspec" \
|
||||
@@ -67,9 +66,9 @@ go run mvdan.cc/gofumpt@v0.4.0 -w -l "${TMPDIR}/${DEST_FILENAME}"
|
||||
# Add a header so that Go recognizes this as a generated file.
|
||||
if grep -q -- "\[-i extension\]" < <(sed -h 2>&1); then
|
||||
# darwin sed
|
||||
sed -i '' '1s/^/\/\/ Code generated by dcspec\/gen.sh. DO NOT EDIT.\n/' "${TMPDIR}/${DEST_FILENAME}"
|
||||
sed -i '' '1s/^/\/\/ Code generated by dcspec\/gen.sh. DO NOT EDIT.\n\/\/\n/' "${TMPDIR}/${DEST_FILENAME}"
|
||||
else
|
||||
sed -i'' '1s/^/\/\/ Code generated by dcspec\/gen.sh. DO NOT EDIT.\n/' "${TMPDIR}/${DEST_FILENAME}"
|
||||
sed -i'' '1s/^/\/\/ Code generated by dcspec\/gen.sh. DO NOT EDIT.\n\/\/\n/' "${TMPDIR}/${DEST_FILENAME}"
|
||||
fi
|
||||
|
||||
mv -v "${TMPDIR}/${DEST_FILENAME}" "${DEST_PATH}"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"image": "test-image",
|
||||
"runArgs": ["--network=host", "--privileged"],
|
||||
"forwardPorts": [8080, "3000:3000"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["mads-hartmann.bash-ide-vscode", "dbaeumer.vscode-eslint"]
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "npm install -g @devcontainers/cli"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "image": "test-image" }
|
||||
Reference in New Issue
Block a user