github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/terraform/terraformpublish.go (about)

     1  package terraform
     2  
     3  import (
     4  	"errors"
     5  	buildInfo "github.com/jfrog/build-info-go/entities"
     6  	ioutils "github.com/jfrog/gofrog/io"
     7  	"github.com/jfrog/gofrog/parallel"
     8  	commandsUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
     9  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
    10  	"github.com/jfrog/jfrog-cli-core/v2/common/build"
    11  	"github.com/jfrog/jfrog-cli-core/v2/common/project"
    12  	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
    13  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    14  	"github.com/jfrog/jfrog-client-go/artifactory/services"
    15  	servicesUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
    16  	clientUtils "github.com/jfrog/jfrog-client-go/utils"
    17  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    18  	"github.com/jfrog/jfrog-client-go/utils/log"
    19  	"golang.org/x/exp/slices"
    20  	"io"
    21  	"io/fs"
    22  	"os"
    23  	"path"
    24  	"path/filepath"
    25  	"strings"
    26  )
    27  
    28  const threads = 3
    29  
    30  type TerraformPublishCommandArgs struct {
    31  	namespace          string
    32  	provider           string
    33  	tag                string
    34  	exclusions         []string
    35  	buildConfiguration *build.BuildConfiguration
    36  	collectBuildInfo   bool
    37  	buildProps         string
    38  }
    39  
    40  type TerraformPublishCommand struct {
    41  	*TerraformPublishCommandArgs
    42  	args           []string
    43  	repo           string
    44  	configFilePath string
    45  	serverDetails  *config.ServerDetails
    46  	result         *commandsUtils.Result
    47  }
    48  
    49  func NewTerraformPublishCommand() *TerraformPublishCommand {
    50  	return &TerraformPublishCommand{TerraformPublishCommandArgs: NewTerraformPublishCommandArgs(), result: new(commandsUtils.Result)}
    51  }
    52  
    53  func NewTerraformPublishCommandArgs() *TerraformPublishCommandArgs {
    54  	return &TerraformPublishCommandArgs{}
    55  }
    56  
    57  func (tpc *TerraformPublishCommand) SetArgs(terraformArg []string) *TerraformPublishCommand {
    58  	tpc.args = terraformArg
    59  	return tpc
    60  }
    61  
    62  func (tpc *TerraformPublishCommand) setServerDetails(serverDetails *config.ServerDetails) {
    63  	tpc.serverDetails = serverDetails
    64  }
    65  
    66  func (tpc *TerraformPublishCommand) setRepoConfig(conf *project.RepositoryConfig) *TerraformPublishCommand {
    67  	serverDetails, _ := conf.ServerDetails()
    68  	tpc.setRepo(conf.TargetRepo()).setServerDetails(serverDetails)
    69  	return tpc
    70  }
    71  
    72  func (tpc *TerraformPublishCommand) setRepo(repo string) *TerraformPublishCommand {
    73  	tpc.repo = repo
    74  	return tpc
    75  }
    76  
    77  func (tpc *TerraformPublishCommand) ServerDetails() (*config.ServerDetails, error) {
    78  	return tpc.serverDetails, nil
    79  }
    80  
    81  func (tpc *TerraformPublishCommand) CommandName() string {
    82  	return "rt_terraform_publish"
    83  }
    84  
    85  func (tpc *TerraformPublishCommand) SetConfigFilePath(configFilePath string) *TerraformPublishCommand {
    86  	tpc.configFilePath = configFilePath
    87  	return tpc
    88  }
    89  
    90  func (tpc *TerraformPublishCommand) Result() *commandsUtils.Result {
    91  	return tpc.result
    92  }
    93  
    94  func (tpc *TerraformPublishCommand) Run() error {
    95  	log.Info("Running Terraform publish")
    96  	err := tpc.publish()
    97  	if err != nil {
    98  		return err
    99  	}
   100  	log.Info("Terraform publish finished successfully.")
   101  	return nil
   102  }
   103  
   104  func (tpc *TerraformPublishCommand) Init() error {
   105  	err := tpc.extractTerraformPublishOptionsFromArgs(tpc.args)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	if tpc.namespace == "" || tpc.provider == "" || tpc.tag == "" {
   110  		return errorutils.CheckErrorf("the --namespace, --provider and --tag options are mandatory")
   111  	}
   112  	if err = tpc.setRepoFromConfiguration(); err != nil {
   113  		return err
   114  	}
   115  	artDetails, err := tpc.serverDetails.CreateArtAuthConfig()
   116  	if err != nil {
   117  		return err
   118  	}
   119  	if err = utils.ValidateRepoExists(tpc.repo, artDetails); err != nil {
   120  		return err
   121  	}
   122  	tpc.collectBuildInfo, err = tpc.buildConfiguration.IsCollectBuildInfo()
   123  	if err != nil {
   124  		return err
   125  	}
   126  	if tpc.collectBuildInfo {
   127  		tpc.buildProps, err = build.CreateBuildPropsFromConfiguration(tpc.buildConfiguration)
   128  	}
   129  	return err
   130  }
   131  
   132  func (tpc *TerraformPublishCommand) publish() error {
   133  	log.Debug("Deploying terraform module...")
   134  	success, failed, err := tpc.terraformPublish()
   135  	if err != nil {
   136  		return err
   137  	}
   138  	tpc.result.SetSuccessCount(success)
   139  	tpc.result.SetFailCount(failed)
   140  	return nil
   141  }
   142  
   143  func (tpa *TerraformPublishCommandArgs) extractTerraformPublishOptionsFromArgs(args []string) (err error) {
   144  	// Extract namespace information from the args.
   145  	var flagIndex, valueIndex int
   146  	flagIndex, valueIndex, tpa.namespace, err = coreutils.FindFlag("--namespace", args)
   147  	if err != nil {
   148  		return
   149  	}
   150  	coreutils.RemoveFlagFromCommand(&args, flagIndex, valueIndex)
   151  	// Extract provider information from the args.
   152  	flagIndex, valueIndex, tpa.provider, err = coreutils.FindFlag("--provider", args)
   153  	if err != nil {
   154  		return
   155  	}
   156  	coreutils.RemoveFlagFromCommand(&args, flagIndex, valueIndex)
   157  	// Extract tag information from the args.
   158  	flagIndex, valueIndex, tpa.tag, err = coreutils.FindFlag("--tag", args)
   159  	if err != nil {
   160  		return
   161  	}
   162  	coreutils.RemoveFlagFromCommand(&args, flagIndex, valueIndex)
   163  	// Extract exclusions information from the args.
   164  	flagIndex, valueIndex, exclusionsString, err := coreutils.FindFlag("--exclusions", args)
   165  	if err != nil {
   166  		return
   167  	}
   168  	tpa.exclusions = append(tpa.exclusions, strings.Split(exclusionsString, ";")...)
   169  	coreutils.RemoveFlagFromCommand(&args, flagIndex, valueIndex)
   170  	args, tpa.buildConfiguration, err = build.ExtractBuildDetailsFromArgs(args)
   171  	if err != nil {
   172  		return err
   173  	}
   174  	if len(args) != 0 {
   175  		err = errorutils.CheckErrorf("Unknown flag:" + strings.Split(args[0], "=")[0] + ". for a terraform publish command please provide --namespace, --provider, --tag and optionally --exclusions.")
   176  	}
   177  	return
   178  }
   179  
   180  func (tpc *TerraformPublishCommand) terraformPublish() (int, int, error) {
   181  	uploadSummary := getNewUploadSummaryMultiArray()
   182  	producerConsumer := parallel.NewRunner(3, 20000, false)
   183  	errorsQueue := clientUtils.NewErrorsQueue(threads)
   184  
   185  	tpc.prepareTerraformPublishTasks(producerConsumer, errorsQueue, uploadSummary)
   186  	tpc.performTerraformPublishTasks(producerConsumer)
   187  	e := errorsQueue.GetError()
   188  	if e != nil {
   189  		return 0, 0, e
   190  	}
   191  	return tpc.aggregateSummaryResults(uploadSummary)
   192  }
   193  
   194  func (tpc *TerraformPublishCommand) prepareTerraformPublishTasks(producer parallel.Runner, errorsQueue *clientUtils.ErrorsQueue, uploadSummary *[][]*servicesUtils.OperationSummary) {
   195  	go func() {
   196  		defer producer.Done()
   197  		pwd, err := os.Getwd()
   198  		if err != nil {
   199  			log.Error(err)
   200  			errorsQueue.AddError(err)
   201  		}
   202  		// Walk and upload directories which contain '.tf' files.
   203  		err = tpc.walkDirAndUploadTerraformModules(pwd, producer, errorsQueue, uploadSummary, addTaskWithError)
   204  		if err != nil && err != io.EOF {
   205  			log.Error(err)
   206  			errorsQueue.AddError(err)
   207  		}
   208  	}()
   209  }
   210  
   211  // ProduceTaskFunc is provided as an argument to 'walkDirAndUploadTerraformModules' function for testing purposes.
   212  type ProduceTaskFunc func(producer parallel.Runner, serverDetails *config.ServerDetails, uploadSummary *[][]*servicesUtils.OperationSummary, uploadParams *services.UploadParams, errorsQueue *clientUtils.ErrorsQueue) (int, error)
   213  
   214  func addTaskWithError(producer parallel.Runner, serverDetails *config.ServerDetails, uploadSummary *[][]*servicesUtils.OperationSummary, uploadParams *services.UploadParams, errorsQueue *clientUtils.ErrorsQueue) (int, error) {
   215  	return producer.AddTaskWithError(uploadModuleTask(serverDetails, uploadSummary, uploadParams), errorsQueue.AddError)
   216  }
   217  
   218  func uploadModuleTask(serverDetails *config.ServerDetails, uploadSummary *[][]*servicesUtils.OperationSummary, uploadParams *services.UploadParams) parallel.TaskFunc {
   219  	return func(threadId int) (err error) {
   220  		summary, err := createServiceManagerAndUpload(serverDetails, uploadParams, false)
   221  		if err != nil {
   222  			return err
   223  		}
   224  		// Add summary to the thread's summary array.
   225  		(*uploadSummary)[threadId] = append((*uploadSummary)[threadId], summary)
   226  		return nil
   227  	}
   228  }
   229  
   230  func createServiceManagerAndUpload(serverDetails *config.ServerDetails, uploadParams *services.UploadParams, dryRun bool) (operationSummary *servicesUtils.OperationSummary, err error) {
   231  	serviceManager, err := utils.CreateServiceManagerWithThreads(serverDetails, dryRun, 1, -1, 0)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  	return serviceManager.UploadFilesWithSummary(*uploadParams)
   236  }
   237  
   238  func (tpc *TerraformPublishCommand) walkDirAndUploadTerraformModules(pwd string, producer parallel.Runner, errorsQueue *clientUtils.ErrorsQueue, uploadSummary *[][]*servicesUtils.OperationSummary, produceTaskFunc ProduceTaskFunc) error {
   239  	return filepath.WalkDir(pwd, func(path string, info fs.DirEntry, err error) error {
   240  		if err != nil {
   241  			return err
   242  		}
   243  		pathInfo, e := os.Lstat(path)
   244  		if e != nil {
   245  			return errorutils.CheckError(e)
   246  		}
   247  		// Skip files and check only directories.
   248  		if !pathInfo.IsDir() {
   249  			return nil
   250  		}
   251  		isTerraformModule, e := checkIfTerraformModule(path)
   252  		if e != nil {
   253  			return e
   254  		}
   255  		if isTerraformModule {
   256  			uploadParams := tpc.uploadParamsForTerraformPublish(pathInfo.Name(), strings.TrimPrefix(path, pwd+string(filepath.Separator)))
   257  			_, e = produceTaskFunc(producer, tpc.serverDetails, uploadSummary, uploadParams, errorsQueue)
   258  			if e != nil {
   259  				log.Error(e)
   260  				errorsQueue.AddError(e)
   261  			}
   262  
   263  			// SkipDir will not stop the walk, but it will make us jump to the next directory.
   264  			return filepath.SkipDir
   265  		}
   266  		return nil
   267  	})
   268  }
   269  
   270  func (tpc *TerraformPublishCommand) performTerraformPublishTasks(consumer parallel.Runner) {
   271  	// Blocking until consuming is finished.
   272  	consumer.Run()
   273  }
   274  
   275  // Aggregate the operation summaries from all threads, to get the number of successful and failed uploads.
   276  // If collecting build info, also aggregate the artifacts uploaded.
   277  func (tpc *TerraformPublishCommand) aggregateSummaryResults(uploadSummary *[][]*servicesUtils.OperationSummary) (totalUploaded, totalFailed int, err error) {
   278  	var artifacts []buildInfo.Artifact
   279  	for i := 0; i < threads; i++ {
   280  		threadSummary := (*uploadSummary)[i]
   281  		for j := range threadSummary {
   282  			// Operation summary should always be returned.
   283  			if threadSummary[j] == nil {
   284  				return 0, 0, errorutils.CheckErrorf("unexpected nil operation summary")
   285  			}
   286  			totalUploaded += threadSummary[j].TotalSucceeded
   287  			totalFailed += threadSummary[j].TotalFailed
   288  
   289  			if tpc.collectBuildInfo {
   290  				buildArtifacts, err := readArtifactsFromSummary(threadSummary[j])
   291  				if err != nil {
   292  					return 0, 0, err
   293  				}
   294  				artifacts = append(artifacts, buildArtifacts...)
   295  			}
   296  		}
   297  	}
   298  	if tpc.collectBuildInfo {
   299  		err = build.PopulateBuildArtifactsAsPartials(artifacts, tpc.buildConfiguration, buildInfo.Terraform)
   300  	}
   301  	return
   302  }
   303  
   304  func readArtifactsFromSummary(summary *servicesUtils.OperationSummary) (artifacts []buildInfo.Artifact, err error) {
   305  	artifactsDetailsReader := summary.ArtifactsDetailsReader
   306  	if artifactsDetailsReader == nil {
   307  		return []buildInfo.Artifact{}, nil
   308  	}
   309  	defer ioutils.Close(artifactsDetailsReader, &err)
   310  	return servicesUtils.ConvertArtifactsDetailsToBuildInfoArtifacts(artifactsDetailsReader)
   311  }
   312  
   313  func (tpc *TerraformPublishCommand) uploadParamsForTerraformPublish(moduleName, dirPath string) *services.UploadParams {
   314  	uploadParams := services.NewUploadParams()
   315  	uploadParams.Target = tpc.getPublishTarget(moduleName)
   316  	uploadParams.Pattern = dirPath + "/(*)"
   317  	uploadParams.TargetPathInArchive = "{1}"
   318  	uploadParams.Archive = "zip"
   319  	uploadParams.Recursive = true
   320  	uploadParams.CommonParams.TargetProps = servicesUtils.NewProperties()
   321  	uploadParams.CommonParams.Exclusions = append(slices.Clone(tpc.exclusions), "*.git", "*.DS_Store")
   322  	uploadParams.BuildProps = tpc.buildProps
   323  	return &uploadParams
   324  }
   325  
   326  // Module's path in terraform repository : namespace/moduleName/provider/tag.zip
   327  func (tpc *TerraformPublishCommand) getPublishTarget(moduleName string) string {
   328  	return path.Join(tpc.repo, tpc.namespace, moduleName, tpc.provider, tpc.tag+".zip")
   329  }
   330  
   331  // We identify a Terraform module by having at least one file with a ".tf" extension inside the module directory.
   332  func checkIfTerraformModule(path string) (isModule bool, err error) {
   333  	dirname := path + string(filepath.Separator)
   334  	d, err := os.Open(dirname)
   335  	if err != nil {
   336  		return false, errorutils.CheckError(err)
   337  	}
   338  	defer func() {
   339  		err = errors.Join(err, d.Close())
   340  	}()
   341  
   342  	files, err := d.Readdir(-1)
   343  	if err != nil {
   344  		return false, errorutils.CheckError(err)
   345  	}
   346  	for _, file := range files {
   347  		if file.Mode().IsRegular() {
   348  			if filepath.Ext(file.Name()) == ".tf" {
   349  				return true, nil
   350  			}
   351  		}
   352  	}
   353  	return false, nil
   354  }
   355  
   356  func (tpc *TerraformPublishCommand) setRepoFromConfiguration() error {
   357  	// Read config file.
   358  	log.Debug("Preparing to read the config file", tpc.configFilePath)
   359  	vConfig, err := project.ReadConfigFile(tpc.configFilePath, project.YAML)
   360  	if err != nil {
   361  		return err
   362  	}
   363  	deployerParams, err := project.GetRepoConfigByPrefix(tpc.configFilePath, project.ProjectConfigDeployerPrefix, vConfig)
   364  	if err != nil {
   365  		return err
   366  	}
   367  	tpc.setRepoConfig(deployerParams)
   368  	return nil
   369  }
   370  
   371  // Each thread will save the summary of all its operations, in an array at its corresponding index.
   372  func getNewUploadSummaryMultiArray() *[][]*servicesUtils.OperationSummary {
   373  	uploadSummary := make([][]*servicesUtils.OperationSummary, threads)
   374  	return &uploadSummary
   375  }