Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
H
helm3
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
go
helm3
Commits
5ed1dc25
Commit
5ed1dc25
authored
Mar 23, 2016
by
Dave Cunningham
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Write some new unit tests for expandybird
parent
69f31b9e
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
640 additions
and
112 deletions
+640
-112
expander.go
cmd/expandybird/expander/expander.go
+6
-1
expander_test.go
cmd/expandybird/expander/expander_test.go
+634
-111
No files found.
cmd/expandybird/expander/expander.go
View file @
5ed1dc25
...
...
@@ -58,6 +58,11 @@ func (e *expander) ExpandChart(request *common.ExpansionRequest) (*common.Expans
chartInv
:=
request
.
ChartInvocation
chartFile
:=
request
.
Chart
.
Chartfile
chartMembers
:=
request
.
Chart
.
Members
if
chartInv
.
Type
!=
chartFile
.
Name
{
return
nil
,
fmt
.
Errorf
(
"Request chart invocation does not match provided chart"
)
}
schemaName
:=
chartInv
.
Type
+
".schema"
if
chartFile
.
Expander
==
nil
{
...
...
@@ -89,7 +94,7 @@ func (e *expander) ExpandChart(request *common.ExpansionRequest) (*common.Expans
message
:=
fmt
.
Sprintf
(
"The entrypoint in the chart.yaml cannot be found: %s"
,
chartFile
.
Expander
.
Entrypoint
)
return
nil
,
fmt
.
Errorf
(
"%s: %s"
,
chartInv
.
Name
,
message
)
}
if
schemaIndex
==
-
1
{
if
chartFile
.
Schema
!=
""
&&
schemaIndex
==
-
1
{
message
:=
fmt
.
Sprintf
(
"The schema in the chart.yaml cannot be found: %s"
,
chartFile
.
Schema
)
return
nil
,
fmt
.
Errorf
(
"%s: %s"
,
chartInv
.
Name
,
message
)
}
...
...
cmd/expandybird/expander/expander_test.go
View file @
5ed1dc25
...
...
@@ -16,168 +16,691 @@ limitations under the License.
package
expander
/*
import
(
"fmt"
"io"
"io/ioutil"
"os"
"path"
"reflect"
"runtime"
"strings"
"testing"
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/util"
)
var importFileNames = []string{
"../test/replicatedservice.py",
}
var validFileName = "../test/ValidContent.yaml"
var outputFileName = "../test/ExpectedOutput.yaml"
var archiveFileName = "../test/TestArchive.tar"
var
expanderName
=
"../../../expansion/expansion.py"
type
ExpanderT
estCase struct {
type
t
estCase
struct
{
Description
string
TemplateFileName string
ImportFileNames []string
Request
*
common
.
ExpansionRequest
ExpectedResponse
*
common
.
ExpansionResponse
ExpectedError
string
}
func (etc *ExpanderTestCase) GetTemplate(t *testing.T) *common.Template {
template, err := util.NewTemplateFromFileNames(etc.TemplateFileName, etc.ImportFileNames)
if err != nil {
t.Fatalf("cannot create template for test case '%s': %s", etc.Description, err)
}
// content provides an easy way to provide file content verbatim in tests.
func
content
(
lines
[]
string
)
[]
byte
{
return
[]
byte
(
strings
.
Join
(
lines
,
"
\n
"
)
+
"
\n
"
)
}
return template
// funcName returns the name of the calling function.
func
funcName
()
string
{
pc
,
_
,
_
,
_
:=
runtime
.
Caller
(
1
)
return
runtime
.
FuncForPC
(
pc
)
.
Name
()
}
func GetOutputString(t *testing.T, description string) string {
output, err := ioutil.ReadFile(outputFileName)
func
testExpansion
(
t
*
testing
.
T
,
req
*
common
.
ExpansionRequest
,
expResponse
*
common
.
ExpansionResponse
,
expError
string
)
{
backend
:=
NewExpander
(
expanderName
)
response
,
err
:=
backend
.
ExpandChart
(
req
)
if
err
!=
nil
{
t.Fatalf("cannot read output file for test case '%s': %s", description, err)
message
:=
err
.
Error
()
if
expResponse
!=
nil
||
!
strings
.
Contains
(
message
,
expError
)
{
t
.
Fatalf
(
"unexpected error: %s
\n
"
,
message
)
}
}
else
{
if
expResponse
==
nil
{
t
.
Fatalf
(
"expected error did not occur: %s
\n
"
,
expError
)
}
if
!
reflect
.
DeepEqual
(
response
,
expResponse
)
{
message
:=
fmt
.
Sprintf
(
"want:
\n
%s
\n
have:
\n
%s
\n
"
,
expResponse
,
response
)
t
.
Fatalf
(
"output mismatch:
\n
%s
\n
"
,
message
)
}
}
}
return string(output)
var
pyExpander
=
&
chart
.
Expander
{
Name
:
"ExpandyBird"
,
Entrypoint
:
"templates/main.py"
,
}
func expandAndVerifyOutput(t *testing.T, actualOutput, description string) {
actualResult, err := NewExpansionResult(actualOutput)
if err != nil {
t.Fatalf("error in test case '%s': %s\n", description, err)
}
var
jinjaExpander
=
&
chart
.
Expander
{
Name
:
"ExpandyBird"
,
Entrypoint
:
"templates/main.jinja"
,
}
expectedOutput := GetOutputString(t, description)
expectedResult, err := NewExpansionResult(expectedOutput)
if err != nil {
t.Fatalf("error in test case '%s': %s\n", description, err)
}
func
TestEmptyJinja
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
jinjaExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"templates/main.jinja"
,
Content
:
content
([]
string
{
"resources:"
}),
},
},
},
},
&
common
.
ExpansionResponse
{
Resources
:
[]
interface
{}{},
},
""
,
// Error
)
}
if !reflect.DeepEqual(actualResult, expectedResult) {
message := fmt.Sprintf("want:\n%s\nhave:\n%s\n", expectedOutput, actualOutput)
t.Fatalf("error in test case '%s':\n%s\n", description, message)
}
func
TestEmptyPython
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
pyExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"templates/main.py"
,
Content
:
content
([]
string
{
"def GenerateConfig(ctx):"
,
" return 'resources:'"
,
}),
},
},
},
},
&
common
.
ExpansionResponse
{
Resources
:
[]
interface
{}{},
},
""
,
// Error
)
}
func testExpandTemplateFromFile(t *testing.T, fileName, baseName string, importFileNames []string,
constructor func(string, io.Reader, []string) (*common.Template, error)) {
file, err := os.Open(fileName)
if err != nil {
t.Fatalf("cannot open file %s: %s", fileName, err)
}
func
TestSimpleJinja
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
jinjaExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"templates/main.jinja"
,
Content
:
content
([]
string
{
"resources:"
,
"- name: foo"
,
" type: bar"
,
}),
},
},
},
},
&
common
.
ExpansionResponse
{
Resources
:
[]
interface
{}{
map
[
string
]
interface
{}{
"name"
:
"foo"
,
"type"
:
"bar"
,
},
},
},
""
,
// Error
)
}
template, err := constructor(baseName, file, importFileNames)
if err != nil {
t.Fatalf("cannot create template from file %s: %s", fileName, err)
}
func
TestSimplePython
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
pyExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"templates/main.py"
,
Content
:
content
([]
string
{
"def GenerateConfig(ctx):"
,
" return '''resources:"
,
"- name: foo"
,
" type: bar"
,
"'''"
,
}),
},
},
},
},
&
common
.
ExpansionResponse
{
Resources
:
[]
interface
{}{
map
[
string
]
interface
{}{
"name"
:
"foo"
,
"type"
:
"bar"
,
},
},
},
""
,
// Error
)
}
backend := NewExpander(expanderName)
actualOutput, err := backend.ExpandTemplate(template)
if err != nil {
t.Fatalf("cannot expand template from file %s: %s", fileName, err)
}
func
TestPropertiesJinja
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
Properties
:
map
[
string
]
interface
{}{
"prop1"
:
3.0
,
"prop2"
:
"foo"
,
},
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
jinjaExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"templates/main.jinja"
,
Content
:
content
([]
string
{
"resources:"
,
"- name: foo"
,
" type: {{ properties.prop2 }}"
,
" properties:"
,
" something: {{ properties.prop1 }}"
,
}),
},
},
},
},
&
common
.
ExpansionResponse
{
Resources
:
[]
interface
{}{
map
[
string
]
interface
{}{
"name"
:
"foo"
,
"properties"
:
map
[
string
]
interface
{}{
"something"
:
3.0
,
},
"type"
:
"foo"
,
},
},
},
""
,
// Error
)
}
description := fmt.Sprintf("test expand template from file: %s", fileName)
expandAndVerifyOutput(t, actualOutput, description)
func
TestPropertiesPython
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
Properties
:
map
[
string
]
interface
{}{
"prop1"
:
3.0
,
"prop2"
:
"foo"
,
},
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
pyExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"templates/main.py"
,
Content
:
content
([]
string
{
"def GenerateConfig(ctx):"
,
" return '''resources:"
,
"- name: foo"
,
" type: %(prop2)s"
,
" properties:"
,
" something: %(prop1)s"
,
"''' % ctx.properties"
,
}),
},
},
},
},
&
common
.
ExpansionResponse
{
Resources
:
[]
interface
{}{
map
[
string
]
interface
{}{
"name"
:
"foo"
,
"properties"
:
map
[
string
]
interface
{}{
"something"
:
3.0
,
},
"type"
:
"foo"
,
},
},
},
""
,
// Error
)
}
func TestExpandTemplateFromReader(t *testing.T) {
baseName := path.Base(validFileName)
testExpandTemplateFromFile(t, validFileName, baseName, importFileNames, util.NewTemplateFromReader)
func
TestMultiFileJinja
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
jinjaExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"templates/main.jinja"
,
Content
:
content
([]
string
{
"{% include 'templates/secondary.jinja' %}"
}),
},
{
Path
:
"templates/secondary.jinja"
,
Content
:
content
([]
string
{
"resources:"
,
"- name: foo"
,
" type: bar"
,
}),
},
},
},
},
&
common
.
ExpansionResponse
{
Resources
:
[]
interface
{}{
map
[
string
]
interface
{}{
"name"
:
"foo"
,
"type"
:
"bar"
,
},
},
},
""
,
// Error
)
}
func TestExpandTemplateFromArchive(t *testing.T) {
baseName := path.Base(validFileName)
testExpandTemplateFromFile(t, archiveFileName, baseName, nil, util.NewTemplateFromArchive)
var
schemaContent
=
content
([]
string
{
`{`
,
` "required": ["prop1", "prop2"],`
,
` "additionalProperties": false,`
,
` "properties": {`
,
` "prop1": {`
,
` "description": "Nice description.",`
,
` "type": "integer"`
,
` },`
,
` "prop2": {`
,
` "description": "Nice description.",`
,
` "type": "string"`
,
` }`
,
` }`
,
`}`
,
})
func
TestSchema
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
Properties
:
map
[
string
]
interface
{}{
"prop1"
:
3.0
,
"prop2"
:
"foo"
,
},
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
jinjaExpander
,
Schema
:
"Schema.yaml"
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"Schema.yaml"
,
Content
:
schemaContent
,
},
{
Path
:
"templates/main.jinja"
,
Content
:
content
([]
string
{
"resources:"
,
"- name: foo"
,
" type: {{ properties.prop2 }}"
,
" properties:"
,
" something: {{ properties.prop1 }}"
,
}),
},
},
},
},
&
common
.
ExpansionResponse
{
Resources
:
[]
interface
{}{
map
[
string
]
interface
{}{
"name"
:
"foo"
,
"properties"
:
map
[
string
]
interface
{}{
"something"
:
3.0
,
},
"type"
:
"foo"
,
},
},
},
""
,
// Error
)
}
var ExpanderTestCases = []ExpanderTestCase{
func
TestSchemaFail
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
Properties
:
map
[
string
]
interface
{}{
"prop1"
:
3.0
,
"prop3"
:
"foo"
,
},
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
jinjaExpander
,
Schema
:
"Schema.yaml"
,
},
Members
:
[]
*
chart
.
Member
{
{
"expect error for invalid file name",
"../test/InvalidFileName.yaml",
importFileNames,
"ExpansionError: Exception",
Path
:
"Schema.yaml"
,
Content
:
schemaContent
,
},
{
"expect error for invalid property",
"../test/InvalidProperty.yaml",
importFileNames,
"ExpansionError: Exception",
Path
:
"templates/main.jinja"
,
Content
:
content
([]
string
{
"resources:"
,
"- name: foo"
,
" type: {{ properties.prop2 }}"
,
" properties:"
,
" something: {{ properties.prop1 }}"
,
}),
},
},
},
},
nil
,
// Response.
"Invalid properties for"
,
)
}
func
TestMultiFileJinjaMissing
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
jinjaExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
"expect error for malformed content
",
"../test/MalformedContent.yaml"
,
importFileNames
,
"ExpansionError: Error parsing YAML: mapping values are not allowed here"
,
Path
:
"templates/main.jinja
"
,
Content
:
content
([]
string
{
"{% include 'templates/secondary.jinja' %}"
})
,
}
,
}
,
},
},
nil
,
// Response
"TemplateNotFound: templates/secondary.jinja"
,
)
}
func
TestMultiFilePython
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
pyExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
"expect error for missing imports",
"../test/MissingImports.yaml",
importFileNames,
"ExpansionError: Exception",
Path
:
"templates/main.py"
,
Content
:
content
([]
string
{
"from templates import second"
,
"import templates.third"
,
"def GenerateConfig(ctx):"
,
" t2 = second.Gen()"
,
" t3 = templates.third.Gen()"
,
" return t2"
,
}),
},
{
"expect error for missing resource name",
"../test/MissingResourceName.yaml",
importFileNames,
"ExpansionError: Resource does not have a name",
Path
:
"templates/second.py"
,
Content
:
content
([]
string
{
"def Gen():"
,
" return '''resources:"
,
"- name: foo"
,
" type: bar"
,
"'''"
,
}),
},
{
"expect error for missing type name",
"../test/MissingTypeName.yaml",
importFileNames,
"ExpansionError: Resource does not have type defined",
Path
:
"templates/third.py"
,
Content
:
content
([]
string
{
"def Gen():"
,
" return '''resources:"
,
"- name: foo"
,
" type: bar"
,
"'''"
,
}),
},
},
},
},
&
common
.
ExpansionResponse
{
Resources
:
[]
interface
{}{
map
[
string
]
interface
{}{
"name"
:
"foo"
,
"type"
:
"bar"
,
},
},
},
""
,
// Error
)
}
func
TestMultiFilePythonMissing
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
pyExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
"expect success",
validFileName,
importFileNames,
"",
Path
:
"templates/main.py"
,
Content
:
content
([]
string
{
"from templates import second"
,
}),
},
},
},
},
nil
,
// Response
"cannot import name second"
,
// Error
)
}
func TestExpandTemplate(t *testing.T) {
backend := NewExpander(expanderName)
for _, etc := range ExpanderTestCases {
template := etc.GetTemplate(t)
actualOutput, err := backend.ExpandTemplate(template)
if err != nil {
message := err.Error()
if !strings.Contains(message, etc.ExpectedError) {
t.Fatalf("error in test case '%s': %s\n", etc.Description, message)
}
} else {
if etc.ExpectedError != "" {
t.Fatalf("expected error did not occur in test case '%s': %s\n",
etc.Description, etc.ExpectedError)
}
func
TestWrongChartName
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
"WrongName"
,
Expander
:
jinjaExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"templates/main.jinja"
,
Content
:
content
([]
string
{
"resources:"
}),
},
},
},
},
nil
,
// Response
"Request chart invocation does not match provided chart"
,
)
}
expandAndVerifyOutput(t, actualOutput, etc.Description)
}
}
func
TestEntrypointNotFound
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
jinjaExpander
,
},
Members
:
[]
*
chart
.
Member
{},
},
},
nil
,
// Response
"The entrypoint in the chart.yaml cannot be found"
,
)
}
func
TestMalformedResource
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
jinjaExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"templates/main.jinja"
,
Content
:
content
([]
string
{
"resources:"
,
"fail"
,
}),
},
},
},
},
nil
,
// Response
"could not found expected ':'"
,
// [sic]
)
}
func
TestResourceNoName
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
jinjaExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"templates/main.jinja"
,
Content
:
content
([]
string
{
"resources:"
,
"- type: bar"
,
}),
},
},
},
},
nil
,
// Response.
"Resource does not have a name"
,
)
}
func
TestResourceNoType
(
t
*
testing
.
T
)
{
testExpansion
(
t
,
&
common
.
ExpansionRequest
{
ChartInvocation
:
&
common
.
Resource
{
Name
:
"test_invocation"
,
Type
:
funcName
(),
},
Chart
:
&
chart
.
Content
{
Chartfile
:
&
chart
.
Chartfile
{
Name
:
funcName
(),
Expander
:
jinjaExpander
,
},
Members
:
[]
*
chart
.
Member
{
{
Path
:
"templates/main.jinja"
,
Content
:
content
([]
string
{
"resources:"
,
"- name: foo"
,
}),
},
},
},
},
nil
,
// Response.
"Resource does not have type defined"
,
)
}
*/
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment