go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/cli/cmds/generate/generate.go (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package generate implements 'generate' subcommand.
    16  package generate
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"os"
    22  	"path/filepath"
    23  
    24  	"github.com/bazelbuild/buildtools/build"
    25  	"github.com/maruel/subcommands"
    26  
    27  	"go.chromium.org/luci/common/cli"
    28  	"go.chromium.org/luci/common/logging"
    29  
    30  	"go.chromium.org/luci/lucicfg"
    31  	"go.chromium.org/luci/lucicfg/buildifier"
    32  	"go.chromium.org/luci/lucicfg/cli/base"
    33  )
    34  
    35  // Cmd is 'generate' subcommand.
    36  func Cmd(params base.Parameters) *subcommands.Command {
    37  	return &subcommands.Command{
    38  		UsageLine: "generate SCRIPT",
    39  		ShortDesc: "interprets a high-level config, generating *.cfg files",
    40  		LongDesc: `Interprets a high-level config, generating *.cfg files.
    41  
    42  Writes generated configs to the directory given via -config-dir or via
    43  lucicfg.config(config_dir=...) statement in the script. If it is '-', just
    44  prints them to stdout.
    45  
    46  If -validate is given, sends the generated config to LUCI Config service for
    47  validation. This can also be done separately via 'validate' subcommand.
    48  
    49  If the generation stage fails, doesn't overwrite any files on disk. If the
    50  generation succeeds, but the validation fails, the new generated files are kept
    51  on disk, so they can be manually examined for reasons they are invalid.
    52  `,
    53  		CommandRun: func() subcommands.CommandRun {
    54  			gr := &generateRun{}
    55  			gr.Init(params)
    56  			gr.AddGeneratorFlags()
    57  			gr.Flags.BoolVar(&gr.force, "force", false, "Rewrite existing output files on disk even if they are semantically equal to generated ones")
    58  			gr.Flags.BoolVar(&gr.validate, "validate", false, "Validate the generate configs by sending them to LUCI Config")
    59  			gr.Flags.StringVar(&gr.emitToStdout, "emit-to-stdout", "",
    60  				"When set to a path, keep generated configs in memory (don't touch disk) and just emit this single config file to stdout")
    61  			return gr
    62  		},
    63  	}
    64  }
    65  
    66  type generateRun struct {
    67  	base.Subcommand
    68  
    69  	force        bool
    70  	validate     bool
    71  	emitToStdout string
    72  }
    73  
    74  type generateResult struct {
    75  	// Meta is the final meta parameters used by the generator.
    76  	Meta *lucicfg.Meta `json:"meta,omitempty"`
    77  	// LinterFindings is linter findings (if enabled).
    78  	LinterFindings []*buildifier.Finding `json:"linter_findings,omitempty"`
    79  	// Validation is per config set validation results (if -validate was used).
    80  	Validation []*lucicfg.ValidationResult `json:"validation,omitempty"`
    81  
    82  	// Changed is a list of config files that have changed or been created.
    83  	Changed []string `json:"changed,omitempty"`
    84  	// Unchanged is a list of config files that haven't changed.
    85  	Unchanged []string `json:"unchanged,omitempty"`
    86  	// Deleted is a list of config files deleted from disk due to staleness.
    87  	Deleted []string `json:"deleted,omitempty"`
    88  }
    89  
    90  func (gr *generateRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
    91  	if !gr.CheckArgs(args, 1, 1) {
    92  		return 1
    93  	}
    94  	ctx := cli.GetContext(a, gr, env)
    95  	return gr.Done(gr.run(ctx, args[0]))
    96  }
    97  
    98  func (gr *generateRun) run(ctx context.Context, inputFile string) (*generateResult, error) {
    99  	meta := gr.DefaultMeta()
   100  	state, err := base.GenerateConfigs(ctx, inputFile, &meta, &gr.Meta, gr.Vars)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	output := state.Output
   105  
   106  	result := &generateResult{Meta: &meta}
   107  
   108  	switch {
   109  	case gr.emitToStdout != "":
   110  		// When using -emit-to-stdout, just print the requested file to stdout and
   111  		// do not touch configs on disk. This also overrides `config_dir = "-"`,
   112  		// since we don't want to print two different sources to stdout.
   113  		datum := output.Data[gr.emitToStdout]
   114  		if datum == nil {
   115  			return nil, fmt.Errorf("-emit-to-stdout: no such generated file %q", gr.emitToStdout)
   116  		}
   117  		blob, err := datum.Bytes()
   118  		if err != nil {
   119  			return nil, err
   120  		}
   121  		if _, err := os.Stdout.Write(blob); err != nil {
   122  			return nil, fmt.Errorf("when writing to stdout: %s", err)
   123  		}
   124  
   125  	case meta.ConfigDir == "-":
   126  		// Note: the result of this output is generally not parsable and should not
   127  		// be used in any scripting.
   128  		output.DebugDump()
   129  
   130  	default:
   131  		// Get rid of stale output in ConfigDir by deleting tracked files that are
   132  		// no longer in the output. Note that if TrackedFiles is empty (default),
   133  		// nothing is deleted, it is the responsibility of lucicfg users to make
   134  		// sure there's no stale output in this case.
   135  		tracked, err := lucicfg.FindTrackedFiles(meta.ConfigDir, meta.TrackedFiles)
   136  		if err != nil {
   137  			return result, err
   138  		}
   139  		for _, f := range tracked {
   140  			if _, present := output.Data[f]; !present {
   141  				result.Deleted = append(result.Deleted, f)
   142  				logging.Warningf(ctx, "Deleting tracked file no longer present in the output: %q", f)
   143  				if err := os.Remove(filepath.Join(meta.ConfigDir, filepath.FromSlash(f))); err != nil {
   144  					return result, err
   145  				}
   146  			}
   147  		}
   148  		// Write the new output there.
   149  		result.Changed, result.Unchanged, err = output.Write(meta.ConfigDir, gr.force)
   150  		if err != nil {
   151  			return result, err
   152  		}
   153  	}
   154  
   155  	entryPath, err := filepath.Abs(filepath.Dir(inputFile))
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	if err := base.CheckForBogusConfig(entryPath); err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	rewriterFactory, err := base.GetRewriterFactory(filepath.Join(entryPath, base.ConfigName))
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	// Optionally validate via RPC and apply linters. This is slow, thus off by
   170  	// default.
   171  	if gr.validate {
   172  		result.LinterFindings, result.Validation, err = base.Validate(ctx, base.ValidateParams{
   173  			Loader:                    state.Inputs.Code,
   174  			Source:                    state.Visited,
   175  			Output:                    output,
   176  			Meta:                      meta,
   177  			LegacyConfigServiceClient: gr.LegacyConfigServiceClient,
   178  			ConfigServiceConn:         gr.MakeConfigServiceConn,
   179  		}, func(path string) (*build.Rewriter, error) {
   180  			// GetRewriter needs to see absolute paths; In Validate the paths are all
   181  			// relative to the entrypoint (e.g. main.star) becuase they refer to
   182  			// Starlark module import paths.
   183  			//
   184  			// Adjusting state.Visited above will fail because part of Validate's
   185  			// functionality needs to retain these relative paths.
   186  			return rewriterFactory.GetRewriter(filepath.Join(entryPath, path))
   187  		})
   188  	}
   189  	return result, err
   190  }