v2/tools/generator/internal/config/configuration.go (225 lines of code) (raw):
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/
package config
import (
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"github.com/rotisserie/eris"
"golang.org/x/mod/modfile"
"gopkg.in/yaml.v3"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/astmodel"
)
type GenerationPipeline string
const (
GenerationPipelineAzure = GenerationPipeline("azure")
GenerationPipelineCrossplane = GenerationPipeline("crossplane")
)
// Configuration is used to control which types get generated
type Configuration struct {
// Where to load Swagger schemas from
SchemaRoot string `yaml:"schemaRoot"`
// Information about where to locate status (Swagger) files
Status StatusConfiguration `yaml:"status"`
// The pipeline that should be used for code generation
Pipeline GenerationPipeline `yaml:"pipeline"`
// The path to the go.mod file where the code will be generated
DestinationGoModuleFile string `yaml:"destinationGoModuleFile"`
// The folder relative to the go.mod file path where the code should be generated
TypesOutputPath string `yaml:"typesOutputPath"`
// The file relative to the go.mod file path where registration of the Go types should be generated. If omitted, this step is skipped.
TypeRegistrationOutputFile string `yaml:"typeRegistrationOutputFile"`
// AnyTypePackages lists packages which we expect to generate
// interface{} fields.
AnyTypePackages []string `yaml:"anyTypePackages"`
// Filters used to control which types are created from the JSON schema
TypeFilters []*TypeFilter `yaml:"typeFilters"`
// Renamers used to resolve naming collisions during loading
TypeLoaderRenames []*TypeLoaderRename `yaml:"typeLoaderRenames"`
// Transformers used to remap types
Transformers []*TypeTransformer `yaml:"typeTransformers"`
// RootURL is the root URL for ASOv2 repo, paths are appended to this to generate resource links.
RootURL string `yaml:"rootUrl"`
// SamplesPath is the Path the samples are accessible at. This is used to walk through the samples directory and generate sample links.
SamplesPath string `yaml:"samplesPath"`
// EmitDocFiles is used as a signal to create doc.go files for packages. If omitted, default is false.
EmitDocFiles bool `yaml:"emitDocFiles"`
// Destination file and additional information for our supported resources report
SupportedResourcesReport *SupportedResourcesReport `yaml:"supportedResourcesReport"`
// Additional information about our object model
ObjectModelConfiguration *ObjectModelConfiguration `yaml:"objectModelConfiguration"`
goModulePath string
}
type RewriteRule struct {
From string `yaml:"from"`
To string `yaml:"to"`
}
func (config *Configuration) LocalPathPrefix() string {
return path.Join(config.goModulePath, config.TypesOutputPath)
}
func (config *Configuration) FullTypesOutputPath() string {
return filepath.Join(
filepath.Dir(config.DestinationGoModuleFile),
config.TypesOutputPath)
}
func (config *Configuration) FullTypesRegistrationOutputFilePath() string {
if config.TypeRegistrationOutputFile == "" {
return ""
}
return filepath.Join(
filepath.Dir(config.DestinationGoModuleFile),
config.TypeRegistrationOutputFile)
}
func (config *Configuration) FullSamplesPath() string {
if filepath.IsAbs(config.SamplesPath) {
return config.SamplesPath
}
if config.DestinationGoModuleFile != "" {
return filepath.Join(
filepath.Dir(config.DestinationGoModuleFile),
config.SamplesPath)
}
result, err := filepath.Abs(config.SamplesPath)
if err != nil {
panic(fmt.Sprintf("unable to make %q absolute: %s", result, err))
}
return result
}
func (config *Configuration) GetTypeFiltersError() error {
for _, filter := range config.TypeFilters {
if err := filter.RequiredTypesWereMatched(); err != nil {
return eris.Wrapf(err, "type filter action: %q", filter.Action)
}
}
return nil
}
func (config *Configuration) GetTransformersError() error {
for _, filter := range config.Transformers {
if err := filter.RequiredTypesWereMatched(); err != nil {
return eris.Wrap(err, "type transformer")
}
if filter.Property.IsRestrictive() {
if err := filter.RequiredTypesWereMatched(); err != nil {
return eris.Wrap(err, "type transformer property")
}
}
}
return nil
}
func (config *Configuration) SetGoModulePath(path string) {
config.goModulePath = path
}
// NewConfiguration returns a new empty Configuration
func NewConfiguration() *Configuration {
result := &Configuration{
ObjectModelConfiguration: NewObjectModelConfiguration(),
}
result.SupportedResourcesReport = NewSupportedResourcesReport(result)
return result
}
// LoadConfiguration loads a `Configuration` from the specified file
func LoadConfiguration(configurationFile string) (*Configuration, error) {
f, err := os.Open(configurationFile)
if err != nil {
return nil, err
}
// MUST use this to ensure that ObjectModelConfiguration is instantiated correctly
// TODO: split Configuration struct so that domain model is not used for serialization!
result := NewConfiguration()
decoder := yaml.NewDecoder(f)
decoder.KnownFields(true) // Error on unknown fields
err = decoder.Decode(result)
if err != nil {
return nil, eris.Wrapf(err, "configuration file loaded from %q is not valid YAML", configurationFile)
}
err = result.initialize(configurationFile)
if err != nil {
return nil, eris.Wrapf(err, "configuration file loaded from %q is invalid", configurationFile)
}
return result, nil
}
// ShouldPruneResult is returned by ShouldPrune to indicate whether the supplied type should be exported
type ShouldPruneResult string
const (
// Include indicates the specified type should be included in the type graph
Include ShouldPruneResult = "include"
// Prune indicates the type (and all types only referenced by it) should be pruned from the type graph
Prune ShouldPruneResult = "prune"
)
// initialize checks for common errors and initializes structures inside the configuration
// which need additional setup after json deserialization
func (config *Configuration) initialize(configPath string) error {
if config.SchemaRoot == "" {
return eris.New("SchemaRoot missing")
}
absConfigLocation, err := filepath.Abs(configPath)
if err != nil {
return eris.Wrapf(err, "unable to find absolute config file location")
}
configDirectory := filepath.Dir(absConfigLocation)
// resolve SchemaRoot relative to config file directory
config.SchemaRoot = filepath.Join(configDirectory, config.SchemaRoot)
if config.TypesOutputPath == "" {
// Default to an apis folder if not specified
config.TypesOutputPath = "apis"
}
// Trim any trailing slashes on the output path
config.TypesOutputPath = strings.TrimSuffix(config.TypesOutputPath, "/")
var errs []error
if config.Pipeline == "" {
// Default to the standard Azure pipeline
config.Pipeline = GenerationPipelineAzure
} else {
switch pipeline := strings.ToLower(string(config.Pipeline)); pipeline {
case string(GenerationPipelineAzure):
config.Pipeline = GenerationPipelineAzure
case string(GenerationPipelineCrossplane):
config.Pipeline = GenerationPipelineCrossplane
default:
errs = append(errs, eris.Errorf("unknown pipeline kind %s", config.Pipeline))
}
}
if config.DestinationGoModuleFile == "" {
errs = append(errs, eris.Errorf("destination Go module must be specified"))
}
// Ensure config.DestinationGoModuleFile is a fully qualified path
if !filepath.IsAbs(config.DestinationGoModuleFile) {
config.DestinationGoModuleFile = filepath.Join(configDirectory, config.DestinationGoModuleFile)
}
modPath, err := getModulePathFromModFile(config.DestinationGoModuleFile)
if err != nil {
errs = append(errs, err)
} else {
config.goModulePath = modPath
}
return kerrors.NewAggregate(errs)
}
func absDirectoryPathToURL(path string) *url.URL {
if strings.Contains(path, "\\") {
// assume it's a Windows path:
// fixup to work in URI
path = "/" + strings.ReplaceAll(path, "\\", "/")
}
result, err := url.Parse("file://" + path + "/")
if err != nil {
panic(err)
}
return result
}
// ShouldPrune tests for whether a given type should be extracted from the JSON schema or pruned
func (config *Configuration) ShouldPrune(typeName astmodel.InternalTypeName) (result ShouldPruneResult, because string) {
for _, f := range config.TypeFilters {
if f.AppliesToType(typeName) {
switch f.Action {
case TypeFilterPrune:
return Prune, f.Because
case TypeFilterInclude:
return Include, f.Because
default:
panic(eris.Errorf("unknown typefilter directive: %s", f.Action))
}
}
}
// If the type comes from a group that we don't expect, prune it.
// We don't also check for whether the version is expected because it's common for types to be shared
// between versions of an API. While end up pulling them into the package alongside the resource, at this
// point we haven't done that yet, so it's premature to filter by version.
// Sometimes in testing, configuration will be empty, and we don't want to do any filtering when that's the case
if !config.ObjectModelConfiguration.IsEmpty() &&
!config.ObjectModelConfiguration.IsGroupConfigured(typeName.InternalPackageReference()) {
return Prune, fmt.Sprintf(
"No resources configured for export from %s", typeName.InternalPackageReference().PackagePath())
}
// By default, we include all types
return Include, ""
}
// MakeLocalPackageReference creates a local package reference based on the configured destination location
func (config *Configuration) MakeLocalPackageReference(group string, version string) astmodel.LocalPackageReference {
return astmodel.MakeLocalPackageReference(config.LocalPathPrefix(), group, astmodel.GeneratorVersion, version)
}
func getModulePathFromModFile(modFilePath string) (string, error) {
// Calculate the actual destination
modFileData, err := os.ReadFile(modFilePath)
if err != nil {
return "", err
}
modPath := modfile.ModulePath(modFileData)
if modPath == "" {
return "", eris.Errorf("couldn't find module path in mod file %s", modFilePath)
}
return modPath, nil
}
// StatusConfiguration provides configuration options for the
// status parts of resources, which are generated from the
// Azure Swagger specs.
type StatusConfiguration struct {
// Custom per-group configuration
Overrides []SchemaOverride `yaml:"overrides"`
}
// SchemaOverride provides configuration to override namespaces (groups)
// this is used (for example) to distinguish Microsoft.Network.Frontdoor
// from Microsoft.Network, even though both use Microsoft.Network in
// their Swagger specs.
type SchemaOverride struct {
// The root for this group (relative to SchemaRoot)
BasePath string `yaml:"basePath"`
// A specific namespace (group name, in our domain language)
Namespace string `yaml:"namespace"`
// A suffix to add on to the group name
Suffix string `yaml:"suffix"`
// We don't use this now
ResourceConfig []ResourceConfig `yaml:"resourceConfig"`
// We don't use this now
PostProcessor string `yaml:"postProcessor"`
}
type ResourceConfig struct {
Type string `yaml:"type"`
// TODO: Not sure that this datatype should be string, but we don't use it right now so keeping it as
// TODO: string for simplicity
Scopes string `yaml:"scopes"`
}