tooling/templatize/pkg/pipeline/validation.go (112 lines of code) (raw):
// Copyright 2025 Microsoft 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.
package pipeline
import (
_ "embed"
"encoding/json"
"fmt"
"strings"
"github.com/santhosh-tekuri/jsonschema/v6"
"gopkg.in/yaml.v3"
)
//go:embed pipeline.schema.v1.json
var pipelineSchemaV1Content []byte
var pipelineSchemaV1Ref = "pipeline.schema.v1"
var defaultSchemaRef = pipelineSchemaV1Ref
func ValidatePipelineSchema(pipelineContent []byte) error {
// unmarshal pipeline content
pipelineMap := make(map[string]interface{})
err := yaml.Unmarshal(pipelineContent, &pipelineMap)
if err != nil {
return fmt.Errorf("failed to unmarshal pipeline YAML content: %v", err)
}
// load pipeline schema
pipelineSchema, schemaRef, err := getSchemaForPipeline(pipelineMap)
if err != nil {
return fmt.Errorf("failed to load pipeline schema: %v", err)
}
// validate pipeline schema
err = pipelineSchema.Validate(pipelineMap)
if err != nil {
return fmt.Errorf("pipeline is not compliant with schema %s: %v", schemaRef, err)
}
return nil
}
func ValidatePipelineSchemaForStruct(pipeline *Pipeline) error {
pipelineBytes, err := yaml.Marshal(pipeline)
if err != nil {
return fmt.Errorf("failed to marshal pipeline: %w", err)
}
return ValidatePipelineSchema(pipelineBytes)
}
func getSchemaForPipeline(pipelineMap map[string]interface{}) (pipelineSchema *jsonschema.Schema, schemaRef string, err error) {
schemaRef, _ = pipelineMap["$schema"].(string)
return getSchemaForRef(schemaRef)
}
func getSchemaForRef(schemaRef string) (*jsonschema.Schema, string, error) {
if schemaRef == "" {
schemaRef = defaultSchemaRef
}
switch schemaRef {
case pipelineSchemaV1Ref:
pipelineSchema, err := compileSchema(schemaRef, pipelineSchemaV1Content)
return pipelineSchema, schemaRef, err
default:
return nil, "", fmt.Errorf("unsupported schema reference: %s", schemaRef)
}
}
func getVariableRefStepProperties(pipelineSchema *jsonschema.Schema) (map[string]*jsonschema.Schema, error) {
stepProperties := pipelineSchema.Properties["resourceGroups"].Items.(*jsonschema.Schema).Properties["steps"].Items.(*jsonschema.Schema).Properties
variableRefProperties := make(map[string]*jsonschema.Schema)
for propName, propValue := range stepProperties {
if propValue.Ref != nil && strings.HasSuffix(propValue.Ref.Location, "#/definitions/variableRef") {
variableRefProperties[propName] = propValue
}
}
return variableRefProperties, nil
}
func compileSchema(schemaRef string, schemaBytes []byte) (*jsonschema.Schema, error) {
// parse schema content
schemaMap := make(map[string]interface{})
err := json.Unmarshal(schemaBytes, &schemaMap)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal schema content: %v", err)
}
// compile schema
c := jsonschema.NewCompiler()
err = c.AddResource(schemaRef, schemaMap)
if err != nil {
return nil, fmt.Errorf("failed to add schema resource %s: %v", schemaRef, err)
}
pipelineSchema, err := c.Compile(schemaRef)
if err != nil {
return nil, fmt.Errorf("failed to compile schema %s: %v", schemaRef, err)
}
return pipelineSchema, nil
}
func (p *Pipeline) Validate() error {
// collect all steps from all resourcegroups and fail if there are duplicates
stepMap := make(map[string]Step)
for _, rg := range p.ResourceGroups {
for _, step := range rg.Steps {
if _, ok := stepMap[step.StepName()]; ok {
return fmt.Errorf("duplicate step name %q", step.StepName())
}
stepMap[step.StepName()] = step
}
}
// validate dependsOn for a step exists
for _, step := range stepMap {
for _, dep := range step.Dependencies() {
if _, ok := stepMap[dep]; !ok {
return fmt.Errorf("invalid dependency on step %s: dependency %s does not exist", step.StepName(), dep)
}
}
}
// todo check for circular dependencies
// validate resource groups
for _, rg := range p.ResourceGroups {
err := rg.Validate()
if err != nil {
return err
}
}
return nil
}
func (rg *ResourceGroup) Validate() error {
if rg.Name == "" {
return fmt.Errorf("resource group name is required")
}
if rg.Subscription == "" {
return fmt.Errorf("subscription is required")
}
return nil
}