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 }