Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .coverage-baseline
Original file line number Diff line number Diff line change
@@ -1 +1 @@
75.3
75.4
4 changes: 4 additions & 0 deletions Taskfile-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ tasks:
- fmt
- vet
- setup-envtest
# cmd's chart-args test runs `helm template`, which evaluates every template —
# including rbac.yaml, which reads the gitignored chart/config/role.yaml this
# syncs. Without it a fresh checkout fails the unit suite.
- helm-sync
cmds:
- |
export KUBEBUILDER_ASSETS="$({{.ENVTEST}} use {{.ENVTEST_K8S_VERSION}} --bin-dir "$(pwd)/bin" -p path)"
Expand Down
5 changes: 4 additions & 1 deletion charts/gitops-reverser/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,12 @@ spec:
{{- end }}
- --redis-addr={{ .Values.queue.redis.addr }}
{{- with .Values.queue.redis.auth.username }}
- --redis-username={{ . | quote }}
- {{ printf "--redis-username=%s" . | quote }}
{{- end }}
- --redis-db={{ .Values.queue.redis.db }}
{{- with .Values.queue.redis.keyPrefix }}
- {{ printf "--redis-key-prefix=%s" . | quote }}
{{- end }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
{{- if not .Values.queue.redis.tls.enabled }}
- --redis-insecure
{{- end }}
Expand Down
6 changes: 6 additions & 0 deletions charts/gitops-reverser/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ queue:
# Optional Redis username for ACL-based auth (Redis 6+). Not sensitive; stays as a plain value.
username: ""
db: 0
# Root namespace for every key this release writes (cursors, attribution facts, command author
# records). Give each reverser its own prefix to share one Redis/Valkey between more than the 16
# logical databases `db` can separate. A Valkey ACL can enforce it (~<prefix>:*) rather than trust
# it. Changing it orphans the keys written under the previous prefix: cursors then cold-replay
# once, which is safe. Allowed characters: [A-Za-z0-9], '-', '_', '.' and ':'.
keyPrefix: "gitops-reverser"
tls:
enabled: false

Expand Down
17 changes: 17 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,12 @@ func main() {
AuthValue: cfg.redisPassword,
DB: cfg.redisDB,
TLSEnabled: !cfg.redisInsecure,
KeyPrefix: cfg.redisKeyPrefix,
})
fatalIfErr(err, "unable to build Redis cursor store")
watchMgr.WatchCursorStore = redisStore
setupLog.Info("Redis keyspace", "addr", cfg.redisAddr, "db", cfg.redisDB,
"keyPrefix", redisStore.KeyPrefix())

