Skip to content

Commit 613401b

Browse files
authored
refactor: move tfbuddy-owned config to viper (#64)
1 parent 3e3607a commit 613401b

52 files changed

Lines changed: 996 additions & 379 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: docs ci
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
pull_request:
8+
paths:
9+
- '.github/workflows/on_pull_request_docs.yaml'
10+
- 'Makefile'
11+
- 'docs/usage.md'
12+
- 'hack/gen-config-docs.go'
13+
- 'hack/templates/usage_config.tmpl'
14+
- 'internal/config/**'
15+
- 'go.mod'
16+
- 'go.sum'
17+
18+
jobs:
19+
ci-docs:
20+
runs-on: ubuntu-22.04
21+
steps:
22+
- uses: actions/checkout@v3
23+
24+
- uses: wistia/parse-tool-versions@v1.0
25+
26+
- uses: actions/setup-go@v5
27+
with:
28+
go-version: ${{ env.GOLANG_TOOL_VERSION }}
29+
30+
- name: Verify generated docs
31+
run: |
32+
make rebuild-docs
33+
if ! git diff --quiet; then
34+
echo "::error::Generated docs are out of date."
35+
echo "Run: make rebuild-docs"
36+
echo "Then commit the updated files."
37+
git diff
38+
exit 1
39+
fi

.github/workflows/on_pull_request_go.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ jobs:
2020
- uses: earthly/actions-setup@v1
2121
with: { version: "v${{ env.EARTHLY_TOOL_VERSION }}" }
2222

23-
- run: earthly +ci-golang --GOLANG_VERSION=${{ env.GOLANG_TOOL_VERSION }}
23+
- run: earthly +ci-golang --GOLANG_VERSION=${{ env.GOLANG_TOOL_VERSION }}

Earthfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ ARG token=""
44
ARG CI_REGISTRY_IMAGE="tfbuddy"
55
ARG CHART_RELEASER_VERSION="1.4.1"
66
ARG CHART_TESTING_VERSION="3.7.1"
7-
ARG GOLANG_VERSION="1.25.3"
7+
ARG GOLANG_VERSION="1.26.2"
88
ARG HELM_VERSION="3.8.1"
99
ARG HELM_UNITTEST_VERSION="0.2.8"
1010
ARG KUBECONFORM_VERSION="0.5.0"

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ tail-local:
3535
stern --context minikube --namespace tfbuddy-localdev tfbuddy-v -s10s
3636

3737
generate-requirements-file:
38-
poetry export -f requirements.txt --output docs/requirements.txt --without-hashes
38+
poetry export -f requirements.txt --output docs/requirements.txt --without-hashes
39+
40+
rebuild-docs:
41+
go generate ./internal/config

cmd/root.go

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import (
55
"fmt"
66
"log"
77
"os"
8-
"strconv"
9-
"strings"
108

119
"github.com/rs/zerolog"
1210
"github.com/spf13/cobra"
1311

12+
"github.com/zapier/tfbuddy/internal/config"
1413
"github.com/zapier/tfbuddy/internal/logging"
1514
"github.com/zapier/tfbuddy/internal/telemetry"
1615
"github.com/zapier/tfbuddy/pkg"
@@ -19,25 +18,26 @@ import (
1918
)
2019

2120
var cfgFile string
22-
var logLevel string
2321

2422
// rootCmd represents the base command when called without any subcommands
2523
var rootCmd = &cobra.Command{
2624
Use: "tfbuddy",
2725
Short: "Various utilties to aid Terraform CI pipelines & Terraform Cloud runs",
2826
Long: ``,
2927
PersistentPreRun: func(cmd *cobra.Command, args []string) {
30-
logging.SetupLogOutput(resolveLogLevel())
28+
cfg := loadConfig()
29+
logging.SetupLogOutput(cfg.DevMode, resolveLogLevel(cfg))
3130
},
3231
}
3332

34-
func resolveLogLevel() zerolog.Level {
35-
logLevelEnv := os.Getenv("TFBUDDY_LOG_LEVEL")
36-
if logLevelEnv != "" {
37-
logLevel = logLevelEnv
38-
}
33+
func loadConfig() config.Config {
34+
cfg, err := config.Load()
35+
cobra.CheckErr(err)
36+
return cfg
37+
}
3938

40-
lvl, err := zerolog.ParseLevel(logLevel)
39+
func resolveLogLevel(cfg config.Config) zerolog.Level {
40+
lvl, err := zerolog.ParseLevel(cfg.LogLevel)
4141
if err != nil {
4242
log.Println("could not parse log level, defaulting to 'info'")
4343
lvl = zerolog.InfoLevel
@@ -48,26 +48,21 @@ func resolveLogLevel() zerolog.Level {
4848
// Execute adds all child commands to the root command and sets flags appropriately.
4949
// This is called by main.main(). It only needs to happen once to the rootCmd.
5050
func Execute() {
51-
logging.SetupLogOutput(resolveLogLevel())
5251
cobra.CheckErr(rootCmd.Execute())
5352
}
5453

5554
func init() {
55+
config.Init()
5656
cobra.OnInitialize(initConfig)
5757

58-
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "v", "info", "Set the log output level (info, debug, trace)")
58+
cobra.CheckErr(config.RegisterFlags(rootCmd.PersistentFlags()))
5959
}
6060

61-
func initTelemetry(ctx context.Context) (*telemetry.OperatorTelemetry, error) {
62-
enableOtel, err := strconv.ParseBool(os.Getenv("TFBUDDY_OTEL_ENABLED"))
63-
if err != nil {
64-
log.Printf("could not parse env var TFBUDDY_OTEL_ENABLED, defaulting to false: %v", err)
65-
enableOtel = false
66-
}
61+
func initTelemetry(ctx context.Context, cfg config.Config) (*telemetry.OperatorTelemetry, error) {
6762
return telemetry.Init(ctx, "tfbuddy", telemetry.Options{
68-
Enabled: enableOtel,
69-
Host: os.Getenv("TFBUDDY_OTEL_COLLECTOR_HOST"),
70-
Port: os.Getenv("TFBUDDY_OTEL_COLLECTOR_PORT"),
63+
Enabled: cfg.OTELEnabled,
64+
Host: cfg.OTELCollectorHost,
65+
Port: cfg.OTELCollectorPort,
7166
Version: pkg.GitTag,
7267
CommitSHA: pkg.GitCommit,
7368
})
@@ -89,10 +84,6 @@ func initConfig() {
8984
viper.SetConfigName(".tfbuddy")
9085
}
9186

92-
viper.SetEnvPrefix("TFBUDDY")
93-
viper.EnvKeyReplacer(strings.NewReplacer("-", "_"))
94-
viper.AutomaticEnv() // read in environment variables that match
95-
9687
// If a config file is found, read it in.
9788
if err := viper.ReadInConfig(); err == nil {
9889
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())

cmd/tfc_hook_handler.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ var tfcHookHandlerCmd = &cobra.Command{
2020
Long: ``,
2121
Run: func(cmd *cobra.Command, args []string) {
2222
ctx := context.Background()
23+
cfg := loadConfig()
2324

24-
t, err := initTelemetry(ctx)
25+
t, err := initTelemetry(ctx, cfg)
2526
if err != nil {
2627
panic(err)
2728
}
2829
defer t.Shutdown()
2930

30-
hooks.StartServer()
31+
hooks.StartServer(cfg)
3132

3233
},
3334
PreRunE: func(cmd *cobra.Command, args []string) error {

docs/usage.md

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,35 @@ helm install tfbuddy charts/tfbuddy \
4040

4141
The default helm values can be found [here](https://github.com/zapier/tfbuddy/blob/main/charts/tfbuddy/values.yaml).
4242

43-
### Configuration
44-
45-
Set the necessary environment variables for your setup.
46-
```yaml
47-
env:
48-
TFBUDDY_LOG_LEVEL: info
49-
TFBUDDY_NATS_SERVICE_URL: nats://tfbuddy-nats:4222
50-
TFBUDDY_PROJECT_ALLOW_LIST: tfc-project/
51-
TFBUDDY_WORKSPACE_ALLOW_LIST: tfc-workspace
52-
TFBUDDY_DEFAULT_TFC_ORGANIZATION: companyX
53-
# Optional setting to disable auto merging MRs after a successful apply. This is enabled by default.
54-
TFBUDDY_ALLOW_AUTO_MERGE: "false"
55-
# Optional: fail CI if Sentinel policy checks soft-fail on plan (default: disabled)
56-
# Set to "true" to mark the plan commit status as failed when policies soft-fail
57-
TFBUDDY_FAIL_CI_ON_SENTINEL_SOFT_FAIL: "false"
58-
# Optional: enable automatic cleanup of old discussion comments per workspace and action.
59-
# When enabled ("true"), TFBuddy deletes previous plan/apply discussions for the same
60-
# workspace, keeping only the most recent one. Discussions for other workspaces or
61-
# actions are preserved. Set to "false" or remove to disable.
62-
TFBUDDY_DELETE_OLD_COMMENTS: "true"
63-
```
43+
<!-- BEGIN GENERATED CONFIGURATION -->
44+
## Configuration
45+
46+
`tfbuddy` can be configured to meet your specific setup through environment variables or flags.
47+
48+
The full list of supported environment variables and flags is described below:
49+
50+
|Env Var|Flag|Description|Default Value|
51+
|---|---|---|---|
52+
|`TFBUDDY_LOG_LEVEL`|`--log-level, -v`|Set the log output level (info, debug, trace)|`info`|
53+
|`TFBUDDY_DEV_MODE`|`--dev-mode`|Enable developer-friendly console logging output.|`false`|
54+
|`TFBUDDY_OTEL_ENABLED`|`--otel-enabled`|Enable OpenTelemetry export for TFBuddy.|`false`|
55+
|`TFBUDDY_OTEL_COLLECTOR_HOST`|`--otel-collector-host`|OpenTelemetry collector host.||
56+
|`TFBUDDY_OTEL_COLLECTOR_PORT`|`--otel-collector-port`|OpenTelemetry collector port.||
57+
|`TFBUDDY_GITLAB_HOOK_SECRET_KEY`|`--gitlab-hook-secret-key`|Secret key used to validate incoming GitLab webhooks.||
58+
|`TFBUDDY_GITHUB_HOOK_SECRET_KEY`|`--github-hook-secret-key`|Secret key used to validate incoming GitHub webhooks.||
59+
|`TFBUDDY_DEFAULT_TFC_ORGANIZATION`|`--default-tfc-organization`|Default Terraform Cloud organization for workspaces that omit one in .tfbuddy.yaml.||
60+
|`TFBUDDY_WORKSPACE_ALLOW_LIST`|`--workspace-allow-list`|Comma-separated workspace allow list. Entries without an organization use the default Terraform Cloud organization.||
61+
|`TFBUDDY_WORKSPACE_DENY_LIST`|`--workspace-deny-list`|Comma-separated workspace deny list. Entries without an organization use the default Terraform Cloud organization.||
62+
|`TFBUDDY_ALLOW_AUTO_MERGE`|`--allow-auto-merge`|Globally enable or disable TFBuddy-managed auto-merge.|`true`|
63+
|`TFBUDDY_FAIL_CI_ON_SENTINEL_SOFT_FAIL`|`--fail-ci-on-sentinel-soft-fail`|Mark CI as failed when Terraform policy checks soft-fail.|`false`|
64+
|`TFBUDDY_DELETE_OLD_COMMENTS`|`--delete-old-comments`|Delete older bot comments for the same workspace and action after posting a newer one.|`false`|
65+
|`TFBUDDY_NATS_SERVICE_URL`|`--nats-service-url`|NATS connection URL. When empty, TFBuddy falls back to the NATS client default.||
66+
|`TFBUDDY_GITLAB_PROJECT_ALLOW_LIST`|`--gitlab-project-allow-list`|Comma-separated GitLab project allow list prefixes.||
67+
|`TFBUDDY_PROJECT_ALLOW_LIST`|`--project-allow-list`|Deprecated comma-separated GitLab project allow list prefixes.||
68+
|`TFBUDDY_GITHUB_REPO_ALLOW_LIST`|`--github-repo-allow-list`|Comma-separated GitHub repository allow list prefixes.||
69+
|`TFBUDDY_GITHUB_CLONE_DEPTH`|`--github-clone-depth`|Git clone depth to use for GitHub merge request checkouts. Zero means full history.|`0`|
70+
|`TFBUDDY_GITLAB_CLONE_DEPTH`|`--gitlab-clone-depth`|Git clone depth to use for GitLab merge request checkouts. Zero means full history.|`0`|
71+
<!-- END GENERATED CONFIGURATION -->
6472

6573
For sensitive environment variables use `secrets.envs` which can contain a list of key/value pairs
6674
```yaml
@@ -121,4 +129,3 @@ TF Buddy uses [doublestar](https://github.com/bmatcuk/doublestar#about) for its
121129
* `terraform/staging/**/*.tf` - any Terraform files that have `terraform/staging` as an ancestor
122130
* `terraform/staging/{foo,bar}/**` - anything that has `terraform/staging/foo` or `terraform/staging/bar` as an ancestor
123131
* `terraform/staging/**/[^0-9]*` - anything that has `terraform/staging` as an ancestor and does _not_ start with an integer
124-

hack/gen-config-docs.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
_ "embed"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"text/template"
11+
12+
"github.com/zapier/tfbuddy/internal/config"
13+
)
14+
15+
const (
16+
beginMarker = "<!-- BEGIN GENERATED CONFIGURATION -->"
17+
endMarker = "<!-- END GENERATED CONFIGURATION -->"
18+
)
19+
20+
//go:embed templates/usage_config.tmpl
21+
var usageTemplate string
22+
23+
type templateOption struct {
24+
Env string
25+
Flag string
26+
Description string
27+
Default string
28+
}
29+
30+
type templateData struct {
31+
Options []templateOption
32+
}
33+
34+
func main() {
35+
if err := run(); err != nil {
36+
fmt.Fprintln(os.Stderr, err)
37+
os.Exit(1)
38+
}
39+
}
40+
41+
func run() error {
42+
tmpl, err := template.New("usage").Parse(usageTemplate)
43+
if err != nil {
44+
return fmt.Errorf("parse template: %w", err)
45+
}
46+
47+
var rendered bytes.Buffer
48+
data := templateData{Options: make([]templateOption, 0, len(config.DocumentationOptions()))}
49+
for _, option := range config.DocumentationOptions() {
50+
data.Options = append(data.Options, templateOption{
51+
Env: strings.Join(option.EnvVars, "<br>"),
52+
Flag: option.Flag,
53+
Description: option.Description,
54+
Default: option.DefaultValue,
55+
})
56+
}
57+
if err := tmpl.Execute(&rendered, data); err != nil {
58+
return fmt.Errorf("render template: %w", err)
59+
}
60+
61+
usagePath, err := findUsagePath()
62+
if err != nil {
63+
return err
64+
}
65+
current, err := os.ReadFile(usagePath)
66+
if err != nil {
67+
return fmt.Errorf("read usage doc: %w", err)
68+
}
69+
70+
updated, err := replaceGeneratedBlock(string(current), rendered.String())
71+
if err != nil {
72+
return err
73+
}
74+
75+
if err := os.WriteFile(usagePath, []byte(updated), 0o644); err != nil {
76+
return fmt.Errorf("write usage doc: %w", err)
77+
}
78+
return nil
79+
}
80+
81+
func findUsagePath() (string, error) {
82+
candidates := []string{
83+
filepath.Join("docs", "usage.md"),
84+
filepath.Join("..", "..", "docs", "usage.md"),
85+
}
86+
for _, candidate := range candidates {
87+
path := filepath.Clean(candidate)
88+
if _, err := os.Stat(path); err == nil {
89+
return path, nil
90+
}
91+
}
92+
return "", fmt.Errorf("could not locate docs/usage.md from current working directory")
93+
}
94+
95+
func replaceGeneratedBlock(current string, generated string) (string, error) {
96+
start := strings.Index(current, beginMarker)
97+
if start == -1 {
98+
return "", fmt.Errorf("missing begin marker %q", beginMarker)
99+
}
100+
end := strings.Index(current, endMarker)
101+
if end == -1 {
102+
return "", fmt.Errorf("missing end marker %q", endMarker)
103+
}
104+
if end < start {
105+
return "", fmt.Errorf("configuration markers out of order")
106+
}
107+
108+
var output strings.Builder
109+
output.WriteString(current[:start+len(beginMarker)])
110+
output.WriteString("\n")
111+
output.WriteString(strings.TrimRight(generated, "\n"))
112+
output.WriteString("\n")
113+
output.WriteString(current[end:])
114+
return output.String(), nil
115+
}

hack/templates/usage_config.tmpl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Configuration
2+
3+
`tfbuddy` can be configured to meet your specific setup through environment variables or flags.
4+
5+
The full list of supported environment variables and flags is described below:
6+
7+
|Env Var|Flag|Description|Default Value|
8+
|---|---|---|---|
9+
{{- range .Options }}
10+
|`{{ .Env }}`|`{{ .Flag }}`|{{ .Description }}|{{ if .Default }}`{{ .Default }}`{{ end }}|
11+
{{- end }}

0 commit comments

Comments
 (0)