sigs.k8s.io/controller-tools@v0.15.1-0.20240515195456-85686cb69316/pkg/genall/genall.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  	"encoding/json"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  
    25  	"golang.org/x/tools/go/packages"
    26  	rawyaml "gopkg.in/yaml.v2"
    27  
    28  	"sigs.k8s.io/controller-tools/pkg/loader"
    29  	"sigs.k8s.io/controller-tools/pkg/markers"
    30  )
    31  
    32  // Generators are a list of Generators.
    33  // NB(directxman12): this is a pointer so that we can uniquely identify each
    34  // instance of a generator, even if it's not hashable.  Different *instances*
    35  // of a generator are treated differently.
    36  type Generators []*Generator
    37  
    38  // RegisterMarkers registers all markers defined by each of the Generators in
    39  // this list into the given registry.
    40  func (g Generators) RegisterMarkers(reg *markers.Registry) error {
    41  	for _, gen := range g {
    42  		if err := (*gen).RegisterMarkers(reg); err != nil {
    43  			return err
    44  		}
    45  	}
    46  	return nil
    47  }
    48  
    49  // CheckFilters returns the set of NodeFilters for all Generators that
    50  // implement NeedsTypeChecking.
    51  func (g Generators) CheckFilters() []loader.NodeFilter {
    52  	var filters []loader.NodeFilter
    53  	for _, gen := range g {
    54  		withFilter, needsChecking := (*gen).(NeedsTypeChecking)
    55  		if !needsChecking {
    56  			continue
    57  		}
    58  		filters = append(filters, withFilter.CheckFilter())
    59  	}
    60  	return filters
    61  }
    62  
    63  // NeedsTypeChecking indicates that a particular generator needs & has opinions
    64  // on typechecking.  If this is not implemented, a generator will be given a
    65  // context with a nil typechecker.
    66  type NeedsTypeChecking interface {
    67  	// CheckFilter indicates the loader.NodeFilter (if any) that should be used
    68  	// to prune out unused types/packages when type-checking (nodes for which
    69  	// the filter returns true are considered "interesting").  This filter acts
    70  	// as a baseline -- all types the pass through this filter will be checked,
    71  	// but more than that may also be checked due to other generators' filters.
    72  	CheckFilter() loader.NodeFilter
    73  }
    74  
    75  // Generator knows how to register some set of markers, and then produce
    76  // output artifacts based on loaded code containing those markers,
    77  // sharing common loaded data.
    78  type Generator interface {
    79  	// RegisterMarkers registers all markers needed by this Generator
    80  	// into the given registry.
    81  	RegisterMarkers(into *markers.Registry) error
    82  	// Generate generates artifacts produced by this marker.
    83  	// It's called *after* RegisterMarkers has been called.
    84  	Generate(*GenerationContext) error
    85  }
    86  
    87  // HasHelp is some Generator, OutputRule, etc with a help method.
    88  type HasHelp interface {
    89  	// Help returns help for this generator.
    90  	Help() *markers.DefinitionHelp
    91  }
    92  
    93  // Runtime collects generators, loaded program data (Collector, root Packages),
    94  // and I/O rules, running them together.
    95  type Runtime struct {
    96  	// Generators are the Generators to be run by this Runtime.
    97  	Generators Generators
    98  	// GenerationContext is the base generation context that's copied
    99  	// to produce the context for each Generator.
   100  	GenerationContext
   101  	// OutputRules defines how to output artifacts for each Generator.
   102  	OutputRules OutputRules
   103  	// ErrorWriter defines where to write error messages.
   104  	ErrorWriter io.Writer
   105  }
   106  
   107  // GenerationContext defines the common information needed for each Generator
   108  // to run.
   109  type GenerationContext struct {
   110  	// Collector is the shared marker collector.
   111  	Collector *markers.Collector
   112  	// Roots are the base packages to be processed.
   113  	Roots []*loader.Package
   114  	// Checker is the shared partial type-checker.
   115  	Checker *loader.TypeChecker
   116  	// OutputRule describes how to output artifacts.
   117  	OutputRule
   118  	// InputRule describes how to load associated boilerplate artifacts.
   119  	// It should *not* be used to load source files.
   120  	InputRule
   121  }
   122  
   123  // WriteYAMLOptions implements the Options Pattern for WriteYAML.
   124  type WriteYAMLOptions struct {
   125  	transform func(obj map[string]interface{}) error
   126  }
   127  
   128  // WithTransform applies a transformation to objects just before writing them.
   129  func WithTransform(transform func(obj map[string]interface{}) error) *WriteYAMLOptions {
   130  	return &WriteYAMLOptions{
   131  		transform: transform,
   132  	}
   133  }
   134  
   135  // TransformRemoveCreationTimestamp ensures we do not write the metadata.creationTimestamp field.
   136  func TransformRemoveCreationTimestamp(obj map[string]interface{}) error {
   137  	metadata := obj["metadata"].(map[interface{}]interface{})
   138  	delete(metadata, "creationTimestamp")
   139  	return nil
   140  }
   141  
   142  // WriteYAML writes the given objects out, serialized as YAML, using the
   143  // context's OutputRule.  Objects are written as separate documents, separated
   144  // from each other by `---` (as per the YAML spec).
   145  func (g GenerationContext) WriteYAML(itemPath, headerText string, objs []interface{}, options ...*WriteYAMLOptions) error {
   146  	out, err := g.Open(nil, itemPath)
   147  	if err != nil {
   148  		return err
   149  	}
   150  	defer out.Close()
   151  
   152  	_, err = out.Write([]byte(headerText))
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	for _, obj := range objs {
   158  		yamlContent, err := yamlMarshal(obj, options...)
   159  		if err != nil {
   160  			return err
   161  		}
   162  		n, err := out.Write(append([]byte("---\n"), yamlContent...))
   163  		if err != nil {
   164  			return err
   165  		}
   166  		if n < len(yamlContent) {
   167  			return io.ErrShortWrite
   168  		}
   169  	}
   170  
   171  	return nil
   172  }
   173  
   174  // yamlMarshal is based on sigs.k8s.io/yaml.Marshal, but allows for transforming the final data before writing.
   175  func yamlMarshal(o interface{}, options ...*WriteYAMLOptions) ([]byte, error) {
   176  	j, err := json.Marshal(o)
   177  	if err != nil {
   178  		return nil, fmt.Errorf("error marshaling into JSON: %v", err)
   179  	}
   180  
   181  	return yamlJSONToYAMLWithFilter(j, options...)
   182  }
   183  
   184  // yamlJSONToYAMLWithFilter is based on sigs.k8s.io/yaml.JSONToYAML, but allows for transforming the final data before writing.
   185  func yamlJSONToYAMLWithFilter(j []byte, options ...*WriteYAMLOptions) ([]byte, error) {
   186  	// Convert the JSON to an object.
   187  	var jsonObj map[string]interface{}
   188  	// We are using yaml.Unmarshal here (instead of json.Unmarshal) because the
   189  	// Go JSON library doesn't try to pick the right number type (int, float,
   190  	// etc.) when unmarshalling to interface{}, it just picks float64
   191  	// universally. go-yaml does go through the effort of picking the right
   192  	// number type, so we can preserve number type throughout this process.
   193  	if err := rawyaml.Unmarshal(j, &jsonObj); err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	for _, option := range options {
   198  		if option.transform != nil {
   199  			if err := option.transform(jsonObj); err != nil {
   200  				return nil, err
   201  			}
   202  		}
   203  	}
   204  
   205  	// Marshal this object into YAML.
   206  	return rawyaml.Marshal(jsonObj)
   207  }
   208  
   209  // ReadFile reads the given boilerplate artifact using the context's InputRule.
   210  func (g GenerationContext) ReadFile(path string) ([]byte, error) {
   211  	file, err := g.OpenForRead(path)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	defer file.Close()
   216  	return io.ReadAll(file)
   217  }
   218  
   219  // ForRoots produces a Runtime to run the given generators against the
   220  // given packages.  It outputs to /dev/null by default.
   221  func (g Generators) ForRoots(rootPaths ...string) (*Runtime, error) {
   222  	roots, err := loader.LoadRoots(rootPaths...)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	rt := &Runtime{
   227  		Generators: g,
   228  		GenerationContext: GenerationContext{
   229  			Collector: &markers.Collector{
   230  				Registry: &markers.Registry{},
   231  			},
   232  			Roots:     roots,
   233  			InputRule: InputFromFileSystem,
   234  			Checker: &loader.TypeChecker{
   235  				NodeFilters: g.CheckFilters(),
   236  			},
   237  		},
   238  		OutputRules: OutputRules{Default: OutputToNothing},
   239  	}
   240  	if err := rt.Generators.RegisterMarkers(rt.Collector.Registry); err != nil {
   241  		return nil, err
   242  	}
   243  	return rt, nil
   244  }
   245  
   246  // Run runs the Generators in this Runtime against its packages, printing
   247  // errors (except type errors, which common result from using TypeChecker with
   248  // filters), returning true if errors were found.
   249  func (r *Runtime) Run() bool {
   250  	// TODO(directxman12): we could make this parallel,
   251  	// but we'd need to ensure all underlying machinery is threadsafe
   252  
   253  	if r.ErrorWriter == nil {
   254  		r.ErrorWriter = os.Stderr
   255  	}
   256  	if len(r.Generators) == 0 {
   257  		fmt.Fprintln(r.ErrorWriter, "no generators to run")
   258  		return true
   259  	}
   260  
   261  	hadErrs := false
   262  	for _, gen := range r.Generators {
   263  		ctx := r.GenerationContext // make a shallow copy
   264  		ctx.OutputRule = r.OutputRules.ForGenerator(gen)
   265  
   266  		// don't pass a typechecker to generators that don't provide a filter
   267  		// to avoid accidents
   268  		if _, needsChecking := (*gen).(NeedsTypeChecking); !needsChecking {
   269  			ctx.Checker = nil
   270  		}
   271  
   272  		if err := (*gen).Generate(&ctx); err != nil {
   273  			fmt.Fprintln(r.ErrorWriter, err)
   274  			hadErrs = true
   275  		}
   276  	}
   277  
   278  	// skip TypeErrors -- they're probably just from partial typechecking in crd-gen
   279  	return loader.PrintErrors(r.Roots, packages.TypeError) || hadErrs
   280  }