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 }