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) +}