redisGate = newRedisReadinessGate(redisStore)
fatalIfErr(mgr.Add(redisGate), "unable to add redis readiness gate")
Expand Down Expand Up @@ -346,6 +349,7 @@ type appConfig struct {
redisUsername string
redisPassword string
redisDB int
redisKeyPrefix string
redisInsecure bool
authorAttribution bool
attributionFactTTL time.Duration
Expand Down Expand Up @@ -433,6 +437,12 @@ func parseFlagsWithArgs(fs *flag.FlagSet, args []string) (appConfig, error) {
"Redis password. Prefer setting via REDIS_PASSWORD env var from a Secret.",
)
fs.IntVar(&cfg.redisDB, "redis-db", 0, "Redis database index (default 0).")
fs.StringVar(&cfg.redisKeyPrefix, "redis-key-prefix", queue.DefaultKeyPrefix,
"Root namespace for every key this operator writes to Redis/Valkey (cursors, attribution "+
"facts, command author records). Give each reverser its own prefix to share one "+
"Redis/Valkey between more than the 16 logical databases --redis-db can separate. "+
"Allowed characters are [A-Za-z0-9], '-', '_', '.' and ':'; a Valkey ACL can enforce "+
"the prefix (~<prefix>:*) rather than trust it.")
fs.BoolVar(&cfg.redisInsecure, "redis-insecure", false,
"Connect to Redis over plain TCP instead of TLS (default false; TLS). Redis carries each "+
"GitTarget's watch cursors and, when attribution is on, the audit facts — prefer TLS. Set "+
Expand Down Expand Up @@ -476,6 +486,13 @@ func parseFlagsWithArgs(fs *flag.FlagSet, args []string) (appConfig, error) {
return appConfig{}, err
}
cfg.redisAddr = strings.TrimSpace(cfg.redisAddr)
// Validated even when redis-addr is empty: a typo'd prefix is a configuration error
// whether or not this run happens to open the connection it names.
normalizedKeyPrefix, err := queue.ValidateKeyPrefix(cfg.redisKeyPrefix)
if err != nil {
return appConfig{}, err
}
cfg.redisKeyPrefix = normalizedKeyPrefix
if err := validateAuditConfig(cfg); err != nil {
return appConfig{}, err
}
Expand Down
29 changes: 29 additions & 0 deletions cmd/main_audit_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ConfigButler/gitops-reverser/internal/queue"
)

func TestParseFlagsWithArgs_Defaults(t *testing.T) {
Expand Down Expand Up @@ -97,6 +99,7 @@ func TestParseFlagsWithArgs_CustomAuditValues(t *testing.T) {
"--redis-username=user",
"--redis-password=pass",
"--redis-db=2",
"--redis-key-prefix=cell-a:tenant-7:",
"--redis-insecure",
"--author-attribution-ttl=20m",
"--author-attribution-grace=750ms",
Expand All @@ -119,11 +122,37 @@ func TestParseFlagsWithArgs_CustomAuditValues(t *testing.T) {
assert.Equal(t, "user", cfg.redisUsername)
assert.Equal(t, "pass", cfg.redisPassword)
assert.Equal(t, 2, cfg.redisDB)
// The trailing colon is normalized away, so "tenant-7:" and "tenant-7" name one keyspace.
assert.Equal(t, "cell-a:tenant-7", cfg.redisKeyPrefix)
assert.True(t, cfg.redisInsecure)
assert.Equal(t, 20*time.Minute, cfg.attributionFactTTL)
assert.Equal(t, 750*time.Millisecond, cfg.attributionGrace)
}

func TestParseFlagsWithArgs_RedisKeyPrefixDefault(t *testing.T) {
fs := flag.NewFlagSet("test-redis-key-prefix-default", flag.ContinueOnError)
cfg, err := parseFlagsWithArgs(fs, []string{"--author-attribution=false"})
require.NoError(t, err)
// An upgrade that sets nothing must keep writing the keys the previous release wrote.
assert.Equal(t, queue.DefaultKeyPrefix, cfg.redisKeyPrefix)
}

func TestParseFlagsWithArgs_RedisKeyPrefixRejectsGlob(t *testing.T) {
fs := flag.NewFlagSet("test-redis-key-prefix-glob", flag.ContinueOnError)
// A glob metacharacter would corrupt the attribution fact-index SCAN pattern.
_, err := parseFlagsWithArgs(fs, []string{"--redis-key-prefix=tenant-*"})
require.Error(t, err)
assert.Contains(t, err.Error(), "redis-key-prefix")
}

func TestParseFlagsWithArgs_RedisKeyPrefixRejectsEmpty(t *testing.T) {
fs := flag.NewFlagSet("test-redis-key-prefix-empty", flag.ContinueOnError)
// Rejected even without Redis: an empty prefix silently un-namespaces the keyspace.
_, err := parseFlagsWithArgs(fs, []string{"--author-attribution=false", "--redis-addr=", "--redis-key-prefix="})
require.Error(t, err)
assert.Contains(t, err.Error(), "non-empty")
}

func TestParseFlagsWithArgs_RedisAddrRequiredWhenAttributionEnabled(t *testing.T) {
fs := flag.NewFlagSet("test-redis-required", flag.ContinueOnError)
// Default --author-attribution=true, so an empty redis-addr must be rejected.
Expand Down
108 changes: 108 additions & 0 deletions cmd/main_chart_args_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: Apache-2.0

package main

import (
"flag"
"os"
"os/exec"
"testing"

"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
"sigs.k8s.io/yaml"
)

// chartInputs are read by the test process itself so `go test` records them as cache
// inputs. helm reads the chart in a subprocess, which the test log cannot see, so without
// these reads a chart-only edit would replay a cached PASS.
var chartInputs = []string{
"../charts/gitops-reverser/values.yaml",
"../charts/gitops-reverser/templates/deployment.yaml",
}

// renderManagerArgs renders the chart's Deployment and returns the manager container's
// argv, exactly as the kubelet would hand it to the binary.
func renderManagerArgs(t *testing.T, setValues ...string) []string {
t.Helper()

for _, path := range chartInputs {
_, err := os.ReadFile(path)
require.NoError(t, err)
}

args := []string{
"template", "gitops-reverser", "../charts/gitops-reverser",
"--show-only", "templates/deployment.yaml",
}
for _, sv := range setValues {
args = append(args, "--set", sv)
}
out, err := exec.Command("helm", args...).CombinedOutput()
require.NoErrorf(t, err, "helm template failed: %s", out)

var deploy appsv1.Deployment
require.NoError(t, yaml.Unmarshal(out, &deploy))

for _, c := range deploy.Spec.Template.Spec.Containers {
if c.Name == "manager" {
return c.Args
}
}
t.Fatalf("no manager container in rendered Deployment:\n%s", out)
return nil
}

// The chart writes the manager's argv and the binary parses it; nothing else enforces that
// the two agree. A value interpolated as a bare `{{ . | quote }}` inside a YAML plain scalar
// keeps its double quotes as literal characters, so the flag arrives with them attached and
// a validating flag rejects it — the manager then CrashLoopBackOffs on a chart that renders,
// lints and packages cleanly. Parse what the chart actually emits.
func TestChartRendersArgsTheBinaryAccepts(t *testing.T) {
tests := map[string]struct {
setValues []string
wantKeyPrefix string
wantRedisAddr string
wantRedisDBSet int
wantUsername string
}{
"chart defaults": {
wantKeyPrefix: "gitops-reverser",
},
"nested tenant prefix": {
setValues: []string{"queue.redis.keyPrefix=cell-a:tenant-7"},
wantKeyPrefix: "cell-a:tenant-7",
},
"redis enabled with auth": {
setValues: []string{
"queue.redis.addr=redis.example.com:6379",
"queue.redis.auth.username=reverser",
"queue.redis.db=3",
"queue.redis.keyPrefix=tenant-7",
},
wantKeyPrefix: "tenant-7",
wantRedisAddr: "redis.example.com:6379",
wantRedisDBSet: 3,
wantUsername: "reverser",
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
argv := renderManagerArgs(t, tc.setValues...)

cfg, err := parseFlagsWithArgs(flag.NewFlagSet("manager", flag.ContinueOnError), argv)
require.NoError(t, err, "the binary must accept the argv the chart renders")

require.Equal(t, tc.wantKeyPrefix, cfg.redisKeyPrefix)
if tc.wantRedisAddr != "" {
require.Equal(t, tc.wantRedisAddr, cfg.redisAddr)
require.Equal(t, tc.wantRedisDBSet, cfg.redisDB)
}
if tc.wantUsername != "" {
// A quoted-value render would reach Redis as `"reverser"` and fail ACL auth.
require.Equal(t, tc.wantUsername, cfg.redisUsername)
}
})
}
}
Loading