sigs.k8s.io/controller-tools@v0.15.1-0.20240515195456-85686cb69316/pkg/genall/options.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package genall
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  
    23  	"sigs.k8s.io/controller-tools/pkg/markers"
    24  )
    25  
    26  var (
    27  	InputPathsMarker = markers.Must(markers.MakeDefinition("paths", markers.DescribesPackage, InputPaths(nil)))
    28  )
    29  
    30  // +controllertools:marker:generateHelp:category=""
    31  
    32  // InputPaths represents paths and go-style path patterns to use as package roots.
    33  //
    34  // Multiple paths can be specified using "{path1, path2, path3}".
    35  type InputPaths []string
    36  
    37  // RegisterOptionsMarkers registers "mandatory" options markers for FromOptions into the given registry.
    38  // At this point, that's just InputPaths.
    39  func RegisterOptionsMarkers(into *markers.Registry) error {
    40  	if err := into.Register(InputPathsMarker); err != nil {
    41  		return err
    42  	}
    43  	// NB(directxman12): we make this optional so we don't have a bootstrap problem with helpgen
    44  	if helpGiver, hasHelp := ((interface{})(InputPaths(nil))).(HasHelp); hasHelp {
    45  		into.AddHelp(InputPathsMarker, helpGiver.Help())
    46  	}
    47  	return nil
    48  }
    49  
    50  // RegistryFromOptions produces just the marker registry that would be used by FromOptions, without
    51  // attempting to produce a full Runtime.  This can be useful if you want to display help without
    52  // trying to load roots.
    53  func RegistryFromOptions(optionsRegistry *markers.Registry, options []string) (*markers.Registry, error) {
    54  	protoRt, err := protoFromOptions(optionsRegistry, options)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	reg := &markers.Registry{}
    59  	if err := protoRt.Generators.RegisterMarkers(reg); err != nil {
    60  		return nil, err
    61  	}
    62  	return reg, nil
    63  }
    64  
    65  // FromOptions parses the options from markers stored in the given registry out into a runtime.
    66  // The markers in the registry must be either
    67  //
    68  // a) Generators
    69  // b) OutputRules
    70  // c) InputPaths
    71  //
    72  // The paths specified in InputPaths are loaded as package roots, and the combined with
    73  // the generators and the specified output rules to produce a runtime that can be run or
    74  // further modified.  Not default generators are used if none are specified -- you can check
    75  // the output and rerun for that.
    76  func FromOptions(optionsRegistry *markers.Registry, options []string) (*Runtime, error) {
    77  	protoRt, err := protoFromOptions(optionsRegistry, options)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	// make the runtime
    83  	genRuntime, err := protoRt.Generators.ForRoots(protoRt.Paths...)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	// attempt to figure out what the user wants without a lot of verbose specificity:
    89  	// if the user specifies a default rule, assume that they probably want to fall back
    90  	// to that.  Otherwise, assume that they just wanted to customize one option from the
    91  	// set, and leave the rest in the standard configuration.
    92  	if protoRt.OutputRules.Default != nil {
    93  		genRuntime.OutputRules = protoRt.OutputRules
    94  		return genRuntime, nil
    95  	}
    96  
    97  	outRules := DirectoryPerGenerator("config", protoRt.GeneratorsByName)
    98  	for gen, rule := range protoRt.OutputRules.ByGenerator {
    99  		outRules.ByGenerator[gen] = rule
   100  	}
   101  
   102  	genRuntime.OutputRules = outRules
   103  	return genRuntime, nil
   104  }
   105  
   106  // protoFromOptions returns a proto-Runtime from the given options registry and
   107  // options set.  This can then be used to construct an actual Runtime.  See the
   108  // FromOptions function for more details about how the options work.
   109  func protoFromOptions(optionsRegistry *markers.Registry, options []string) (protoRuntime, error) {
   110  	var gens Generators
   111  	rules := OutputRules{
   112  		ByGenerator: make(map[*Generator]OutputRule),
   113  	}
   114  	var paths []string
   115  
   116  	// collect the generators first, so that we can key the output on the actual
   117  	// generator, which matters if there's settings in the gen object and it's not a pointer.
   118  	outputByGen := make(map[string]OutputRule)
   119  	gensByName := make(map[string]*Generator)
   120  
   121  	for _, rawOpt := range options {
   122  		if rawOpt[0] != '+' {
   123  			rawOpt = "+" + rawOpt // add a `+` to make it acceptable for usage with the registry
   124  		}
   125  		defn := optionsRegistry.Lookup(rawOpt, markers.DescribesPackage)
   126  		if defn == nil {
   127  			return protoRuntime{}, fmt.Errorf("unknown option %q", rawOpt[1:])
   128  		}
   129  
   130  		val, err := defn.Parse(rawOpt)
   131  		if err != nil {
   132  			return protoRuntime{}, fmt.Errorf("unable to parse option %q: %w", rawOpt[1:], err)
   133  		}
   134  
   135  		switch val := val.(type) {
   136  		case Generator:
   137  			gens = append(gens, &val)
   138  			if _, alreadyExists := gensByName[defn.Name]; alreadyExists {
   139  				return protoRuntime{}, fmt.Errorf("multiple instances of '%s' generator specified", defn.Name)
   140  			}
   141  			gensByName[defn.Name] = &val
   142  		case OutputRule:
   143  			_, genName := splitOutputRuleOption(defn.Name)
   144  			if genName == "" {
   145  				// it's a default rule
   146  				rules.Default = val
   147  				continue
   148  			}
   149  
   150  			outputByGen[genName] = val
   151  			continue
   152  		case InputPaths:
   153  			paths = append(paths, val...)
   154  		default:
   155  			return protoRuntime{}, fmt.Errorf("unknown option marker %q", defn.Name)
   156  		}
   157  	}
   158  
   159  	// actually associate the rules now that we know the generators
   160  	for genName, outputRule := range outputByGen {
   161  		gen, knownGen := gensByName[genName]
   162  		if !knownGen {
   163  			return protoRuntime{}, fmt.Errorf("non-invoked generator %q", genName)
   164  		}
   165  
   166  		rules.ByGenerator[gen] = outputRule
   167  	}
   168  
   169  	return protoRuntime{
   170  		Paths:            paths,
   171  		Generators:       Generators(gens),
   172  		OutputRules:      rules,
   173  		GeneratorsByName: gensByName,
   174  	}, nil
   175  }
   176  
   177  // protoRuntime represents the raw pieces needed to compose a runtime, as
   178  // parsed from some options.
   179  type protoRuntime struct {
   180  	Paths            []string
   181  	Generators       Generators
   182  	OutputRules      OutputRules
   183  	GeneratorsByName map[string]*Generator
   184  }
   185  
   186  // splitOutputRuleOption splits a marker name of "output:rule:gen" or "output:rule"
   187  // into its compontent rule and generator name.
   188  func splitOutputRuleOption(name string) (ruleName string, genName string) {
   189  	parts := strings.SplitN(name, ":", 3)
   190  	if len(parts) == 3 {
   191  		// output:<generator>:<rule>
   192  		return parts[2], parts[1]
   193  	}
   194  	// output:<rule>
   195  	return parts[1], ""
   196  }