From f399c7289887824424d7a7e2ffc58cf34536ca8e Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Tue, 21 Apr 2026 17:46:51 -0700 Subject: [PATCH] fix(cli): preserve password-only secret in `flux create source git` `sourcesecret.buildGitSecret` previously wrote the Username and Password secret fields only when *both* were set. With the Azure DevOps PAT flow, `flux create source git --password=` has no username (the token is the credential), so both fields were silently dropped and the resulting secret was empty, breaking authentication. Write the two fields independently, so: - `--username` + `--password` -> username + password keys (unchanged); - `--password` alone -> password key only (fixes #3892); - `--username` alone -> username key only. The SSH-passphrase case also becomes simpler: the duplicated `if options.Password != ""` inside the keypair branch collapses into the top-level write. Adds a `buildGitSecret` unit test covering all four credential shapes, including the Azure DevOps PAT scenario. Closes #3892. Signed-off-by: SAY-5 --- pkg/manifestgen/sourcesecret/sourcesecret.go | 12 ++-- .../sourcesecret/sourcesecret_test.go | 64 +++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/pkg/manifestgen/sourcesecret/sourcesecret.go b/pkg/manifestgen/sourcesecret/sourcesecret.go index 54cca4e8..702c11b0 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret.go @@ -357,8 +357,14 @@ func buildGitSecret(keypair *ssh.KeyPair, hostKey []byte, options Options) (secr secret.Labels = options.Labels secret.StringData = map[string]string{} - if options.Username != "" && options.Password != "" { + // Username and Password are written independently so that callers can + // create a password-only secret (e.g. an Azure DevOps PAT, where the + // username is unused). When a keypair is also present, Password acts as + // the SSH private-key passphrase. + if options.Username != "" { secret.StringData[UsernameSecretKey] = options.Username + } + if options.Password != "" { secret.StringData[PasswordSecretKey] = options.Password } if options.BearerToken != "" { @@ -374,10 +380,6 @@ func buildGitSecret(keypair *ssh.KeyPair, hostKey []byte, options Options) (secr secret.StringData[PrivateKeySecretKey] = string(keypair.PrivateKey) secret.StringData[PublicKeySecretKey] = string(keypair.PublicKey) secret.StringData[KnownHostsSecretKey] = string(hostKey) - // set password if present - if options.Password != "" { - secret.StringData[PasswordSecretKey] = string(options.Password) - } } return secret diff --git a/pkg/manifestgen/sourcesecret/sourcesecret_test.go b/pkg/manifestgen/sourcesecret/sourcesecret_test.go index 8eb619d4..d57a3aad 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret_test.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret_test.go @@ -88,3 +88,67 @@ func Test_PasswordlessLoadKeyPair(t *testing.T) { }) } } + +// Test_buildGitSecret_BasicAuthFields covers the regression reported in +// https://github.com/fluxcd/flux2/issues/3892: providing just --password +// (e.g. an Azure DevOps PAT, which ignores the username) must still +// produce a secret containing the password field. +func Test_buildGitSecret_BasicAuthFields(t *testing.T) { + tests := []struct { + name string + opts Options + wantUsername string + wantPassword string + wantHasUser bool + wantHasPass bool + }{ + { + name: "username and password", + opts: Options{Username: "git", Password: "pw"}, + wantUsername: "git", + wantPassword: "pw", + wantHasUser: true, + wantHasPass: true, + }, + { + name: "password only (Azure DevOps PAT)", + opts: Options{Password: "pat-token"}, + wantPassword: "pat-token", + wantHasUser: false, + wantHasPass: true, + }, + { + name: "username only", + opts: Options{Username: "git"}, + wantUsername: "git", + wantHasUser: true, + wantHasPass: false, + }, + { + name: "no credentials", + opts: Options{}, + wantHasUser: false, + wantHasPass: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + secret := buildGitSecret(nil, nil, tt.opts) + gotUser, hasUser := secret.StringData[UsernameSecretKey] + gotPass, hasPass := secret.StringData[PasswordSecretKey] + if hasUser != tt.wantHasUser { + t.Errorf("username presence = %v, want %v", hasUser, tt.wantHasUser) + } + if hasPass != tt.wantHasPass { + t.Errorf("password presence = %v, want %v", hasPass, tt.wantHasPass) + } + if tt.wantHasUser && gotUser != tt.wantUsername { + t.Errorf("username = %q, want %q", gotUser, tt.wantUsername) + } + if tt.wantHasPass && gotPass != tt.wantPassword { + t.Errorf("password = %q, want %q", gotPass, tt.wantPassword) + } + }) + } +}