diff --git a/.secrets.baseline b/.secrets.baseline index 53545144..914a812a 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "go.sum|package-lock.json|^.secrets.baseline$", "lines": null }, - "generated_at": "2024-10-07T16:10:03Z", + "generated_at": "2024-10-11T12:56:06Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -100,7 +100,15 @@ "hashed_secret": "5996c731c43c6191af2324af0230bd65e723fcdb", "is_secret": false, "is_verified": false, - "line_number": 356, + "line_number": 357, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "03e60e3e0d9675b19754e2a81bbb48a26af858e7", + "is_secret": false, + "is_verified": false, + "line_number": 825, "type": "Secret Keyword", "verified_result": null } diff --git a/cloudinfo/projects.go b/cloudinfo/projects.go index 8a1d5dca..d80e9339 100644 --- a/cloudinfo/projects.go +++ b/cloudinfo/projects.go @@ -9,6 +9,7 @@ import ( "math/rand" "os" "reflect" + "regexp" "sort" "strconv" "strings" @@ -396,8 +397,8 @@ func (infoSvc *CloudInfoService) CreateStackFromConfigFile(stackConfig *ConfigDe // Update stack inputs from catalog configuration updateInputsFromCatalog(stackConfig, catalogConfig, catalogProductIndex, catalogFlavorIndex, doNotOverrideInputs) - // Check if all catalog inputs exist in the stack definition - checkCatalogInputsExistInStackDefinition(stackJson, catalogConfig, catalogProductIndex, catalogFlavorIndex, &errorMessages) + // Check if all catalog inputs exist in the stack definition and match types + validateCatalogInputsInStackDefinition(stackJson, catalogConfig, catalogProductIndex, catalogFlavorIndex, &errorMessages) // If there are any errors from the validation process, return them if len(errorMessages) > 0 { @@ -407,8 +408,13 @@ func (infoSvc *CloudInfoService) CreateStackFromConfigFile(stackConfig *ConfigDe // Sort stack inputs by name for consistency sortInputsByName(stackConfig) - // Set stack name and description using catalog product and flavor labels - stackConfig.Name = fmt.Sprintf("%s-%s", catalogConfig.Products[catalogProductIndex].Label, catalogConfig.Products[catalogProductIndex].Flavors[catalogFlavorIndex].Label) + // Set stack name and description using catalog product and flavor labels, ensure the name is valid. For the test the name does not really matter so strip invalid characters. + // Define the pattern to match valid characters + var validNamePattern = regexp.MustCompile(`[^a-zA-Z0-9-_ ]`) + + // Generate the stack name and strip invalid characters + rawName := fmt.Sprintf("%s-%s", catalogConfig.Products[catalogProductIndex].Label, catalogConfig.Products[catalogProductIndex].Flavors[catalogFlavorIndex].Label) + stackConfig.Name = validNamePattern.ReplaceAllString(rawName, "") stackConfig.Description = fmt.Sprintf("%s-%s", catalogConfig.Products[catalogProductIndex].Label, catalogConfig.Products[catalogProductIndex].Flavors[catalogFlavorIndex].Label) // Create the new stack @@ -452,45 +458,34 @@ func checkStackForDuplicates(stackJson Stack) []string { errorMessages := []string{} inputNames := make(map[string]bool) - duplicateInputs := []string{} for _, input := range stackJson.Inputs { if _, exists := inputNames[input.Name]; exists { - duplicateInputs = append(duplicateInputs, input.Name) + errorMessages = append(errorMessages, fmt.Sprintf("duplicate stack input variable found: %s", input.Name)) } else { inputNames[input.Name] = true } } - if len(duplicateInputs) > 0 { - errorMessages = append(errorMessages, fmt.Sprintf("duplicate stack input variable found: %s", strings.Join(duplicateInputs, ", "))) - } outputNames := make(map[string]bool) - duplicateOutputs := []string{} for _, output := range stackJson.Outputs { if _, exists := outputNames[output.Name]; exists { - duplicateOutputs = append(duplicateOutputs, output.Name) + errorMessages = append(errorMessages, fmt.Sprintf("duplicate stack output variable found: %s", output.Name)) } else { outputNames[output.Name] = true } } - if len(duplicateOutputs) > 0 { - errorMessages = append(errorMessages, fmt.Sprintf("duplicate stack output variable found: %s", strings.Join(duplicateOutputs, ", "))) - } for _, member := range stackJson.Members { memberInputNames := make(map[string]bool) - duplicateMemberInputs := []string{} for _, input := range member.Inputs { if _, exists := memberInputNames[input.Name]; exists { - duplicateMemberInputs = append(duplicateMemberInputs, fmt.Sprintf("member: %s input: %s", member.Name, input.Name)) + errorMessages = append(errorMessages, fmt.Sprintf("duplicate member input variable found member: %s input: %s", member.Name, input.Name)) } else { memberInputNames[input.Name] = true } } - if len(duplicateMemberInputs) > 0 { - errorMessages = append(errorMessages, fmt.Sprintf("duplicate member input variable found %s", strings.Join(duplicateMemberInputs, ", "))) - } } + return errorMessages } @@ -622,6 +617,8 @@ func defineStackIO(stackJson Stack, stackConfig *ConfigDetails, doNotOverrideInp // This function reads and unmarshals the catalog configuration, and identifies the product and flavor indices based on the stack configuration. func readCatalogConfig(catalogJsonPath string, stackConfig *ConfigDetails, errorMessages *[]string) (CatalogJson, int, int, error) { jsonFile, err := os.ReadFile(catalogJsonPath) + duplicateErrorMessages := []string{} + if err != nil { log.Println("Error reading catalog JSON file:", err) return CatalogJson{}, 0, 0, err @@ -659,16 +656,18 @@ func readCatalogConfig(catalogJsonPath string, stackConfig *ConfigDetails, error } catalogInputNames := make(map[string]bool) - duplicateCatalogInputs := []string{} + productName := catalogConfig.Products[catalogProductIndex].Name + flavorName := catalogConfig.Products[catalogProductIndex].Flavors[catalogFlavorIndex].Name for _, input := range catalogConfig.Products[catalogProductIndex].Flavors[catalogFlavorIndex].Configuration { if _, exists := catalogInputNames[input.Key]; exists { - duplicateCatalogInputs = append(duplicateCatalogInputs, input.Key) + duplicateErrorMessages = append(duplicateErrorMessages, fmt.Sprintf("duplicate catalog input variable found in product '%s', flavor '%s': %s", productName, flavorName, input.Key)) } else { catalogInputNames[input.Key] = true } } - if len(duplicateCatalogInputs) > 0 { - *errorMessages = append(*errorMessages, fmt.Sprintf("duplicate catalog input variable found: %s", strings.Join(duplicateCatalogInputs, ", "))) + if len(duplicateErrorMessages) > 0 { + + *errorMessages = append(*errorMessages, strings.Join(duplicateErrorMessages, "\n")) } return catalogConfig, catalogProductIndex, catalogFlavorIndex, nil @@ -694,6 +693,12 @@ func updateInputsFromCatalog(stackConfig *ConfigDetails, catalogConfig CatalogJs } else { inputDefault = doNotOverrideInputs["stack"][input.Key] } + + // Skip updating if the default value is nil + if inputDefault == nil { + continue + } + inputDefault = convertSliceToString(inputDefault) found := false @@ -718,7 +723,7 @@ func updateInputsFromCatalog(stackConfig *ConfigDetails, catalogConfig CatalogJs if val, ok := inputDefault.(string); ok { stackConfig.StackDefinition.Inputs[i].Default = &val } - case "boolean": + case "bool", "boolean": if val, ok := inputDefault.(bool); ok { stackConfig.StackDefinition.Inputs[i].Default = &val } else if val, err := strconv.ParseBool(inputDefault.(string)); err == nil { @@ -742,30 +747,100 @@ func updateInputsFromCatalog(stackConfig *ConfigDetails, catalogConfig CatalogJs } } -// checkCatalogInputsExistInStackDefinition checks if all catalog inputs exist in the stack definition. -// This function ensures that each input defined in the catalog configuration is also present in the stack definition, +// validateCatalogInputsInStackDefinition validates that all catalog inputs exist in the stack definition and their types match. +// This function ensures that each input defined in the catalog configuration is also present in the stack definition with the correct type, // thereby ensuring consistency between the catalog and stack configurations. -func checkCatalogInputsExistInStackDefinition(stackJson Stack, catalogConfig CatalogJson, catalogProductIndex, catalogFlavorIndex int, errorMessages *[]string) { +func validateCatalogInputsInStackDefinition(stackJson Stack, catalogConfig CatalogJson, catalogProductIndex, catalogFlavorIndex int, errorMessages *[]string) { catalogInputs := catalogConfig.Products[catalogProductIndex].Flavors[catalogFlavorIndex].Configuration - missingInputs := []string{} + productName := catalogConfig.Products[catalogProductIndex].Name + flavorName := catalogConfig.Products[catalogProductIndex].Flavors[catalogFlavorIndex].Name + typeMismatches := []string{} + defaultTypeMismatches := []string{} + extraInputs := []string{} - // Iterate over each catalog input and check if it exists in the stack definition + // Iterate over each catalog input and check if it exists in the stack definition and if the types match for _, catalogInput := range catalogInputs { + // Skip the ibmcloud_api_key input as it may not part of the stack definition, but is always valid + if catalogInput.Key == "ibmcloud_api_key" { + continue + } found := false for _, stackInput := range stackJson.Inputs { if catalogInput.Key == stackInput.Name { found = true + expectedType := convertGoTypeToExpectedType(stackInput.Type) + if !isValidType(catalogInput.Type, expectedType) { + typeMismatches = append(typeMismatches, fmt.Sprintf("catalog configuration type mismatch in product '%s', flavor '%s': %s expected type: %s, got: %s", productName, flavorName, catalogInput.Key, expectedType, catalogInput.Type)) + } + // Check if the default value type matches the expected type + if catalogInput.DefaultValue != nil { + defaultValueType := reflect.TypeOf(catalogInput.DefaultValue).String() + expectedDefaultValueType := convertGoTypeToExpectedType(defaultValueType) + if !isValidType(expectedType, expectedDefaultValueType) { + defaultTypeMismatches = append(defaultTypeMismatches, fmt.Sprintf("catalog configuration default value type mismatch in product '%s', flavor '%s': %s expected type: %s, got: %s", productName, flavorName, catalogInput.Key, expectedType, expectedDefaultValueType)) + } + } break } } if !found { - missingInputs = append(missingInputs, catalogInput.Key) + extraInputs = append(extraInputs, fmt.Sprintf("extra catalog input variable not found in stack definition in product '%s', flavor '%s': %s", productName, flavorName, catalogInput.Key)) } } - if len(missingInputs) > 0 { - *errorMessages = append(*errorMessages, fmt.Sprintf("catalog input variable not found in stack definition: %s", strings.Join(missingInputs, ", "))) + if len(typeMismatches) > 0 { + *errorMessages = append(*errorMessages, strings.Join(typeMismatches, "\n")) + } + + if len(defaultTypeMismatches) > 0 { + *errorMessages = append(*errorMessages, strings.Join(defaultTypeMismatches, "\n")) + } + + if len(extraInputs) > 0 { + *errorMessages = append(*errorMessages, strings.Join(extraInputs, "\n")) + } +} + +// convertGoTypeToExpectedType converts Go types to the expected type names as defined in catalog json. +func convertGoTypeToExpectedType(goType string) string { + switch goType { + case "string": + return "string" + case "int", "int64", "float32", "float64": + return "int" + case "[]interface {}": + return "array" + case "map[string]interface {}": + return "object" + case "bool": + return "bool" + default: + return goType + } +} + +// isValidType checks if the provided type is valid considering additional valid types +func isValidType(expectedType, actualType string) bool { + validTypes := map[string][]string{ + "password": {"password", "string"}, + "boolean": {"boolean", "bool"}, + "array": {"array"}, + "object": {"object"}, + "int": {"int", "int64", "float32", "float64"}, + "string": {"string"}, + "float": {"float32", "float64"}, + // Add more type mappings as needed + } + + if valid, exists := validTypes[expectedType]; exists { + for _, t := range valid { + if t == actualType { + return true + } + } + return false } + return expectedType == actualType } // sortInputsByName sorts the stack inputs by their name. diff --git a/cloudinfo/projects_test.go b/cloudinfo/projects_test.go index fd9efcbf..e6c76e7f 100644 --- a/cloudinfo/projects_test.go +++ b/cloudinfo/projects_test.go @@ -441,8 +441,10 @@ func (suite *ProjectsServiceTestSuite) TestCreateStackFromConfigFile() { { name: "Default values from ibm_catalog.json, should override values from stack_definition.json", stackConfig: &ConfigDetails{ - ProjectID: "mockProjectID", - ConfigID: "54321", + ProjectID: "mockProjectID", + ConfigID: "54321", + CatalogProductName: "Product Name", + CatalogFlavorName: "Flavor Name", }, stackConfigPath: "testdata/stack_definition_stack_inputs_extended.json", catalogJsonPath: "testdata/ibm_catalog_with_config_overrides.json", @@ -470,7 +472,7 @@ func (suite *ProjectsServiceTestSuite) TestCreateStackFromConfigFile() { Name: core.StringPtr("input3"), Type: core.StringPtr("array"), Required: core.BoolPtr(false), - Default: core.StringPtr("[\"stack_def_arr_value1\", \"stack_def_arr_value2\"]"), + Default: core.StringPtr("[\"catalog_arr_value1\", \"catalog_arr_value2\"]"), Description: core.StringPtr(""), Hidden: core.BoolPtr(false), }, @@ -511,6 +513,79 @@ func (suite *ProjectsServiceTestSuite) TestCreateStackFromConfigFile() { }, expectedError: nil, }, + { + name: "Default values from ibm_catalog.json with a default not set, should override values from stack_definition.json", + stackConfig: &ConfigDetails{ + ProjectID: "mockProjectID", + ConfigID: "54321", + }, + stackConfigPath: "testdata/stack_definition_stack_inputs_extended.json", + catalogJsonPath: "testdata/ibm_catalog_with_config_overrides_and_defaults_not_set.json", + expectedConfig: &projects.StackDefinition{ + ID: core.StringPtr("mockProjectID"), // This would be generated on the server side and not part of the input + StackDefinition: &projects.StackDefinitionBlock{ + Inputs: []projects.StackDefinitionInputVariable{ + { + Name: core.StringPtr("input1"), + Type: core.StringPtr("string"), + Required: core.BoolPtr(true), + Default: core.StringPtr("stack_def_Value1"), + Description: core.StringPtr(""), + Hidden: core.BoolPtr(false), + }, + { + Name: core.StringPtr("input2"), + Type: core.StringPtr("int"), + Required: core.BoolPtr(false), + Default: core.Int64Ptr(80), + Description: core.StringPtr(""), + Hidden: core.BoolPtr(false), + }, + { + Name: core.StringPtr("input3"), + Type: core.StringPtr("array"), + Required: core.BoolPtr(false), + Default: core.StringPtr("[\"stack_def_arr_value1\", \"stack_def_arr_value2\"]"), + Description: core.StringPtr(""), + Hidden: core.BoolPtr(false), + }, + { + Name: core.StringPtr("input4"), + Type: core.StringPtr("bool"), + Required: core.BoolPtr(false), + Default: core.BoolPtr(false), + Description: core.StringPtr(""), + Hidden: core.BoolPtr(false), + }, + }, + Outputs: []projects.StackDefinitionOutputVariable{ + {Name: core.StringPtr("output1"), Value: core.StringPtr("ref:../members/member1/outputs/output1")}, + {Name: core.StringPtr("output2"), Value: core.StringPtr("ref:../members/member2/outputs/output2")}, + }, + Members: []projects.StackDefinitionMember{ + { + Name: core.StringPtr("member1"), + VersionLocator: core.StringPtr("version1"), + Inputs: []projects.StackDefinitionMemberInput{ + {Name: core.StringPtr("input1"), Value: core.StringPtr("ref:../../inputs/input1")}, + {Name: core.StringPtr("input2"), Value: core.StringPtr("20")}, + {Name: core.StringPtr("input3"), Value: core.StringPtr("stack_def_value3")}, + }, + }, + { + Name: core.StringPtr("member2"), + VersionLocator: core.StringPtr("version2"), + Inputs: []projects.StackDefinitionMemberInput{ + {Name: core.StringPtr("input1"), Value: core.StringPtr("ref:../../inputs/input2")}, + {Name: core.StringPtr("input2"), Value: core.StringPtr("30")}, + {Name: core.StringPtr("input3"), Value: core.StringPtr("stack_def_value4")}, + }, + }, + }, + }, + }, + expectedError: nil, + }, { name: "Default values from stack_definition.json, this should be the default values if no other values are provided", stackConfig: &ConfigDetails{ @@ -877,7 +952,8 @@ func (suite *ProjectsServiceTestSuite) TestCreateStackFromConfigFile() { stackConfigPath: "testdata/stack_definition_duplicate_stack_inputs.json", catalogJsonPath: "testdata/ibm_catalog_no_config_overrides.json", expectedConfig: nil, - expectedError: fmt.Errorf("duplicate stack input variable found: input1, input2"), + expectedError: fmt.Errorf("duplicate stack input variable found: input1\n" + + "duplicate stack input variable found: input2"), }, { name: "duplicate stack outputs, should return an error", @@ -910,7 +986,7 @@ func (suite *ProjectsServiceTestSuite) TestCreateStackFromConfigFile() { stackConfigPath: "testdata/stack_definition_stack_inputs.json", catalogJsonPath: "testdata/ibm_catalog_extra_input.json", expectedConfig: nil, - expectedError: fmt.Errorf("catalog input variable not found in stack definition: input5"), + expectedError: fmt.Errorf("extra catalog input variable not found in stack definition in product 'Product Name', flavor 'Flavor Name': input5"), }, { name: "catalog input duplicate found, should return an error", @@ -921,7 +997,36 @@ func (suite *ProjectsServiceTestSuite) TestCreateStackFromConfigFile() { stackConfigPath: "testdata/stack_definition_stack_inputs.json", catalogJsonPath: "testdata/ibm_catalog_duplicate_input.json", expectedConfig: nil, - expectedError: fmt.Errorf("duplicate catalog input variable found: input1"), + expectedError: fmt.Errorf("duplicate catalog input variable found in product 'Product Name', flavor 'Flavor Name': input1"), + }, + { + name: "catalog input type mismatch, should return an error", + stackConfig: &ConfigDetails{ + ProjectID: "mockProjectID", + ConfigID: "54321", + }, + stackConfigPath: "testdata/stack_definition_stack_inputs_extended.json", + catalogJsonPath: "testdata/ibm_catalog_with_config_overrides_type_mismatch.json", + expectedConfig: nil, + expectedError: fmt.Errorf("catalog configuration type mismatch in product 'Product Name', flavor 'Flavor Name': input1 expected type: string, got: array\n" + + "catalog configuration type mismatch in product 'Product Name', flavor 'Flavor Name': input2 expected type: int, got: string\n" + + "catalog configuration type mismatch in product 'Product Name', flavor 'Flavor Name': input3 expected type: array, got: bool\n" + + "catalog configuration type mismatch in product 'Product Name', flavor 'Flavor Name': input4 expected type: bool, got: array"), + }, + { + // This is checking the type of the actual default value + name: "catalog input default type mismatch, should return an error", + stackConfig: &ConfigDetails{ + ProjectID: "mockProjectID", + ConfigID: "54321", + }, + stackConfigPath: "testdata/stack_definition_stack_inputs_extended.json", + catalogJsonPath: "testdata/ibm_catalog_with_config_overrides_value_type_mismatch.json", + expectedConfig: nil, + expectedError: fmt.Errorf("catalog configuration default value type mismatch in product 'Product Name', flavor 'Flavor Name': input1 expected type: string, got: bool\n" + + "catalog configuration default value type mismatch in product 'Product Name', flavor 'Flavor Name': input2 expected type: int, got: string\n" + + "catalog configuration default value type mismatch in product 'Product Name', flavor 'Flavor Name': input3 expected type: array, got: string\n" + + "catalog configuration default value type mismatch in product 'Product Name', flavor 'Flavor Name': input4 expected type: bool, got: string"), }, { name: "multiple duplicates or extra inputs, should return a single error with multiple messages", @@ -933,11 +1038,13 @@ func (suite *ProjectsServiceTestSuite) TestCreateStackFromConfigFile() { catalogJsonPath: "testdata/ibm_catalog_multiple_errors.json", expectedConfig: nil, expectedError: fmt.Errorf( - "duplicate stack input variable found: input1, input2\n" + + "duplicate stack input variable found: input1\n" + + "duplicate stack input variable found: input2\n" + "duplicate stack output variable found: output1\n" + "duplicate member input variable found member: member1 input: input1\n" + - "duplicate catalog input variable found: input1\n" + - "catalog input variable not found in stack definition: input5"), + "duplicate catalog input variable found in product 'Product Name', flavor 'Flavor Name': input1\n" + + "catalog configuration default value type mismatch in product 'Product Name', flavor 'Flavor Name': input2 expected type: int, got: string\n" + + "extra catalog input variable not found in stack definition in product 'Product Name', flavor 'Flavor Name': input5"), }, } diff --git a/cloudinfo/testdata/ibm_catalog_duplicate_input.json b/cloudinfo/testdata/ibm_catalog_duplicate_input.json index 84deea46..39d21f27 100644 --- a/cloudinfo/testdata/ibm_catalog_duplicate_input.json +++ b/cloudinfo/testdata/ibm_catalog_duplicate_input.json @@ -70,7 +70,7 @@ "key": "input2", "type": "int", "description": "Description for input2", - "default_value": "80", + "default_value": 80, "required": true, "display_name": "Input 2" }, diff --git a/cloudinfo/testdata/ibm_catalog_extra_input.json b/cloudinfo/testdata/ibm_catalog_extra_input.json index 81c177c5..498c0861 100644 --- a/cloudinfo/testdata/ibm_catalog_extra_input.json +++ b/cloudinfo/testdata/ibm_catalog_extra_input.json @@ -70,7 +70,7 @@ "key": "input2", "type": "int", "description": "Description for input2", - "default_value": "80", + "default_value": 80, "required": true, "display_name": "Input 2" }, diff --git a/cloudinfo/testdata/ibm_catalog_multiple_products_flavors.json b/cloudinfo/testdata/ibm_catalog_multiple_products_flavors.json index 4c725c99..b8321781 100644 --- a/cloudinfo/testdata/ibm_catalog_multiple_products_flavors.json +++ b/cloudinfo/testdata/ibm_catalog_multiple_products_flavors.json @@ -70,7 +70,7 @@ "key": "input2", "type": "int", "description": "Description for input2", - "default_value": "70", + "default_value": 70, "required": true, "display_name": "Input 2" } @@ -155,7 +155,7 @@ "key": "input2", "type": "int", "description": "Description for input2", - "default_value": "85", + "default_value": 85, "required": true, "display_name": "Input 2" } @@ -219,7 +219,7 @@ "key": "input2", "type": "int", "description": "Description for input2", - "default_value": "95", + "default_value": 95, "required": true, "display_name": "Input 2" } diff --git a/cloudinfo/testdata/ibm_catalog_with_config_overrides.json b/cloudinfo/testdata/ibm_catalog_with_config_overrides.json index 67cdd27d..c5ff83e0 100644 --- a/cloudinfo/testdata/ibm_catalog_with_config_overrides.json +++ b/cloudinfo/testdata/ibm_catalog_with_config_overrides.json @@ -76,17 +76,17 @@ }, { "key": "input3", - "type": "bool", + "type": "array", "description": "Description for input3", - "default_value": true, + "default_value": ["catalog_arr_value1", "catalog_arr_value2"], "required": true, "display_name": "Input 3" }, { "key": "input4", - "type": "array", + "type": "bool", "description": "Description for input4", - "default_value": ["catalog_default4"], + "default_value": true, "required": true, "display_name": "Input 4" } diff --git a/cloudinfo/testdata/ibm_catalog_with_config_overrides_and_defaults_not_set.json b/cloudinfo/testdata/ibm_catalog_with_config_overrides_and_defaults_not_set.json new file mode 100644 index 00000000..d902b84f --- /dev/null +++ b/cloudinfo/testdata/ibm_catalog_with_config_overrides_and_defaults_not_set.json @@ -0,0 +1,102 @@ +{ + "products": [ + { + "label": "Product Label", + "name": "Product Name", + "product_kind": "Product Kind", + "tags": ["tag1", "tag2"], + "keywords": ["keyword1", "keyword2"], + "short_description": "Short description", + "long_description": "Long description", + "offering_docs_url": "http://example.com/docs", + "offering_icon_url": "http://example.com/icon", + "provider_name": "Provider Name", + "features": [ + { + "title": "Feature Title", + "description": "Feature Description" + } + ], + "support_details": "Support details", + "flavors": [ + { + "label": "Flavor Label", + "name": "Flavor Name", + "working_directory": "Working Directory", + "compliance": { + "authority": "Authority", + "profiles": [ + { + "profile_name": "Profile Name", + "profile_version": "Profile Version" + } + ] + }, + "iam_permissions": [ + { + "service_name": "Service Name", + "role_crns": ["crn1", "crn2"] + } + ], + "architecture": { + "features": [ + { + "title": "Architecture Feature Title", + "description": "Architecture Feature Description" + } + ], + "diagrams": [ + { + "diagram": { + "url": "http://example.com/diagram", + "caption": "Diagram Caption", + "type": "Diagram Type", + "thumbnail_url": "http://example.com/thumbnail" + }, + "description": "Diagram Description" + } + ] + }, + "configuration": [ + { + "key": "input1", + "type": "string", + "description": "Description for input1", + "required": true, + "display_name": "Input 1" + }, + { + "key": "input2", + "type": "int", + "description": "Description for input2", + "default_value": 80, + "required": true, + "display_name": "Input 2" + }, + { + "key": "input3", + "type": "array", + "description": "Description for input3", + "required": true, + "display_name": "Input 3" + }, + { + "key": "input4", + "type": "bool", + "description": "Description for input4", + "required": true, + "display_name": "Input 4" + } + ], + "outputs": [ + { + "key": "output1", + "description": "Description for output1" + } + ], + "install_type": "Install Type" + } + ] + } + ] +} diff --git a/cloudinfo/testdata/ibm_catalog_with_config_overrides_type_mismatch.json b/cloudinfo/testdata/ibm_catalog_with_config_overrides_type_mismatch.json new file mode 100644 index 00000000..e9ed233a --- /dev/null +++ b/cloudinfo/testdata/ibm_catalog_with_config_overrides_type_mismatch.json @@ -0,0 +1,105 @@ +{ + "products": [ + { + "label": "Product Label", + "name": "Product Name", + "product_kind": "Product Kind", + "tags": ["tag1", "tag2"], + "keywords": ["keyword1", "keyword2"], + "short_description": "Short description", + "long_description": "Long description", + "offering_docs_url": "http://example.com/docs", + "offering_icon_url": "http://example.com/icon", + "provider_name": "Provider Name", + "features": [ + { + "title": "Feature Title", + "description": "Feature Description" + } + ], + "support_details": "Support details", + "flavors": [ + { + "label": "Flavor Label", + "name": "Flavor Name", + "working_directory": "Working Directory", + "compliance": { + "authority": "Authority", + "profiles": [ + { + "profile_name": "Profile Name", + "profile_version": "Profile Version" + } + ] + }, + "iam_permissions": [ + { + "service_name": "Service Name", + "role_crns": ["crn1", "crn2"] + } + ], + "architecture": { + "features": [ + { + "title": "Architecture Feature Title", + "description": "Architecture Feature Description" + } + ], + "diagrams": [ + { + "diagram": { + "url": "http://example.com/diagram", + "caption": "Diagram Caption", + "type": "Diagram Type", + "thumbnail_url": "http://example.com/thumbnail" + }, + "description": "Diagram Description" + } + ] + }, + "configuration": [ + { + "key": "input1", + "type": "array", + "description": "Description for input1", + "default_value": "catalog_default1", + "required": true, + "display_name": "Input 1" + }, + { + "key": "input2", + "type": "string", + "description": "Description for input2", + "default_value": 80, + "required": true, + "display_name": "Input 2" + }, + { + "key": "input3", + "type": "bool", + "description": "Description for input3", + "default_value": ["catalog_default3"], + "required": true, + "display_name": "Input 3" + }, + { + "key": "input4", + "type": "array", + "description": "Description for input4", + "default_value": false, + "required": true, + "display_name": "Input 4" + } + ], + "outputs": [ + { + "key": "output1", + "description": "Description for output1" + } + ], + "install_type": "Install Type" + } + ] + } + ] +} diff --git a/cloudinfo/testdata/ibm_catalog_with_config_overrides_value_type_mismatch.json b/cloudinfo/testdata/ibm_catalog_with_config_overrides_value_type_mismatch.json new file mode 100644 index 00000000..ba27792c --- /dev/null +++ b/cloudinfo/testdata/ibm_catalog_with_config_overrides_value_type_mismatch.json @@ -0,0 +1,105 @@ +{ + "products": [ + { + "label": "Product Label", + "name": "Product Name", + "product_kind": "Product Kind", + "tags": ["tag1", "tag2"], + "keywords": ["keyword1", "keyword2"], + "short_description": "Short description", + "long_description": "Long description", + "offering_docs_url": "http://example.com/docs", + "offering_icon_url": "http://example.com/icon", + "provider_name": "Provider Name", + "features": [ + { + "title": "Feature Title", + "description": "Feature Description" + } + ], + "support_details": "Support details", + "flavors": [ + { + "label": "Flavor Label", + "name": "Flavor Name", + "working_directory": "Working Directory", + "compliance": { + "authority": "Authority", + "profiles": [ + { + "profile_name": "Profile Name", + "profile_version": "Profile Version" + } + ] + }, + "iam_permissions": [ + { + "service_name": "Service Name", + "role_crns": ["crn1", "crn2"] + } + ], + "architecture": { + "features": [ + { + "title": "Architecture Feature Title", + "description": "Architecture Feature Description" + } + ], + "diagrams": [ + { + "diagram": { + "url": "http://example.com/diagram", + "caption": "Diagram Caption", + "type": "Diagram Type", + "thumbnail_url": "http://example.com/thumbnail" + }, + "description": "Diagram Description" + } + ] + }, + "configuration": [ + { + "key": "input1", + "type": "string", + "description": "Description for input1", + "default_value": false, + "required": true, + "display_name": "Input 1" + }, + { + "key": "input2", + "type": "int", + "description": "Description for input2", + "default_value": "80", + "required": true, + "display_name": "Input 2" + }, + { + "key": "input3", + "type": "array", + "description": "Description for input3", + "default_value": "[\"catalog_arr_value1\", \"catalog_arr_value2\"]", + "required": true, + "display_name": "Input 3" + }, + { + "key": "input4", + "type": "bool", + "description": "Description for input4", + "default_value": "true", + "required": true, + "display_name": "Input 4" + } + ], + "outputs": [ + { + "key": "output1", + "description": "Description for output1" + } + ], + "install_type": "Install Type" + } + ] + } + ] +} diff --git a/cloudinfo/testdata/stack_definition_stack_inputs_extended.json b/cloudinfo/testdata/stack_definition_stack_inputs_extended.json index 861d538e..612941ec 100644 --- a/cloudinfo/testdata/stack_definition_stack_inputs_extended.json +++ b/cloudinfo/testdata/stack_definition_stack_inputs_extended.json @@ -26,7 +26,7 @@ "required": false, "type": "bool", "hidden": false, - "default": true + "default": false } ], "outputs": [ diff --git a/testprojects/tests.go b/testprojects/tests.go index 8aa73599..850db1b5 100644 --- a/testprojects/tests.go +++ b/testprojects/tests.go @@ -3,9 +3,9 @@ package testprojects import ( "errors" "fmt" - "github.com/gruntwork-io/terratest/modules/logger" "os" "path" + "runtime" "strconv" "strings" "sync" @@ -636,8 +636,15 @@ func (options *TestProjectsOptions) RunProjectsTest() error { // ensure we always run the test tear down, even if a panic occurs defer func() { if r := recover(); r != nil { + options.Testing.Fail() - options.Logger.ShortInfo(fmt.Sprintf("Recovered from panic: %v", r)) + // Get the file and line number where the panic occurred + _, file, line, ok := runtime.Caller(4) + if ok { + options.Logger.ShortError(fmt.Sprintf("Recovered from panic: %v\nOccurred at: %s:%d\n", r, file, line)) + } else { + options.Logger.ShortError(fmt.Sprintf("Recovered from panic: %v", r)) + } } options.TestTearDown() }() @@ -827,7 +834,11 @@ func (options *TestProjectsOptions) executeResourceTearDown() bool { // if test failed and we are not executing, add a log line stating this if options.Testing.Failed() && !execute { - options.Logger.ShortError("Terratest failed. Debug the Test and delete resources manually.") + if options.currentStackConfig == nil || options.currentStackConfig.ConfigID == "" { + options.Logger.ShortError("Terratest failed. No resources to delete.") + } else { + options.Logger.ShortError("Terratest failed. Debug the Test and delete resources manually.") + } } return execute @@ -857,7 +868,11 @@ func (options *TestProjectsOptions) executeProjectTearDown() bool { // if test failed and we are not executing, add a log line stating this if options.Testing.Failed() && !execute { - logger.Log(options.Testing, "Terratest failed. Debug the Test and delete the project manually.") + if options.currentProject == nil { + options.Logger.ShortError("Terratest failed. No project to delete.") + } else { + options.Logger.ShortError("Terratest failed. Debug the Test and delete the project manually.") + } } return execute