diff --git a/apply/apply.go b/apply/apply.go index 6efe4e3b9..1d95c62a4 100644 --- a/apply/apply.go +++ b/apply/apply.go @@ -298,6 +298,31 @@ func applyRepo(fs afero.Fs, p *plan.Plan, repoTemplates, commonTemplates fs.FS) return applyTree(fs, repoTemplates, commonTemplates, "", p) } +func applyExtraTemplates(fs afero.Fs, p plan.ComponentCommon, commonBox fs.FS, path string) error { + for filename, templateCfg := range p.ExtraTemplates { + target := getTargetPath(path, filename) + _, err := fs.Stat(target) + if err == nil && !templateCfg.Overwrite { + // file exists and we don't want to overwrite + continue + } + + err = applyTemplate(strings.NewReader(templateCfg.Content), commonBox, fs, target, p) + if err != nil { + return errs.WrapUser(err, "applying extra templates") + } + + if filepath.Ext(filename) == ".tf" { + err = fmtHcl(fs, target, true) + if err != nil { + return errs.WrapUser(err, "formating HCL of extra templates") + } + } + } + + return nil +} + func applyGlobal(fs afero.Fs, p plan.Component, repoBox, commonBox fs.FS) error { logrus.Debug("applying global") path := fmt.Sprintf("%s/global", rootPath) @@ -305,34 +330,44 @@ func applyGlobal(fs afero.Fs, p plan.Component, repoBox, commonBox fs.FS) error if e != nil { return errs.WrapUserf(e, "unable to make directory %s", path) } - return applyTree(fs, repoBox, commonBox, path, p) + err := applyTree(fs, repoBox, commonBox, path, p) + if err != nil { + return err + } + + return applyExtraTemplates(fs, p.ComponentCommon, commonBox, path) } -func applyAccounts(fs afero.Fs, p *plan.Plan, accountBox, commonBox fs.FS) (e error) { +func applyAccounts(fs afero.Fs, p *plan.Plan, accountBox, commonBox fs.FS) error { for account, accountPlan := range p.Accounts { path := fmt.Sprintf("%s/accounts/%s", rootPath, account) - e = fs.MkdirAll(path, 0755) - if e != nil { - return errs.WrapUser(e, "unable to make directories for accounts") + err := fs.MkdirAll(path, 0755) + if err != nil { + return errs.WrapUser(err, "unable to make directories for accounts") } - e = applyTree(fs, accountBox, commonBox, path, accountPlan) - if e != nil { - return errs.WrapUser(e, "unable to apply templates to account") + err = applyTree(fs, accountBox, commonBox, path, accountPlan) + if err != nil { + return errs.WrapUser(err, "unable to apply templates to account") + } + + err = applyExtraTemplates(fs, accountPlan.ComponentCommon, commonBox, path) + if err != nil { + return errs.WrapUser(err, "apply extra templates") } } return nil } -func applyModules(fs afero.Fs, p map[string]plan.Module, moduleBox, commonBox fs.FS) (e error) { +func applyModules(fs afero.Fs, p map[string]plan.Module, moduleBox, commonBox fs.FS) error { for module, modulePlan := range p { path := fmt.Sprintf("%s/modules/%s", rootPath, module) - e = fs.MkdirAll(path, 0755) - if e != nil { - return errs.WrapUserf(e, "unable to make path %s", path) + err := fs.MkdirAll(path, 0755) + if err != nil { + return errs.WrapUserf(err, "unable to make path %s", path) } - e = applyTree(fs, moduleBox, commonBox, path, modulePlan) - if e != nil { - return errs.WrapUser(e, "unable to apply tree") + err = applyTree(fs, moduleBox, commonBox, path, modulePlan) + if err != nil { + return errs.WrapUser(err, "unable to apply tree") } } return nil @@ -375,6 +410,10 @@ func applyEnvs( if err != nil { return errs.WrapUser(err, "unable to apply templates for component") } + err = applyExtraTemplates(fs, componentPlan.ComponentCommon, commonBox, path) + if err != nil { + return errs.WrapUser(err, "apply extra templates") + } if componentPlan.ModuleSource != nil { downloader, err := util.MakeDownloader(*componentPlan.ModuleSource) diff --git a/config/v2/config.go b/config/v2/config.go index 01728fe3f..f8bba49ed 100644 --- a/config/v2/config.go +++ b/config/v2/config.go @@ -74,16 +74,22 @@ type TFE struct { AdditionalGithubRequiredChecks *[]string `yaml:"additional_gh_required_checks,omitempty"` } +type ExtraTemplate struct { + Overwrite *bool + Content *string +} + type Common struct { - Backend *Backend `yaml:"backend,omitempty"` - ExtraVars map[string]string `yaml:"extra_vars,omitempty"` - Owner *string `yaml:"owner,omitempty"` - Project *string `yaml:"project,omitempty"` - Providers *Providers `yaml:"providers,omitempty"` - DependsOn *DependsOn `yaml:"depends_on,omitempty"` - TerraformVersion *string `yaml:"terraform_version,omitempty"` - Tools *Tools `yaml:"tools,omitempty"` - NeedsAWSAccountsVariable *bool `yaml:"needs_aws_accounts_variable,omitempty"` + Backend *Backend `yaml:"backend,omitempty"` + ExtraVars map[string]string `yaml:"extra_vars,omitempty"` + Owner *string `yaml:"owner,omitempty"` + Project *string `yaml:"project,omitempty"` + Providers *Providers `yaml:"providers,omitempty"` + DependsOn *DependsOn `yaml:"depends_on,omitempty"` + TerraformVersion *string `yaml:"terraform_version,omitempty"` + Tools *Tools `yaml:"tools,omitempty"` + NeedsAWSAccountsVariable *bool `yaml:"needs_aws_accounts_variable,omitempty"` + ExtraTemplates *map[string]ExtraTemplate `yaml:"extra_templates,omitempty"` } type Defaults struct { diff --git a/config/v2/resolvers.go b/config/v2/resolvers.go index e215780b1..4069024f6 100644 --- a/config/v2/resolvers.go +++ b/config/v2/resolvers.go @@ -6,45 +6,12 @@ import ( "github.com/chanzuckerberg/fogg/util" ) -// lastNonNilBool, despite its name can return nil if all results are nil -func lastNonNilBool(getter func(Common) *bool, commons ...Common) *bool { - var s *bool - for _, c := range commons { - t := getter(c) - if t != nil { - s = t - } - } - return s +type Nillable interface { + *bool | *float64 | *int64 | *string | []string | *[]ExtraTemplate } -// lastNonNil, despite its name can return nil if all results are nil -func lastNonNil(getter func(Common) *string, commons ...Common) *string { - var s *string - for _, c := range commons { - t := getter(c) - if t != nil { - s = t - } - } - return s -} - -// lastNonNilInt64, despite its name can return nil if all results are nil -func lastNonNilInt64(getter func(Common) *int64, commons ...Common) *int64 { - var s *int64 - for _, c := range commons { - t := getter(c) - if t != nil { - s = t - } - } - return s -} - -// lastNonNilStringSlice, despite its name can return nil if all results are nil -func lastNonNilStringSlice(getter func(Common) []string, commons ...Common) []string { - var s []string +func lastNonNil[T Nillable](getter func(Common) T, commons ...Common) T { + var s T for _, c := range commons { t := getter(c) if t != nil { @@ -61,7 +28,7 @@ func ResolveRequiredString(getter func(Common) *string, commons ...Common) strin // ResolveRequiredInt64 will resolve the value and panic if it is nil. Only to be used after validations are run. func ResolveRequiredInt64(getter func(Common) *int64, commons ...Common) int64 { - return *lastNonNilInt64(getter, commons...) + return *lastNonNil(getter, commons...) } func ResolveOptionalString(getter func(Common) *string, commons ...Common) *string { @@ -69,7 +36,7 @@ func ResolveOptionalString(getter func(Common) *string, commons ...Common) *stri } func ResolveOptionalStringSlice(getter func(Common) []string, commons ...Common) []string { - return lastNonNilStringSlice(getter, commons...) + return lastNonNil(getter, commons...) } func ResolveStringArray(def []string, override []string) []string { @@ -302,7 +269,7 @@ func ResolveGithubProvider(commons ...Common) *GithubProvider { BaseURL: lastNonNil(GithubProviderBaseURLGetter, commons...), CommonProvider: CommonProvider{ Enabled: enabled, - CustomProvider: lastNonNilBool(GithubProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(GithubProviderCustomProviderGetter, commons...), Version: lastNonNil(GithubProviderVersionGetter, commons...), }, } @@ -316,13 +283,44 @@ func AWSAccountsNeededGetter(comm Common) *bool { } func ResolveAWSAccountsNeeded(commons ...Common) bool { - accountsNeeded := lastNonNilBool(AWSAccountsNeededGetter, commons...) + accountsNeeded := lastNonNil(AWSAccountsNeededGetter, commons...) if accountsNeeded == nil { return true } return *accountsNeeded } +func ResolveExtraTemplates(commons ...Common) map[string]ExtraTemplate { + templates := map[string]ExtraTemplate{} + for _, common := range commons { + if common.ExtraTemplates == nil { + continue + } + + for filename, cfg := range *common.ExtraTemplates { + if _, exists := templates[filename]; !exists { + templates[filename] = cfg + continue + } + + prevTempl := ExtraTemplate{ + Overwrite: templates[filename].Overwrite, + Content: templates[filename].Content, + } + + if cfg.Overwrite != nil { + prevTempl.Overwrite = cfg.Overwrite + } + if cfg.Content != nil { + prevTempl.Content = cfg.Content + } + templates[filename] = prevTempl + } + } + + return templates +} + func ResolveSnowflakeProvider(commons ...Common) *SnowflakeProvider { account := lastNonNil(SnowflakeProviderAccountGetter, commons...) role := lastNonNil(SnowflakeProviderRoleGetter, commons...) @@ -335,7 +333,7 @@ func ResolveSnowflakeProvider(commons ...Common) *SnowflakeProvider { Role: role, Region: region, CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(SnowflakeProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(SnowflakeProviderCustomProviderGetter, commons...), Enabled: defaultEnabled(true), Version: version, }, @@ -359,7 +357,7 @@ func ResolveOktaProvider(commons ...Common) *OktaProvider { BaseURL: baseURL, RegistryNamespace: registryNamespace, CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(OktaProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(OktaProviderCustomProviderGetter, commons...), Enabled: defaultEnabled(true), Version: lastNonNil(OktaProviderVersionGetter, commons...), }, @@ -381,7 +379,7 @@ func ResolveBlessProvider(commons ...Common) *BlessProvider { AWSRegion: region, RoleArn: roleArn, CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(BlessProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(BlessProviderCustomProviderGetter, commons...), Enabled: defaultEnabled(true), Version: lastNonNil(BlessProviderVersionGetter, commons...), }, @@ -406,7 +404,7 @@ func ResolveHerokuProvider(commons ...Common) *HerokuProvider { if version != nil { return &HerokuProvider{ CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(HerokuProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(HerokuProviderCustomProviderGetter, commons...), Enabled: defaultEnabled(true), Version: version, }, @@ -432,7 +430,7 @@ func ResolveDatadogProvider(commons ...Common) *DatadogProvider { if version != nil { return &DatadogProvider{ CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(DatadogProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(DatadogProviderCustomProviderGetter, commons...), Enabled: defaultEnabled(true), Version: version, }, @@ -458,7 +456,7 @@ func ResolvePagerdutyProvider(commons ...Common) *PagerdutyProvider { if version != nil { return &PagerdutyProvider{ CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(PagerDutyProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(PagerDutyProviderCustomProviderGetter, commons...), Enabled: defaultEnabled(true), Version: version, }, @@ -484,7 +482,7 @@ func ResolveOpsGenieProvider(commons ...Common) *OpsGenieProvider { if version != nil { return &OpsGenieProvider{ CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(OpsGenieProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(OpsGenieProviderCustomProviderGetter, commons...), Enabled: defaultEnabled(true), Version: version, }, @@ -507,7 +505,7 @@ func ResolveDatabricksProvider(commons ...Common) *DatabricksProvider { if version != nil { return &DatabricksProvider{ CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(DatabricksProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(DatabricksProviderCustomProviderGetter, commons...), Enabled: defaultEnabled(true), Version: version, }, @@ -534,7 +532,7 @@ func ResolveSentryProvider(commons ...Common) *SentryProvider { if version != nil { return &SentryProvider{ CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(SentryProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(SentryProviderCustomProviderGetter, commons...), Enabled: defaultEnabled(true), Version: version, }, @@ -580,7 +578,7 @@ func ResolveTfeProvider(commons ...Common) *TfeProvider { if version != nil { return &TfeProvider{ CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(TFEProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(TFEProviderCustomProviderGetter, commons...), Enabled: enabled, Version: version, }, @@ -623,7 +621,7 @@ func ResolveKubernetesProvider(commons ...Common) *KubernetesProvider { return &KubernetesProvider{ ClusterComponentName: clusterComponentName, CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(KubernetesProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(KubernetesProviderCustomProviderGetter, commons...), Enabled: enabled, Version: version, }, @@ -662,7 +660,7 @@ func ResolveHelmProvider(commons ...Common) *HelmProvider { if version != nil { return &HelmProvider{ CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(HelmProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(HelmProviderCustomProviderGetter, commons...), Enabled: enabled, Version: version, }, @@ -702,7 +700,7 @@ func ResolveKubectlProvider(commons ...Common) *KubectlProvider { if version != nil { return &KubectlProvider{ CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(KubectlProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(KubectlProviderCustomProviderGetter, commons...), Enabled: enabled, Version: version, }, @@ -742,7 +740,7 @@ func ResolveGrafanaProvider(commons ...Common) *GrafanaProvider { if version != nil { return &GrafanaProvider{ CommonProvider: CommonProvider{ - CustomProvider: lastNonNilBool(GrafanaProviderCustomProviderGetter, commons...), + CustomProvider: lastNonNil(GrafanaProviderCustomProviderGetter, commons...), Enabled: enabled, Version: version, }, diff --git a/plan/plan.go b/plan/plan.go index e4e19705b..de12508bb 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -38,6 +38,11 @@ type Common struct { TerraformVersion string `yaml:"terraform_version"` } +type ExtraTemplate struct { + Overwrite bool + Content string +} + // ComponentCommon represents common fields for components type ComponentCommon struct { Common `yaml:",inline"` @@ -54,6 +59,7 @@ type ComponentCommon struct { ProviderConfiguration ProviderConfiguration `yaml:"providers_configuration"` ProviderVersions map[string]ProviderVersion `yaml:"provider_versions"` NeedsAWSAccountsVariable bool `yaml:"needs_aws_accounts_variable"` + ExtraTemplates map[string]ExtraTemplate `yaml:"extra_templates"` TfLint TfLint `yaml:"tf_lint"` @@ -1254,7 +1260,24 @@ func resolveComponentCommon(commons ...v2.Common) ComponentCommon { } } + extraTemplates := map[string]ExtraTemplate{} + for k, v := range v2.ResolveExtraTemplates(commons...) { + resolvedTempl := ExtraTemplate{} + + if v.Content != nil { + resolvedTempl.Content = *v.Content + } + if v.Overwrite != nil { + resolvedTempl.Overwrite = *v.Overwrite + } + extraTemplates[k] = ExtraTemplate{ + Overwrite: resolvedTempl.Overwrite, + Content: resolvedTempl.Content, + } + } + return ComponentCommon{ + ExtraTemplates: extraTemplates, NeedsAWSAccountsVariable: v2.ResolveAWSAccountsNeeded(commons...), Backend: backend, ProviderConfiguration: ProviderConfiguration{