This is an automated email from the ASF dual-hosted git repository.

squakez pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-k.git

commit 5634a0a2b8ace569733f99194e8a036c417f8823
Author: Jan Bouecke <[email protected]>
AuthorDate: Thu Oct 8 13:14:53 2026 +0200

    Fix #6869: make the gitops trait Git username configurable
    
    The gitops trait authenticated clone and push with the hardcoded
    username "camel-k". Bitbucket Cloud access tokens require the username
    "x-token-auth", so the trait could not be used with Bitbucket.
    
    Add the `gitops.username` trait property (default `camel-k`, backward
    compatible) and use it for both clone and push.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 docs/modules/ROOT/partials/apis/camel-k-crds.adoc  |   8 ++
 docs/modules/traits/pages/gitops.adoc              |   5 +
 helm/camel-k/crds/camel-k-crds.yaml                |  40 ++++++++
 pkg/apis/camel/v1/trait/gitops.go                  |   3 +
 .../camel.apache.org_integrationplatforms.yaml     |  10 ++
 .../camel.apache.org_integrationprofiles.yaml      |  10 ++
 .../crd/bases/camel.apache.org_integrations.yaml   |  10 ++
 .../config/crd/bases/camel.apache.org_pipes.yaml   |  10 ++
 pkg/trait/gitops.go                                |  13 ++-
 pkg/trait/gitops_test.go                           |  42 +++++++++
 pkg/util/gitops/gitops.go                          |  16 +++-
 pkg/util/gitops/gitops_test.go                     | 103 +++++++++++++++++++++
 12 files changed, 267 insertions(+), 3 deletions(-)

diff --git a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc 
b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc
index 7f8518c7b..a9ff2589e 100644
--- a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc
+++ b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc
@@ -7502,6 +7502,14 @@ string
 
 the Kubernetes secret where the Git token is stored. The operator will pick up 
the first secret key only, whichever the name it is.
 
+|`username` +
+string
+|
+
+
+the username used to authenticate against the Git server, together with the 
token stored in `secret` (default `camel-k`).
+Some Git servers validate it, for example Bitbucket Cloud access tokens 
require `x-token-auth`.
+
 |`branch` +
 string
 |
diff --git a/docs/modules/traits/pages/gitops.adoc 
b/docs/modules/traits/pages/gitops.adoc
index 2f46a5ba5..b3227cfa3 100644
--- a/docs/modules/traits/pages/gitops.adoc
+++ b/docs/modules/traits/pages/gitops.adoc
@@ -37,6 +37,11 @@ The following configuration options are available:
 | string
 | the Kubernetes secret where the Git token is stored. The operator will pick 
up the first secret key only, whichever the name it is.
 
+| gitops.username
+| string
+| the username used to authenticate against the Git server, together with the 
token stored in `secret` (default `camel-k`).
+Some Git servers validate it, for example Bitbucket Cloud access tokens 
require `x-token-auth`.
+
 | gitops.branch
 | string
 | the git branch to check out.
diff --git a/helm/camel-k/crds/camel-k-crds.yaml 
b/helm/camel-k/crds/camel-k-crds.yaml
index 1a775f7e8..d5b1c4b28 100644
--- a/helm/camel-k/crds/camel-k-crds.yaml
+++ b/helm/camel-k/crds/camel-k-crds.yaml
@@ -4509,6 +4509,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
@@ -7084,6 +7089,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
@@ -9620,6 +9630,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
@@ -12144,6 +12159,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
@@ -21730,6 +21750,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
@@ -24142,6 +24167,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
@@ -35091,6 +35121,11 @@ spec:
                             description: the URL of the repository where the 
project
                               is stored.
                             type: string
+                          username:
+                            description: |-
+                              the username used to authenticate against the 
Git server, together with the token stored in `secret` (default `camel-k`).
+                              Some Git servers validate it, for example 
Bitbucket Cloud access tokens require `x-token-auth`.
+                            type: string
                         type: object
                       health:
                         description: The configuration of Health trait
