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:
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user