2020-08-05 23:27:17 +00:00
|
|
|
// Copyright 2016-2020, Pulumi Corporation.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2023-01-06 00:07:45 +00:00
|
|
|
//nolint:lll
|
2020-08-05 23:27:17 +00:00
|
|
|
package schema
|
|
|
|
|
|
|
|
import (
|
2024-03-04 21:54:05 +00:00
|
|
|
"context"
|
2020-08-05 23:27:17 +00:00
|
|
|
"encoding/json"
|
2024-03-04 21:54:05 +00:00
|
|
|
"fmt"
|
2023-07-26 19:36:37 +00:00
|
|
|
"math"
|
2020-10-29 23:41:12 +00:00
|
|
|
"net/url"
|
2023-01-06 22:39:16 +00:00
|
|
|
"os"
|
2020-08-05 23:27:17 +00:00
|
|
|
"path/filepath"
|
2020-10-29 23:41:12 +00:00
|
|
|
"reflect"
|
2021-09-08 05:23:30 +00:00
|
|
|
"sort"
|
2021-11-12 00:00:03 +00:00
|
|
|
"strings"
|
2020-08-05 23:27:17 +00:00
|
|
|
"testing"
|
|
|
|
|
2020-10-29 23:41:12 +00:00
|
|
|
"github.com/blang/semver"
|
2024-03-04 21:54:05 +00:00
|
|
|
"github.com/hashicorp/hcl/v2"
|
2020-08-05 23:27:17 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
2023-05-23 13:50:23 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
2024-03-04 21:54:05 +00:00
|
|
|
"google.golang.org/grpc"
|
|
|
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
2023-01-11 22:17:14 +00:00
|
|
|
"gopkg.in/yaml.v3"
|
2024-03-04 21:54:05 +00:00
|
|
|
|
|
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/testing/utils"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
|
|
|
|
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
|
2020-08-05 23:27:17 +00:00
|
|
|
)
|
|
|
|
|
2020-09-21 19:23:04 +00:00
|
|
|
func readSchemaFile(file string) (pkgSpec PackageSpec) {
|
2020-08-05 23:27:17 +00:00
|
|
|
// Read in, decode, and import the schema.
|
2023-01-06 22:39:16 +00:00
|
|
|
schemaBytes, err := os.ReadFile(filepath.Join("..", "testing", "test", "testdata", file))
|
2020-08-05 23:27:17 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2023-01-11 22:17:14 +00:00
|
|
|
if strings.HasSuffix(file, ".json") {
|
|
|
|
if err = json.Unmarshal(schemaBytes, &pkgSpec); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
} else if strings.HasSuffix(file, ".yaml") || strings.HasSuffix(file, ".yml") {
|
|
|
|
if err = yaml.Unmarshal(schemaBytes, &pkgSpec); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
} else {
|
2023-12-12 12:19:42 +00:00
|
|
|
panic("unknown schema file extension while parsing " + file)
|
2020-08-05 23:27:17 +00:00
|
|
|
}
|
|
|
|
|
2020-09-21 19:23:04 +00:00
|
|
|
return pkgSpec
|
|
|
|
}
|
|
|
|
|
2023-05-23 13:50:23 +00:00
|
|
|
func TestRoundtripRemoteTypeRef(t *testing.T) {
|
|
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/13000
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
testdataPath := filepath.Join("..", "testing", "test", "testdata")
|
|
|
|
loader := NewPluginLoader(utils.NewHost(testdataPath))
|
|
|
|
pkgSpec := readSchemaFile("remoteref-1.0.0.json")
|
|
|
|
pkg, diags, err := BindSpec(pkgSpec, loader)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, diags)
|
|
|
|
newSpec, err := pkg.MarshalSpec()
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, newSpec)
|
2023-08-08 17:20:56 +00:00
|
|
|
|
|
|
|
// Try and bind again
|
|
|
|
_, diags, err = BindSpec(*newSpec, loader)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, diags)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRoundtripLocalTypeRef(t *testing.T) {
|
|
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/13671
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
testdataPath := filepath.Join("..", "testing", "test", "testdata")
|
|
|
|
loader := NewPluginLoader(utils.NewHost(testdataPath))
|
|
|
|
pkgSpec := readSchemaFile("localref-1.0.0.json")
|
|
|
|
pkg, diags, err := BindSpec(pkgSpec, loader)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, diags)
|
|
|
|
newSpec, err := pkg.MarshalSpec()
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, newSpec)
|
2023-05-23 13:50:23 +00:00
|
|
|
|
|
|
|
// Try and bind again
|
|
|
|
_, diags, err = BindSpec(*newSpec, loader)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, diags)
|
|
|
|
}
|
|
|
|
|
2023-09-12 18:34:40 +00:00
|
|
|
func TestRoundtripEnum(t *testing.T) {
|
|
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/13921
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
assertEnum := func(t *testing.T, pkg *Package) {
|
|
|
|
typ, ok := pkg.GetType("enum:index:Color")
|
|
|
|
assert.True(t, ok)
|
|
|
|
enum, ok := typ.(*EnumType)
|
|
|
|
assert.True(t, ok)
|
|
|
|
assert.Equal(t, "An enum representing a color", enum.Comment)
|
|
|
|
assert.ElementsMatch(t, []*Enum{
|
|
|
|
{Value: "red"},
|
|
|
|
{Value: "green"},
|
|
|
|
{Value: "blue"},
|
|
|
|
}, enum.Elements)
|
|
|
|
}
|
|
|
|
|
|
|
|
testdataPath := filepath.Join("..", "testing", "test", "testdata")
|
|
|
|
loader := NewPluginLoader(utils.NewHost(testdataPath))
|
|
|
|
pkgSpec := readSchemaFile("enum-1.0.0.json")
|
|
|
|
pkg, diags, err := BindSpec(pkgSpec, loader)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, diags)
|
|
|
|
assertEnum(t, pkg)
|
|
|
|
|
|
|
|
newSpec, err := pkg.MarshalSpec()
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, newSpec)
|
|
|
|
|
|
|
|
// Try and bind again
|
|
|
|
pkg, diags, err = BindSpec(*newSpec, loader)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, diags)
|
|
|
|
assertEnum(t, pkg)
|
|
|
|
}
|
|
|
|
|
2023-11-27 11:05:03 +00:00
|
|
|
func TestRoundtripPlainProperties(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
2023-12-01 19:41:51 +00:00
|
|
|
assertPlainnessFromType := func(t *testing.T, pkg *Package) {
|
2023-11-27 11:05:03 +00:00
|
|
|
exampleType, ok := pkg.GetType("plain-properties:index:ExampleType")
|
|
|
|
assert.True(t, ok)
|
|
|
|
exampleObjectType, ok := exampleType.(*ObjectType)
|
|
|
|
assert.True(t, ok)
|
2023-12-01 19:41:51 +00:00
|
|
|
|
|
|
|
assert.Equal(t, 2, len(exampleObjectType.Properties))
|
|
|
|
var exampleProperty *Property
|
|
|
|
var nonPlainProperty *Property
|
|
|
|
for _, p := range exampleObjectType.Properties {
|
|
|
|
if p.Name == "exampleProperty" {
|
|
|
|
exampleProperty = p
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.Name == "nonPlainProperty" {
|
|
|
|
nonPlainProperty = p
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.NotNil(t, exampleProperty)
|
|
|
|
assert.NotNil(t, nonPlainProperty)
|
|
|
|
|
|
|
|
assert.True(t, exampleProperty.Plain)
|
|
|
|
assert.False(t, nonPlainProperty.Plain)
|
2023-11-27 11:05:03 +00:00
|
|
|
}
|
|
|
|
|
2023-12-01 19:41:51 +00:00
|
|
|
assertPlainnessFromResource := func(t *testing.T, pkg *Package) {
|
2023-11-27 11:05:03 +00:00
|
|
|
exampleResource, ok := pkg.GetResource("plain-properties:index:ExampleResource")
|
|
|
|
assert.True(t, ok)
|
2023-12-01 19:41:51 +00:00
|
|
|
|
|
|
|
check := func(properties []*Property) {
|
|
|
|
var exampleProperty *Property
|
|
|
|
var nonPlainProperty *Property
|
|
|
|
for _, p := range exampleResource.InputProperties {
|
|
|
|
if p.Name == "exampleProperty" {
|
|
|
|
exampleProperty = p
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.Name == "nonPlainProperty" {
|
|
|
|
nonPlainProperty = p
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// assert that the input property "exampleProperty" is plain
|
|
|
|
assert.NotNil(t, exampleProperty)
|
|
|
|
assert.True(t, exampleProperty.Plain)
|
|
|
|
|
|
|
|
// assert that the output property is not plain
|
|
|
|
assert.NotNil(t, nonPlainProperty)
|
|
|
|
assert.False(t, nonPlainProperty.Plain)
|
|
|
|
}
|
|
|
|
|
|
|
|
check(exampleResource.InputProperties)
|
|
|
|
check(exampleResource.Properties)
|
2023-11-27 11:05:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
testdataPath := filepath.Join("..", "testing", "test", "testdata")
|
|
|
|
loader := NewPluginLoader(utils.NewHost(testdataPath))
|
|
|
|
pkgSpec := readSchemaFile("plain-properties-1.0.0.json")
|
|
|
|
pkg, diags, err := BindSpec(pkgSpec, loader)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, diags)
|
2023-12-01 19:41:51 +00:00
|
|
|
assertPlainnessFromType(t, pkg)
|
|
|
|
assertPlainnessFromResource(t, pkg)
|
2023-11-27 11:05:03 +00:00
|
|
|
|
|
|
|
newSpec, err := pkg.MarshalSpec()
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, newSpec)
|
|
|
|
|
|
|
|
// Try and bind again
|
|
|
|
pkg, diags, err = BindSpec(*newSpec, loader)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, diags)
|
2023-12-01 19:41:51 +00:00
|
|
|
assertPlainnessFromType(t, pkg)
|
|
|
|
assertPlainnessFromResource(t, pkg)
|
2023-11-27 11:05:03 +00:00
|
|
|
}
|
|
|
|
|
2020-09-21 19:23:04 +00:00
|
|
|
func TestImportSpec(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-09-21 19:23:04 +00:00
|
|
|
// Read in, decode, and import the schema.
|
2022-10-11 12:56:29 +00:00
|
|
|
pkgSpec := readSchemaFile("kubernetes-3.7.2.json")
|
2020-09-21 19:23:04 +00:00
|
|
|
|
2020-08-05 23:27:17 +00:00
|
|
|
pkg, err := ImportSpec(pkgSpec, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("ImportSpec() error = %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, r := range pkg.Resources {
|
2022-12-12 14:55:35 +00:00
|
|
|
assert.NotNil(t, r.PackageReference, "expected resource %s to have an associated Package", r.Token)
|
2020-08-05 23:27:17 +00:00
|
|
|
}
|
|
|
|
}
|
2020-09-21 19:23:04 +00:00
|
|
|
|
|
|
|
var enumTests = []struct {
|
|
|
|
filename string
|
|
|
|
shouldError bool
|
|
|
|
expected *EnumType
|
|
|
|
}{
|
|
|
|
{"bad-enum-1.json", true, nil},
|
|
|
|
{"bad-enum-2.json", true, nil},
|
|
|
|
{"bad-enum-3.json", true, nil},
|
|
|
|
{"bad-enum-4.json", true, nil},
|
|
|
|
{"good-enum-1.json", false, &EnumType{
|
|
|
|
Token: "fake-provider:module1:Color",
|
|
|
|
ElementType: stringType,
|
|
|
|
Elements: []*Enum{
|
|
|
|
{Value: "Red"},
|
|
|
|
{Value: "Orange"},
|
|
|
|
{Value: "Yellow"},
|
|
|
|
{Value: "Green"},
|
|
|
|
},
|
|
|
|
}},
|
|
|
|
{"good-enum-2.json", false, &EnumType{
|
|
|
|
Token: "fake-provider:module1:Number",
|
|
|
|
ElementType: intType,
|
|
|
|
Elements: []*Enum{
|
|
|
|
{Value: int32(1), Name: "One"},
|
|
|
|
{Value: int32(2), Name: "Two"},
|
|
|
|
{Value: int32(3), Name: "Three"},
|
|
|
|
{Value: int32(6), Name: "Six"},
|
|
|
|
},
|
|
|
|
}},
|
|
|
|
{"good-enum-3.json", false, &EnumType{
|
|
|
|
Token: "fake-provider:module1:Boolean",
|
|
|
|
ElementType: boolType,
|
|
|
|
Elements: []*Enum{
|
|
|
|
{Value: true, Name: "One"},
|
|
|
|
{Value: false, Name: "Zero"},
|
|
|
|
},
|
|
|
|
}},
|
|
|
|
{"good-enum-4.json", false, &EnumType{
|
|
|
|
Token: "fake-provider:module1:Number2",
|
|
|
|
ElementType: numberType,
|
|
|
|
Comment: "what a great description",
|
|
|
|
Elements: []*Enum{
|
|
|
|
{Value: float64(1), Comment: "one", Name: "One"},
|
|
|
|
{Value: float64(2), Comment: "two", Name: "Two"},
|
|
|
|
{Value: 3.4, Comment: "3.4", Name: "ThreePointFour"},
|
|
|
|
{Value: float64(6), Comment: "six", Name: "Six"},
|
|
|
|
},
|
|
|
|
}},
|
|
|
|
}
|
|
|
|
|
2023-01-11 22:17:14 +00:00
|
|
|
func TestUnmarshalYAMLFunctionSpec(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
var functionSpec *FunctionSpec
|
|
|
|
fnYaml := `
|
|
|
|
description: Test function
|
|
|
|
outputs:
|
|
|
|
type: number`
|
|
|
|
|
|
|
|
err := yaml.Unmarshal([]byte(fnYaml), &functionSpec)
|
|
|
|
assert.Nil(t, err, "Unmarshalling should work")
|
|
|
|
assert.Equal(t, "Test function", functionSpec.Description)
|
|
|
|
assert.NotNil(t, functionSpec.ReturnType, "Return type is not nil")
|
|
|
|
assert.NotNil(t, functionSpec.ReturnType.TypeSpec, "Return type is a type spec")
|
|
|
|
assert.Equal(t, "number", functionSpec.ReturnType.TypeSpec.Type, "Return type is a number")
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUnmarshalJSONFunctionSpec(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
var functionSpec *FunctionSpec
|
|
|
|
fnJSON := `{"description":"Test function", "outputs": { "type": "number" } }`
|
|
|
|
err := json.Unmarshal([]byte(fnJSON), &functionSpec)
|
|
|
|
assert.Nil(t, err, "Unmarshalling should work")
|
|
|
|
assert.Equal(t, "Test function", functionSpec.Description)
|
|
|
|
assert.NotNil(t, functionSpec.ReturnType, "Return type is not nil")
|
|
|
|
assert.NotNil(t, functionSpec.ReturnType.TypeSpec, "Return type is a type spec")
|
|
|
|
assert.Equal(t, "number", functionSpec.ReturnType.TypeSpec.Type, "Return type is a number")
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestMarshalJSONFunctionSpec(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
functionSpec := &FunctionSpec{
|
|
|
|
Description: "Test function",
|
|
|
|
ReturnType: &ReturnTypeSpec{
|
|
|
|
TypeSpec: &TypeSpec{Type: "number"},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
dataJSON, err := json.Marshal(functionSpec)
|
|
|
|
data := string(dataJSON)
|
|
|
|
expectedJSON := `{"description":"Test function","outputs":{"type":"number"}}`
|
|
|
|
assert.Nil(t, err, "Unmarshalling should work")
|
|
|
|
assert.Equal(t, expectedJSON, data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestMarshalJSONFunctionSpecWithOutputs(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
functionSpec := &FunctionSpec{
|
|
|
|
Description: "Test function",
|
|
|
|
Outputs: &ObjectTypeSpec{
|
|
|
|
Type: "object",
|
|
|
|
Properties: map[string]PropertySpec{
|
|
|
|
"foo": {
|
|
|
|
TypeSpec: TypeSpec{Type: "string"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
dataJSON, err := json.Marshal(functionSpec)
|
|
|
|
data := string(dataJSON)
|
|
|
|
expectedJSON := `{"description":"Test function","outputs":{"properties":{"foo":{"type":"string"}},"type":"object"}}`
|
|
|
|
assert.Nil(t, err, "Unmarshalling should work")
|
|
|
|
assert.Equal(t, expectedJSON, data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestMarshalYAMLFunctionSpec(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
functionSpec := &FunctionSpec{
|
|
|
|
Description: "Test function",
|
|
|
|
ReturnType: &ReturnTypeSpec{
|
|
|
|
TypeSpec: &TypeSpec{Type: "number"},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
dataYAML, err := yaml.Marshal(functionSpec)
|
|
|
|
data := string(dataYAML)
|
|
|
|
expectedYAML := `description: Test function
|
|
|
|
outputs:
|
|
|
|
type: number
|
|
|
|
`
|
|
|
|
|
|
|
|
assert.Nil(t, err, "Unmarshalling should work")
|
|
|
|
assert.Equal(t, expectedYAML, data)
|
|
|
|
}
|
|
|
|
|
2023-01-24 13:43:26 +00:00
|
|
|
func TestInvalidTypes(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
2023-03-03 16:36:39 +00:00
|
|
|
tests := []struct {
|
2023-01-24 13:43:26 +00:00
|
|
|
filename string
|
|
|
|
expected string
|
|
|
|
}{
|
|
|
|
{"bad-type-1.json", "invalid token 'fake-provider:index:provider' (provider is a reserved word for the root module)"},
|
|
|
|
{"bad-type-2.json", "invalid token 'fake-provider::provider' (provider is a reserved word for the root module)"},
|
|
|
|
{"bad-type-3.json", "invalid token 'fake-provider:noModulePart' (should have three parts)"},
|
|
|
|
{"bad-type-4.json", "invalid token 'noParts' (should have three parts); "},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
tt := tt
|
|
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
pkgSpec := readSchemaFile(filepath.Join("schema", tt.filename))
|
|
|
|
|
|
|
|
_, err := ImportSpec(pkgSpec, nil)
|
2023-12-15 17:45:32 +00:00
|
|
|
assert.ErrorContains(t, err, tt.expected)
|
2023-01-24 13:43:26 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-21 19:23:04 +00:00
|
|
|
func TestEnums(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-09-21 19:23:04 +00:00
|
|
|
for _, tt := range enumTests {
|
2022-03-04 08:17:41 +00:00
|
|
|
tt := tt
|
2020-09-21 19:23:04 +00:00
|
|
|
t.Run(tt.filename, func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-09-21 19:23:04 +00:00
|
|
|
pkgSpec := readSchemaFile(filepath.Join("schema", tt.filename))
|
|
|
|
|
|
|
|
pkg, err := ImportSpec(pkgSpec, nil)
|
|
|
|
if tt.shouldError {
|
|
|
|
assert.Error(t, err)
|
|
|
|
} else {
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
result := pkg.Types[0]
|
2022-06-23 23:27:44 +00:00
|
|
|
tt.expected.PackageReference = pkg.Reference()
|
2020-09-21 19:23:04 +00:00
|
|
|
assert.Equal(t, tt.expected, result)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-09-22 17:09:27 +00:00
|
|
|
|
2023-10-14 08:32:43 +00:00
|
|
|
//nolint:paralleltest // needs to set plugin acquisition env var
|
2020-09-22 17:09:27 +00:00
|
|
|
func TestImportResourceRef(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
schemaFile string
|
|
|
|
wantErr bool
|
|
|
|
validator func(pkg *Package)
|
|
|
|
}{
|
|
|
|
{
|
2020-10-29 23:41:12 +00:00
|
|
|
"simple",
|
2020-09-29 23:00:03 +00:00
|
|
|
"simple-resource-schema/schema.json",
|
2020-09-22 17:09:27 +00:00
|
|
|
false,
|
|
|
|
func(pkg *Package) {
|
|
|
|
for _, r := range pkg.Resources {
|
|
|
|
if r.Token == "example::OtherResource" {
|
|
|
|
for _, p := range r.Properties {
|
|
|
|
if p.Name == "foo" {
|
2021-06-24 16:17:55 +00:00
|
|
|
assert.IsType(t, &ResourceType{}, plainType(p.Type))
|
2020-09-22 17:09:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
2020-10-29 23:41:12 +00:00
|
|
|
{
|
|
|
|
"external-ref",
|
|
|
|
"external-resource-schema/schema.json",
|
|
|
|
false,
|
|
|
|
func(pkg *Package) {
|
2020-11-19 20:56:28 +00:00
|
|
|
typ, ok := pkg.GetType("example::Pet")
|
|
|
|
assert.True(t, ok)
|
|
|
|
pet, ok := typ.(*ObjectType)
|
|
|
|
assert.True(t, ok)
|
|
|
|
name, ok := pet.Property("name")
|
|
|
|
assert.True(t, ok)
|
2021-06-24 16:17:55 +00:00
|
|
|
assert.IsType(t, &ResourceType{}, plainType(name.Type))
|
|
|
|
resource := plainType(name.Type).(*ResourceType)
|
2020-11-19 20:56:28 +00:00
|
|
|
assert.NotNil(t, resource.Resource)
|
|
|
|
|
2020-10-29 23:41:12 +00:00
|
|
|
for _, r := range pkg.Resources {
|
|
|
|
switch r.Token {
|
|
|
|
case "example::Cat":
|
|
|
|
for _, p := range r.Properties {
|
|
|
|
if p.Name == "name" {
|
2021-06-24 16:17:55 +00:00
|
|
|
assert.IsType(t, stringType, plainType(p.Type))
|
2020-10-29 23:41:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
case "example::Workload":
|
|
|
|
for _, p := range r.Properties {
|
|
|
|
if p.Name == "pod" {
|
2021-06-24 16:17:55 +00:00
|
|
|
assert.IsType(t, &ObjectType{}, plainType(p.Type))
|
2020-10-29 23:41:12 +00:00
|
|
|
|
2021-06-24 16:17:55 +00:00
|
|
|
obj := plainType(p.Type).(*ObjectType)
|
2020-10-29 23:41:12 +00:00
|
|
|
assert.NotNil(t, obj.Properties)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
2020-09-22 17:09:27 +00:00
|
|
|
}
|
2023-10-14 08:32:43 +00:00
|
|
|
//nolint:paralleltest // needs to set plugin acquisition env var
|
2020-09-22 17:09:27 +00:00
|
|
|
for _, tt := range tests {
|
2022-03-04 08:17:41 +00:00
|
|
|
tt := tt
|
2020-09-22 17:09:27 +00:00
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2023-10-14 08:32:43 +00:00
|
|
|
t.Setenv("PULUMI_DISABLE_AUTOMATIC_PLUGIN_ACQUISITION", "false")
|
2022-03-04 08:17:41 +00:00
|
|
|
|
2020-09-22 17:09:27 +00:00
|
|
|
// Read in, decode, and import the schema.
|
2023-01-06 22:39:16 +00:00
|
|
|
schemaBytes, err := os.ReadFile(
|
2022-02-07 11:10:04 +00:00
|
|
|
filepath.Join("..", "testing", "test", "testdata", tt.schemaFile))
|
2020-09-22 17:09:27 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
var pkgSpec PackageSpec
|
|
|
|
err = json.Unmarshal(schemaBytes, &pkgSpec)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
pkg, err := ImportSpec(pkgSpec, nil)
|
|
|
|
if (err != nil) != tt.wantErr {
|
|
|
|
t.Errorf("ImportSpec() error = %v, wantErr %v", err, tt.wantErr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
tt.validator(pkg)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-10-29 23:41:12 +00:00
|
|
|
|
|
|
|
func Test_parseTypeSpecRef(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-10-29 23:41:12 +00:00
|
|
|
toVersionPtr := func(version string) *semver.Version { v := semver.MustParse(version); return &v }
|
|
|
|
toURL := func(rawurl string) *url.URL {
|
|
|
|
parsed, err := url.Parse(rawurl)
|
|
|
|
assert.NoError(t, err, "failed to parse ref")
|
|
|
|
|
|
|
|
return parsed
|
|
|
|
}
|
|
|
|
|
2020-11-20 17:16:45 +00:00
|
|
|
typs := &types{
|
|
|
|
pkg: &Package{
|
|
|
|
Name: "test",
|
|
|
|
Version: toVersionPtr("1.2.3"),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2020-10-29 23:41:12 +00:00
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
ref string
|
|
|
|
want typeSpecRef
|
|
|
|
wantErr bool
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "resourceRef",
|
|
|
|
ref: "#/resources/example::Resource",
|
|
|
|
want: typeSpecRef{
|
2020-11-20 17:16:45 +00:00
|
|
|
URL: toURL("#/resources/example::Resource"),
|
|
|
|
Package: "test",
|
|
|
|
Version: toVersionPtr("1.2.3"),
|
|
|
|
Kind: "resources",
|
|
|
|
Token: "example::Resource",
|
2020-10-29 23:41:12 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "typeRef",
|
2020-11-20 17:16:45 +00:00
|
|
|
ref: "#/types/kubernetes:admissionregistration.k8s.io%2fv1:WebhookClientConfig",
|
2020-10-29 23:41:12 +00:00
|
|
|
want: typeSpecRef{
|
2020-11-20 17:16:45 +00:00
|
|
|
URL: toURL("#/types/kubernetes:admissionregistration.k8s.io%2fv1:WebhookClientConfig"),
|
|
|
|
Package: "test",
|
|
|
|
Version: toVersionPtr("1.2.3"),
|
|
|
|
Kind: "types",
|
|
|
|
Token: "kubernetes:admissionregistration.k8s.io/v1:WebhookClientConfig",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "providerRef",
|
|
|
|
ref: "#/provider",
|
|
|
|
want: typeSpecRef{
|
|
|
|
URL: toURL("#/provider"),
|
|
|
|
Package: "test",
|
|
|
|
Version: toVersionPtr("1.2.3"),
|
|
|
|
Kind: "provider",
|
|
|
|
Token: "pulumi:providers:test",
|
2020-10-29 23:41:12 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "externalResourceRef",
|
2020-11-20 17:16:45 +00:00
|
|
|
ref: "/random/v2.3.1/schema.json#/resources/random:index%2frandomPet:RandomPet",
|
2020-10-29 23:41:12 +00:00
|
|
|
want: typeSpecRef{
|
2020-11-20 17:16:45 +00:00
|
|
|
URL: toURL("/random/v2.3.1/schema.json#/resources/random:index%2frandomPet:RandomPet"),
|
|
|
|
Package: "random",
|
|
|
|
Version: toVersionPtr("2.3.1"),
|
|
|
|
Kind: "resources",
|
|
|
|
Token: "random:index/randomPet:RandomPet",
|
2020-10-29 23:41:12 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "invalid externalResourceRef",
|
2020-11-20 17:16:45 +00:00
|
|
|
ref: "/random/schema.json#/resources/random:index%2frandomPet:RandomPet",
|
2020-10-29 23:41:12 +00:00
|
|
|
wantErr: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "externalTypeRef",
|
2020-11-20 17:16:45 +00:00
|
|
|
ref: "/kubernetes/v2.6.3/schema.json#/types/kubernetes:admissionregistration.k8s.io%2Fv1:WebhookClientConfig",
|
2020-10-29 23:41:12 +00:00
|
|
|
want: typeSpecRef{
|
2020-11-20 17:16:45 +00:00
|
|
|
URL: toURL("/kubernetes/v2.6.3/schema.json#/types/kubernetes:admissionregistration.k8s.io%2Fv1:WebhookClientConfig"),
|
|
|
|
Package: "kubernetes",
|
|
|
|
Version: toVersionPtr("2.6.3"),
|
|
|
|
Kind: "types",
|
|
|
|
Token: "kubernetes:admissionregistration.k8s.io/v1:WebhookClientConfig",
|
2020-10-29 23:41:12 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "externalHostResourceRef",
|
2020-11-20 17:16:45 +00:00
|
|
|
ref: "https://example.com/random/v2.3.1/schema.json#/resources/random:index%2FrandomPet:RandomPet",
|
|
|
|
want: typeSpecRef{
|
|
|
|
URL: toURL("https://example.com/random/v2.3.1/schema.json#/resources/random:index%2FrandomPet:RandomPet"),
|
|
|
|
Package: "random",
|
|
|
|
Version: toVersionPtr("2.3.1"),
|
|
|
|
Kind: "resources",
|
|
|
|
Token: "random:index/randomPet:RandomPet",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "externalProviderRef",
|
|
|
|
ref: "/kubernetes/v2.6.3/schema.json#/provider",
|
2020-10-29 23:41:12 +00:00
|
|
|
want: typeSpecRef{
|
2020-11-20 17:16:45 +00:00
|
|
|
URL: toURL("/kubernetes/v2.6.3/schema.json#/provider"),
|
|
|
|
Package: "kubernetes",
|
|
|
|
Version: toVersionPtr("2.6.3"),
|
|
|
|
Kind: "provider",
|
|
|
|
Token: "pulumi:providers:kubernetes",
|
2020-10-29 23:41:12 +00:00
|
|
|
},
|
|
|
|
},
|
2021-08-23 21:23:01 +00:00
|
|
|
{
|
|
|
|
name: "hyphenatedUrlPath",
|
|
|
|
ref: "/azure-native/v1.22.0/schema.json#/resources/azure-native:web:WebApp",
|
|
|
|
want: typeSpecRef{
|
|
|
|
URL: toURL("/azure-native/v1.22.0/schema.json#/resources/azure-native:web:WebApp"),
|
|
|
|
Package: "azure-native",
|
|
|
|
Version: toVersionPtr("1.22.0"),
|
|
|
|
Kind: "resources",
|
|
|
|
Token: "azure-native:web:WebApp",
|
|
|
|
},
|
|
|
|
},
|
2020-10-29 23:41:12 +00:00
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
2022-03-04 08:17:41 +00:00
|
|
|
tt := tt
|
2020-10-29 23:41:12 +00:00
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2021-08-31 02:29:24 +00:00
|
|
|
got, diags := typs.parseTypeSpecRef("ref", tt.ref)
|
|
|
|
if diags.HasErrors() != tt.wantErr {
|
|
|
|
t.Errorf("parseTypeSpecRef() diags = %v, wantErr %v", diags, tt.wantErr)
|
2020-10-29 23:41:12 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
|
|
t.Errorf("parseTypeSpecRef() got = %v, want %v", got, tt.want)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-06-10 16:47:25 +00:00
|
|
|
|
2024-01-18 00:35:51 +00:00
|
|
|
func TestUsingUrnInResourcePropertiesEmitsWarning(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
loader := NewPluginLoader(utils.NewHost(testdataPath))
|
|
|
|
pkgSpec := PackageSpec{
|
|
|
|
Name: "test",
|
|
|
|
Version: "1.0.0",
|
|
|
|
Resources: map[string]ResourceSpec{
|
|
|
|
"test:index:TestResource": {
|
|
|
|
ObjectTypeSpec: ObjectTypeSpec{
|
|
|
|
Properties: map[string]PropertySpec{
|
|
|
|
"urn": {
|
|
|
|
TypeSpec: TypeSpec{
|
|
|
|
Type: "string",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"test:index:TestComponent": {
|
|
|
|
IsComponent: true,
|
|
|
|
ObjectTypeSpec: ObjectTypeSpec{
|
|
|
|
Properties: map[string]PropertySpec{
|
|
|
|
"urn": {
|
|
|
|
TypeSpec: TypeSpec{
|
|
|
|
Type: "string",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
pkg, diags, err := BindSpec(pkgSpec, loader)
|
|
|
|
// No error as binding should work fine even with warnings
|
|
|
|
assert.NoError(t, err)
|
|
|
|
// assert that there are 2 warnings in the diagnostics because of using URN as a property
|
|
|
|
assert.Len(t, diags, 2)
|
|
|
|
for _, diag := range diags {
|
|
|
|
assert.Equal(t, diag.Severity, hcl.DiagWarning)
|
|
|
|
assert.Contains(t, diag.Summary, "urn is a reserved property name")
|
|
|
|
}
|
|
|
|
assert.NotNil(t, pkg)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUsingIdInResourcePropertiesEmitsWarning(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
loader := NewPluginLoader(utils.NewHost(testdataPath))
|
|
|
|
pkgSpec := PackageSpec{
|
|
|
|
Name: "test",
|
|
|
|
Version: "1.0.0",
|
|
|
|
Resources: map[string]ResourceSpec{
|
|
|
|
"test:index:TestResource": {
|
|
|
|
ObjectTypeSpec: ObjectTypeSpec{
|
|
|
|
Properties: map[string]PropertySpec{
|
|
|
|
"id": {
|
|
|
|
TypeSpec: TypeSpec{
|
|
|
|
Type: "string",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
pkg, diags, err := BindSpec(pkgSpec, loader)
|
|
|
|
// No error as binding should work fine even with warnings
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.NotNil(t, pkg)
|
|
|
|
// assert that there is 1 warning in the diagnostics because of using ID as a property
|
|
|
|
assert.Len(t, diags, 1)
|
|
|
|
assert.Equal(t, diags[0].Severity, hcl.DiagWarning)
|
|
|
|
assert.Contains(t, diags[0].Summary, "id is a reserved property name")
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUsingIdInComponentResourcePropertiesEmitsNoWarning(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
loader := NewPluginLoader(utils.NewHost(testdataPath))
|
|
|
|
pkgSpec := PackageSpec{
|
|
|
|
Name: "test",
|
|
|
|
Version: "1.0.0",
|
|
|
|
Resources: map[string]ResourceSpec{
|
|
|
|
"test:index:TestComponent": {
|
|
|
|
IsComponent: true,
|
|
|
|
ObjectTypeSpec: ObjectTypeSpec{
|
|
|
|
Properties: map[string]PropertySpec{
|
|
|
|
"id": {
|
|
|
|
TypeSpec: TypeSpec{
|
|
|
|
Type: "string",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
pkg, diags, err := BindSpec(pkgSpec, loader)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Empty(t, diags)
|
|
|
|
assert.NotNil(t, pkg)
|
|
|
|
}
|
|
|
|
|
2021-06-10 16:47:25 +00:00
|
|
|
func TestMethods(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2023-03-03 16:36:39 +00:00
|
|
|
tests := []struct {
|
2021-06-10 16:47:25 +00:00
|
|
|
filename string
|
|
|
|
validator func(pkg *Package)
|
|
|
|
expectedError string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
filename: "good-methods-1.json",
|
|
|
|
validator: func(pkg *Package) {
|
|
|
|
assert.Len(t, pkg.Resources, 1)
|
|
|
|
assert.Len(t, pkg.Resources[0].Methods, 1)
|
|
|
|
|
|
|
|
assert.NotNil(t, pkg.Resources[0].Methods[0].Function.Inputs)
|
|
|
|
assert.Len(t, pkg.Resources[0].Methods[0].Function.Inputs.Properties, 1)
|
|
|
|
inputs := pkg.Resources[0].Methods[0].Function.Inputs.Properties
|
|
|
|
assert.Equal(t, "__self__", inputs[0].Name)
|
|
|
|
assert.Equal(t, &ResourceType{
|
|
|
|
Token: pkg.Resources[0].Token,
|
|
|
|
Resource: pkg.Resources[0],
|
|
|
|
}, inputs[0].Type)
|
|
|
|
|
2023-01-11 22:17:14 +00:00
|
|
|
var objectReturnType *ObjectType
|
|
|
|
if objectType, ok := pkg.Resources[0].Methods[0].Function.ReturnType.(*ObjectType); ok && objectType != nil {
|
|
|
|
objectReturnType = objectType
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.NotNil(t, objectReturnType)
|
|
|
|
assert.Len(t, objectReturnType.Properties, 1)
|
|
|
|
outputs := objectReturnType.Properties
|
2021-06-10 16:47:25 +00:00
|
|
|
assert.Equal(t, "someValue", outputs[0].Name)
|
|
|
|
assert.Equal(t, StringType, outputs[0].Type)
|
|
|
|
|
|
|
|
assert.Len(t, pkg.Functions, 1)
|
|
|
|
assert.True(t, pkg.Functions[0].IsMethod)
|
|
|
|
assert.Same(t, pkg.Resources[0].Methods[0].Function, pkg.Functions[0])
|
|
|
|
},
|
|
|
|
},
|
2023-01-11 22:17:14 +00:00
|
|
|
{
|
|
|
|
filename: "good-simplified-methods.json",
|
|
|
|
validator: func(pkg *Package) {
|
|
|
|
assert.Len(t, pkg.Functions, 1)
|
|
|
|
assert.NotNil(t, pkg.Functions[0].ReturnType, "There should be a return type")
|
|
|
|
assert.Equal(t, pkg.Functions[0].ReturnType, NumberType)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
filename: "good-simplified-methods.yml",
|
|
|
|
validator: func(pkg *Package) {
|
|
|
|
assert.Len(t, pkg.Functions, 1)
|
|
|
|
assert.NotNil(t, pkg.Functions[0].ReturnType, "There should be a return type")
|
|
|
|
assert.Equal(t, pkg.Functions[0].ReturnType, NumberType)
|
|
|
|
},
|
|
|
|
},
|
2021-06-10 16:47:25 +00:00
|
|
|
{
|
|
|
|
filename: "bad-methods-1.json",
|
2021-08-31 02:29:24 +00:00
|
|
|
expectedError: "unknown function xyz:index:Foo/bar",
|
2021-06-10 16:47:25 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
filename: "bad-methods-2.json",
|
2021-08-31 02:29:24 +00:00
|
|
|
expectedError: "function xyz:index:Foo/bar is already a method",
|
2021-06-10 16:47:25 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
filename: "bad-methods-3.json",
|
2021-08-31 02:29:24 +00:00
|
|
|
expectedError: "invalid function token format xyz:index:Foo",
|
2021-06-10 16:47:25 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
filename: "bad-methods-4.json",
|
2021-08-31 02:29:24 +00:00
|
|
|
expectedError: "invalid function token format xyz:index:Baz/bar",
|
2021-06-10 16:47:25 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
filename: "bad-methods-5.json",
|
2021-08-31 02:29:24 +00:00
|
|
|
expectedError: "function xyz:index:Foo/bar has no __self__ parameter",
|
2021-06-10 16:47:25 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
filename: "bad-methods-6.json",
|
2021-08-31 02:29:24 +00:00
|
|
|
expectedError: "xyz:index:Foo already has a property named bar",
|
2021-06-10 16:47:25 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
2022-03-04 08:17:41 +00:00
|
|
|
tt := tt
|
2021-06-10 16:47:25 +00:00
|
|
|
t.Run(tt.filename, func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2021-06-10 16:47:25 +00:00
|
|
|
pkgSpec := readSchemaFile(filepath.Join("schema", tt.filename))
|
|
|
|
|
|
|
|
pkg, err := ImportSpec(pkgSpec, nil)
|
|
|
|
if tt.expectedError != "" {
|
2023-12-15 17:45:32 +00:00
|
|
|
assert.ErrorContains(t, err, tt.expectedError)
|
2021-06-10 16:47:25 +00:00
|
|
|
} else {
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
tt.validator(pkg)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-09-08 05:23:30 +00:00
|
|
|
|
2021-11-12 00:00:03 +00:00
|
|
|
// TestIsOverlay tests that the IsOverlay field is set correctly for resources, types, and functions. Does not test
|
|
|
|
// codegen.
|
|
|
|
func TestIsOverlay(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2021-11-12 00:00:03 +00:00
|
|
|
t.Run("overlay", func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2021-11-12 00:00:03 +00:00
|
|
|
pkgSpec := readSchemaFile(filepath.Join("schema", "overlay.json"))
|
|
|
|
|
|
|
|
pkg, err := ImportSpec(pkgSpec, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
for _, v := range pkg.Resources {
|
|
|
|
if strings.Contains(v.Token, "Overlay") {
|
|
|
|
assert.Truef(t, v.IsOverlay, "resource %q", v.Token)
|
|
|
|
} else {
|
|
|
|
assert.Falsef(t, v.IsOverlay, "resource %q", v.Token)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, v := range pkg.Types {
|
|
|
|
switch v := v.(type) {
|
|
|
|
case *ObjectType:
|
|
|
|
if strings.Contains(v.Token, "Overlay") {
|
|
|
|
assert.Truef(t, v.IsOverlay, "object type %q", v.Token)
|
|
|
|
} else {
|
|
|
|
assert.Falsef(t, v.IsOverlay, "object type %q", v.Token)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, v := range pkg.Functions {
|
|
|
|
if strings.Contains(v.Token, "Overlay") {
|
|
|
|
assert.Truef(t, v.IsOverlay, "function %q", v.Token)
|
|
|
|
} else {
|
|
|
|
assert.Falsef(t, v.IsOverlay, "function %q", v.Token)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2023-01-11 22:17:14 +00:00
|
|
|
}
|
|
|
|
|
2024-07-09 14:54:50 +00:00
|
|
|
// TestOverlaySupportedLanguages tests that the OverlaySupportedLanguages field is set correctly for resources, types, and functions.
|
|
|
|
// Does not test codegen.
|
|
|
|
func TestOverlaySupportedLanguages(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
t.Run("overlay", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
pkgSpec := readSchemaFile(filepath.Join("schema", "overlay-supported-languages.json"))
|
|
|
|
|
|
|
|
pkg, err := ImportSpec(pkgSpec, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
for _, v := range pkg.Resources {
|
|
|
|
if strings.Contains(v.Token, "Overlay") {
|
|
|
|
assert.Truef(t, v.IsOverlay, "resource %q", v.Token)
|
|
|
|
} else {
|
|
|
|
assert.Falsef(t, v.IsOverlay, "resource %q", v.Token)
|
|
|
|
}
|
|
|
|
if strings.Contains(v.Token, "ConstrainedLanguages") {
|
|
|
|
assert.Equalf(t, []string{"go", "nodejs", "python"}, v.OverlaySupportedLanguages, "resource %q", v.Token)
|
|
|
|
} else {
|
|
|
|
assert.Nilf(t, v.OverlaySupportedLanguages, "resource %q", v.Token)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, v := range pkg.Types {
|
|
|
|
switch v := v.(type) {
|
|
|
|
case *ObjectType:
|
|
|
|
if strings.Contains(v.Token, "Overlay") {
|
|
|
|
assert.Truef(t, v.IsOverlay, "object type %q", v.Token)
|
|
|
|
} else {
|
|
|
|
assert.Falsef(t, v.IsOverlay, "object type %q", v.Token)
|
|
|
|
}
|
|
|
|
assert.Nilf(t, v.OverlaySupportedLanguages, "resource %q", v.Token)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, v := range pkg.Functions {
|
|
|
|
if strings.Contains(v.Token, "Overlay") {
|
|
|
|
assert.Truef(t, v.IsOverlay, "function %q", v.Token)
|
|
|
|
} else {
|
|
|
|
assert.Falsef(t, v.IsOverlay, "function %q", v.Token)
|
|
|
|
}
|
|
|
|
if strings.Contains(v.Token, "ConstrainedLanguages") {
|
|
|
|
assert.Equalf(t, []string{"go", "nodejs", "python"}, v.OverlaySupportedLanguages, "resource %q", v.Token)
|
|
|
|
} else {
|
|
|
|
assert.Nilf(t, v.OverlaySupportedLanguages, "resource %q", v.Token)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-01-11 22:17:14 +00:00
|
|
|
func TestBindingOutputsPopulatesReturnType(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
// Test that using Outputs in PackageSpec correctly populates the return type of the function.
|
|
|
|
pkgSpec := PackageSpec{
|
|
|
|
Name: "xyz",
|
|
|
|
Version: "0.0.1",
|
|
|
|
Functions: map[string]FunctionSpec{
|
|
|
|
"xyz:index:abs": {
|
|
|
|
MultiArgumentInputs: []string{"value"},
|
|
|
|
Inputs: &ObjectTypeSpec{
|
|
|
|
Properties: map[string]PropertySpec{
|
|
|
|
"value": {
|
|
|
|
TypeSpec: TypeSpec{
|
|
|
|
Type: "number",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Outputs: &ObjectTypeSpec{
|
|
|
|
Required: []string{"result"},
|
|
|
|
Properties: map[string]PropertySpec{
|
|
|
|
"result": {
|
|
|
|
TypeSpec: TypeSpec{
|
|
|
|
Type: "number",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
pkg, err := ImportSpec(pkgSpec, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.NotNil(t, pkg.Functions[0].ReturnType)
|
|
|
|
objectType, ok := pkg.Functions[0].ReturnType.(*ObjectType)
|
|
|
|
assert.True(t, ok)
|
|
|
|
assert.Equal(t, NumberType, objectType.Properties[0].Type)
|
2021-11-12 00:00:03 +00:00
|
|
|
}
|
|
|
|
|
2021-09-08 05:23:30 +00:00
|
|
|
// Tests that the method ReplaceOnChanges works as expected. Does not test
|
|
|
|
// codegen.
|
|
|
|
func TestReplaceOnChanges(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2021-09-08 05:23:30 +00:00
|
|
|
for _, tt := range []struct {
|
|
|
|
name string
|
|
|
|
filePath string
|
|
|
|
resource string
|
|
|
|
result []string
|
|
|
|
errors []string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "Simple case",
|
|
|
|
filePath: "replace-on-changes-1.json",
|
|
|
|
resource: "example::Dog",
|
|
|
|
result: []string{"bone"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "No replaceOnChanges",
|
|
|
|
filePath: "replace-on-changes-2.json",
|
|
|
|
resource: "example::Dog",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Mutually Recursive",
|
|
|
|
filePath: "replace-on-changes-3.json",
|
|
|
|
resource: "example::Pets",
|
|
|
|
result: []string{
|
|
|
|
"cat.fish",
|
|
|
|
"dog.bone",
|
|
|
|
"dog.cat.fish",
|
2023-03-03 16:36:39 +00:00
|
|
|
"cat.dog.bone",
|
|
|
|
},
|
2021-09-08 05:23:30 +00:00
|
|
|
errors: []string{
|
|
|
|
"Failed to genereate full `ReplaceOnChanges`: Found recursive object \"cat\"",
|
2023-03-03 16:36:39 +00:00
|
|
|
"Failed to genereate full `ReplaceOnChanges`: Found recursive object \"dog\"",
|
|
|
|
},
|
2021-09-08 05:23:30 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Singularly Recursive",
|
|
|
|
filePath: "replace-on-changes-4.json",
|
|
|
|
resource: "example::Pets",
|
|
|
|
result: []string{"dog.bone"},
|
|
|
|
errors: []string{"Failed to genereate full `ReplaceOnChanges`: Found recursive object \"dog\""},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Drill Correctly",
|
|
|
|
filePath: "replace-on-changes-5.json",
|
|
|
|
resource: "example::Pets",
|
|
|
|
result: []string{"foes.*.color", "friends[*].color", "name", "toy.color"},
|
|
|
|
},
|
2021-09-10 21:56:56 +00:00
|
|
|
{
|
|
|
|
name: "No replace on changes and recursive",
|
|
|
|
filePath: "replace-on-changes-6.json",
|
|
|
|
resource: "example::Child",
|
|
|
|
result: []string{},
|
|
|
|
errors: []string{},
|
|
|
|
},
|
2021-09-08 05:23:30 +00:00
|
|
|
} {
|
2022-03-04 08:17:41 +00:00
|
|
|
tt := tt
|
2021-09-08 05:23:30 +00:00
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2021-09-08 05:23:30 +00:00
|
|
|
// We sort each result before comparison. We don't enforce that the
|
|
|
|
// results have the same order, just the same content.
|
|
|
|
sort.Strings(tt.result)
|
|
|
|
sort.Strings(tt.errors)
|
|
|
|
pkgSpec := readSchemaFile(
|
|
|
|
filepath.Join("schema", tt.filePath))
|
|
|
|
pkg, err := ImportSpec(pkgSpec, nil)
|
|
|
|
assert.NoError(t, err, "Import should be successful")
|
|
|
|
resource, found := pkg.GetResource(tt.resource)
|
|
|
|
assert.True(t, found, "The resource should exist")
|
|
|
|
replaceOnChanges, errListErrors := resource.ReplaceOnChanges()
|
|
|
|
errList := make([]string, len(errListErrors))
|
|
|
|
for i, e := range errListErrors {
|
|
|
|
errList[i] = e.Error()
|
|
|
|
}
|
|
|
|
actualResult := PropertyListJoinToString(replaceOnChanges,
|
|
|
|
func(x string) string { return x })
|
|
|
|
sort.Strings(actualResult)
|
|
|
|
if tt.result != nil || len(actualResult) > 0 {
|
|
|
|
assert.Equal(t, tt.result, actualResult,
|
|
|
|
"Get the correct result")
|
|
|
|
}
|
|
|
|
if tt.errors != nil || len(errList) > 0 {
|
|
|
|
assert.Equal(t, tt.errors, errList,
|
|
|
|
"Get correct error messages")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-12-06 19:35:27 +00:00
|
|
|
|
|
|
|
func TestValidateTypeToken(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2021-12-06 19:35:27 +00:00
|
|
|
cases := []struct {
|
2021-12-08 05:21:04 +00:00
|
|
|
name string
|
|
|
|
input string
|
|
|
|
expectError bool
|
|
|
|
allowedExtras []string
|
2021-12-06 19:35:27 +00:00
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "valid",
|
|
|
|
input: "example::typename",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "invalid",
|
|
|
|
input: "xyz::typename",
|
|
|
|
expectError: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "valid-has-subsection",
|
|
|
|
input: "example:index:typename",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "invalid-has-subsection",
|
|
|
|
input: "not:index:typename",
|
|
|
|
expectError: true,
|
|
|
|
},
|
2021-12-08 05:21:04 +00:00
|
|
|
{
|
|
|
|
name: "allowed-extras-valid",
|
|
|
|
input: "other:index:typename",
|
|
|
|
allowedExtras: []string{"other"},
|
|
|
|
},
|
2021-12-06 19:35:27 +00:00
|
|
|
}
|
|
|
|
for _, c := range cases {
|
2022-03-04 08:17:41 +00:00
|
|
|
c := c
|
2021-12-06 19:35:27 +00:00
|
|
|
t.Run(c.name, func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2021-12-06 19:35:27 +00:00
|
|
|
spec := &PackageSpec{Name: "example"}
|
2021-12-08 05:21:04 +00:00
|
|
|
allowed := map[string]bool{"example": true}
|
|
|
|
for _, e := range c.allowedExtras {
|
|
|
|
allowed[e] = true
|
|
|
|
}
|
|
|
|
errors := spec.validateTypeToken(allowed, "type", c.input)
|
2021-12-06 19:35:27 +00:00
|
|
|
if c.expectError {
|
|
|
|
assert.True(t, errors.HasErrors())
|
|
|
|
} else {
|
|
|
|
assert.False(t, errors.HasErrors())
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2022-05-24 22:05:16 +00:00
|
|
|
|
|
|
|
func TestTypeString(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
cases := []struct {
|
|
|
|
input Type
|
|
|
|
output string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
input: &UnionType{
|
|
|
|
ElementTypes: []Type{
|
|
|
|
StringType,
|
|
|
|
NumberType,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
output: "Union<string, number>",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: &UnionType{
|
|
|
|
ElementTypes: []Type{
|
|
|
|
StringType,
|
|
|
|
},
|
|
|
|
DefaultType: NumberType,
|
|
|
|
},
|
|
|
|
output: "Union<string, default=number>",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, c := range cases {
|
|
|
|
c := c
|
|
|
|
t.Run(c.output, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
assert.Equal(t, c.output, c.input.String())
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2022-05-25 00:47:43 +00:00
|
|
|
|
|
|
|
func TestPackageIdentity(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
cases := []struct {
|
|
|
|
nameA string
|
|
|
|
versionA string
|
|
|
|
nameB string
|
|
|
|
versionB string
|
|
|
|
equal bool
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
nameA: "example",
|
|
|
|
nameB: "example",
|
|
|
|
equal: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
nameA: "example",
|
|
|
|
versionA: "1.0.0",
|
|
|
|
nameB: "example",
|
|
|
|
versionB: "1.0.0",
|
|
|
|
equal: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
nameA: "example",
|
|
|
|
versionA: "1.2.3-beta",
|
|
|
|
nameB: "example",
|
|
|
|
versionB: "1.2.3-beta",
|
|
|
|
equal: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
nameA: "example",
|
|
|
|
versionA: "1.2.3-beta+1234",
|
|
|
|
nameB: "example",
|
|
|
|
versionB: "1.2.3-beta+1234",
|
|
|
|
equal: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
nameA: "example",
|
|
|
|
versionA: "1.0.0",
|
|
|
|
nameB: "example",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
nameA: "example",
|
|
|
|
nameB: "example",
|
|
|
|
versionB: "1.0.0",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
nameA: "example",
|
|
|
|
versionA: "1.0.0",
|
|
|
|
nameB: "example",
|
|
|
|
versionB: "1.2.3-beta",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
nameA: "example",
|
|
|
|
versionA: "1.2.3-beta+1234",
|
|
|
|
nameB: "example",
|
|
|
|
versionB: "1.2.3-beta+5678",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, c := range cases {
|
|
|
|
c := c
|
|
|
|
t.Run(c.nameA, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
var verA *semver.Version
|
|
|
|
if c.versionA != "" {
|
|
|
|
v := semver.MustParse(c.versionA)
|
|
|
|
verA = &v
|
|
|
|
}
|
|
|
|
|
|
|
|
var verB *semver.Version
|
|
|
|
if c.versionB != "" {
|
|
|
|
v := semver.MustParse(c.versionB)
|
|
|
|
verB = &v
|
|
|
|
}
|
|
|
|
|
|
|
|
pkgA := &Package{Name: c.nameA, Version: verA}
|
|
|
|
pkgB := &Package{Name: c.nameB, Version: verB}
|
|
|
|
if c.equal {
|
|
|
|
assert.Equal(t, packageIdentity(c.nameA, verA), packageIdentity(c.nameB, verB))
|
|
|
|
assert.Equal(t, pkgA.Identity(), pkgB.Identity())
|
|
|
|
assert.True(t, pkgA.Equals(pkgB))
|
|
|
|
} else {
|
|
|
|
assert.NotEqual(t, packageIdentity(c.nameA, verA), packageIdentity(c.nameB, verB))
|
|
|
|
assert.NotEqual(t, pkgA.Identity(), pkgB.Identity())
|
|
|
|
assert.False(t, pkgA.Equals(pkgB))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2023-07-26 19:36:37 +00:00
|
|
|
|
|
|
|
func TestBindDefaultInt(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
dv, diag := bindDefaultValue("fake-path", int(32), nil, IntType)
|
|
|
|
if diag.HasErrors() {
|
|
|
|
t.Fail()
|
|
|
|
}
|
|
|
|
assert.Equal(t, int32(32), dv.Value)
|
|
|
|
|
|
|
|
// Check that we error on overflow/underflow when casting int to int32.
|
|
|
|
if _, diag := bindDefaultValue("fake-path", int(math.MaxInt64), nil, IntType); !diag.HasErrors() {
|
|
|
|
assert.Fail(t, "did not catch oveflow")
|
|
|
|
t.Fail()
|
|
|
|
}
|
|
|
|
if _, diag := bindDefaultValue("fake-path", int(math.MinInt64), nil, IntType); !diag.HasErrors() {
|
|
|
|
assert.Fail(t, "did not catch underflow")
|
|
|
|
}
|
|
|
|
}
|
Support returning plain values from methods (#13592)
Support returning plain values from methods.
Implements Node, Python and Go support.
Remaining:
- [x] test receiving unknowns
- [x] acceptance tests written and passing locally for Node, Python, Go
clients against a Go server
- [x] acceptance tests passing in CI
- [x] tickets filed for remaining languages
- [x] https://github.com/pulumi/pulumi-yaml/issues/499
- [x] https://github.com/pulumi/pulumi-java/issues/1193
- [x] https://github.com/pulumi/pulumi-dotnet/issues/170
Known limitations:
- this is technically a breaking change in case there is code out there
that already uses methods that return Plain: true
- struct-wrapping limitation: the provider for the component resource
needs to still wrap the plain-returning Method response with a 1-arg
struct; by convention the field is named "res", and this is how it
travels through the plumbing
- resources cannot return plain values yet
- the provider for the component resource cannot have unknown
configuration, if it does, the methods will not be called
- Per Luke https://github.com/pulumi/pulumi/issues/11520 this might not
be supported/realizable yet
<!---
Thanks so much for your contribution! If this is your first time
contributing, please ensure that you have read the
[CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md)
documentation.
-->
# Description
<!--- Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context. -->
Fixes https://github.com/pulumi/pulumi/issues/12709
## Checklist
- [ ] I have run `make tidy` to update any new dependencies
- [ ] I have run `make lint` to verify my code passes the lint check
- [ ] I have formatted my code using `gofumpt`
<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [ ] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [ ] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @Pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->
2023-11-18 06:02:06 +00:00
|
|
|
|
2023-11-29 16:35:08 +00:00
|
|
|
func TestMarshalResourceWithLanguageSettings(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
prop := &Property{
|
|
|
|
Name: "prop1",
|
|
|
|
Language: map[string]interface{}{
|
|
|
|
"csharp": map[string]string{
|
|
|
|
"name": "CSharpProp1",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Type: stringType,
|
|
|
|
}
|
|
|
|
r := Resource{
|
|
|
|
Token: "xyz:index:resource",
|
|
|
|
Properties: []*Property{
|
|
|
|
prop,
|
|
|
|
},
|
|
|
|
Language: map[string]interface{}{
|
|
|
|
"csharp": map[string]string{
|
|
|
|
"name": "CSharpResource",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
p := Package{
|
|
|
|
Name: "xyz",
|
|
|
|
DisplayName: "xyz package",
|
|
|
|
Version: &semver.Version{
|
|
|
|
Major: 0,
|
|
|
|
Minor: 0,
|
|
|
|
Patch: 0,
|
|
|
|
},
|
|
|
|
Provider: &Resource{
|
|
|
|
IsProvider: true,
|
|
|
|
Token: "provider",
|
|
|
|
},
|
|
|
|
Resources: []*Resource{
|
|
|
|
&r,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
pspec, err := p.MarshalSpec()
|
|
|
|
assert.NoError(t, err)
|
|
|
|
res, ok := pspec.Resources[r.Token]
|
|
|
|
assert.True(t, ok)
|
|
|
|
assert.Contains(t, res.Language, "csharp")
|
|
|
|
assert.IsType(t, RawMessage{}, res.Language["csharp"])
|
|
|
|
|
|
|
|
prspec, ok := res.Properties[prop.Name]
|
|
|
|
assert.True(t, ok)
|
|
|
|
assert.Contains(t, prspec.Language, "csharp")
|
|
|
|
assert.IsType(t, RawMessage{}, prspec.Language["csharp"])
|
|
|
|
}
|
|
|
|
|
Support returning plain values from methods (#13592)
Support returning plain values from methods.
Implements Node, Python and Go support.
Remaining:
- [x] test receiving unknowns
- [x] acceptance tests written and passing locally for Node, Python, Go
clients against a Go server
- [x] acceptance tests passing in CI
- [x] tickets filed for remaining languages
- [x] https://github.com/pulumi/pulumi-yaml/issues/499
- [x] https://github.com/pulumi/pulumi-java/issues/1193
- [x] https://github.com/pulumi/pulumi-dotnet/issues/170
Known limitations:
- this is technically a breaking change in case there is code out there
that already uses methods that return Plain: true
- struct-wrapping limitation: the provider for the component resource
needs to still wrap the plain-returning Method response with a 1-arg
struct; by convention the field is named "res", and this is how it
travels through the plumbing
- resources cannot return plain values yet
- the provider for the component resource cannot have unknown
configuration, if it does, the methods will not be called
- Per Luke https://github.com/pulumi/pulumi/issues/11520 this might not
be supported/realizable yet
<!---
Thanks so much for your contribution! If this is your first time
contributing, please ensure that you have read the
[CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md)
documentation.
-->
# Description
<!--- Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context. -->
Fixes https://github.com/pulumi/pulumi/issues/12709
## Checklist
- [ ] I have run `make tidy` to update any new dependencies
- [ ] I have run `make lint` to verify my code passes the lint check
- [ ] I have formatted my code using `gofumpt`
<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [ ] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [ ] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @Pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->
2023-11-18 06:02:06 +00:00
|
|
|
func TestFunctionSpecToJSONAndYAMLTurnaround(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
type testCase struct {
|
|
|
|
name string
|
|
|
|
fspec FunctionSpec
|
|
|
|
serial any
|
|
|
|
// For legacy forms, after turning around through serde FunctionSpec will be
|
|
|
|
// normalized and not exactly equal to the original; tests will check against the
|
|
|
|
// normalized form if provided.
|
|
|
|
normalized *FunctionSpec
|
|
|
|
}
|
|
|
|
|
|
|
|
ots := &ObjectTypeSpec{
|
|
|
|
Type: "object",
|
|
|
|
Properties: map[string]PropertySpec{
|
|
|
|
"x": {
|
|
|
|
TypeSpec: TypeSpec{
|
|
|
|
Type: "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
otsPlain := &ObjectTypeSpec{
|
|
|
|
Type: "object",
|
|
|
|
Properties: map[string]PropertySpec{
|
|
|
|
"x": {
|
|
|
|
TypeSpec: TypeSpec{
|
|
|
|
Type: "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Plain: []string{"x"},
|
|
|
|
}
|
|
|
|
|
|
|
|
testCases := []testCase{
|
|
|
|
{
|
|
|
|
name: "legacy-outputs-form",
|
|
|
|
fspec: FunctionSpec{
|
|
|
|
Outputs: ots,
|
|
|
|
},
|
|
|
|
serial: map[string]interface{}{
|
|
|
|
"outputs": map[string]interface{}{
|
|
|
|
"properties": map[string]interface{}{
|
|
|
|
"x": map[string]interface{}{
|
|
|
|
"type": "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"type": "object",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
normalized: &FunctionSpec{
|
|
|
|
ReturnType: &ReturnTypeSpec{
|
|
|
|
ObjectTypeSpec: ots,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "legacy-outputs-form-plain-array",
|
|
|
|
fspec: FunctionSpec{
|
|
|
|
Outputs: otsPlain,
|
|
|
|
},
|
|
|
|
serial: map[string]interface{}{
|
|
|
|
"outputs": map[string]interface{}{
|
|
|
|
"properties": map[string]interface{}{
|
|
|
|
"x": map[string]interface{}{
|
|
|
|
"type": "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"plain": []interface{}{"x"},
|
|
|
|
"type": "object",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
normalized: &FunctionSpec{
|
|
|
|
ReturnType: &ReturnTypeSpec{
|
|
|
|
ObjectTypeSpec: otsPlain,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "return-plain-integer",
|
|
|
|
fspec: FunctionSpec{
|
|
|
|
ReturnType: &ReturnTypeSpec{
|
|
|
|
TypeSpec: &TypeSpec{
|
|
|
|
Type: "integer",
|
|
|
|
Plain: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
serial: map[string]interface{}{
|
|
|
|
"outputs": map[string]interface{}{
|
|
|
|
"plain": true,
|
|
|
|
"type": "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "return-integer",
|
|
|
|
fspec: FunctionSpec{
|
|
|
|
ReturnType: &ReturnTypeSpec{
|
|
|
|
TypeSpec: &TypeSpec{
|
|
|
|
Type: "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
serial: map[string]interface{}{
|
|
|
|
"outputs": map[string]interface{}{
|
|
|
|
"type": "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "return-plain-object",
|
|
|
|
fspec: FunctionSpec{
|
|
|
|
ReturnType: &ReturnTypeSpec{
|
|
|
|
ObjectTypeSpec: ots,
|
|
|
|
ObjectTypeSpecIsPlain: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
serial: map[string]interface{}{
|
|
|
|
"outputs": map[string]interface{}{
|
|
|
|
"plain": true,
|
|
|
|
"properties": map[string]interface{}{
|
|
|
|
"x": map[string]interface{}{
|
|
|
|
"type": "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"type": "object",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "return-object",
|
|
|
|
fspec: FunctionSpec{
|
|
|
|
ReturnType: &ReturnTypeSpec{
|
|
|
|
ObjectTypeSpec: ots,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
serial: map[string]interface{}{
|
|
|
|
"outputs": map[string]interface{}{
|
|
|
|
"properties": map[string]interface{}{
|
|
|
|
"x": map[string]interface{}{
|
|
|
|
"type": "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"type": "object",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "return-object-plain-array",
|
|
|
|
fspec: FunctionSpec{
|
|
|
|
ReturnType: &ReturnTypeSpec{
|
|
|
|
ObjectTypeSpec: otsPlain,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
serial: map[string]interface{}{
|
|
|
|
"outputs": map[string]interface{}{
|
|
|
|
"plain": []interface{}{"x"},
|
|
|
|
"properties": map[string]interface{}{
|
|
|
|
"x": map[string]interface{}{
|
|
|
|
"type": "integer",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"type": "object",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tc := range testCases {
|
|
|
|
tc := tc
|
|
|
|
fspec := tc.fspec
|
|
|
|
expectSerial := tc.serial
|
|
|
|
expectFSpec := fspec
|
|
|
|
if tc.normalized != nil {
|
|
|
|
expectFSpec = *tc.normalized
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test JSON serialization and turnaround.
|
|
|
|
t.Run(tc.name+"/json", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
var serial any
|
|
|
|
|
|
|
|
bytes, err := json.MarshalIndent(fspec, "", " ")
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
err = json.Unmarshal(bytes, &serial)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equalf(t, expectSerial, serial, "Unexpected JSON serial form")
|
|
|
|
|
|
|
|
var actual FunctionSpec
|
|
|
|
err = json.Unmarshal(bytes, &actual)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, expectFSpec, actual)
|
|
|
|
})
|
|
|
|
|
|
|
|
// Test YAML serialization and turnaround.
|
|
|
|
t.Run(tc.name+"/yaml", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
var serial any
|
|
|
|
|
|
|
|
bytes, err := yaml.Marshal(fspec)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
err = yaml.Unmarshal(bytes, &serial)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equalf(t, expectSerial, serial, "Unexpected YAML serial form")
|
|
|
|
|
|
|
|
var actual FunctionSpec
|
|
|
|
err = yaml.Unmarshal(bytes, &actual)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, expectFSpec, actual)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestFunctionToFunctionSpecTurnaround(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
type testCase struct {
|
|
|
|
name string
|
|
|
|
fn *Function
|
|
|
|
fspec FunctionSpec
|
|
|
|
}
|
|
|
|
|
|
|
|
testCases := []testCase{
|
|
|
|
{
|
|
|
|
name: "return-type-plain",
|
|
|
|
fn: &Function{
|
|
|
|
PackageReference: packageDefRef{},
|
|
|
|
Token: "token",
|
|
|
|
ReturnType: IntType,
|
|
|
|
ReturnTypePlain: true,
|
|
|
|
Language: map[string]interface{}{},
|
|
|
|
},
|
|
|
|
fspec: FunctionSpec{
|
|
|
|
ReturnType: &ReturnTypeSpec{
|
|
|
|
TypeSpec: &TypeSpec{
|
|
|
|
Type: "integer",
|
|
|
|
Plain: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tc := range testCases {
|
|
|
|
tc := tc
|
|
|
|
t.Run(tc.name+"/marshalFunction", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
pkg := Package{}
|
|
|
|
fspec, err := pkg.marshalFunction(tc.fn)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc.fspec, fspec)
|
|
|
|
})
|
|
|
|
t.Run(tc.name+"/bindFunctionDef", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
ts := types{
|
|
|
|
spec: packageSpecSource{
|
|
|
|
&PackageSpec{
|
|
|
|
Functions: map[string]FunctionSpec{
|
|
|
|
"token": tc.fspec,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
functionDefs: map[string]*Function{},
|
|
|
|
}
|
|
|
|
fn, diags, err := ts.bindFunctionDef("token")
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.False(t, diags.HasErrors())
|
|
|
|
require.Equal(t, tc.fn, fn)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2024-03-04 21:54:05 +00:00
|
|
|
|
|
|
|
//nolint:paralleltest // using t.Setenv which is incompatible with t.Parallel
|
|
|
|
func TestLoaderRespectsDebugProviders(t *testing.T) {
|
|
|
|
host := debugProvidersHelperHost(t)
|
|
|
|
loader := NewPluginLoader(host)
|
|
|
|
cancel := make(chan bool)
|
|
|
|
handle, err := rpcutil.ServeWithOptions(rpcutil.ServeOptions{
|
|
|
|
Cancel: cancel,
|
|
|
|
Init: func(srv *grpc.Server) error {
|
|
|
|
pulumirpc.RegisterResourceProviderServer(srv, &debugProvidersHelperServer{})
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
require.NoError(t, host.SignalCancellation())
|
|
|
|
cancel <- true
|
|
|
|
require.NoError(t, <-handle.Done)
|
|
|
|
require.NoError(t, host.Close())
|
|
|
|
})
|
|
|
|
|
|
|
|
// Instruct to attach to the imaginary provider.
|
|
|
|
t.Setenv("PULUMI_DEBUG_PROVIDERS", fmt.Sprintf("imaginary:%d", handle.Port))
|
|
|
|
|
|
|
|
// Load from the in-process provider.
|
|
|
|
pref, err := loader.LoadPackageReference("imaginary", nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "imaginary", pref.Name())
|
|
|
|
}
|
|
|
|
|
|
|
|
type debugProvidersHelperServer struct {
|
|
|
|
pulumirpc.UnimplementedResourceProviderServer
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*debugProvidersHelperServer) GetSchema(
|
|
|
|
ctx context.Context, req *pulumirpc.GetSchemaRequest,
|
|
|
|
) (*pulumirpc.GetSchemaResponse, error) {
|
|
|
|
schema := PackageSpec{
|
|
|
|
Name: "imaginary",
|
|
|
|
Version: "0.0.1",
|
|
|
|
}
|
|
|
|
bytes, err := json.Marshal(schema)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &pulumirpc.GetSchemaResponse{Schema: string(bytes)}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*debugProvidersHelperServer) GetPluginInfo(
|
|
|
|
context.Context, *emptypb.Empty,
|
|
|
|
) (*pulumirpc.PluginInfo, error) {
|
|
|
|
return &pulumirpc.PluginInfo{Version: "0.0.1"}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*debugProvidersHelperServer) Attach(
|
|
|
|
context.Context, *pulumirpc.PluginAttach,
|
|
|
|
) (*emptypb.Empty, error) {
|
|
|
|
return &emptypb.Empty{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is the host that pulumi-yaml is using. Somehow the test does not work with utils.NewHost,
|
|
|
|
// perhaps that does not support PULUMI_DEBUG_PROVIDERS yet.
|
|
|
|
func debugProvidersHelperHost(t *testing.T) plugin.Host {
|
|
|
|
cwd := t.TempDir()
|
|
|
|
sink := diag.DefaultSink(os.Stderr, os.Stderr, diag.FormatOptions{
|
|
|
|
Color: cmdutil.GetGlobalColorization(),
|
|
|
|
})
|
|
|
|
pluginCtx, err := plugin.NewContext(sink, sink, nil, nil, cwd, nil, true, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
return pluginCtx.Host
|
|
|
|
}
|