internal/processor/processor.go (291 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package processor
import (
"errors"
"fmt"
"io"
"io/fs"
"path/filepath"
"regexp"
"slices"
"strings"
"github.com/Azure/alzlib/internal/environment"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armpolicy"
)
// These are the file prefixes for the resource types.
const (
architectureDefinitionSuffix = ".+\\.alz_architecture_definition\\.(?:json|yaml|yml)$"
archetypeDefinitionSuffix = ".+\\.alz_archetype_definition\\.(?:json|yaml|yml)$"
archetypeOverrideSuffix = ".+\\.alz_archetype_override\\.(?:json|yaml|yml)$"
policyAssignmentSuffix = ".+\\.alz_policy_assignment\\.(?:json|yaml|yml)$"
policyDefinitionSuffix = ".+\\.alz_policy_definition\\.(?:json|yaml|yml)$"
policySetDefinitionSuffix = ".+\\.alz_policy_set_definition\\.(?:json|yaml|yml)$"
roleDefinitionSuffix = ".+\\.alz_role_definition\\.(?:json|yaml|yml)$"
policyDefaultValueFileName = "^alz_policy_default_values\\.(?:json|yaml|yml)$"
)
const (
alzLibraryMetadataFile = "alz_library_metadata.json"
)
var supportedFileTypes = []string{".json", ".yaml", ".yml"}
var architectureDefinitionRegex = regexp.MustCompile(architectureDefinitionSuffix)
var archetypeDefinitionRegex = regexp.MustCompile(archetypeDefinitionSuffix)
var archetypeOverrideRegex = regexp.MustCompile(archetypeOverrideSuffix)
var policyAssignmentRegex = regexp.MustCompile(policyAssignmentSuffix)
var policyDefinitionRegex = regexp.MustCompile(policyDefinitionSuffix)
var policySetDefinitionRegex = regexp.MustCompile(policySetDefinitionSuffix)
var roleDefinitionRegex = regexp.MustCompile(roleDefinitionSuffix)
var policyDefaultValuesRegex = regexp.MustCompile(policyDefaultValueFileName)
// Result is the structure that gets built by scanning the library files.
type Result struct {
PolicyDefinitions map[string]*armpolicy.Definition
PolicySetDefinitions map[string]*armpolicy.SetDefinition
PolicyAssignments map[string]*armpolicy.Assignment
RoleDefinitions map[string]*armauthorization.RoleDefinition
LibArchetypes map[string]*LibArchetype
LibArchetypeOverrides map[string]*LibArchetypeOverride
LibDefaultPolicyValues map[string]*LibDefaultPolicyValuesDefaults
LibArchitectures map[string]*LibArchitecture
Metadata *LibMetadata
libDefaultPolicyValuesFileProcessed bool
}
func NewResult() *Result {
return &Result{
PolicyDefinitions: make(map[string]*armpolicy.Definition),
PolicySetDefinitions: make(map[string]*armpolicy.SetDefinition),
PolicyAssignments: make(map[string]*armpolicy.Assignment),
RoleDefinitions: make(map[string]*armauthorization.RoleDefinition),
LibArchetypes: make(map[string]*LibArchetype),
LibArchetypeOverrides: make(map[string]*LibArchetypeOverride),
LibDefaultPolicyValues: make(map[string]*LibDefaultPolicyValuesDefaults),
LibArchitectures: make(map[string]*LibArchitecture),
Metadata: nil,
libDefaultPolicyValuesFileProcessed: false,
}
}
// processFunc is the function signature that is used to process different types of lib file.
type processFunc func(result *Result, data unmarshaler) error
// ProcessorClient is the client that is used to process the library files.
type ProcessorClient struct {
fs fs.FS
}
func NewProcessorClient(fs fs.FS) *ProcessorClient {
return &ProcessorClient{
fs: fs,
}
}
// Metadata returns the metadata of the library.
func (client *ProcessorClient) Metadata() (*LibMetadata, error) {
metadataFile, err := client.fs.Open(alzLibraryMetadataFile)
var pe *fs.PathError
if errors.As(err, &pe) {
return &LibMetadata{
Name: "",
DisplayName: "",
Description: "",
Path: "",
Dependencies: make([]LibMetadataDependency, 0),
}, nil
}
if err != nil {
return nil, fmt.Errorf("ProcessorClient.Metadata: error opening metadata file: %w", err)
}
defer metadataFile.Close() // nolint: errcheck
data, err := io.ReadAll(metadataFile)
if err != nil {
return nil, fmt.Errorf("ProcessorClient.Metadata: error reading metadata file: %w", err)
}
unmar := newUnmarshaler(data, ".json")
metadata := new(LibMetadata)
err = unmar.unmarshal(metadata)
if err != nil {
return nil, fmt.Errorf("ProcessorClient.Metadata: error unmarshaling metadata: %w", err)
}
for _, dep := range metadata.Dependencies {
switch {
case dep.Path != "" && dep.Ref != "" && dep.CustomUrl == "":
continue
case dep.Path == "" && dep.Ref == "" && dep.CustomUrl != "":
continue
default:
return nil, fmt.Errorf("ProcessorClient.Metadata: invalid dependency, either path & ref should be set, or custom_url: %v", dep)
}
}
return metadata, nil
}
// Process reads the library files and processes them into a Result.
// Pass in a pointer to a Result struct to store the processed data,
// create a new *Result with NewResult().
func (client *ProcessorClient) Process(res *Result) error {
// Open the metadata file and store contents in the result
metad, err := client.Metadata()
if err != nil {
return fmt.Errorf("ProcessorClient.Process: error getting metadata: %w", err)
}
res.Metadata = metad
// Walk the embedded lib FS and process files
if err := fs.WalkDir(client.fs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("ProcessorClient.Process: error walking directory %s: %w", path, err)
}
// Skip directories
if d.IsDir() {
return nil
}
// Skip files where path contains base of the `ALZLIB_DIR`.
alzLibDirBase := filepath.Base(environment.AlzLibDir())
if strings.Contains(path, alzLibDirBase) {
return nil
}
// Skip files that are not json or yaml
if !slices.Contains(supportedFileTypes, strings.ToLower(filepath.Ext(path))) {
return nil
}
file, err := client.fs.Open(path)
if err != nil {
return fmt.Errorf("ProcessorClient.Process: error opening file %s: %w", path, err)
}
return classifyLibFile(res, file, d.Name())
}); err != nil {
return err
}
return nil
}
// classifyLibFile identifies the supplied file and adds calls the appropriate processFunc.
func classifyLibFile(res *Result, file fs.File, name string) error {
err := error(nil)
// process by file type
switch n := strings.ToLower(name); {
// if the file is a policy definition
case policyDefinitionRegex.MatchString(n):
err = readAndProcessFile(res, file, processPolicyDefinition)
// if the file is a policy set definition
case policySetDefinitionRegex.MatchString(n):
err = readAndProcessFile(res, file, processPolicySetDefinition)
// if the file is a policy assignment
case policyAssignmentRegex.MatchString(n):
err = readAndProcessFile(res, file, processPolicyAssignment)
// if the file is a role definition
case roleDefinitionRegex.MatchString(n):
err = readAndProcessFile(res, file, processRoleDefinition)
// if the file is an archetype definition
case archetypeDefinitionRegex.MatchString(n):
err = readAndProcessFile(res, file, processArchetype)
// if the file is an archetype override
case archetypeOverrideRegex.MatchString(n):
err = readAndProcessFile(res, file, processArchetypeOverride)
// if the file is an policy default values file
case policyDefaultValuesRegex.MatchString(n):
err = readAndProcessFile(res, file, processDefaultPolicyValue)
}
// if the file is an architecture definition
if architectureDefinitionRegex.MatchString(name) {
err = readAndProcessFile(res, file, processArchitecture)
}
if err != nil {
err = fmt.Errorf("classifyLibFile: error processing file: %w", err)
}
return err
}
// processArchitecture is a processFunc that reads the default_policy_values
// bytes, processes, then adds the created processArchitecture to the result.
func processArchitecture(res *Result, unmar unmarshaler) error {
arch := new(LibArchitecture)
if err := unmar.unmarshal(arch); err != nil {
return fmt.Errorf("processArchitecture: error unmarshaling: %w", err)
}
if arch.Name == "" {
return fmt.Errorf("processArchitecture: architecture name is empty")
}
if _, exists := res.LibArchitectures[arch.Name]; exists {
return fmt.Errorf("processArchitecture: architecture with name `%s` already exists", arch.Name)
}
res.LibArchitectures[arch.Name] = arch
return nil
}
// processDefaultPolicyValue is a processFunc that reads the default_policy_value
// bytes, processes, then adds the created LibDefaultPolicyValues to the result.
func processDefaultPolicyValue(res *Result, unmar unmarshaler) error {
if res.libDefaultPolicyValuesFileProcessed {
return fmt.Errorf("processDefaultPolicyValues: multiple default policy values files found, only one is allowed")
}
lpv := new(LibDefaultPolicyValues)
if err := unmar.unmarshal(lpv); err != nil {
return fmt.Errorf("processDefaultPolicyValues: error unmarshaling: %w", err)
}
for _, def := range lpv.Defaults {
if _, exists := res.LibDefaultPolicyValues[def.DefaultName]; exists {
return fmt.Errorf("processDefaultPolicyValues: default policy values with name `%s` already exists", def.DefaultName)
}
res.LibDefaultPolicyValues[def.DefaultName] = &def
}
res.libDefaultPolicyValuesFileProcessed = true
return nil
}
// processArchetype is a processFunc that reads the archetype_definition
// bytes, processes, then adds the created LibArchetype to the result.
func processArchetype(res *Result, unmar unmarshaler) error {
la := new(LibArchetype)
if err := unmar.unmarshal(la); err != nil {
return fmt.Errorf("processArchetype: error unmarshaling: %w", err)
}
if _, exists := res.LibArchetypes[la.Name]; exists {
return fmt.Errorf("processArchetype: archetype with name `%s` already exists", la.Name)
}
res.LibArchetypes[la.Name] = la
return nil
}
// processArchetypeOverride is a processFunc that reads the archetype_override
// bytes, processes, then adds the created LibArchetypeOverride to the result.
func processArchetypeOverride(res *Result, unmar unmarshaler) error {
lao := new(LibArchetypeOverride)
if err := unmar.unmarshal(lao); err != nil {
return fmt.Errorf("processArchetypeOverride: error unmarshaling: %w", err)
}
if _, exists := res.LibArchetypeOverrides[lao.Name]; exists {
return fmt.Errorf("processArchetypeOverride: archetype override with name `%s` already exists", lao.Name)
}
res.LibArchetypeOverrides[lao.Name] = lao
return nil
}
// processPolicyAssignment is a processFunc that reads the policy_assignment
// bytes, processes, then adds the created armpolicy.Assignment to the result.
func processPolicyAssignment(res *Result, unmar unmarshaler) error {
pa := new(armpolicy.Assignment)
if err := unmar.unmarshal(pa); err != nil {
return fmt.Errorf("processPolicyAssignment: error unmarshaling: %w", err)
}
if pa.Name == nil || *pa.Name == "" {
return fmt.Errorf("processPolicyAssignment: policy assignment name is empty or not present")
}
if _, exists := res.PolicyAssignments[*pa.Name]; exists {
return fmt.Errorf("processPolicyAssignment: policy assignment with name `%s` already exists", *pa.Name)
}
res.PolicyAssignments[*pa.Name] = pa
return nil
}
// processPolicyAssignment is a processFunc that reads the policy_definition
// bytes, processes, then adds the created armpolicy.Definition to the result.
func processPolicyDefinition(res *Result, unmar unmarshaler) error {
pd := new(armpolicy.Definition)
if err := unmar.unmarshal(pd); err != nil {
return fmt.Errorf("processPolicyDefinition: error unmarshaling: %w", err)
}
if pd.Name == nil || *pd.Name == "" {
return fmt.Errorf("processPolicyDefinition: policy definition name is empty or not present")
}
if _, exists := res.PolicyDefinitions[*pd.Name]; exists {
return fmt.Errorf("processPolicyDefinition: policy definition with name `%s` already exists", *pd.Name)
}
res.PolicyDefinitions[*pd.Name] = pd
return nil
}
// processPolicyAssignment is a processFunc that reads the policy_set_definition
// bytes, processes, then adds the created armpolicy.SetDefinition to the result.
func processPolicySetDefinition(res *Result, unmar unmarshaler) error {
psd := new(armpolicy.SetDefinition)
if err := unmar.unmarshal(psd); err != nil {
return fmt.Errorf("processPolicySetDefinition: error unmarshaling: %w", err)
}
if psd.Name == nil || *psd.Name == "" {
return fmt.Errorf("processPolicySetDefinition: policy set definition name is empty or not present")
}
if _, exists := res.PolicySetDefinitions[*psd.Name]; exists {
return fmt.Errorf("processPolicySetDefinition: policy set definition with name `%s` already exists", *psd.Name)
}
res.PolicySetDefinitions[*psd.Name] = psd
return nil
}
// processRoleDefinition is a processFunc that reads the role_definition
// bytes, processes, then adds the created armauthorization.RoleDefinition to the result.
// We use Properties.RoleName as the key in the result map, as the GUID must be unique and a role definition may be deployed at multiple scopes.
func processRoleDefinition(res *Result, unmar unmarshaler) error {
rd := new(armauthorization.RoleDefinition)
if err := unmar.unmarshal(rd); err != nil {
return fmt.Errorf("processRoleDefinition: error unmarshalling: %w", err)
}
if rd.Properties == nil || rd.Properties.RoleName == nil || *rd.Properties.RoleName == "" {
return fmt.Errorf("processRoleDefinition: role definition role name is empty or not present")
}
if _, exists := res.PolicySetDefinitions[*rd.Properties.RoleName]; exists {
return fmt.Errorf("processRoleDefinition: role definition with role name `%s` already exists", *rd.Properties.RoleName)
}
// Use roleName here not the name, which is a GUID
res.RoleDefinitions[*rd.Properties.RoleName] = rd
return nil
}
// readAndProcessFile reads the file bytes at the supplied path and processes it using the supplied processFunc.
func readAndProcessFile(res *Result, file fs.File, processFn processFunc) error {
s, err := file.Stat()
if err != nil {
return err
}
data := make([]byte, s.Size())
defer file.Close() // nolint: errcheck
if _, err := file.Read(data); err != nil {
return err
}
ext := filepath.Ext(s.Name())
// create a new unmarshaler
unmar := newUnmarshaler(data, ext)
// pass the data to the supplied process function
if err := processFn(res, unmar); err != nil {
return err
}
return nil
}