feat(app/sources): source create view (#2680)

Co-authored-by: Chaim Lev-Ari <chaim.lev-ari@portainer.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
LP B
2026-06-10 20:34:46 +02:00
committed by GitHub
parent d7a1d34be7
commit 0c2f07988a
61 changed files with 2272 additions and 282 deletions
+8 -3
View File
@@ -16,6 +16,8 @@ import (
"github.com/portainer/portainer/api/set"
"github.com/portainer/portainer/api/slicesx"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/rs/zerolog/log"
)
func EndpointMatchesStackType(ep portainer.Endpoint, stackType portainer.StackType) bool {
@@ -115,7 +117,8 @@ func buildEndpointAccessMap(k8sFactory *cli.ClientFactory, sc *security.Restrict
access, err := resolveKubeAccess(k8sFactory, sc, &ep)
if err != nil {
return nil, err
log.Warn().Err(err).Str("context", "buildEndpointAccessMap").Int("endpoint_id", int(epID)).Msg("Failed to resolve kube access for endpoint, skipping")
continue
}
result[epID] = access
@@ -148,7 +151,8 @@ func filterK8SStacks(items []portainer.Stack, endpointMap map[portainer.Endpoint
kcl, err := k8sFactory.GetPrivilegedKubeClient(&ep)
if err != nil {
return nil, err
log.Warn().Err(err).Str("context", "filterK8SStacks").Int("endpoint_id", int(envID)).Msg("Failed to get kube client for endpoint, skipping")
continue
}
access := accessMap[envID]
@@ -157,7 +161,8 @@ func filterK8SStacks(items []portainer.Stack, endpointMap map[portainer.Endpoint
apps, err := kcl.GetApplications("", "")
if err != nil {
return nil, err
log.Warn().Err(err).Str("context", "filterK8SStacks").Int("endpoint_id", int(envID)).Msg("Failed to get kube applications for endpoint, skipping")
continue
}
for _, s := range stacks {
+15 -3
View File
@@ -348,8 +348,9 @@ func gitAuthMatches(a, b *gittypes.GitAuthentication) bool {
return a.Username == b.Username && a.Password == b.Password && a.GitCredentialID == b.GitCredentialID
}
// ValidateUniqueSourceURL validates there are no other sources with the same URL
func ValidateUniqueSourceURL(tx gitSourceStore, url string, sourceID portainer.SourceID) (bool, error) {
// ValidateUniqueSource validates there are no other sources with the same URL and credentials.
// Pass empty strings for username and password when the source has no authentication.
func ValidateUniqueSource(tx gitSourceStore, url, username, password string, sourceID portainer.SourceID) (bool, error) {
normalizedURL, err := gittypes.NormalizeURL(gittypes.SanitizeURL(url))
if err != nil {
return false, err
@@ -361,8 +362,12 @@ func ValidateUniqueSourceURL(tx gitSourceStore, url string, sourceID portainer.S
}
normalized, err := gittypes.NormalizeURL(gittypes.SanitizeURL(s.Git.URL))
if err != nil || normalized != normalizedURL {
return false
}
return err == nil && normalized == normalizedURL
existingUsername, existingPassword := gitAuthCredentials(s.Git.Authentication)
return existingUsername == username && existingPassword == password
})
if err != nil {
@@ -371,3 +376,10 @@ func ValidateUniqueSourceURL(tx gitSourceStore, url string, sourceID portainer.S
return len(existing) == 0, nil
}
func gitAuthCredentials(auth *gittypes.GitAuthentication) (username, password string) {
if auth == nil {
return "", ""
}
return auth.Username, auth.Password
}
@@ -769,3 +769,97 @@ func TestFindOrCreateGitSource_StripsEmbeddedCredentialsFromURL(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
}
func newSourceWithAuth(url, username, password string) *portainer.Source {
return &portainer.Source{
Type: portainer.SourceTypeGit,
Git: &gittypes.RepoConfig{
URL: url,
Authentication: &gittypes.GitAuthentication{
Username: username,
Password: password,
},
},
}
}
func newAuthlessSource(url string) *portainer.Source {
return &portainer.Source{
Type: portainer.SourceTypeGit,
Git: &gittypes.RepoConfig{URL: url},
}
}
func validateUniqueSourceInStore(t *testing.T, store *datastore.Store, url, username, password string, sourceID portainer.SourceID) bool {
t.Helper()
var isUnique bool
require.NoError(t, store.ViewTx(func(tx dataservices.DataStoreTx) error {
var err error
isUnique, err = ValidateUniqueSource(tx, url, username, password, sourceID)
return err
}))
return isUnique
}
func TestValidateUniqueSource_SameURLAndCreds_IsDuplicate(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, true)
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
return tx.Source().Create(newSourceWithAuth("https://github.com/org/repo.git", "alice", "secret"))
}))
require.False(t, validateUniqueSourceInStore(t, store, "https://github.com/org/repo.git", "alice", "secret", 0))
}
func TestValidateUniqueSource_SameURLDifferentCreds_IsUnique(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, true)
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
return tx.Source().Create(newSourceWithAuth("https://github.com/org/repo.git", "alice", "secret"))
}))
require.True(t, validateUniqueSourceInStore(t, store, "https://github.com/org/repo.git", "bob", "other", 0))
}
func TestValidateUniqueSource_TwoAuthlessSameURL_IsDuplicate(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, true)
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
return tx.Source().Create(newAuthlessSource("https://github.com/org/repo.git"))
}))
require.False(t, validateUniqueSourceInStore(t, store, "https://github.com/org/repo.git", "", "", 0))
}
func TestValidateUniqueSource_AuthlessVsAuthenticated_IsUnique(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, true)
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
return tx.Source().Create(newAuthlessSource("https://github.com/org/repo.git"))
}))
require.True(t, validateUniqueSourceInStore(t, store, "https://github.com/org/repo.git", "alice", "secret", 0))
}
func TestValidateUniqueSource_ExcludesSelf(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, true)
var srcID portainer.SourceID
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
src := newSourceWithAuth("https://github.com/org/repo.git", "alice", "secret")
if err := tx.Source().Create(src); err != nil {
return err
}
srcID = src.ID
return nil
}))
require.True(t, validateUniqueSourceInStore(t, store, "https://github.com/org/repo.git", "alice", "secret", srcID))
}