2023-08-07 13:14:25 +00:00
package template
import (
"context"
2023-12-22 15:43:08 +00:00
"errors"
2023-08-07 13:14:25 +00:00
"fmt"
"github.com/databricks/cli/libs/cmdio"
"github.com/databricks/cli/libs/jsonschema"
2024-01-11 15:41:13 +00:00
"github.com/databricks/cli/libs/log"
2023-09-07 14:36:06 +00:00
"golang.org/x/exp/maps"
2023-08-07 13:14:25 +00:00
)
2023-11-30 14:28:51 +00:00
// The latest template schema version supported by the CLI
const latestSchemaVersion = 1
2023-12-22 15:43:08 +00:00
type retriableError struct {
err error
}
func ( e retriableError ) Error ( ) string {
return e . err . Error ( )
}
2023-08-07 13:14:25 +00:00
type config struct {
ctx context . Context
values map [ string ] any
schema * jsonschema . Schema
}
func newConfig ( ctx context . Context , schemaPath string ) ( * config , error ) {
// Read config schema
2023-08-15 14:28:04 +00:00
schema , err := jsonschema . Load ( schemaPath )
2023-08-07 13:14:25 +00:00
if err != nil {
return nil , err
}
2023-08-15 14:28:04 +00:00
if err := validateSchema ( schema ) ; err != nil {
2023-08-07 13:14:25 +00:00
return nil , err
}
2023-11-08 16:48:37 +00:00
// Validate that all properties have a description
for name , p := range schema . Properties {
if p . Description == "" {
return nil , fmt . Errorf ( "template property %s is missing a description" , name )
}
}
2023-09-07 14:36:06 +00:00
// Do not allow template input variables that are not defined in the schema.
schema . AdditionalProperties = false
2023-08-07 13:14:25 +00:00
// Return config
return & config {
ctx : ctx ,
schema : schema ,
values : make ( map [ string ] any , 0 ) ,
} , nil
}
2023-08-15 14:28:04 +00:00
func validateSchema ( schema * jsonschema . Schema ) error {
for _ , v := range schema . Properties {
if v . Type == jsonschema . ArrayType || v . Type == jsonschema . ObjectType {
return fmt . Errorf ( "property type %s is not supported by bundle templates" , v . Type )
}
}
2023-11-30 14:28:51 +00:00
if schema . Version != nil && * schema . Version > latestSchemaVersion {
return fmt . Errorf ( "template schema version %d is not supported by this version of the CLI. Please upgrade your CLI to the latest version" , * schema . Version )
}
2023-08-15 14:28:04 +00:00
return nil
}
2023-08-07 13:14:25 +00:00
// Reads json file at path and assigns values from the file
func ( c * config ) assignValuesFromFile ( path string ) error {
2023-09-07 14:36:06 +00:00
// Load the config file.
configFromFile , err := c . schema . LoadInstance ( path )
2023-08-07 13:14:25 +00:00
if err != nil {
2023-09-07 14:36:06 +00:00
return fmt . Errorf ( "failed to load config from file %s: %w" , path , err )
2023-08-07 13:14:25 +00:00
}
// Write configs from the file to the input map, not overwriting any existing
// configurations.
for name , val := range configFromFile {
if _ , ok := c . values [ name ] ; ok {
continue
}
c . values [ name ] = val
}
return nil
}
// Assigns default values from schema to input config map
2023-10-19 07:08:36 +00:00
func ( c * config ) assignDefaultValues ( r * renderer ) error {
2023-08-07 13:14:25 +00:00
for name , property := range c . schema . Properties {
// Config already has a value assigned
if _ , ok := c . values [ name ] ; ok {
continue
}
// No default value defined for the property
if property . Default == nil {
continue
}
2023-11-06 15:05:17 +00:00
defaultVal , err := property . DefaultString ( )
2023-10-19 07:08:36 +00:00
if err != nil {
return err
}
defaultVal , err = r . executeTemplate ( defaultVal )
if err != nil {
return err
}
2023-11-06 15:05:17 +00:00
defaultValTyped , err := property . ParseString ( defaultVal )
2023-10-19 07:08:36 +00:00
if err != nil {
return err
}
c . values [ name ] = defaultValTyped
2023-08-07 13:14:25 +00:00
}
return nil
}
2023-11-30 16:07:45 +00:00
func ( c * config ) skipPrompt ( p jsonschema . Property , r * renderer ) ( bool , error ) {
// Config already has a value assigned. We don't have to prompt for a user input.
if _ , ok := c . values [ p . Name ] ; ok {
return true , nil
}
if p . Schema . SkipPromptIf == nil {
return false , nil
}
// Check if conditions specified by template author for skipping the prompt
// are satisfied. If they are not, we have to prompt for a user input.
for name , property := range p . Schema . SkipPromptIf . Properties {
if v , ok := c . values [ name ] ; ok && v == property . Const {
continue
}
return false , nil
}
if p . Schema . Default == nil {
return false , fmt . Errorf ( "property %s has skip_prompt_if set but no default value" , p . Name )
}
// Assign default value to property if we are skipping it.
if p . Schema . Type != jsonschema . StringType {
c . values [ p . Name ] = p . Schema . Default
return true , nil
}
// Execute the default value as a template and assign it to the property.
var err error
c . values [ p . Name ] , err = r . executeTemplate ( p . Schema . Default . ( string ) )
if err != nil {
return false , err
}
return true , nil
}
2023-12-22 15:43:08 +00:00
func ( c * config ) promptOnce ( property * jsonschema . Schema , name , defaultVal , description string ) error {
var userInput string
if property . Enum != nil {
// List options for the user to select from
options , err := property . EnumStringSlice ( )
if err != nil {
return err
}
userInput , err = cmdio . AskSelect ( c . ctx , description , options )
if err != nil {
return err
}
} else {
var err error
userInput , err = cmdio . Ask ( c . ctx , description , defaultVal )
if err != nil {
return err
}
}
// Convert user input string back to a Go value
var err error
c . values [ name ] , err = property . ParseString ( userInput )
if err != nil {
// Show error and retry if validation fails
cmdio . LogString ( c . ctx , fmt . Sprintf ( "Validation failed: %s" , err . Error ( ) ) )
return retriableError { err : err }
}
// Validate the partial config which includes the new value
err = c . schema . ValidateInstance ( c . values )
if err != nil {
// Show error and retry if validation fails
cmdio . LogString ( c . ctx , fmt . Sprintf ( "Validation failed: %s" , err . Error ( ) ) )
return retriableError { err : err }
}
return nil
}
2023-08-07 13:14:25 +00:00
// Prompts user for values for properties that do not have a value set yet
2023-10-19 07:08:36 +00:00
func ( c * config ) promptForValues ( r * renderer ) error {
2023-09-05 11:08:25 +00:00
for _ , p := range c . schema . OrderedProperties ( ) {
name := p . Name
property := p . Schema
2023-11-30 16:07:45 +00:00
// Skip prompting if we can.
skip , err := c . skipPrompt ( p , r )
if err != nil {
return err
}
if skip {
2023-08-07 13:14:25 +00:00
continue
}
// Compute default value to display by converting it to a string
2023-08-15 14:50:20 +00:00
var defaultVal string
2023-08-07 13:14:25 +00:00
if property . Default != nil {
2023-11-06 15:05:17 +00:00
defaultValRaw , err := property . DefaultString ( )
2023-10-19 07:08:36 +00:00
if err != nil {
return err
}
defaultVal , err = r . executeTemplate ( defaultValRaw )
2023-08-07 13:14:25 +00:00
if err != nil {
return err
}
}
2023-12-22 15:43:08 +00:00
// Compute description for the prompt
2023-10-19 07:08:36 +00:00
description , err := r . executeTemplate ( property . Description )
if err != nil {
return err
}
2023-12-22 15:43:08 +00:00
// We wrap this function in a retry loop to allow retries when the user
// entered value is invalid.
for {
err = c . promptOnce ( property , name , defaultVal , description )
if err == nil {
break
2023-09-08 12:07:22 +00:00
}
2023-12-22 15:43:08 +00:00
if ! errors . As ( err , & retriableError { } ) {
2023-09-08 12:07:22 +00:00
return err
}
2023-09-25 09:53:38 +00:00
}
2023-08-07 13:14:25 +00:00
}
return nil
}
// Prompt user for any missing config values. Assign default values if
// terminal is not TTY
2023-10-19 07:08:36 +00:00
func ( c * config ) promptOrAssignDefaultValues ( r * renderer ) error {
2023-12-19 09:58:46 +00:00
// TODO: replace with IsPromptSupported call (requires fixing TestAccBundleInitErrorOnUnknownFields test)
2023-12-20 12:01:53 +00:00
if cmdio . IsOutTTY ( c . ctx ) && cmdio . IsInTTY ( c . ctx ) && ! cmdio . IsGitBash ( c . ctx ) {
2023-10-19 07:08:36 +00:00
return c . promptForValues ( r )
2023-08-07 13:14:25 +00:00
}
2024-01-11 15:41:13 +00:00
log . Debugf ( c . ctx , "Terminal is not TTY. Assigning default values to template input parameters" )
2023-10-19 07:08:36 +00:00
return c . assignDefaultValues ( r )
2023-08-07 13:14:25 +00:00
}
// Validates the configuration. If passes, the configuration is ready to be used
// to initialize the template.
func ( c * config ) validate ( ) error {
2023-10-19 07:08:36 +00:00
// For final validation, all properties in the JSON schema should have a value defined.
2023-09-07 14:36:06 +00:00
c . schema . Required = maps . Keys ( c . schema . Properties )
if err := c . schema . ValidateInstance ( c . values ) ; err != nil {
return fmt . Errorf ( "validation for template input parameters failed. %w" , err )
2023-08-07 13:14:25 +00:00
}
return nil
}