get.porter.sh/porter@v1.3.0/pkg/porter/schema.go (about)

     1  package porter
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"get.porter.sh/porter/pkg/tracing"
    11  	"github.com/PaesslerAG/jsonpath"
    12  )
    13  
    14  type jsonSchema = map[string]interface{}
    15  type jsonObject = map[string]interface{}
    16  
    17  func (p *Porter) PrintManifestSchema(ctx context.Context) error {
    18  	schemaMap, err := p.GetManifestSchema(ctx)
    19  	if err != nil {
    20  		return err
    21  	}
    22  
    23  	schema, err := json.MarshalIndent(&schemaMap, "", "  ")
    24  	if err != nil {
    25  		return fmt.Errorf("could not marshal the composite porter manifest schema: %w", err)
    26  	}
    27  
    28  	fmt.Fprintln(p.Out, string(schema))
    29  	return nil
    30  }
    31  
    32  func (p *Porter) GetManifestSchema(ctx context.Context) (jsonSchema, error) {
    33  	ctx, span := tracing.StartSpan(ctx)
    34  	defer span.EndSpan()
    35  
    36  	replacementSchema, err := p.GetReplacementSchema()
    37  	if err != nil {
    38  		span.Debugf("ignoring replacement schema: %w", err)
    39  	}
    40  	if replacementSchema != nil {
    41  		return replacementSchema, nil
    42  	}
    43  
    44  	b, err := p.Templates.GetSchema()
    45  	if err != nil {
    46  		return nil, span.Error(err)
    47  	}
    48  
    49  	manifestSchema := make(jsonSchema)
    50  	err = json.Unmarshal(b, &manifestSchema)
    51  	if err != nil {
    52  		return nil, span.Error(fmt.Errorf("could not unmarshal the root porter manifest schema: %w", err))
    53  	}
    54  
    55  	combinedSchema, err := p.injectMixinSchemas(ctx, manifestSchema)
    56  	if err != nil {
    57  		span.Warn(err.Error())
    58  		// Fallback to the porter schema, without any mixins
    59  		return manifestSchema, nil
    60  	}
    61  
    62  	return combinedSchema, nil
    63  }
    64  
    65  func (p *Porter) injectMixinSchemas(ctx context.Context, manifestSchema jsonSchema) (jsonSchema, error) {
    66  	ctx, span := tracing.StartSpan(ctx)
    67  	defer span.EndSpan()
    68  
    69  	propertiesSchema, ok := manifestSchema["properties"].(jsonSchema)
    70  	if !ok {
    71  		return nil, span.Error(fmt.Errorf("root porter manifest schema has invalid properties type, expected map[string]interface{} but got %T", manifestSchema["properties"]))
    72  	}
    73  
    74  	additionalPropertiesSchema, ok := manifestSchema["additionalProperties"].(jsonSchema)
    75  	if !ok {
    76  		return nil, span.Error(fmt.Errorf("root porter manifest schema has invalid additionalProperties type, expected map[string]interface{} but got %T", manifestSchema["additionalProperties"]))
    77  	}
    78  
    79  	mixinSchema, ok := propertiesSchema["mixins"].(jsonSchema)
    80  	if !ok {
    81  		return nil, span.Error(fmt.Errorf("root porter manifest schema has invalid properties.mixins type, expected map[string]interface{} but got %T", propertiesSchema["mixins"]))
    82  	}
    83  
    84  	mixinItemSchema, ok := mixinSchema["items"].(jsonSchema)
    85  	if !ok {
    86  		return nil, span.Error(fmt.Errorf("root porter manifest schema has invalid properties.mixins.items type, expected map[string]interface{} but got %T", mixinSchema["items"]))
    87  	}
    88  
    89  	// the set of acceptable ways to declare a mixin
    90  	// e.g.
    91  	// mixins:
    92  	// - exec
    93  	// - helm3:
    94  	//     clientVersion: 1.2.3
    95  	mixinDeclSchema, ok := mixinItemSchema["oneOf"].([]interface{})
    96  	if !ok {
    97  		return nil, span.Error(fmt.Errorf("root porter manifest schema has invalid properties.mixins.items.oneOf type, expected []interface{} but got %T", mixinItemSchema["oneOf"]))
    98  	}
    99  
   100  	// The first item is an enum of all the mixin names
   101  	if len(mixinDeclSchema) > 1 {
   102  		return nil, span.Errorf("root porter manifest schema has invalid properties.mixins.items.oneOf, expected a string type to list the names of all the mixins")
   103  	}
   104  	mixinNameDecl, ok := mixinDeclSchema[0].(jsonSchema)
   105  	if !ok {
   106  		return nil, span.Error(fmt.Errorf("root porter manifest schema has invalid properties.mixins.items.oneOf[0] type, expected []map[string]interface{} but got %T", mixinNameDecl))
   107  	}
   108  	mixinNameEnum, ok := mixinNameDecl["enum"].([]interface{})
   109  	if !ok {
   110  		return nil, span.Error(fmt.Errorf("root porter manifest schema has invalid properties.mixins.items.oneOf[0].enum type, expected []interface{} but got %T", mixinNameDecl["enum"]))
   111  	}
   112  
   113  	coreActions := []string{"install", "upgrade", "uninstall"} // custom actions are defined in json schema as additionalProperties
   114  	actionSchemas := make(map[string]jsonSchema, len(coreActions)+1)
   115  	for _, action := range coreActions {
   116  		actionSchema, ok := propertiesSchema[action].(jsonSchema)
   117  		if !ok {
   118  			return nil, span.Error(fmt.Errorf("root porter manifest schema has invalid properties.%s type, expected map[string]interface{} but got %T", action, propertiesSchema[string(action)]))
   119  		}
   120  		actionSchemas[action] = actionSchema
   121  	}
   122  
   123  	mixins, err := p.Mixins.List()
   124  	if err != nil {
   125  		return nil, span.Error(err)
   126  	}
   127  
   128  	// If there is an error with any mixin, print a warning and skip the mixin, do not return an error
   129  	for _, mixin := range mixins {
   130  		mixinSchema, err := p.Mixins.GetSchema(ctx, mixin)
   131  		if err != nil {
   132  			// if a mixin can't report its schema, don't include it and keep going
   133  			span.Debugf("could not query mixin %s for its schema: %w", mixin, err)
   134  			continue
   135  		}
   136  
   137  		// Update relative refs with the new location and reload
   138  		mixinSchema = strings.Replace(mixinSchema, "#/", fmt.Sprintf("#/mixin.%s/", mixin), -1)
   139  
   140  		mixinSchemaMap := make(jsonSchema)
   141  		err = json.Unmarshal([]byte(mixinSchema), &mixinSchemaMap)
   142  		if err != nil {
   143  			span.Debugf("could not unmarshal mixin schema for %s, %q: %w", mixin, mixinSchema, err)
   144  			continue
   145  		}
   146  
   147  		// Support declaring the mixin just by name
   148  		mixinNameEnum = append(mixinNameEnum, mixin)
   149  
   150  		// Support configuring the mixin, if available
   151  		// We know it's supported if it has a config definition included in its schema
   152  		if _, err := jsonpath.Get("$.definitions.config", mixinSchemaMap); err == nil {
   153  			mixinConfigRef := fmt.Sprintf("#/mixin.%s/definitions/config", mixin)
   154  			mixinDeclSchema = append(mixinDeclSchema, jsonObject{"$ref": mixinConfigRef})
   155  		}
   156  
   157  		// embed the entire mixin schema in the root
   158  		manifestSchema["mixin."+mixin] = mixinSchemaMap
   159  
   160  		for _, action := range coreActions {
   161  			actionItemSchema, ok := actionSchemas[action]["items"].(jsonSchema)
   162  			if !ok {
   163  				span.Debugf("root porter manifest schema has invalid properties.%s.items type, expected map[string]interface{} but got %T", action, actionSchemas[string(action)]["items"])
   164  				continue
   165  			}
   166  
   167  			actionAnyOfSchema, ok := actionItemSchema["anyOf"].([]interface{})
   168  			if !ok {
   169  				span.Debugf("root porter manifest schema has invalid properties.%s.items.anyOf type, expected []interface{} but got %T", action, actionItemSchema["anyOf"])
   170  				continue
   171  			}
   172  
   173  			actionRef := fmt.Sprintf("#/mixin.%s/definitions/%sStep", mixin, action)
   174  			actionAnyOfSchema = append(actionAnyOfSchema, jsonObject{"$ref": actionRef})
   175  			actionItemSchema["anyOf"] = actionAnyOfSchema
   176  		}
   177  
   178  		// Some mixins don't support custom actions, if the mixin has invokeStep defined,
   179  		// then use it in our additionalProperties list of acceptable root level elements.
   180  		_, err = jsonpath.Get("$.definitions.invokeStep", mixinSchemaMap)
   181  		if err == nil {
   182  			actionItemSchema, ok := additionalPropertiesSchema["items"].(jsonSchema)
   183  			if !ok {
   184  				span.Debugf("root porter manifest schema has invalid additionalProperties.items type, expected map[string]interface{} but got %T", additionalPropertiesSchema["items"])
   185  				continue
   186  			}
   187  
   188  			actionAnyOfSchema, ok := actionItemSchema["anyOf"].([]interface{})
   189  			if !ok {
   190  				span.Debugf("root porter manifest schema has invalid additionalProperties.items.anyOf type, expected []interface{} but got %T", actionItemSchema["anyOf"])
   191  				continue
   192  			}
   193  
   194  			actionRef := fmt.Sprintf("#/mixin.%s/definitions/invokeStep", mixin)
   195  			actionAnyOfSchema = append(actionAnyOfSchema, jsonObject{"$ref": actionRef})
   196  			actionItemSchema["anyOf"] = actionAnyOfSchema
   197  		}
   198  	}
   199  
   200  	// Save the updated arrays into the json schema document
   201  	mixinNameDecl["enum"] = mixinNameEnum
   202  	mixinItemSchema["oneOf"] = mixinDeclSchema
   203  
   204  	return manifestSchema, span.Error(err)
   205  }
   206  
   207  func (p *Porter) GetReplacementSchema() (jsonSchema, error) {
   208  	home, err := p.GetHomeDir()
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	replacementSchemaPath := filepath.Join(home, "porter.json")
   214  	if exists, _ := p.FileSystem.Exists(replacementSchemaPath); !exists {
   215  		return nil, nil
   216  	}
   217  
   218  	b, err := p.FileSystem.ReadFile(replacementSchemaPath)
   219  	if err != nil {
   220  		return nil, fmt.Errorf("could not read replacement schema at %s: %w", replacementSchemaPath, err)
   221  	}
   222  
   223  	replacementSchema := make(jsonSchema)
   224  	err = json.Unmarshal(b, &replacementSchema)
   225  	if err != nil {
   226  		return nil, fmt.Errorf("could not unmarshal replacement schema in %s: %w", replacementSchemaPath, err)
   227  	}
   228  
   229  	return replacementSchema, nil
   230  }