@@ -37425,6 +37460,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
diff --git a/pkg/apis/camel/v1/trait/gitops.go 
b/pkg/apis/camel/v1/trait/gitops.go
index b1457a867..1db98c3fe 100644
--- a/pkg/apis/camel/v1/trait/gitops.go
+++ b/pkg/apis/camel/v1/trait/gitops.go
@@ -31,6 +31,9 @@ type GitOpsTrait struct {
        URL string `json:"url,omitempty" property:"url"`
        // the Kubernetes secret where the Git token is stored. The operator 
will pick up the first secret key only, whichever the name it is.
        Secret string `json:"secret,omitempty" property:"secret"`
+       // the username used to authenticate against the Git server, together 
with the token stored in `secret` (default `camel-k`).
+       // Some Git servers validate it, for example Bitbucket Cloud access 
tokens require `x-token-auth`.
+       Username string `json:"username,omitempty" property:"username"`
        // the git branch to check out.
        Branch string `json:"branch,omitempty" property:"branch"`
        // the git tag to check out.
diff --git 
a/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml 
b/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml
index 09ad21815..2b1454c90 100644
--- a/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml
+++ b/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml
@@ -1197,6 +1197,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
@@ -3772,6 +3777,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
diff --git 
a/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml 
b/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml
index 69e52466a..6a77205b6 100644
--- a/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml
+++ b/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml
@@ -1122,6 +1122,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
@@ -3646,6 +3651,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml 
b/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml
index 9466171e1..3d495a82d 100644
--- a/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml
+++ b/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml
@@ -8175,6 +8175,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
@@ -10587,6 +10592,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
diff --git a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml 
b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml
index 893ae100a..abbc7fd46 100644
--- a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml
+++ b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml
@@ -8230,6 +8230,11 @@ spec:
                             description: the URL of the repository where the 
project
                               is stored.
                             type: string
+                          username:
+                            description: |-
+                              the username used to authenticate against the 
Git server, together with the token stored in `secret` (default `camel-k`).
+                              Some Git servers validate it, for example 
Bitbucket Cloud access tokens require `x-token-auth`.
+                            type: string
                         type: object
                       health:
                         description: The configuration of Health trait
@@ -10564,6 +10569,11 @@ spec:
                         description: the URL of the repository where the 
project is
                           stored.
                         type: string
+                      username:
+                        description: |-
+                          the username used to authenticate against the Git 
server, together with the token stored in `secret` (default `camel-k`).
+                          Some Git servers validate it, for example Bitbucket 
Cloud access tokens require `x-token-auth`.
+                        type: string
                     type: object
                   health:
                     description: The configuration of Health trait
diff --git a/pkg/trait/gitops.go b/pkg/trait/gitops.go
index f854e20b8..6f58c274b 100644
--- a/pkg/trait/gitops.go
+++ b/pkg/trait/gitops.go
@@ -117,7 +117,7 @@ func (t *gitOpsTrait) pushGitOpsItInGitRepo(ctx 
context.Context, it *v1.Integrat
        }
 
        // Clone repo
-       repo, err := util.CloneGitProject(gitConf, dir, token)
+       repo, err := util.CloneGitProjectWithUsername(gitConf, dir, 
t.getUsername(), token)
        if err != nil {
                return err
        }
@@ -195,7 +195,7 @@ func (t *gitOpsTrait) pushGitOpsItInGitRepo(ctx 
context.Context, it *v1.Integrat
        gitPushOptions := &git.PushOptions{
                RemoteURL: gitConf.URL,
                Auth: &http.BasicAuth{
-                       Username: "camel-k",
+                       Username: t.getUsername(),
                        Password: token,
                },
                RefSpecs: []config.RefSpec{
@@ -294,6 +294,15 @@ func (t *gitOpsTrait) getIntegrationsDirectory() string {
        return t.IntegrationDirectory
 }
 
+// Return the trait username parameter or a default value.
+func (t *gitOpsTrait) getUsername() string {
+       if t.Username == "" {
+               return util.DefaultGitUsername
+       }
+
+       return t.Username
+}
+
 // Return the trait committer name parameter or a default value.
 func (t *gitOpsTrait) getCommitterName() string {
        if t.CommiterName == "" {
diff --git a/pkg/trait/gitops_test.go b/pkg/trait/gitops_test.go
index 227d4acc9..5f33a9649 100644
--- a/pkg/trait/gitops_test.go
+++ b/pkg/trait/gitops_test.go
@@ -20,6 +20,8 @@ package trait
 import (
        "context"
        "errors"
+       "net/http"
+       "net/http/httptest"
        "os"
        "path/filepath"
        "testing"
@@ -277,3 +279,43 @@ func TestGitOpsPipeRequiresURL(t *testing.T) {
        require.Error(t, err)
        assert.Contains(t, err.Error(), "gitops trait requires a git URL when 
used with a Pipe")
 }
+
+func TestGitOpsUsername(t *testing.T) {
+       trait, _ := newGitOpsTrait().(*gitOpsTrait)
+       assert.Equal(t, "camel-k", trait.getUsername())
+       trait.Username = "x-token-auth"
+       assert.Equal(t, "x-token-auth", trait.getUsername())
+}
+
+func TestGitOpsPushRepoUsesUsername(t *testing.T) {
+       testCases := []struct {
+               name     string
+               username string
+               expected string
+       }{
+               {name: "default", username: "", expected: "camel-k"},
+               {name: "custom", username: "x-token-auth", expected: 
"x-token-auth"},
+       }
+       for _, tc := range testCases {
+               t.Run(tc.name, func(t *testing.T) {
+                       var gotUsername, gotPassword string
+                       var gotAuth bool
+                       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+                               gotUsername, gotPassword, gotAuth = 
r.BasicAuth()
+                               w.WriteHeader(http.StatusUnauthorized)
+                       }))
+                       defer server.Close()
+
+                       trait, _ := newGitOpsTrait().(*gitOpsTrait)
+                       trait.URL = server.URL + "/repo.git"
+                       trait.Username = tc.username
+                       it := v1.NewIntegration("default", "test")
+
+                       err := trait.pushGitOpsItInGitRepo(context.TODO(), &it, 
t.TempDir(), "my-token")
+                       require.Error(t, err)
+                       assert.True(t, gotAuth)
+                       assert.Equal(t, tc.expected, gotUsername)
+                       assert.Equal(t, "my-token", gotPassword)
+               })
+       }
+}
diff --git a/pkg/util/gitops/gitops.go b/pkg/util/gitops/gitops.go
index c16214216..bed29e976 100644
--- a/pkg/util/gitops/gitops.go
+++ b/pkg/util/gitops/gitops.go
@@ -477,6 +477,10 @@ patches:
        return createOrAppendAll(destinationDir, dstPipe.Name, namespaceDest)
 }
 
+// DefaultGitUsername is the username used to authenticate against the Git 
server when none is provided.
+// Most Git servers (ie, GitHub) accept any non empty value when 
authenticating with a token.
+const DefaultGitUsername = "camel-k"
+
 // GitToken read the first secret data provided by the Integration Git Secret.
 // Returns an empty string if the secret provided is empty.
 func GitToken(ctx context.Context, c client.Client, namespace, secret string) 
(string, error) {
@@ -499,6 +503,13 @@ func GitToken(ctx context.Context, c client.Client, 
namespace, secret string) (s
 // CloneGitProject is in charge to clone the project from a given Git repo 
configuration.
 // If no secretToken is provided, then, it is assumed the project is public.
 func CloneGitProject(gitConf v1.GitConfigSpec, dir, secretToken string) 
(*git.Repository, error) {
+       return CloneGitProjectWithUsername(gitConf, dir, "", secretToken)
+}
+
+// CloneGitProjectWithUsername is in charge to clone the project from a given 
Git repo configuration,
+// authenticating with the given username and secretToken. If no username is 
provided, then, DefaultGitUsername is used.
+// If no secretToken is provided, then, it is assumed the project is public.
+func CloneGitProjectWithUsername(gitConf v1.GitConfigSpec, dir, username, 
secretToken string) (*git.Repository, error) {
        depth := 1
        if gitConf.Commit != "" {
                // only the commit checkout requires full git project history
@@ -527,8 +538,11 @@ func CloneGitProject(gitConf v1.GitConfigSpec, dir, 
secretToken string) (*git.Re
        }
 
        if secretToken != "" {
+               if username == "" {
+                       username = DefaultGitUsername
+               }
                gitCloneOptions.Auth = &http.BasicAuth{
-                       Username: "camel-k", // yes, this can be anything 
except an empty string
+                       Username: username,
                        Password: secretToken,
                }
        }
diff --git a/pkg/util/gitops/gitops_test.go b/pkg/util/gitops/gitops_test.go
new file mode 100644
index 000000000..552fa94d8
--- /dev/null
+++ b/pkg/util/gitops/gitops_test.go
@@ -0,0 +1,103 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package util
+
+import (
+       "net/http"
+       "net/http/httptest"
+       "sync"
+       "testing"
+
+       v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+// basicAuthRecorder is a fake Git HTTP server which records the basic auth 
credentials it receives
+// and rejects every request.
+type basicAuthRecorder struct {
+       mu       sync.Mutex
+       called   bool
+       ok       bool
+       username string
+       password string
+}
+
+func (r *basicAuthRecorder) ServeHTTP(w http.ResponseWriter, req 
*http.Request) {
+       r.mu.Lock()
+       defer r.mu.Unlock()
+       r.called = true
+       r.username, r.password, r.ok = req.BasicAuth()
+       w.WriteHeader(http.StatusUnauthorized)
+}
+
+func cloneWithRecorder(t *testing.T, clone func(conf v1.GitConfigSpec, dir 
string) error) *basicAuthRecorder {
+       t.Helper()
+       recorder := &basicAuthRecorder{}
+       server := httptest.NewServer(recorder)
+       defer server.Close()
+
+       err := clone(v1.GitConfigSpec{URL: server.URL + "/repo.git"}, 
t.TempDir())
+       require.Error(t, err)
+       recorder.mu.Lock()
+       defer recorder.mu.Unlock()
+       assert.True(t, recorder.called)
+
+       return recorder
+}
+
+func TestCloneGitProjectDefaultUsername(t *testing.T) {
+       recorder := cloneWithRecorder(t, func(conf v1.GitConfigSpec, dir 
string) error {
+               _, err := CloneGitProject(conf, dir, "my-token")
+
+               return err
+       })
+       assert.True(t, recorder.ok)
+       assert.Equal(t, DefaultGitUsername, recorder.username)
+       assert.Equal(t, "my-token", recorder.password)
+}
+
+func TestCloneGitProjectWithUsername(t *testing.T) {
+       recorder := cloneWithRecorder(t, func(conf v1.GitConfigSpec, dir 
string) error {
+               _, err := CloneGitProjectWithUsername(conf, dir, 
"x-token-auth", "my-token")
+
+               return err
+       })
+       assert.True(t, recorder.ok)
+       assert.Equal(t, "x-token-auth", recorder.username)
+       assert.Equal(t, "my-token", recorder.password)
+}
+
+func TestCloneGitProjectWithEmptyUsername(t *testing.T) {
+       recorder := cloneWithRecorder(t, func(conf v1.GitConfigSpec, dir 
string) error {
+               _, err := CloneGitProjectWithUsername(conf, dir, "", "my-token")
+
+               return err
+       })
+       assert.True(t, recorder.ok)
+       assert.Equal(t, DefaultGitUsername, recorder.username)
+}
+
+func TestCloneGitProjectWithoutToken(t *testing.T) {
+       recorder := cloneWithRecorder(t, func(conf v1.GitConfigSpec, dir 
string) error {
+               _, err := CloneGitProjectWithUsername(conf, dir, 
"x-token-auth", "")
+
+               return err
+       })
+       assert.False(t, recorder.ok)
+}

Reply via email to