github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/step/helm/step_helm_apply.go (about) 1 package helm 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/pkg/errors" 11 "github.com/spf13/cobra" 12 13 "github.com/jenkins-x/jx-logging/pkg/log" 14 "github.com/olli-ai/jx/v2/pkg/cmd/helper" 15 "github.com/olli-ai/jx/v2/pkg/cmd/opts" 16 "github.com/olli-ai/jx/v2/pkg/cmd/opts/step" 17 "github.com/olli-ai/jx/v2/pkg/cmd/templates" 18 "github.com/olli-ai/jx/v2/pkg/config" 19 "github.com/olli-ai/jx/v2/pkg/gits" 20 "github.com/olli-ai/jx/v2/pkg/helm" 21 configio "github.com/olli-ai/jx/v2/pkg/io" 22 "github.com/olli-ai/jx/v2/pkg/io/secrets" 23 "github.com/olli-ai/jx/v2/pkg/kube" 24 "github.com/olli-ai/jx/v2/pkg/kube/naming" 25 "github.com/olli-ai/jx/v2/pkg/platform" 26 "github.com/olli-ai/jx/v2/pkg/secreturl/fakevault" 27 "github.com/olli-ai/jx/v2/pkg/util" 28 "github.com/olli-ai/jx/v2/pkg/vault" 29 ) 30 31 // StepHelmApplyOptions contains the command line flags 32 type StepHelmApplyOptions struct { 33 StepHelmOptions 34 35 Namespace string 36 ReleaseName string 37 Wait bool 38 Force bool 39 DisableHelmVersion bool 40 Boot bool 41 Vault bool 42 NoVault bool 43 NoMasking bool 44 ProviderValuesDir string 45 MultiTemplates bool 46 } 47 48 var ( 49 StepHelmApplyLong = templates.LongDesc(` 50 Applies the helm chart in a given directory. 51 52 This step is usually used to apply any GitOps promotion changes into a Staging or Production cluster. 53 54 Environment Variables: 55 - JX_NO_DELETE_TMP_DIR="true" - prevents the removal of the temporary directory. 56 `) 57 58 StepHelmApplyExample = templates.Examples(` 59 # apply the chart in the env folder to namespace jx-staging 60 jx step helm apply --dir env --namespace jx-staging 61 62 `) 63 64 defaultValueFileNames = []string{"values.yaml", "myvalues.yaml", helm.SecretsFileName, filepath.Join("env", helm.SecretsFileName)} 65 ) 66 67 func NewCmdStepHelmApply(commonOpts *opts.CommonOptions) *cobra.Command { 68 options := StepHelmApplyOptions{ 69 StepHelmOptions: StepHelmOptions{ 70 StepOptions: step.StepOptions{ 71 CommonOptions: commonOpts, 72 }, 73 }, 74 } 75 cmd := &cobra.Command{ 76 Use: "apply", 77 Short: "Applies the helm chart in a given directory", 78 Aliases: []string{""}, 79 Long: StepHelmApplyLong, 80 Example: StepHelmApplyExample, 81 Run: func(cmd *cobra.Command, args []string) { 82 options.Cmd = cmd 83 options.Args = args 84 err := options.Run() 85 helper.CheckErr(err) 86 }, 87 } 88 options.addStepHelmFlags(cmd) 89 90 cmd.Flags().StringVarP(&options.Namespace, "namespace", "", "", "The Kubernetes namespace to apply the helm chart to") 91 cmd.Flags().StringVarP(&options.ReleaseName, "name", "n", "", "The name of the release") 92 cmd.Flags().BoolVarP(&options.Wait, "wait", "", true, "Wait for Kubernetes readiness probe to confirm deployment") 93 cmd.Flags().BoolVarP(&options.Force, "force", "f", true, "Whether to to pass '--force' to helm to help deal with upgrading if a previous promote failed") 94 cmd.Flags().BoolVar(&options.DisableHelmVersion, "no-helm-version", false, "Don't set Chart version before applying") 95 cmd.Flags().BoolVarP(&options.Vault, "vault", "", false, "Helm secrets are stored in vault") 96 cmd.Flags().BoolVarP(&options.Boot, "boot", "", false, "In Boot mode we load the Version Stream from the 'jx-requirements.yml' and use that to replace any missing versions in the 'requirements.yaml' file from the Version Stream") 97 cmd.Flags().BoolVarP(&options.NoVault, "no-vault", "", false, "Disables loading secrets from Vault. e.g. if bootstrapping core services like Ingress before we have a Vault") 98 cmd.Flags().BoolVarP(&options.NoMasking, "no-masking", "", false, "The effective 'values.yaml' file is output to the console with parameters masked. Enabling this flag will show the unmasked secrets in the console output") 99 cmd.Flags().StringVarP(&options.ProviderValuesDir, "provider-values-dir", "", "", "The optional directory of kubernetes provider specific override values.tmpl.yaml files a kubernetes provider specific folder") 100 cmd.Flags().BoolVarP(&options.MultiTemplates, "multi-templates", "", false, "Calls helm template on each sub chart instead of globally, to avoid conflict among charts with different versions") 101 102 return cmd 103 } 104 105 func (o *StepHelmApplyOptions) Run() error { 106 var err error 107 chartName := o.Dir 108 dir := o.Dir 109 releaseName := o.ReleaseName 110 111 // let allow arguments to be passed in like for `helm install releaseName dir` 112 args := o.Args 113 if releaseName == "" && len(args) > 0 { 114 releaseName = args[0] 115 } 116 if dir == "" && len(args) > 1 { 117 dir = args[1] 118 } 119 120 if dir == "" { 121 dir, err = os.Getwd() 122 if err != nil { 123 return err 124 } 125 } 126 127 if !o.DisableHelmVersion { 128 (&StepHelmVersionOptions{ 129 StepHelmOptions: StepHelmOptions{ 130 StepOptions: step.StepOptions{ 131 CommonOptions: &opts.CommonOptions{}, 132 }, 133 }, 134 }).Run() //nolint:errcheck 135 } 136 helmBinary, noTiller, helmTemplate, err := o.TeamHelmBin() 137 if err != nil { 138 return err 139 } 140 141 ns, err := o.GetDeployNamespace(o.Namespace) 142 if err != nil { 143 return err 144 } 145 146 kubeClient, err := o.KubeClient() 147 if err != nil { 148 return err 149 } 150 151 err = kube.EnsureNamespaceCreated(kubeClient, ns, nil, nil) 152 if err != nil { 153 return err 154 } 155 156 _, devNs, err := o.KubeClientAndDevNamespace() 157 if err != nil { 158 return err 159 } 160 161 if releaseName == "" { 162 if devNs == ns { 163 releaseName = platform.JenkinsXPlatformRelease 164 } else { 165 releaseName = ns 166 167 if helmBinary != "helm" || noTiller || helmTemplate { 168 releaseName = "jx" 169 } 170 } 171 } 172 info := util.ColorInfo 173 174 path, err := filepath.Abs(dir) 175 if err != nil { 176 return errors.Wrapf(err, "could not find absolute path of dir %s", dir) 177 } 178 dir = path 179 180 devGitInfo, err := o.FindGitInfo(dir) 181 if err != nil { 182 log.Logger().Warnf("could not find a git repository in the directory %s: %s\n", dir, err.Error()) 183 } 184 rootTmpDir, err := ioutil.TempDir("", "jx-helm-apply-") 185 if err != nil { 186 return errors.Wrapf(err, "failed to create a temporary directory to apply the helm chart") 187 } 188 if os.Getenv("JX_NO_DELETE_TMP_DIR") != "true" { 189 defer os.RemoveAll(rootTmpDir) //nolint:errcheck 190 } 191 192 if os.Getenv(kube.DisableBuildLockEnvKey) == "" { 193 release, err := kube.AcquireBuildLock(kubeClient, devNs, ns) 194 if err != nil { 195 return errors.Wrapf(err, "fail to acquire the lock") 196 } 197 defer release() //nolint:errcheck 198 } 199 200 // lets use the same child dir name as the original as helm is quite particular about the name of the directory it runs from 201 _, name := filepath.Split(dir) 202 if name == "" { 203 return fmt.Errorf("could not find the relative name of the directory %s", dir) 204 } 205 tmpDir := filepath.Join(rootTmpDir, name) 206 log.Logger().Debugf("Copying the helm source directory %s to a temporary location for building and applying %s\n", info(dir), info(tmpDir)) 207 208 err = os.MkdirAll(tmpDir, util.DefaultWritePermissions) 209 if err != nil { 210 return errors.Wrapf(err, "failed to helm temporary dir %s", tmpDir) 211 } 212 err = util.CopyDir(dir, tmpDir, true) 213 if err != nil { 214 return errors.Wrapf(err, "failed to copy helm dir %s to temporary dir %s", dir, tmpDir) 215 } 216 dir = tmpDir 217 log.Logger().Debugf("Applying helm chart at %s as release name %s to namespace %s", info(dir), info(releaseName), info(ns)) 218 219 o.Helm().SetCWD(dir) 220 221 valueFiles := []string{} 222 for _, name := range defaultValueFileNames { 223 file := filepath.Join(dir, name) 224 exists, err := util.FileExists(file) 225 if exists && err == nil { 226 valueFiles = append(valueFiles, file) 227 } 228 } 229 230 vaultSecretLocation := o.GetSecretsLocation() == secrets.VaultLocationKind 231 if vaultSecretLocation && o.NoVault { 232 // lets install a fake secret URL client to avoid spurious vault errors 233 o.SetSecretURLClient(fakevault.NewFakeClient()) 234 } 235 if (vaultSecretLocation || o.Vault) && !o.NoVault { 236 store := configio.NewFileStore() 237 secretsFiles, err := o.fetchSecretFilesFromVault(dir, store) 238 if err != nil { 239 return errors.Wrap(err, "fetching secrets files from vault") 240 } 241 for _, sf := range secretsFiles { 242 if util.StringArrayIndex(valueFiles, sf) < 0 { 243 log.Logger().Debugf("adding secret file %s", sf) 244 valueFiles = append(valueFiles, sf) 245 } 246 } 247 defer func() { 248 for _, secretsFile := range secretsFiles { 249 err := util.DestroyFile(secretsFile) 250 if err != nil { 251 log.Logger().Warnf("Failed to cleanup the secrets files (%s): %v", 252 strings.Join(secretsFiles, ", "), err) 253 } 254 } 255 }() 256 } 257 258 requirements, requirementsFileName, err := o.getRequirements() 259 if err != nil { 260 return errors.Wrap(err, "loading the requirements") 261 } 262 263 secretURLClient, err := o.GetSecretURLClient(secrets.ToSecretsLocation(string(requirements.SecretStorage))) 264 if err != nil { 265 return errors.Wrap(err, "failed to create a Secret RL client") 266 } 267 268 DefaultEnvironments(requirements, devGitInfo) 269 270 funcMap, err := o.createFuncMap(requirements) 271 if err != nil { 272 return err 273 } 274 chartValues, params, err := helm.GenerateValues(requirements, funcMap, dir, nil, true, secretURLClient) 275 if err != nil { 276 return errors.Wrapf(err, "generating values.yaml for tree from %s", dir) 277 } 278 if o.ProviderValuesDir != "" && requirementsFileName != "" { 279 chartValues, err = o.overwriteProviderValues(requirements, requirementsFileName, chartValues, params, o.ProviderValuesDir) 280 if err != nil { 281 return errors.Wrapf(err, "failed to overwrite provider values in dir: %s", dir) 282 } 283 } 284 285 chartValuesFile := filepath.Join(dir, helm.ValuesFileName) 286 err = ioutil.WriteFile(chartValuesFile, chartValues, 0600) 287 if err != nil { 288 return errors.Wrapf(err, "writing values.yaml for tree to %s", chartValuesFile) 289 } 290 log.Logger().Debugf("Wrote chart values.yaml %s generated from directory tree", chartValuesFile) 291 292 data, err := ioutil.ReadFile(chartValuesFile) 293 if err != nil { 294 log.Logger().Warnf("failed to load file %s: %s", chartValuesFile, err.Error()) 295 } else { 296 log.Logger().Debugf("generated helm %s", chartValuesFile) 297 298 valuesText := string(data) 299 if !o.NoMasking { 300 masker := kube.NewLogMaskerFromMap(params.AsMap()) 301 valuesText = masker.MaskLog(valuesText) 302 } 303 304 log.Logger().Debugf("\n%s\n", util.ColorStatus(valuesText)) 305 } 306 307 log.Logger().Debugf("Using values files: %s", strings.Join(valueFiles, ", ")) 308 309 if o.Boot { 310 err = o.replaceMissingVersionsFromVersionStream(requirements, dir) 311 if err != nil { 312 return errors.Wrapf(err, "failed to replace missing versions in the requirements.yaml in dir %s", dir) 313 } 314 } 315 if o.MultiTemplates { 316 err = o.HelmInitDependencyBuildNoLint(dir, o.DefaultReleaseCharts()) 317 } else { 318 err = o.HelmInitDependencyBuild(dir, o.DefaultReleaseCharts(), valueFiles) 319 } 320 if err != nil { 321 return err 322 } 323 324 // Unpack all dependencies 325 err = o.UnpackCharts(dir) 326 if err != nil { 327 return err 328 } 329 330 // apply the vault URLs to the value files 331 err = filepath.Walk(filepath.Join(dir, "charts"), func(path string, info os.FileInfo, err error) error { 332 if filepath.Base(path) == helm.ValuesFileName { 333 334 newFiles, cleanup, err := helm.DecorateWithSecrets([]string{path}, secretURLClient) 335 defer cleanup() //nolint:errcheck 336 if err != nil { 337 return errors.Wrapf(err, "decorating %s with secrets", path) 338 } 339 err = os.Rename(newFiles[0], path) 340 if err != nil { 341 return errors.Wrapf(err, "moving decorated file %s to %s", newFiles[0], path) 342 } 343 } 344 return nil 345 }) 346 if err != nil { 347 return errors.Wrapf(err, "walking %s/charts", dir) 348 } 349 350 err = o.applyAppsTemplateOverrides(chartName) 351 if err != nil { 352 return errors.Wrap(err, "applying app chart overrides") 353 } 354 err = o.applyTemplateOverrides(chartName) 355 if err != nil { 356 return errors.Wrap(err, "applying chart overrides") 357 } 358 359 setValues, setStrings := o.getChartValues(ns) 360 361 helmOptions := helm.InstallChartOptions{ 362 Chart: chartName, 363 ReleaseName: releaseName, 364 Ns: ns, 365 NoForce: !o.Force, 366 SetValues: setValues, 367 SetStrings: setStrings, 368 ValueFiles: valueFiles, 369 Dir: dir, 370 MultiTemplates: o.MultiTemplates, 371 } 372 if o.Boot { 373 helmOptions.VersionsGitURL = requirements.VersionStream.URL 374 helmOptions.VersionsGitRef = requirements.VersionStream.Ref 375 } 376 377 if o.Wait { 378 helmOptions.Wait = true 379 err = o.InstallChartWithOptionsAndTimeout(helmOptions, "600") 380 } else { 381 err = o.InstallChartWithOptions(helmOptions) 382 } 383 if err != nil { 384 return errors.Wrapf(err, "upgrading helm chart '%s'", chartName) 385 } 386 return nil 387 } 388 389 // getRequirements tries to load the requirements either from the team settings or local requirements file 390 func (o *StepHelmApplyOptions) getRequirements() (*config.RequirementsConfig, string, error) { 391 // Try to load first the requirements from current directory 392 requirements, requirementsFileName, err := config.LoadRequirementsConfig(o.Dir, config.DefaultFailOnValidationError) 393 if err == nil { 394 return requirements, requirementsFileName, nil 395 } 396 397 // When no requirements file is found, try to load the requirements from team settings 398 jxClient, ns, err := o.JXClient() 399 if err != nil { 400 return nil, "", errors.Wrap(err, "getting the jx client") 401 } 402 teamSettings, err := kube.GetDevEnvTeamSettings(jxClient, ns) 403 if err != nil { 404 return nil, "", errors.Wrap(err, "getting the team setting from the cluster") 405 } 406 407 requirements, err = config.GetRequirementsConfigFromTeamSettings(teamSettings) 408 if err != nil { 409 return nil, "", errors.Wrap(err, "getting the requirements from team settings") 410 } 411 // TODO: Workaround for non-boot clusters. Remove when we get rid of jx install. (APB) 412 if requirements == nil { 413 requirements = config.NewRequirementsConfig() 414 requirementsFileName = config.RequirementsConfigFileName 415 return requirements, requirementsFileName, nil 416 } 417 418 return requirements, "", nil 419 } 420 421 // DefaultEnvironments ensures we have valid values for environment owner and repository names. 422 // if none are configured lets default them from smart defaults 423 func DefaultEnvironments(c *config.RequirementsConfig, devGitInfo *gits.GitRepository) { 424 defaultOwner := c.Cluster.EnvironmentGitOwner 425 clusterName := c.Cluster.ClusterName 426 for i := range c.Environments { 427 env := &c.Environments[i] 428 if !c.GitOps { 429 if env.Key == kube.LabelValueDevEnvironment && devGitInfo != nil { 430 if env.Owner == "" { 431 env.Owner = devGitInfo.Organisation 432 } 433 if env.Repository == "" { 434 env.Repository = devGitInfo.Name 435 } 436 if env.GitServer == "" { 437 env.GitServer = devGitInfo.HostURL() 438 } 439 if env.GitKind == "" { 440 env.GitKind = gits.SaasGitKind(env.GitServer) 441 } 442 } 443 } 444 if env.Owner == "" { 445 env.Owner = defaultOwner 446 } 447 if env.Repository == "" { 448 if clusterName != "" { 449 env.Repository = naming.ToValidName("environment-" + clusterName + "-" + env.Key) 450 } else { 451 log.Logger().Warnf("there is no 'cluster.clusterName' value set in the 'jx-requirements.yml' file. Please specify the 'repository' for environment: %s", env.Key) 452 } 453 } 454 } 455 } 456 457 func (o *StepHelmApplyOptions) applyTemplateOverrides(chartName string) error { 458 log.Logger().Debugf("Applying chart overrides") 459 templateOverrides, err := filepath.Glob(chartName + "/../*/templates/*.yaml") 460 for _, overrideSrc := range templateOverrides { 461 if !strings.Contains(overrideSrc, "/env/") { 462 data, err := ioutil.ReadFile(overrideSrc) 463 if err == nil { 464 writeTemplateParts := strings.Split(overrideSrc, string(os.PathSeparator)) 465 depChartsDir := filepath.Join(chartName, "charts") 466 depChartName := writeTemplateParts[len(writeTemplateParts)-3] 467 templateName := writeTemplateParts[len(writeTemplateParts)-1] 468 depChartDir := filepath.Join(depChartsDir, depChartName) 469 if _, err := os.Stat(depChartDir); os.IsNotExist(err) { 470 // If there is no charts/<depChartName> dir it means that it's not a dependency of this chart 471 continue 472 } 473 // // If the chart directory does not exist explode the tgz 474 // if exists, err := util.DirExists(depChartDir); err == nil && !exists { 475 // chartArchives, _ := filepath.Glob(filepath.Join(depChartsDir, depChartName+"*.tgz")) 476 // if len(chartArchives) == 1 { 477 // log.Logger().Debugf("Exploding chart %s", chartArchives[0]) 478 // err = archiver.Unarchive(chartArchives[0], depChartsDir) 479 // if err != nil { 480 // return errors.Wrapf(err, "unable to unarchive %s to destination %s", chartArchives[0], depChartDir) 481 // } 482 // // Remove the unexploded chart 483 // err = os.Remove(chartArchives[0]) 484 // if err != nil { 485 // return errors.Wrapf(err, "unable to remove chart %s", chartArchives[0]) 486 // } 487 // } 488 // } 489 overrideDst := filepath.Join(depChartDir, "templates", templateName) 490 log.Logger().Debugf("Copying chart override %s", overrideSrc) 491 err = ioutil.WriteFile(overrideDst, data, util.DefaultWritePermissions) 492 if err != nil { 493 log.Logger().Warnf("Error copying template %s to %s %v", overrideSrc, overrideDst, err) 494 } 495 496 } 497 } 498 } 499 return err 500 } 501 502 func (o *StepHelmApplyOptions) applyAppsTemplateOverrides(chartName string) error { 503 log.Logger().Debugf("Applying Apps chart overrides") 504 templateOverrides, err := filepath.Glob(chartName + "/../*/*/templates/app.yaml") 505 for _, overrideSrc := range templateOverrides { 506 data, err := ioutil.ReadFile(overrideSrc) 507 if err == nil { 508 writeTemplateParts := strings.Split(overrideSrc, string(os.PathSeparator)) 509 depChartsDir := filepath.Join(chartName, "charts") 510 depChartName := writeTemplateParts[len(writeTemplateParts)-3] 511 templateName := writeTemplateParts[len(writeTemplateParts)-1] 512 depChartDir := filepath.Join(depChartsDir, depChartName) 513 // chartArchives, _ := filepath.Glob(filepath.Join(depChartsDir, depChartName+"*.tgz")) 514 // if len(chartArchives) == 1 { 515 // uuid, _ := uuid.NewUUID() 516 // log.Logger().Debugf("Exploding App chart %s", chartArchives[0]) 517 // explodedChartTempDir := filepath.Join(os.TempDir(), uuid.String()) 518 // if err = archiver.Unarchive(chartArchives[0], explodedChartTempDir); err != nil { 519 // return defineAppsChartOverridingError(chartName, err) 520 // } 521 // overrideDst := filepath.Join(explodedChartTempDir, depChartName, "templates", templateName) 522 if exists, err := util.DirExists(depChartDir); exists { 523 overrideDst := filepath.Join(depChartDir, "templates", templateName) 524 log.Logger().Debugf("Copying chart override %s", overrideSrc) 525 err = ioutil.WriteFile(overrideDst, data, util.DefaultWritePermissions) 526 if err != nil { 527 log.Logger().Warnf("Error copying template %s to %s %v", overrideSrc, overrideDst, err) 528 } 529 // if err = os.Remove(chartArchives[0]); err != nil { 530 // return defineAppsChartOverridingError(chartName, err) 531 // } 532 // if err = archiver.Archive([]string{filepath.Join(explodedChartTempDir, depChartName)}, chartArchives[0]); err != nil { 533 // return defineAppsChartOverridingError(chartName, err) 534 // } 535 // if err = os.RemoveAll(explodedChartTempDir); err != nil { 536 // log.Logger().Warnf("There was a problem deleting the temp folder %s", depChartDir) 537 // } 538 } 539 } 540 } 541 return err 542 } 543 544 func defineAppsChartOverridingError(chartName string, err error) error { 545 return errors.Wrapf(err, "there was a problem overriding the chart %s", chartName) 546 } 547 548 func (o *StepHelmApplyOptions) fetchSecretFilesFromVault(dir string, store configio.ConfigStore) ([]string, error) { 549 log.Logger().Debugf("Fetching secrets from vault into directory %q", dir) 550 files := []string{} 551 client, err := o.SystemVaultClient("") 552 if err != nil { 553 return files, errors.Wrap(err, "retrieving the system Vault") 554 } 555 secretNames, err := client.List(vault.GitOpsSecretsPath) 556 if err != nil { 557 return files, errors.Wrap(err, "listing the GitOps secrets in Vault") 558 } 559 secretPaths := []string{} 560 for _, secretName := range secretNames { 561 if secretName == vault.GitOpsTemplatesPath { 562 templatesPath := vault.GitOpsSecretPath(vault.GitOpsTemplatesPath) 563 templatesSecretNames, err := client.List(templatesPath) 564 if err == nil { 565 for _, templatesSecretName := range templatesSecretNames { 566 templateSecretPath := vault.GitOpsTemplatesPath + templatesSecretName 567 secretPaths = append(secretPaths, templateSecretPath) 568 } 569 } 570 } else { 571 secretPaths = append(secretPaths, secretName) 572 } 573 } 574 575 for _, secretPath := range secretPaths { 576 gitopsSecretPath := vault.GitOpsSecretPath(secretPath) 577 secret, err := client.ReadYaml(gitopsSecretPath) 578 if err != nil { 579 return files, errors.Wrapf(err, "retrieving the secret %q from Vault", secretPath) 580 } 581 if secret == "" { 582 return files, fmt.Errorf("secret %q is empty", secretPath) 583 } 584 secretFile := filepath.Join(dir, secretPath) 585 err = store.Write(secretFile, []byte(secret)) 586 if err != nil { 587 return files, errors.Wrapf(err, "saving the secret file %q", secretFile) 588 } 589 log.Logger().Debugf("Saved secrets file %s", util.ColorInfo(secretFile)) 590 files = append(files, secretFile) 591 } 592 return files, nil 593 }