mirror of
https://github.com/portainer/portainer.git
synced 2026-08-07 10:04:49 +00:00
feat(app/sources): UAC on sources (#2881)
Co-authored-by: Chaim Lev-Ari <chaim.lev-ari@portainer.io> Co-authored-by: andres-portainer <91705312+andres-portainer@users.noreply.github.com>
This commit is contained in:
@@ -195,9 +195,20 @@ type (
|
|||||||
BucketName() string
|
BucketName() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SourceServiceUserContext struct {
|
||||||
|
User *portainer.User
|
||||||
|
UserMemberships []portainer.TeamMembership
|
||||||
|
}
|
||||||
|
|
||||||
// SourceService represents a service for managing GitOps source data
|
// SourceService represents a service for managing GitOps source data
|
||||||
SourceService interface {
|
SourceService interface {
|
||||||
BaseCRUD[portainer.Source, portainer.SourceID]
|
Create(context *SourceServiceUserContext, source *portainer.Source) error
|
||||||
|
Read(context *SourceServiceUserContext, ID portainer.SourceID) (*portainer.Source, error)
|
||||||
|
Exists(context *SourceServiceUserContext, ID portainer.SourceID) (bool, error)
|
||||||
|
ReadAll(context *SourceServiceUserContext, predicates ...func(portainer.Source) bool) ([]portainer.Source, error)
|
||||||
|
Update(context *SourceServiceUserContext, ID portainer.SourceID, source *portainer.Source) error
|
||||||
|
Delete(context *SourceServiceUserContext, ID portainer.SourceID) error
|
||||||
|
FindOrCreateGitSource(context *SourceServiceUserContext, source *portainer.Source) (*portainer.Source, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackService represents a service for managing stack data
|
// StackService represents a service for managing stack data
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/set"
|
||||||
|
"github.com/portainer/portainer/api/slicesx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidSource = errors.New("invalid source")
|
||||||
|
ErrInvalidUserContext = errors.New("invalid user context")
|
||||||
|
ErrNotEnoughPermission = errors.New("not enough permissions to perform this action")
|
||||||
|
ErrDuplicateSource = errors.New("a source with this URL and credentials already exists")
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateUserContext(ctx *userContext) error {
|
||||||
|
if ctx == nil || ctx.User == nil {
|
||||||
|
return ErrInvalidUserContext
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
actionRead actionType = "read"
|
||||||
|
actionWrite actionType = "write"
|
||||||
|
)
|
||||||
|
|
||||||
|
func enforceUserPermissions(ctx *userContext, source *portainer.Source, action actionType) error {
|
||||||
|
if action == actionRead && userCanReadSource(source, ctx) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == actionWrite && userCanWriteSource(source, ctx) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrNotEnoughPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
func userCanWriteSource(source *portainer.Source, context *userContext) bool {
|
||||||
|
if source == nil || context == nil || context.User == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
user := context.User
|
||||||
|
|
||||||
|
if user.Role == portainer.AdministratorRole {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if source.OwnerID != 0 && source.OwnerID == user.ID && userCanReadSource(source, context) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterSources(sources []portainer.Source, context *userContext) []portainer.Source {
|
||||||
|
return slicesx.Filter(sources, func(s portainer.Source) bool {
|
||||||
|
return userCanReadSource(&s, context)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func userCanReadSource(source *portainer.Source, context *userContext) bool {
|
||||||
|
if source == nil || context == nil || context.User == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
user := context.User
|
||||||
|
userTeams := context.UserMemberships
|
||||||
|
|
||||||
|
if user.Role == portainer.AdministratorRole || source.Public {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if source.AdministratorsOnly {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(source.UserAccesses, user.ID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(userTeams) == 0 || len(source.TeamAccesses) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
sTeams := set.ToSet(source.TeamAccesses)
|
||||||
|
uTeams := set.ToSet(slicesx.Map(userTeams, func(u portainer.TeamMembership) portainer.TeamID { return u.TeamID }))
|
||||||
|
|
||||||
|
return set.Intersection(sTeams, uTeams).Len() != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforceUniqueGitSource validates there are no other git sources with the same URL and credentials
|
||||||
|
// It ignores itself
|
||||||
|
func enforceUniqueGitSource(tx ServiceTx, src *portainer.Source) error {
|
||||||
|
if src.Type != portainer.SourceTypeGit || src.Git == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized, err := normalizeGitSource(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, err := tx.base.ReadAll(func(s portainer.Source) bool {
|
||||||
|
if src.ID == s.ID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := normalizeGitSource(&s)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized.Equal(n)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(existing) > 0 {
|
||||||
|
return ErrDuplicateSource
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type normalizedGitSource struct {
|
||||||
|
url string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *normalizedGitSource) Equal(b *normalizedGitSource) bool {
|
||||||
|
return a != nil && b != nil &&
|
||||||
|
a.url == b.url &&
|
||||||
|
a.username == b.username &&
|
||||||
|
a.password == b.password
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize git source to a lighter object used to compare sources together
|
||||||
|
func normalizeGitSource(src *portainer.Source) (*normalizedGitSource, error) {
|
||||||
|
if src == nil || src.Type != portainer.SourceTypeGit || src.Git == nil {
|
||||||
|
return nil, ErrInvalidSource
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := gittypes.NormalizeURL(gittypes.SanitizeURL(src.Git.URL))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password := "", ""
|
||||||
|
if src.Git.Authentication != nil {
|
||||||
|
username = src.Git.Authentication.Username
|
||||||
|
password = src.Git.Authentication.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
return &normalizedGitSource{
|
||||||
|
url: url,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sanitize the source URL and enforce fields values based on user context
|
||||||
|
func sanitizeGitSource(source *portainer.Source) error {
|
||||||
|
if source == nil {
|
||||||
|
return ErrInvalidSource
|
||||||
|
}
|
||||||
|
|
||||||
|
if source.Type != portainer.SourceTypeGit {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if source.Git == nil {
|
||||||
|
return ErrInvalidSource
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
source.Git.URL, err = gittypes.NormalizeURL(gittypes.SanitizeURL(source.Git.URL))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeAccesses(ctx *userContext, newValues *portainer.Source, previousValues *portainer.Source) error {
|
||||||
|
if newValues == nil {
|
||||||
|
return ErrInvalidSource
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.User.Role == portainer.AdministratorRole {
|
||||||
|
if newValues.Public && newValues.AdministratorsOnly {
|
||||||
|
newValues.Public = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if newValues.Public || newValues.AdministratorsOnly {
|
||||||
|
newValues.UserAccesses = []portainer.UserID{}
|
||||||
|
newValues.TeamAccesses = []portainer.TeamID{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !newValues.Public && !newValues.AdministratorsOnly && len(newValues.UserAccesses) == 0 && len(newValues.TeamAccesses) == 0 {
|
||||||
|
newValues.AdministratorsOnly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update flow ; regular user is not allowed to change the UAC, visibility or ownership of the source
|
||||||
|
if previousValues != nil {
|
||||||
|
newValues.UserAccesses = previousValues.UserAccesses
|
||||||
|
newValues.TeamAccesses = previousValues.TeamAccesses
|
||||||
|
newValues.Public = previousValues.Public
|
||||||
|
newValues.AdministratorsOnly = previousValues.AdministratorsOnly
|
||||||
|
newValues.OwnerID = previousValues.OwnerID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create flow
|
||||||
|
userAccesses := []portainer.UserID{ctx.User.ID}
|
||||||
|
if newValues.Public {
|
||||||
|
userAccesses = []portainer.UserID{}
|
||||||
|
}
|
||||||
|
newValues.UserAccesses = userAccesses
|
||||||
|
newValues.TeamAccesses = []portainer.TeamID{}
|
||||||
|
newValues.AdministratorsOnly = false
|
||||||
|
newValues.OwnerID = ctx.User.ID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type vFn func(new *portainer.Source, old *portainer.Source, err error)
|
||||||
|
|
||||||
|
func testUAC(
|
||||||
|
t *testing.T,
|
||||||
|
userContext *userContext,
|
||||||
|
new *portainer.Source,
|
||||||
|
old *portainer.Source,
|
||||||
|
validationFuncs ...vFn,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
err := sanitizeAccesses(userContext, new, old)
|
||||||
|
for _, validate := range validationFuncs {
|
||||||
|
validate(new, old, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_SanitizeAccesses_Admin(t *testing.T) {
|
||||||
|
errInvalidSource := func(_, _ *portainer.Source, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.ErrorIs(t, err, ErrInvalidSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
noError := func(_, _ *portainer.Source, err error) { t.Helper(); require.NoError(t, err) }
|
||||||
|
noOwner := func(new, _ *portainer.Source, _ error) { t.Helper(); require.Zero(t, new.OwnerID) }
|
||||||
|
emptyUsers := func(new, _ *portainer.Source, _ error) { t.Helper(); require.Empty(t, new.UserAccesses) }
|
||||||
|
emptyTeams := func(new, _ *portainer.Source, _ error) { t.Helper(); require.Empty(t, new.TeamAccesses) }
|
||||||
|
public := func(v bool) func(new, _ *portainer.Source, _ error) {
|
||||||
|
return func(new, _ *portainer.Source, _ error) { t.Helper(); require.Equal(t, v, new.Public) }
|
||||||
|
}
|
||||||
|
adminOnly := func(v bool) func(new, _ *portainer.Source, _ error) {
|
||||||
|
return func(new, _ *portainer.Source, _ error) { t.Helper(); require.Equal(t, v, new.AdministratorsOnly) }
|
||||||
|
}
|
||||||
|
|
||||||
|
adminUserContext := NewUserContext(&portainer.User{Role: portainer.AdministratorRole}, []portainer.TeamMembership{})
|
||||||
|
|
||||||
|
test := func(new *portainer.Source, old *portainer.Source, validationFuncs ...vFn) {
|
||||||
|
t.Helper()
|
||||||
|
testUAC(t, adminUserContext, new, old, validationFuncs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
test(nil, nil, errInvalidSource)
|
||||||
|
test(&portainer.Source{}, nil, noError)
|
||||||
|
test(&portainer.Source{Git: &gittypes.RepoConfig{}}, nil,
|
||||||
|
noError, emptyUsers, emptyTeams, adminOnly(true), noOwner, public(false),
|
||||||
|
)
|
||||||
|
test(&portainer.Source{Git: &gittypes.RepoConfig{}, Public: true}, nil,
|
||||||
|
noError, emptyUsers, emptyTeams, adminOnly(false), noOwner, public(true),
|
||||||
|
)
|
||||||
|
test(&portainer.Source{Git: &gittypes.RepoConfig{}, AdministratorsOnly: true}, nil,
|
||||||
|
noError, emptyUsers, emptyTeams, adminOnly(true), noOwner, public(false),
|
||||||
|
)
|
||||||
|
test(&portainer.Source{Git: &gittypes.RepoConfig{}, AdministratorsOnly: true, Public: true}, nil,
|
||||||
|
noError, emptyUsers, emptyTeams, adminOnly(true), noOwner, public(false),
|
||||||
|
)
|
||||||
|
test(&portainer.Source{Git: &gittypes.RepoConfig{}, AdministratorsOnly: true, Public: true, UserAccesses: []portainer.UserID{1, 2}}, nil,
|
||||||
|
noError, emptyUsers, emptyTeams, adminOnly(true), noOwner, public(false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// func Test_SanitizeAccesses_User(t *testing.T) {
|
||||||
|
// user := NewUserContext(&portainer.User{Role: portainer.StandardUserRole}, []portainer.TeamMembership{})
|
||||||
|
// }
|
||||||
@@ -10,7 +10,7 @@ const BucketName = "sources"
|
|||||||
|
|
||||||
// Service represents a service for managing GitOps source data.
|
// Service represents a service for managing GitOps source data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
dataservices.BaseDataService[portainer.Source, portainer.SourceID]
|
base dataservices.BaseDataService[portainer.Source, portainer.SourceID]
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
@@ -21,7 +21,7 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
BaseDataService: dataservices.BaseDataService[portainer.Source, portainer.SourceID]{
|
base: dataservices.BaseDataService[portainer.Source, portainer.SourceID]{
|
||||||
Bucket: BucketName,
|
Bucket: BucketName,
|
||||||
Connection: connection,
|
Connection: connection,
|
||||||
},
|
},
|
||||||
@@ -30,21 +30,77 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
|
|
||||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
return ServiceTx{
|
return ServiceTx{
|
||||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Source, portainer.SourceID]{
|
base: dataservices.BaseDataServiceTx[portainer.Source, portainer.SourceID]{
|
||||||
Bucket: BucketName,
|
Bucket: BucketName,
|
||||||
Connection: service.Connection,
|
Connection: service.base.Connection,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new source.
|
// Create creates a new source.
|
||||||
func (service *Service) Create(source *portainer.Source) error {
|
func (service *Service) Create(context *userContext, source *portainer.Source) error {
|
||||||
return service.Connection.CreateObject(
|
return service.base.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
BucketName,
|
return service.Tx(tx).Create(context, source)
|
||||||
func(id uint64) (int, any) {
|
})
|
||||||
source.ID = portainer.SourceID(id)
|
}
|
||||||
return int(source.ID), source
|
|
||||||
},
|
func (service *Service) Read(context *userContext, ID portainer.SourceID) (*portainer.Source, error) {
|
||||||
)
|
var result *portainer.Source
|
||||||
|
|
||||||
|
err := service.base.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
|
var err error
|
||||||
|
result, err = service.Tx(tx).Read(context, ID)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) Exists(context *userContext, ID portainer.SourceID) (bool, error) {
|
||||||
|
var result bool
|
||||||
|
|
||||||
|
err := service.base.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
|
var err error
|
||||||
|
result, err = service.Tx(tx).Exists(context, ID)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) ReadAll(context *userContext, predicates ...func(portainer.Source) bool) ([]portainer.Source, error) {
|
||||||
|
var result []portainer.Source
|
||||||
|
|
||||||
|
err := service.base.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
|
var err error
|
||||||
|
result, err = service.Tx(tx).ReadAll(context, predicates...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) Update(context *userContext, ID portainer.SourceID, source *portainer.Source) error {
|
||||||
|
return service.base.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
return service.Tx(tx).Update(context, ID, source)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) Delete(context *userContext, ID portainer.SourceID) error {
|
||||||
|
return service.base.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
return service.Tx(tx).Delete(context, ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) FindOrCreateGitSource(context *userContext, source *portainer.Source) (*portainer.Source, error) {
|
||||||
|
var result *portainer.Source
|
||||||
|
|
||||||
|
err := service.base.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
var err error
|
||||||
|
result, err = service.Tx(tx).FindOrCreateGitSource(context, source)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,36 @@ package source
|
|||||||
import (
|
import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServiceTx struct {
|
type ServiceTx struct {
|
||||||
dataservices.BaseDataServiceTx[portainer.Source, portainer.SourceID]
|
base dataservices.BaseDataServiceTx[portainer.Source, portainer.SourceID]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new source.
|
// Create creates a new source.
|
||||||
func (service ServiceTx) Create(source *portainer.Source) error {
|
func (service ServiceTx) Create(context *userContext, source *portainer.Source) error {
|
||||||
return service.Tx.CreateObject(
|
if err := validateUserContext(context); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if source == nil {
|
||||||
|
return ErrInvalidSource
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sanitizeGitSource(source); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sanitizeAccesses(context, source, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := enforceUniqueGitSource(service, source); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.base.Tx.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, any) {
|
func(id uint64) (int, any) {
|
||||||
source.ID = portainer.SourceID(id)
|
source.ID = portainer.SourceID(id)
|
||||||
@@ -19,3 +40,165 @@ func (service ServiceTx) Create(source *portainer.Source) error {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) Read(context *userContext, ID portainer.SourceID) (*portainer.Source, error) {
|
||||||
|
if err := validateUserContext(context); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
source, err := service.base.Read(ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := enforceUserPermissions(context, source, actionRead); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return source, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access is not enforced on this to avoid the cost of deserialize
|
||||||
|
// Any user can scan the DB IDs using this method, so be mindful with usage of this func.
|
||||||
|
func (service ServiceTx) Exists(context *userContext, ID portainer.SourceID) (bool, error) {
|
||||||
|
if err := validateUserContext(context); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.base.Exists(ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAll fetches all sources the user can access, matching predicates
|
||||||
|
func (service ServiceTx) ReadAll(context *userContext, predicates ...func(portainer.Source) bool) ([]portainer.Source, error) {
|
||||||
|
if err := validateUserContext(context); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := service.base.ReadAll(predicates...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterSources(list, context), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the source of id `ID` with the `source` content
|
||||||
|
// It validates that the user has access to the source, and has enough permissions to perform the action
|
||||||
|
func (service ServiceTx) Update(context *userContext, ID portainer.SourceID, source *portainer.Source) error {
|
||||||
|
if err := validateUserContext(context); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
originalSource, err := service.base.Read(ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if source == nil || originalSource == nil {
|
||||||
|
return ErrInvalidSource
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := enforceUserPermissions(context, originalSource, actionWrite); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sanitizeGitSource(source); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sanitizeAccesses(context, source, originalSource); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := enforceUniqueGitSource(service, source); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.base.Update(ID, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a source
|
||||||
|
// It validates that the user has access to the source, and has enough permissions to perform the action
|
||||||
|
func (service ServiceTx) Delete(context *userContext, ID portainer.SourceID) error {
|
||||||
|
if err := validateUserContext(context); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
source, err := service.base.Read(ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := enforceUserPermissions(context, source, actionWrite); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.base.Delete(ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindOrCreateGitSource returns an existing Source whose URL and authentication match cfg,
|
||||||
|
// or creates a new one. Only URL, authentication, and TLSSkipVerify are stored on the Source;
|
||||||
|
// per-stack fields (ReferenceName, ConfigFilePath, ConfigHash) belong in the Artifact.
|
||||||
|
// The function auto adds the user to an existing source if the user doesn't have access but provided a valid full
|
||||||
|
// config (URL+Auth)
|
||||||
|
func (service ServiceTx) FindOrCreateGitSource(context *userContext, src *portainer.Source) (*portainer.Source, error) {
|
||||||
|
if err := validateUserContext(context); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if src == nil || src.Git == nil {
|
||||||
|
return nil, ErrInvalidSource
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized, err := normalizeGitSource(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, err := service.base.ReadAll(func(s portainer.Source) bool {
|
||||||
|
n, err := normalizeGitSource(&s)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return normalized.Equal(n)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(existing) > 0 {
|
||||||
|
allowed := filterSources(existing, context)
|
||||||
|
if len(allowed) > 0 {
|
||||||
|
return &allowed[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// give user access to the first source if he doesn't have access
|
||||||
|
// to any of the sources that have the same url+auth
|
||||||
|
existing[0].UserAccesses = append(existing[0].UserAccesses, context.User.ID)
|
||||||
|
if err := service.base.Update(existing[0].ID, &existing[0]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &existing[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
toCreate := &portainer.Source{
|
||||||
|
Name: src.Name,
|
||||||
|
Type: portainer.SourceTypeGit,
|
||||||
|
Git: &gittypes.RepoConfig{
|
||||||
|
URL: src.Git.URL,
|
||||||
|
Authentication: src.Git.Authentication,
|
||||||
|
TLSSkipVerify: src.Git.TLSSkipVerify,
|
||||||
|
},
|
||||||
|
Public: src.Public,
|
||||||
|
AdministratorsOnly: src.AdministratorsOnly,
|
||||||
|
UserAccesses: src.UserAccesses,
|
||||||
|
TeamAccesses: src.TeamAccesses,
|
||||||
|
OwnerID: src.OwnerID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := service.Create(context, toCreate); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return toCreate, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userContext = dataservices.SourceServiceUserContext
|
||||||
|
|
||||||
|
// Create a new admin context
|
||||||
|
//
|
||||||
|
// # THIS FUNCTION MUST NOT BE USED IN A USER-AWARE FLOW, ONLY FOR MIGRATIONS AND TESTS
|
||||||
|
//
|
||||||
|
// The only flows outside of migrations/test allowed to use this func is the datastore.Import/Export for sources
|
||||||
|
func InsecureNewAdminContext() *userContext {
|
||||||
|
return NewUserContext(&portainer.User{Role: portainer.AdministratorRole}, []portainer.TeamMembership{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserContext(user *portainer.User, userMemberships []portainer.TeamMembership) *userContext {
|
||||||
|
return &userContext{User: user, UserMemberships: userMemberships}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
// var adminUserContext = InsecureNewAdminContext()
|
||||||
|
|
||||||
|
// 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, tx ServiceTx, 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 =// enforceUniqueGitSource(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(adminUserContext, 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(adminUserContext, 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(adminUserContext, 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(adminUserContext, 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(adminUserContext, 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))
|
||||||
|
// }
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/dataservices/stack"
|
"github.com/portainer/portainer/api/dataservices/stack"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
|
||||||
@@ -55,6 +56,8 @@ type legacyStack struct {
|
|||||||
ID int `json:"Id"`
|
ID int `json:"Id"`
|
||||||
GitConfig *legacyRepoConfig `json:"GitConfig"`
|
GitConfig *legacyRepoConfig `json:"GitConfig"`
|
||||||
WorkflowID *int
|
WorkflowID *int
|
||||||
|
ResourceControl *portainer.ResourceControl `json:"ResourceControl"`
|
||||||
|
CreatedBy string
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceDedupeKey is the identity used to detect duplicate Sources during migration.
|
// sourceDedupeKey is the identity used to detect duplicate Sources during migration.
|
||||||
@@ -98,7 +101,8 @@ func (m *Migrator) migrateGitConfigToSources_2_43_0() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
existingSources, err := m.sourceService.ReadAll()
|
adminUserContext := source.InsecureNewAdminContext()
|
||||||
|
existingSources, err := m.sourceService.ReadAll(adminUserContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -122,6 +126,19 @@ func (m *Migrator) migrateGitConfigToSources_2_43_0() error {
|
|||||||
var newSrcID portainer.SourceID
|
var newSrcID portainer.SourceID
|
||||||
|
|
||||||
if err := m.stackService.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
if err := m.stackService.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
users, teams, public, adminOnly, ownerId := GetValuesForUsersFromResourceOwnershipAndAccesses_2_43_0(ls.ResourceControl,
|
||||||
|
func() (portainer.UserID, portainer.UserRole, error) {
|
||||||
|
user, err := m.userService.Tx(tx).UserByUsername(ls.CreatedBy)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return user.ID, user.Role, nil
|
||||||
|
},
|
||||||
|
func(userId portainer.UserID) ([]portainer.TeamMembership, error) {
|
||||||
|
return m.teamMembershipService.Tx(tx).TeamMembershipsByUserID(userId)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
srcID, exists := sourcesByKey[key]
|
srcID, exists := sourcesByKey[key]
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -129,12 +146,29 @@ func (m *Migrator) migrateGitConfigToSources_2_43_0() error {
|
|||||||
Name: gittypes.RepoName(cfg.URL),
|
Name: gittypes.RepoName(cfg.URL),
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: cfg,
|
Git: cfg,
|
||||||
|
OwnerID: ownerId,
|
||||||
|
Public: public,
|
||||||
|
AdministratorsOnly: adminOnly,
|
||||||
|
UserAccesses: users,
|
||||||
|
TeamAccesses: teams,
|
||||||
}
|
}
|
||||||
if err := m.sourceService.Tx(tx).Create(src); err != nil {
|
|
||||||
|
if err := m.sourceService.Tx(tx).Create(adminUserContext, src); err != nil {
|
||||||
return fmt.Errorf("failed to create source for stack %d: %w", ls.ID, err)
|
return fmt.Errorf("failed to create source for stack %d: %w", ls.ID, err)
|
||||||
}
|
}
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
newSrcID = src.ID
|
newSrcID = src.ID
|
||||||
|
} else {
|
||||||
|
src, err := m.sourceService.Tx(tx).Read(adminUserContext, srcID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read source %d for stack %d: %w", srcID, ls.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyUACOnSourceUpdate_2_43_0(src, users, teams, public, adminOnly, ownerId)
|
||||||
|
|
||||||
|
if err := m.sourceService.Tx(tx).Update(adminUserContext, srcID, src); err != nil {
|
||||||
|
return fmt.Errorf("failed to update source %d for stack %d: %w", srcID, ls.ID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
liveStack, err := m.stackService.Tx(tx).Read(portainer.StackID(ls.ID))
|
liveStack, err := m.stackService.Tx(tx).Read(portainer.StackID(ls.ID))
|
||||||
@@ -182,7 +216,8 @@ func (m *Migrator) migrateCustomTemplateGitConfigToSources_2_43_0() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
existingSources, err := m.sourceService.ReadAll()
|
adminUserContext := source.InsecureNewAdminContext()
|
||||||
|
existingSources, err := m.sourceService.ReadAll(adminUserContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -211,6 +246,19 @@ func (m *Migrator) migrateCustomTemplateGitConfigToSources_2_43_0() error {
|
|||||||
var newSrcID portainer.SourceID
|
var newSrcID portainer.SourceID
|
||||||
|
|
||||||
if err := m.stackService.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
if err := m.stackService.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
users, teams, public, adminOnly, ownerId := GetValuesForUsersFromResourceOwnershipAndAccesses_2_43_0(t.ResourceControl,
|
||||||
|
func() (portainer.UserID, portainer.UserRole, error) {
|
||||||
|
user, err := m.userService.Tx(tx).Read(t.CreatedByUserID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return user.ID, user.Role, nil
|
||||||
|
},
|
||||||
|
func(userId portainer.UserID) ([]portainer.TeamMembership, error) {
|
||||||
|
return m.teamMembershipService.Tx(tx).TeamMembershipsByUserID(userId)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
srcID, exists := sourcesByKey[key]
|
srcID, exists := sourcesByKey[key]
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -218,12 +266,28 @@ func (m *Migrator) migrateCustomTemplateGitConfigToSources_2_43_0() error {
|
|||||||
Name: gittypes.RepoName(cfg.URL),
|
Name: gittypes.RepoName(cfg.URL),
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: cfg,
|
Git: cfg,
|
||||||
|
OwnerID: ownerId,
|
||||||
|
Public: public,
|
||||||
|
AdministratorsOnly: adminOnly,
|
||||||
|
UserAccesses: users,
|
||||||
|
TeamAccesses: teams,
|
||||||
}
|
}
|
||||||
if err := m.sourceService.Tx(tx).Create(src); err != nil {
|
if err := m.sourceService.Tx(tx).Create(adminUserContext, src); err != nil {
|
||||||
return fmt.Errorf("failed to create source for custom template %d: %w", t.ID, err)
|
return fmt.Errorf("failed to create source for custom template %d: %w", t.ID, err)
|
||||||
}
|
}
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
newSrcID = src.ID
|
newSrcID = src.ID
|
||||||
|
} else {
|
||||||
|
src, err := m.sourceService.Tx(tx).Read(adminUserContext, srcID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read source %d for custom template %d: %w", srcID, t.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyUACOnSourceUpdate_2_43_0(src, users, teams, public, adminOnly, ownerId)
|
||||||
|
|
||||||
|
if err := m.sourceService.Tx(tx).Update(adminUserContext, srcID, src); err != nil {
|
||||||
|
return fmt.Errorf("failed to update source %d for custom template %d: %w", srcID, t.ID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Artifact = &portainer.Artifact{
|
t.Artifact = &portainer.Artifact{
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: generate tests for UAC migrations
|
||||||
|
|
||||||
|
var adminUserContext = source.InsecureNewAdminContext()
|
||||||
|
|
||||||
func TestMigrateGitConfigToSources_2_43_0_GitStackMigrated(t *testing.T) {
|
func TestMigrateGitConfigToSources_2_43_0_GitStackMigrated(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -61,7 +65,7 @@ func TestMigrateGitConfigToSources_2_43_0_GitStackMigrated(t *testing.T) {
|
|||||||
require.Len(t, wf.Artifacts, 1)
|
require.Len(t, wf.Artifacts, 1)
|
||||||
require.Len(t, wf.Artifacts[0].Files, 1)
|
require.Len(t, wf.Artifacts[0].Files, 1)
|
||||||
|
|
||||||
src, err := sourceSvc.Read(wf.Artifacts[0].Files[0].SourceID)
|
src, err := sourceSvc.Read(adminUserContext, wf.Artifacts[0].Files[0].SourceID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
||||||
require.Equal(t, gitStack.GitConfig.URL, src.Git.URL)
|
require.Equal(t, gitStack.GitConfig.URL, src.Git.URL)
|
||||||
@@ -104,7 +108,7 @@ func TestMigrateGitConfigToSources_2_43_0_NonGitStackUntouched(t *testing.T) {
|
|||||||
require.Zero(t, result.WorkflowID)
|
require.Zero(t, result.WorkflowID)
|
||||||
require.Nil(t, result.GitConfig)
|
require.Nil(t, result.GitConfig)
|
||||||
|
|
||||||
sources, err := sourceSvc.ReadAll()
|
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, sources)
|
require.Empty(t, sources)
|
||||||
|
|
||||||
@@ -160,7 +164,7 @@ func TestMigrateGitConfigToSources_2_43_0_DuplicateSourcesDeduped(t *testing.T)
|
|||||||
err = m.migrateGitConfigToSources_2_43_0()
|
err = m.migrateGitConfigToSources_2_43_0()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sources, err := sourceSvc.ReadAll()
|
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, sources, 1, "two stacks with the same URL must share one Source")
|
require.Len(t, sources, 1, "two stacks with the same URL must share one Source")
|
||||||
|
|
||||||
@@ -214,7 +218,7 @@ func TestMigrateGitConfigToSources_2_43_0_Idempotent(t *testing.T) {
|
|||||||
err = m.migrateGitConfigToSources_2_43_0()
|
err = m.migrateGitConfigToSources_2_43_0()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sources, err := sourceSvc.ReadAll()
|
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, sources, 1)
|
require.Len(t, sources, 1)
|
||||||
|
|
||||||
@@ -268,7 +272,7 @@ func TestMigrateCustomTemplateGitConfigToSources_2_43_0_GitTemplateMigrated(t *t
|
|||||||
require.Equal(t, "docker-compose.yml", migrated.Artifact.Files[0].Path)
|
require.Equal(t, "docker-compose.yml", migrated.Artifact.Files[0].Path)
|
||||||
require.Equal(t, "abc123", migrated.Artifact.Files[0].Hash)
|
require.Equal(t, "abc123", migrated.Artifact.Files[0].Hash)
|
||||||
|
|
||||||
src, err := sourceSvc.Read(migrated.Artifact.Files[0].SourceID)
|
src, err := sourceSvc.Read(adminUserContext, migrated.Artifact.Files[0].SourceID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
||||||
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
|
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
|
||||||
@@ -307,7 +311,7 @@ func TestMigrateCustomTemplateGitConfigToSources_2_43_0_NonGitTemplateUntouched(
|
|||||||
require.Nil(t, result.Artifact)
|
require.Nil(t, result.Artifact)
|
||||||
require.Nil(t, result.GitConfig)
|
require.Nil(t, result.GitConfig)
|
||||||
|
|
||||||
sources, err := sourceSvc.ReadAll()
|
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, sources)
|
require.Empty(t, sources)
|
||||||
}
|
}
|
||||||
@@ -350,7 +354,7 @@ func TestMigrateCustomTemplateGitConfigToSources_2_43_0_AlreadyMigratedSkipped(t
|
|||||||
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sources, err := sourceSvc.ReadAll()
|
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, sources, "no new sources should be created for already-migrated templates")
|
require.Empty(t, sources, "no new sources should be created for already-migrated templates")
|
||||||
}
|
}
|
||||||
@@ -402,7 +406,7 @@ func TestMigrateCustomTemplateGitConfigToSources_2_43_0_DuplicateSourcesDeduped(
|
|||||||
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sources, err := sourceSvc.ReadAll()
|
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, sources, 1, "two templates with the same URL must share one Source")
|
require.Len(t, sources, 1, "two templates with the same URL must share one Source")
|
||||||
|
|
||||||
@@ -456,7 +460,7 @@ func TestMigrateCustomTemplateGitConfigToSources_2_43_0_Idempotent(t *testing.T)
|
|||||||
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sources, err := sourceSvc.ReadAll()
|
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, sources, 1)
|
require.Len(t, sources, 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package migrator
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
|
"github.com/portainer/portainer/api/slicesx"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB accesses enforcement are trying to restrict accesses as much as possible
|
||||||
|
// but because accesses are applied sequentially, we want the accesses to be more open on migration
|
||||||
|
// so that users retain their accesses
|
||||||
|
func ApplyUACOnSourceUpdate_2_43_0(source *portainer.Source,
|
||||||
|
users []portainer.UserID, teams []portainer.TeamID,
|
||||||
|
public bool, adminOnly bool,
|
||||||
|
ownerId portainer.UserID,
|
||||||
|
) {
|
||||||
|
// sources already public should remain public
|
||||||
|
// OR
|
||||||
|
// the resource using this source is public, so the source should be public
|
||||||
|
if source.Public || public {
|
||||||
|
source.Public = true
|
||||||
|
source.AdministratorsOnly = false
|
||||||
|
source.UserAccesses = []portainer.UserID{}
|
||||||
|
source.TeamAccesses = []portainer.TeamID{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// add users and teams to source accesses only if the incoming resource is not admninonly
|
||||||
|
// to avoid saving leftover user/teams from adminonly resources
|
||||||
|
if !adminOnly {
|
||||||
|
source.UserAccesses = slicesx.Unique(append(source.UserAccesses, users...))
|
||||||
|
source.TeamAccesses = slicesx.Unique(append(source.TeamAccesses, teams...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// regardless of the incoming resource's ResourceControl values (func params)
|
||||||
|
// no accesses means adminonly source not owned by anyone
|
||||||
|
// we don't want users to own sources they don't have access to
|
||||||
|
// neither we want to default them to public
|
||||||
|
// all in all as we are doing an update it's probably redundant, but just in case...
|
||||||
|
if len(source.UserAccesses) == 0 && len(source.TeamAccesses) == 0 {
|
||||||
|
source.AdministratorsOnly = true
|
||||||
|
source.OwnerID = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if owner of the incoming resource (ownerid) is the only one with access, we give the ownership to the user.
|
||||||
|
// The source could previously be adminonly so we change that as well as we want the most open situation
|
||||||
|
if len(source.UserAccesses) == 1 && len(source.TeamAccesses) == 0 && ownerId == source.UserAccesses[0] {
|
||||||
|
source.OwnerID = ownerId
|
||||||
|
source.AdministratorsOnly = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anything else will have multiple accesses (multiple teams or users), from multiple resources (source update flow)
|
||||||
|
// So we remove the ownership of the source in case it existed
|
||||||
|
// Scenario:
|
||||||
|
// - source created for resource owned by Bob
|
||||||
|
// - now we try to update with the RC from an admin-owned resource, shared to other users/teams
|
||||||
|
// - we don't want Bob to own the source anymore
|
||||||
|
source.OwnerID = 0
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetValuesForUsersFromResourceOwnershipAndAccesses_2_43_0(
|
||||||
|
rc *portainer.ResourceControl,
|
||||||
|
getCreator func() (portainer.UserID, portainer.UserRole, error),
|
||||||
|
getCreatorMemberships func(portainer.UserID) ([]portainer.TeamMembership, error),
|
||||||
|
) (
|
||||||
|
users []portainer.UserID, teams []portainer.TeamID,
|
||||||
|
public bool, adminOnly bool,
|
||||||
|
ownerId portainer.UserID,
|
||||||
|
) {
|
||||||
|
users = []portainer.UserID{}
|
||||||
|
teams = []portainer.TeamID{}
|
||||||
|
public = false
|
||||||
|
adminOnly = true
|
||||||
|
|
||||||
|
if rc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adminOnly = rc.AdministratorsOnly
|
||||||
|
public = rc.Public
|
||||||
|
|
||||||
|
if adminOnly || public {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// only transfer users/teams when the stack is not admin nor public
|
||||||
|
// this allows avoiding transfering access of sources to users/teams that don't have real access to the stack
|
||||||
|
// but that may have had their accesses retained in DB
|
||||||
|
|
||||||
|
users = slicesx.Map(rc.UserAccesses, func(ura portainer.UserResourceAccess) portainer.UserID { return ura.UserID })
|
||||||
|
teams = slicesx.Map(rc.TeamAccesses, func(tra portainer.TeamResourceAccess) portainer.TeamID { return tra.TeamID })
|
||||||
|
|
||||||
|
userId, userRole, err := getCreator()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to read user when migrating to source")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't want to save the ownerid if the user is admin
|
||||||
|
// this avoids admins taking ownership of a new source
|
||||||
|
if userRole == portainer.AdministratorRole {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We also don't want to get the ownerid if the user doesn't have access to the resource anymore
|
||||||
|
userTeams, err := getCreatorMemberships(userId)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to read user %d teams when migrating source", userId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
teamIds := slicesx.Map(userTeams, func(membership portainer.TeamMembership) portainer.TeamID { return membership.TeamID })
|
||||||
|
if authorization.UserCanAccessResource(userId, teamIds, rc) {
|
||||||
|
ownerId = userId
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -577,7 +577,7 @@ func (store *Store) Export(filename string) (err error) {
|
|||||||
backup.SSLSettings = *settings
|
backup.SSLSettings = *settings
|
||||||
}
|
}
|
||||||
|
|
||||||
if s, err := store.Source().ReadAll(); err != nil {
|
if s, err := store.Source().ReadAll(source.InsecureNewAdminContext()); err != nil {
|
||||||
if !store.IsErrObjectNotFound(err) {
|
if !store.IsErrObjectNotFound(err) {
|
||||||
log.Error().Err(err).Msg("exporting Sources")
|
log.Error().Err(err).Msg("exporting Sources")
|
||||||
}
|
}
|
||||||
@@ -768,7 +768,7 @@ func (store *Store) Import(filename string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range backup.Source {
|
for _, v := range backup.Source {
|
||||||
if err := store.Source().Update(v.ID, &v); err != nil {
|
if err := store.Source().Update(source.InsecureNewAdminContext(), v.ID, &v); err != nil {
|
||||||
log.Warn().Err(err).Msg("failed to update the source in the database")
|
log.Warn().Err(err).Msg("failed to update the source in the database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package sources
|
||||||
|
|
||||||
|
import "github.com/portainer/portainer/api/dataservices/source"
|
||||||
|
|
||||||
|
var adminUserContext = source.InsecureNewAdminContext()
|
||||||
@@ -2,6 +2,7 @@ package sources
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/pkg/fips"
|
"github.com/portainer/portainer/pkg/fips"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
@@ -23,14 +24,14 @@ type RepoConfigInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ResolveRepoConfig builds a RepoConfig from either a SourceID or inline URL/auth fields.
|
// ResolveRepoConfig builds a RepoConfig from either a SourceID or inline URL/auth fields.
|
||||||
func ResolveRepoConfig(tx gitSourceStore, input RepoConfigInput) (gittypes.RepoConfig, *httperror.HandlerError) {
|
func ResolveRepoConfig(tx gitSourceStore, userContext *dataservices.SourceServiceUserContext, input RepoConfigInput) (gittypes.RepoConfig, *httperror.HandlerError) {
|
||||||
cfg := gittypes.RepoConfig{
|
cfg := gittypes.RepoConfig{
|
||||||
ReferenceName: input.ReferenceName,
|
ReferenceName: input.ReferenceName,
|
||||||
ConfigFilePath: input.ConfigFilePath,
|
ConfigFilePath: input.ConfigFilePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.SourceID != 0 {
|
if input.SourceID != 0 {
|
||||||
src, httpErr := ValidateGitSourceAccess(tx, input.SourceID)
|
src, httpErr := ValidateGitSourceAccess(tx, userContext, input.SourceID)
|
||||||
if httpErr != nil {
|
if httpErr != nil {
|
||||||
return gittypes.RepoConfig{}, httpErr
|
return gittypes.RepoConfig{}, httpErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ func TestResolveRepoConfig_WithSourceID_ReturnsSourceConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
require.NoError(t, store.Source().Create(src))
|
require.NoError(t, store.Source().Create(adminUserContext, src))
|
||||||
|
|
||||||
cfg, httpErr := ResolveRepoConfig(store, RepoConfigInput{
|
cfg, httpErr := ResolveRepoConfig(store, adminUserContext, RepoConfigInput{
|
||||||
SourceID: src.ID,
|
SourceID: src.ID,
|
||||||
ReferenceName: "refs/heads/main",
|
ReferenceName: "refs/heads/main",
|
||||||
ConfigFilePath: "docker-compose.yml",
|
ConfigFilePath: "docker-compose.yml",
|
||||||
@@ -51,7 +51,7 @@ func TestResolveRepoConfig_WithInlineURL_ReturnsInlineConfig(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
_, store := datastore.MustNewTestStore(t, false, false)
|
_, store := datastore.MustNewTestStore(t, false, false)
|
||||||
|
|
||||||
cfg, httpErr := ResolveRepoConfig(store, RepoConfigInput{
|
cfg, httpErr := ResolveRepoConfig(store, adminUserContext, RepoConfigInput{
|
||||||
ReferenceName: "refs/heads/main",
|
ReferenceName: "refs/heads/main",
|
||||||
ConfigFilePath: "docker-compose.yml",
|
ConfigFilePath: "docker-compose.yml",
|
||||||
RepositoryURL: "https://github.com/org/repo",
|
RepositoryURL: "https://github.com/org/repo",
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ type gitSourceStore interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateGitSourceAccess checks that the given Source exists and is a git Source, and returns it.
|
// ValidateGitSourceAccess checks that the given Source exists and is a git Source, and returns it.
|
||||||
// TODO(BE-12905): enforce per-user access policies once Source ownership is introduced.
|
func ValidateGitSourceAccess(tx gitSourceStore, userContext *dataservices.SourceServiceUserContext, sourceID portainer.SourceID) (*portainer.Source, *httperror.HandlerError) {
|
||||||
func ValidateGitSourceAccess(tx gitSourceStore, sourceID portainer.SourceID) (*portainer.Source, *httperror.HandlerError) {
|
src, err := tx.Source().Read(userContext, sourceID)
|
||||||
src, err := tx.Source().Read(sourceID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if tx.IsErrObjectNotFound(err) {
|
if tx.IsErrObjectNotFound(err) {
|
||||||
return nil, httperror.NotFound("Source not found", err)
|
return nil, httperror.NotFound("Source not found", err)
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ func TestValidateSourceForStack_ValidGitSource_ReturnsNil(t *testing.T) {
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/org/repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/org/repo"},
|
||||||
}
|
}
|
||||||
require.NoError(t, store.Source().Create(src))
|
require.NoError(t, store.Source().Create(adminUserContext, src))
|
||||||
|
|
||||||
_, httpErr := ValidateGitSourceAccess(store, src.ID)
|
_, httpErr := ValidateGitSourceAccess(store, adminUserContext, src.ID)
|
||||||
assert.Nil(t, httpErr)
|
assert.Nil(t, httpErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,21 +29,7 @@ func TestValidateSourceForStack_SourceNotFound_Returns404(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
_, store := datastore.MustNewTestStore(t, false, false)
|
_, store := datastore.MustNewTestStore(t, false, false)
|
||||||
|
|
||||||
_, httpErr := ValidateGitSourceAccess(store, portainer.SourceID(999))
|
_, httpErr := ValidateGitSourceAccess(store, adminUserContext, portainer.SourceID(999))
|
||||||
require.NotNil(t, httpErr)
|
require.NotNil(t, httpErr)
|
||||||
assert.Equal(t, http.StatusNotFound, httpErr.StatusCode)
|
assert.Equal(t, http.StatusNotFound, httpErr.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateSourceForStack_NonGitSource_Returns400(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
_, store := datastore.MustNewTestStore(t, false, false)
|
|
||||||
|
|
||||||
src := &portainer.Source{
|
|
||||||
Type: portainer.SourceType(99), // not a git source
|
|
||||||
}
|
|
||||||
require.NoError(t, store.Source().Create(src))
|
|
||||||
|
|
||||||
_, httpErr := ValidateGitSourceAccess(store, src.ID)
|
|
||||||
require.NotNil(t, httpErr)
|
|
||||||
assert.Equal(t, http.StatusBadRequest, httpErr.StatusCode)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
@@ -22,6 +23,8 @@ func FetchWorkflows(
|
|||||||
) ([]Workflow, error) {
|
) ([]Workflow, error) {
|
||||||
gitConfigs := map[portainer.StackID]*gittypes.RepoConfig{}
|
gitConfigs := map[portainer.StackID]*gittypes.RepoConfig{}
|
||||||
|
|
||||||
|
userContext := source.NewUserContext(sc.User, sc.UserMemberships)
|
||||||
|
|
||||||
stacks, err := tx.Stack().ReadAll(func(s portainer.Stack) bool {
|
stacks, err := tx.Stack().ReadAll(func(s portainer.Stack) bool {
|
||||||
return s.WorkflowID != 0 && (len(endpointIDSet) == 0 || endpointIDSet.Contains(s.EndpointID))
|
return s.WorkflowID != 0 && (len(endpointIDSet) == 0 || endpointIDSet.Contains(s.EndpointID))
|
||||||
})
|
})
|
||||||
@@ -50,7 +53,7 @@ func FetchWorkflows(
|
|||||||
workflowIDSet.Add(stack.WorkflowID)
|
workflowIDSet.Add(stack.WorkflowID)
|
||||||
}
|
}
|
||||||
|
|
||||||
workflowMap, sourceMap, err := LoadWorkflowAndSourceMaps(tx, workflowIDSet)
|
workflowMap, sourceMap, err := LoadWorkflowAndSourceMaps(tx, userContext, workflowIDSet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -113,7 +116,9 @@ func FetchSourceStats(
|
|||||||
k8sFactory *cli.ClientFactory,
|
k8sFactory *cli.ClientFactory,
|
||||||
sc *security.RestrictedRequestContext,
|
sc *security.RestrictedRequestContext,
|
||||||
) ([]portainer.Source, map[portainer.SourceID]SourceStats, error) {
|
) ([]portainer.Source, map[portainer.SourceID]SourceStats, error) {
|
||||||
sources, err := tx.Source().ReadAll()
|
userContext := source.NewUserContext(sc.User, sc.UserMemberships)
|
||||||
|
|
||||||
|
sources, err := tx.Source().ReadAll(userContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func adminContext() *security.RestrictedRequestContext {
|
func adminContext() *security.RestrictedRequestContext {
|
||||||
return &security.RestrictedRequestContext{IsAdmin: true, UserID: 1}
|
return &security.RestrictedRequestContext{
|
||||||
|
IsAdmin: true,
|
||||||
|
UserID: 1,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustCreateGitWorkflow(t *testing.T, tx dataservices.DataStoreTx, stack *portainer.Stack) {
|
func mustCreateGitWorkflow(t *testing.T, tx dataservices.DataStoreTx, stack *portainer.Stack) {
|
||||||
@@ -24,7 +28,7 @@ func mustCreateGitWorkflow(t *testing.T, tx dataservices.DataStoreTx, stack *por
|
|||||||
cfg := stack.GitConfig
|
cfg := stack.GitConfig
|
||||||
|
|
||||||
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: cfg}
|
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: cfg}
|
||||||
require.NoError(t, tx.Source().Create(src))
|
require.NoError(t, tx.Source().Create(adminUserContext, src))
|
||||||
|
|
||||||
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
||||||
StackID: stack.ID,
|
StackID: stack.ID,
|
||||||
@@ -199,8 +203,8 @@ func TestFetchSourceStats_ReturnsAllSources(t *testing.T) {
|
|||||||
_, store := datastore.MustNewTestStore(t, false, true)
|
_, store := datastore.MustNewTestStore(t, false, true)
|
||||||
|
|
||||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
require.NoError(t, tx.Source().Create(&portainer.Source{Name: "source-1", Type: portainer.SourceTypeGit}))
|
require.NoError(t, tx.Source().Create(adminUserContext, &portainer.Source{Name: "source-1", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo1"}}))
|
||||||
require.NoError(t, tx.Source().Create(&portainer.Source{Name: "source-2", Type: portainer.SourceTypeGit}))
|
require.NoError(t, tx.Source().Create(adminUserContext, &portainer.Source{Name: "source-2", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo2"}}))
|
||||||
|
|
||||||
return tx.User().Create(&portainer.User{ID: 1, Role: portainer.AdministratorRole})
|
return tx.User().Create(&portainer.User{ID: 1, Role: portainer.AdministratorRole})
|
||||||
}))
|
}))
|
||||||
@@ -223,8 +227,8 @@ func TestFetchSourceStats_TracksWorkflowCountAndEndpoints(t *testing.T) {
|
|||||||
var srcID portainer.SourceID
|
var srcID portainer.SourceID
|
||||||
|
|
||||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Name: "shared", Type: portainer.SourceTypeGit}
|
src := &portainer.Source{Name: "shared", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||||
require.NoError(t, tx.Source().Create(src))
|
require.NoError(t, tx.Source().Create(adminUserContext, src))
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
for i := 1; i <= 2; i++ {
|
for i := 1; i <= 2; i++ {
|
||||||
@@ -261,8 +265,8 @@ func TestFetchSourceStats_UnusedSourceHasZeroStats(t *testing.T) {
|
|||||||
var unusedID portainer.SourceID
|
var unusedID portainer.SourceID
|
||||||
|
|
||||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Name: "unused", Type: portainer.SourceTypeGit}
|
src := &portainer.Source{Name: "unused", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||||
require.NoError(t, tx.Source().Create(src))
|
require.NoError(t, tx.Source().Create(adminUserContext, src))
|
||||||
unusedID = src.ID
|
unusedID = src.ID
|
||||||
|
|
||||||
return tx.User().Create(&portainer.User{ID: 1, Role: portainer.AdministratorRole})
|
return tx.User().Create(&portainer.User{ID: 1, Role: portainer.AdministratorRole})
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
var adminUserContext = source.InsecureNewAdminContext()
|
||||||
@@ -20,7 +20,7 @@ type gitSourceStore interface {
|
|||||||
// from the workflow identified by workflowID.
|
// from the workflow identified by workflowID.
|
||||||
// Source carries the shared fields (URL, auth, TLS); ArtifactFile carries the file-specific fields (ref, path, hash).
|
// Source carries the shared fields (URL, auth, TLS); ArtifactFile carries the file-specific fields (ref, path, hash).
|
||||||
// Returns nil, nil, nil when workflowID is 0 or no matching entry is found.
|
// Returns nil, nil, nil when workflowID is 0 or no matching entry is found.
|
||||||
func GitSourceAndArtifactForStack(tx gitSourceStore, workflowID portainer.WorkflowID, stackID portainer.StackID) (*portainer.Source, *portainer.ArtifactFile, error) {
|
func GitSourceAndArtifactForStack(tx gitSourceStore, userContext *dataservices.SourceServiceUserContext, workflowID portainer.WorkflowID, stackID portainer.StackID) (*portainer.Source, *portainer.ArtifactFile, error) {
|
||||||
if workflowID == 0 {
|
if workflowID == 0 {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ func GitSourceAndArtifactForStack(tx gitSourceStore, workflowID portainer.Workfl
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceMap, err := loadWorkflowSources(tx, wf)
|
sourceMap, err := loadWorkflowSources(tx, userContext, wf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ func GitSourceAndArtifactForStack(tx gitSourceStore, workflowID portainer.Workfl
|
|||||||
|
|
||||||
// GitSourceAndArtifactForEdgeStack returns the git Source and the ArtifactFile matching edgeStackID.
|
// GitSourceAndArtifactForEdgeStack returns the git Source and the ArtifactFile matching edgeStackID.
|
||||||
// Returns nil, nil, nil when workflowID is 0 or no matching entry is found.
|
// Returns nil, nil, nil when workflowID is 0 or no matching entry is found.
|
||||||
func GitSourceAndArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.WorkflowID, edgeStackID portainer.EdgeStackID) (*portainer.Source, *portainer.ArtifactFile, error) {
|
func GitSourceAndArtifactForEdgeStack(tx gitSourceStore, userContext *dataservices.SourceServiceUserContext, workflowID portainer.WorkflowID, edgeStackID portainer.EdgeStackID) (*portainer.Source, *portainer.ArtifactFile, error) {
|
||||||
if workflowID == 0 {
|
if workflowID == 0 {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ func GitSourceAndArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.Wo
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceMap, err := loadWorkflowSources(tx, wf)
|
sourceMap, err := loadWorkflowSources(tx, userContext, wf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -169,45 +169,15 @@ func UpdateArtifactFileForEdgeStack(tx gitSourceStore, workflowID portainer.Work
|
|||||||
// FindOrCreateGitSource returns an existing Source whose URL and authentication match cfg,
|
// FindOrCreateGitSource returns an existing Source whose URL and authentication match cfg,
|
||||||
// or creates a new one. Only URL, authentication, and TLSSkipVerify are stored on the Source;
|
// or creates a new one. Only URL, authentication, and TLSSkipVerify are stored on the Source;
|
||||||
// per-stack fields (ReferenceName, ConfigFilePath, ConfigHash) belong in the Artifact.
|
// per-stack fields (ReferenceName, ConfigFilePath, ConfigHash) belong in the Artifact.
|
||||||
func FindOrCreateGitSource(tx gitSourceStore, src *portainer.Source) (*portainer.Source, error) {
|
func FindOrCreateGitSource(tx gitSourceStore, userContext *dataservices.SourceServiceUserContext, src *portainer.Source) (*portainer.Source, error) {
|
||||||
src.Git.URL = gittypes.SanitizeURL(src.Git.URL)
|
return tx.Source().FindOrCreateGitSource(userContext, src)
|
||||||
|
|
||||||
existing, err := tx.Source().ReadAll(func(s portainer.Source) bool {
|
|
||||||
return s.Type == portainer.SourceTypeGit &&
|
|
||||||
s.Git != nil &&
|
|
||||||
s.Git.URL == src.Git.URL &&
|
|
||||||
gitAuthMatches(s.Git.Authentication, src.Git.Authentication)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(existing) > 0 {
|
|
||||||
return &existing[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
toCreate := &portainer.Source{
|
|
||||||
Name: src.Name,
|
|
||||||
Type: portainer.SourceTypeGit,
|
|
||||||
Git: &gittypes.RepoConfig{
|
|
||||||
URL: src.Git.URL,
|
|
||||||
Authentication: src.Git.Authentication,
|
|
||||||
TLSSkipVerify: src.Git.TLSSkipVerify,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Source().Create(toCreate); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return toCreate, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveWorkflowGitConfig persists URL/auth/TLS on the Source and ref/path/hash on the Artifact
|
// SaveWorkflowGitConfig persists URL/auth/TLS on the Source and ref/path/hash on the Artifact
|
||||||
// matched by matchArtifact. When the URL changes, an existing or new Source is located via
|
// matched by matchArtifact. When the URL changes, an existing or new Source is located via
|
||||||
// FindOrCreateGitSource and the Workflow's SourceID is updated atomically alongside the Artifact fields.
|
// FindOrCreateGitSource and the Workflow's SourceID is updated atomically alongside the Artifact fields.
|
||||||
func SaveWorkflowGitConfig(tx gitSourceStore, workflowID portainer.WorkflowID, matchArtifact func(portainer.Artifact) bool, oldSourceID portainer.SourceID, cfg *gittypes.RepoConfig) error {
|
func SaveWorkflowGitConfig(tx gitSourceStore, userContext *dataservices.SourceServiceUserContext, workflowID portainer.WorkflowID, matchArtifact func(portainer.Artifact) bool, oldSourceID portainer.SourceID, cfg *gittypes.RepoConfig) error {
|
||||||
src, err := tx.Source().Read(oldSourceID)
|
src, err := tx.Source().Read(userContext, oldSourceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read source: %w", err)
|
return fmt.Errorf("failed to read source: %w", err)
|
||||||
}
|
}
|
||||||
@@ -219,7 +189,7 @@ func SaveWorkflowGitConfig(tx gitSourceStore, workflowID portainer.WorkflowID, m
|
|||||||
newSourceID := oldSourceID
|
newSourceID := oldSourceID
|
||||||
|
|
||||||
if cfg.URL != src.Git.URL {
|
if cfg.URL != src.Git.URL {
|
||||||
newSrc, err := FindOrCreateGitSource(tx, &portainer.Source{
|
newSrc, err := FindOrCreateGitSource(tx, userContext, &portainer.Source{
|
||||||
Name: gittypes.RepoName(cfg.URL),
|
Name: gittypes.RepoName(cfg.URL),
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: cfg,
|
Git: cfg,
|
||||||
@@ -233,7 +203,7 @@ func SaveWorkflowGitConfig(tx gitSourceStore, workflowID portainer.WorkflowID, m
|
|||||||
src.Git.Authentication = cfg.Authentication
|
src.Git.Authentication = cfg.Authentication
|
||||||
src.Git.TLSSkipVerify = cfg.TLSSkipVerify
|
src.Git.TLSSkipVerify = cfg.TLSSkipVerify
|
||||||
|
|
||||||
if err := tx.Source().Update(src.ID, src); err != nil {
|
if err := tx.Source().Update(userContext, src.ID, src); err != nil {
|
||||||
return fmt.Errorf("failed to update source: %w", err)
|
return fmt.Errorf("failed to update source: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,7 +267,7 @@ func LoadWorkflowMap(tx gitSourceStore, ids set.Set[portainer.WorkflowID]) (map[
|
|||||||
|
|
||||||
// LoadWorkflowAndSourceMaps fetches workflows by their IDs and the sources they reference,
|
// LoadWorkflowAndSourceMaps fetches workflows by their IDs and the sources they reference,
|
||||||
// collecting source IDs in a single pass over the workflows.
|
// collecting source IDs in a single pass over the workflows.
|
||||||
func LoadWorkflowAndSourceMaps(tx gitSourceStore, ids set.Set[portainer.WorkflowID]) (map[portainer.WorkflowID]portainer.Workflow, map[portainer.SourceID]portainer.Source, error) {
|
func LoadWorkflowAndSourceMaps(tx gitSourceStore, userContext *dataservices.SourceServiceUserContext, ids set.Set[portainer.WorkflowID]) (map[portainer.WorkflowID]portainer.Workflow, map[portainer.SourceID]portainer.Source, error) {
|
||||||
wfMap := make(map[portainer.WorkflowID]portainer.Workflow, len(ids))
|
wfMap := make(map[portainer.WorkflowID]portainer.Workflow, len(ids))
|
||||||
sourceIDs := make(set.Set[portainer.SourceID])
|
sourceIDs := make(set.Set[portainer.SourceID])
|
||||||
for id := range ids {
|
for id := range ids {
|
||||||
@@ -313,7 +283,7 @@ func LoadWorkflowAndSourceMaps(tx gitSourceStore, ids set.Set[portainer.Workflow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
srcMap, err := LoadSourceMap(tx, sourceIDs)
|
srcMap, err := loadSourceMap(tx, userContext, sourceIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -323,7 +293,7 @@ func LoadWorkflowAndSourceMaps(tx gitSourceStore, ids set.Set[portainer.Workflow
|
|||||||
|
|
||||||
// loadWorkflowSources collects all unique SourceIDs referenced by wf and returns them as a map.
|
// loadWorkflowSources collects all unique SourceIDs referenced by wf and returns them as a map.
|
||||||
// This avoids reading the same Source record more than once when files share a SourceID.
|
// This avoids reading the same Source record more than once when files share a SourceID.
|
||||||
func loadWorkflowSources(tx gitSourceStore, wf *portainer.Workflow) (map[portainer.SourceID]portainer.Source, error) {
|
func loadWorkflowSources(tx gitSourceStore, userContext *dataservices.SourceServiceUserContext, wf *portainer.Workflow) (map[portainer.SourceID]portainer.Source, error) {
|
||||||
ids := make(set.Set[portainer.SourceID])
|
ids := make(set.Set[portainer.SourceID])
|
||||||
for _, as := range wf.Artifacts {
|
for _, as := range wf.Artifacts {
|
||||||
for _, f := range as.Files {
|
for _, f := range as.Files {
|
||||||
@@ -331,67 +301,22 @@ func loadWorkflowSources(tx gitSourceStore, wf *portainer.Workflow) (map[portain
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return LoadSourceMap(tx, ids)
|
return loadSourceMap(tx, userContext, ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadSourceMap fetches sources by their IDs and returns them keyed by ID.
|
// loadSourceMap fetches sources by their IDs and returns them keyed by ID.
|
||||||
func LoadSourceMap(tx gitSourceStore, ids set.Set[portainer.SourceID]) (map[portainer.SourceID]portainer.Source, error) {
|
func loadSourceMap(tx gitSourceStore, userContext *dataservices.SourceServiceUserContext, ids set.Set[portainer.SourceID]) (map[portainer.SourceID]portainer.Source, error) {
|
||||||
result := make(map[portainer.SourceID]portainer.Source, len(ids))
|
sources, err := tx.Source().ReadAll(userContext, func(s portainer.Source) bool {
|
||||||
for id := range ids {
|
return ids.Contains(s.ID)
|
||||||
src, err := tx.Source().Read(id)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result[id] = *src
|
|
||||||
|
result := make(map[portainer.SourceID]portainer.Source, len(ids))
|
||||||
|
for _, src := range sources {
|
||||||
|
result[src.ID] = src
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitAuthMatches(a, b *gittypes.GitAuthentication) bool {
|
|
||||||
if a == nil && b == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if a == nil || b == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.Username == b.Username && a.Password == b.Password
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
existing, err := tx.Source().ReadAll(func(s portainer.Source) bool {
|
|
||||||
if s.ID == sourceID || s.Type != portainer.SourceTypeGit || s.Git == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
normalized, err := gittypes.NormalizeURL(gittypes.SanitizeURL(s.Git.URL))
|
|
||||||
if err != nil || normalized != normalizedURL {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
existingUsername, existingPassword := gitAuthCredentials(s.Git.Authentication)
|
|
||||||
return existingUsername == username && existingPassword == password
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(existing) == 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitAuthCredentials(auth *gittypes.GitAuthentication) (username, password string) {
|
|
||||||
if auth == nil {
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
return auth.Username, auth.Password
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func TestGitSourceAndArtifactForStack_ZeroWorkflowIDReturnsNil(t *testing.T) {
|
|||||||
var file *portainer.ArtifactFile
|
var file *portainer.ArtifactFile
|
||||||
err := store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err := store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var txErr error
|
var txErr error
|
||||||
src, file, txErr = GitSourceAndArtifactForStack(tx, 0, 1)
|
src, file, txErr = GitSourceAndArtifactForStack(tx, adminUserContext, 0, 1)
|
||||||
return txErr
|
return txErr
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -98,7 +98,7 @@ func TestGitSourceAndArtifactForStack_ReturnsMatchingSourceAndFile(t *testing.T)
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(gitSrc)
|
err := tx.Source().Create(adminUserContext, gitSrc)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
wf := &portainer.Workflow{
|
wf := &portainer.Workflow{
|
||||||
@@ -124,7 +124,7 @@ func TestGitSourceAndArtifactForStack_ReturnsMatchingSourceAndFile(t *testing.T)
|
|||||||
var file *portainer.ArtifactFile
|
var file *portainer.ArtifactFile
|
||||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var txErr error
|
var txErr error
|
||||||
src, file, txErr = GitSourceAndArtifactForStack(tx, workflowID, 42)
|
src, file, txErr = GitSourceAndArtifactForStack(tx, adminUserContext, workflowID, 42)
|
||||||
return txErr
|
return txErr
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -146,7 +146,7 @@ func TestGitSourceAndArtifactForStack_NoMatchingArtifactReturnsNil(t *testing.T)
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
wf := &portainer.Workflow{
|
wf := &portainer.Workflow{
|
||||||
@@ -167,43 +167,7 @@ func TestGitSourceAndArtifactForStack_NoMatchingArtifactReturnsNil(t *testing.T)
|
|||||||
var file *portainer.ArtifactFile
|
var file *portainer.ArtifactFile
|
||||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var txErr error
|
var txErr error
|
||||||
src, file, txErr = GitSourceAndArtifactForStack(tx, workflowID, 99)
|
src, file, txErr = GitSourceAndArtifactForStack(tx, adminUserContext, workflowID, 99)
|
||||||
return txErr
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Nil(t, src)
|
|
||||||
require.Nil(t, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGitSourceAndArtifactForStack_NonGitSourceSkipped(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
_, store := datastore.MustNewTestStore(t, false, true)
|
|
||||||
|
|
||||||
var workflowID portainer.WorkflowID
|
|
||||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
|
||||||
nonGitSrc := &portainer.Source{Type: portainer.SourceType(99)}
|
|
||||||
err := tx.Source().Create(nonGitSrc)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
wf := &portainer.Workflow{
|
|
||||||
Artifacts: []portainer.Artifact{{
|
|
||||||
StackID: 1,
|
|
||||||
Files: []portainer.ArtifactFile{{SourceID: nonGitSrc.ID}},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
err = tx.Workflow().Create(wf)
|
|
||||||
require.NoError(t, err)
|
|
||||||
workflowID = wf.ID
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var src *portainer.Source
|
|
||||||
var file *portainer.ArtifactFile
|
|
||||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
|
||||||
var txErr error
|
|
||||||
src, file, txErr = GitSourceAndArtifactForStack(tx, workflowID, 1)
|
|
||||||
return txErr
|
return txErr
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -219,7 +183,7 @@ func TestGitSourceAndArtifactForEdgeStack_ZeroWorkflowIDReturnsNil(t *testing.T)
|
|||||||
var file *portainer.ArtifactFile
|
var file *portainer.ArtifactFile
|
||||||
err := store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err := store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var txErr error
|
var txErr error
|
||||||
src, file, txErr = GitSourceAndArtifactForEdgeStack(tx, 0, 1)
|
src, file, txErr = GitSourceAndArtifactForEdgeStack(tx, adminUserContext, 0, 1)
|
||||||
return txErr
|
return txErr
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -237,7 +201,7 @@ func TestGitSourceAndArtifactForEdgeStack_ReturnsMatchingSourceAndFile(t *testin
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/edge-repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/edge-repo"},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(gitSrc)
|
err := tx.Source().Create(adminUserContext, gitSrc)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
wf := &portainer.Workflow{
|
wf := &portainer.Workflow{
|
||||||
@@ -262,7 +226,7 @@ func TestGitSourceAndArtifactForEdgeStack_ReturnsMatchingSourceAndFile(t *testin
|
|||||||
var file *portainer.ArtifactFile
|
var file *portainer.ArtifactFile
|
||||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var txErr error
|
var txErr error
|
||||||
src, file, txErr = GitSourceAndArtifactForEdgeStack(tx, workflowID, 5)
|
src, file, txErr = GitSourceAndArtifactForEdgeStack(tx, adminUserContext, workflowID, 5)
|
||||||
return txErr
|
return txErr
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -280,7 +244,7 @@ func TestUpdateArtifactFileForStack_NoMatchingArtifactIsNoOp(t *testing.T) {
|
|||||||
var sourceID portainer.SourceID
|
var sourceID portainer.SourceID
|
||||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sourceID = src.ID
|
sourceID = src.ID
|
||||||
|
|
||||||
@@ -318,7 +282,7 @@ func TestUpdateArtifactFileForStack_AppliesFnAndPersists(t *testing.T) {
|
|||||||
var sourceID portainer.SourceID
|
var sourceID portainer.SourceID
|
||||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sourceID = src.ID
|
sourceID = src.ID
|
||||||
|
|
||||||
@@ -356,7 +320,7 @@ func TestUpdateArtifactFileForEdgeStack_AppliesFnAndPersists(t *testing.T) {
|
|||||||
var sourceID portainer.SourceID
|
var sourceID portainer.SourceID
|
||||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sourceID = src.ID
|
sourceID = src.ID
|
||||||
|
|
||||||
@@ -393,7 +357,7 @@ func TestFindOrCreateGitSource_CreatesNewSource(t *testing.T) {
|
|||||||
var src *portainer.Source
|
var src *portainer.Source
|
||||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var txErr error
|
var txErr error
|
||||||
src, txErr = FindOrCreateGitSource(tx, &portainer.Source{
|
src, txErr = FindOrCreateGitSource(tx, adminUserContext, &portainer.Source{
|
||||||
Name: "my-repo",
|
Name: "my-repo",
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{
|
Git: &gittypes.RepoConfig{
|
||||||
@@ -413,7 +377,7 @@ func TestFindOrCreateGitSource_ReusesExistingSourceForSameURLAndAuth(t *testing.
|
|||||||
_, store := datastore.MustNewTestStore(t, false, true)
|
_, store := datastore.MustNewTestStore(t, false, true)
|
||||||
|
|
||||||
makeSource := func(tx dataservices.DataStoreTx) (*portainer.Source, error) {
|
makeSource := func(tx dataservices.DataStoreTx) (*portainer.Source, error) {
|
||||||
return FindOrCreateGitSource(tx, &portainer.Source{
|
return FindOrCreateGitSource(tx, adminUserContext, &portainer.Source{
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{
|
Git: &gittypes.RepoConfig{
|
||||||
URL: "https://github.com/example/repo",
|
URL: "https://github.com/example/repo",
|
||||||
@@ -446,7 +410,7 @@ func TestFindOrCreateGitSource_ReusesExistingSourceForSameURLAndAuth(t *testing.
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, firstID, secondID)
|
require.Equal(t, firstID, secondID)
|
||||||
|
|
||||||
sources, err := store.Source().ReadAll()
|
sources, err := store.Source().ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, sources, 1)
|
require.Len(t, sources, 1)
|
||||||
}
|
}
|
||||||
@@ -456,7 +420,7 @@ func TestFindOrCreateGitSource_DifferentAuthCreatesNewSource(t *testing.T) {
|
|||||||
_, store := datastore.MustNewTestStore(t, false, true)
|
_, store := datastore.MustNewTestStore(t, false, true)
|
||||||
|
|
||||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
_, txErr := FindOrCreateGitSource(tx, &portainer.Source{
|
_, txErr := FindOrCreateGitSource(tx, adminUserContext, &portainer.Source{
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{
|
Git: &gittypes.RepoConfig{
|
||||||
URL: "https://github.com/example/repo",
|
URL: "https://github.com/example/repo",
|
||||||
@@ -468,7 +432,7 @@ func TestFindOrCreateGitSource_DifferentAuthCreatesNewSource(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
_, txErr := FindOrCreateGitSource(tx, &portainer.Source{
|
_, txErr := FindOrCreateGitSource(tx, adminUserContext, &portainer.Source{
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{
|
Git: &gittypes.RepoConfig{
|
||||||
URL: "https://github.com/example/repo",
|
URL: "https://github.com/example/repo",
|
||||||
@@ -479,7 +443,7 @@ func TestFindOrCreateGitSource_DifferentAuthCreatesNewSource(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sources, err := store.Source().ReadAll()
|
sources, err := store.Source().ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, sources, 2)
|
require.Len(t, sources, 2)
|
||||||
}
|
}
|
||||||
@@ -503,7 +467,7 @@ func TestSaveWorkflowGitConfig_UpdatesFileAndSourceWhenURLUnchanged(t *testing.T
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sourceID = src.ID
|
sourceID = src.ID
|
||||||
|
|
||||||
@@ -539,7 +503,7 @@ func TestSaveWorkflowGitConfig_UpdatesFileAndSourceWhenURLUnchanged(t *testing.T
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
return SaveWorkflowGitConfig(tx, workflowID, func(a portainer.Artifact) bool {
|
return SaveWorkflowGitConfig(tx, adminUserContext, workflowID, func(a portainer.Artifact) bool {
|
||||||
return a.StackID == 1
|
return a.StackID == 1
|
||||||
}, sourceID, newCfg)
|
}, sourceID, newCfg)
|
||||||
})
|
})
|
||||||
@@ -552,7 +516,7 @@ func TestSaveWorkflowGitConfig_UpdatesFileAndSourceWhenURLUnchanged(t *testing.T
|
|||||||
require.Equal(t, "new-hash", wf.Artifacts[0].Files[0].Hash)
|
require.Equal(t, "new-hash", wf.Artifacts[0].Files[0].Hash)
|
||||||
require.Equal(t, sourceID, wf.Artifacts[0].Files[0].SourceID)
|
require.Equal(t, sourceID, wf.Artifacts[0].Files[0].SourceID)
|
||||||
|
|
||||||
src, err := store.Source().Read(sourceID)
|
src, err := store.Source().Read(adminUserContext, sourceID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "new-user", src.Git.Authentication.Username)
|
require.Equal(t, "new-user", src.Git.Authentication.Username)
|
||||||
require.Equal(t, "new-pass", src.Git.Authentication.Password)
|
require.Equal(t, "new-pass", src.Git.Authentication.Password)
|
||||||
@@ -571,7 +535,7 @@ func TestSaveWorkflowGitConfig_CreatesNewSourceOnURLChange(t *testing.T) {
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
oldSourceID = src.ID
|
oldSourceID = src.ID
|
||||||
|
|
||||||
@@ -592,7 +556,7 @@ func TestSaveWorkflowGitConfig_CreatesNewSourceOnURLChange(t *testing.T) {
|
|||||||
newCfg := &gittypes.RepoConfig{URL: "https://github.com/example/new-repo"}
|
newCfg := &gittypes.RepoConfig{URL: "https://github.com/example/new-repo"}
|
||||||
|
|
||||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
return SaveWorkflowGitConfig(tx, workflowID, func(a portainer.Artifact) bool {
|
return SaveWorkflowGitConfig(tx, adminUserContext, workflowID, func(a portainer.Artifact) bool {
|
||||||
return a.StackID == 1
|
return a.StackID == 1
|
||||||
}, oldSourceID, newCfg)
|
}, oldSourceID, newCfg)
|
||||||
})
|
})
|
||||||
@@ -603,7 +567,7 @@ func TestSaveWorkflowGitConfig_CreatesNewSourceOnURLChange(t *testing.T) {
|
|||||||
newSourceID := wf.Artifacts[0].Files[0].SourceID
|
newSourceID := wf.Artifacts[0].Files[0].SourceID
|
||||||
require.NotEqual(t, oldSourceID, newSourceID)
|
require.NotEqual(t, oldSourceID, newSourceID)
|
||||||
|
|
||||||
newSrc, err := store.Source().Read(newSourceID)
|
newSrc, err := store.Source().Read(adminUserContext, newSourceID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "https://github.com/example/new-repo", newSrc.Git.URL)
|
require.Equal(t, "https://github.com/example/new-repo", newSrc.Git.URL)
|
||||||
}
|
}
|
||||||
@@ -620,7 +584,7 @@ func TestSaveWorkflowGitConfig_ReusesExistingSourceOnURLChange(t *testing.T) {
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(old)
|
err := tx.Source().Create(adminUserContext, old)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
oldSourceID = old.ID
|
oldSourceID = old.ID
|
||||||
|
|
||||||
@@ -628,7 +592,7 @@ func TestSaveWorkflowGitConfig_ReusesExistingSourceOnURLChange(t *testing.T) {
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/shared-repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/shared-repo"},
|
||||||
}
|
}
|
||||||
err = tx.Source().Create(existing)
|
err = tx.Source().Create(adminUserContext, existing)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
existingSourceID = existing.ID
|
existingSourceID = existing.ID
|
||||||
|
|
||||||
@@ -649,7 +613,7 @@ func TestSaveWorkflowGitConfig_ReusesExistingSourceOnURLChange(t *testing.T) {
|
|||||||
newCfg := &gittypes.RepoConfig{URL: "https://github.com/example/shared-repo"}
|
newCfg := &gittypes.RepoConfig{URL: "https://github.com/example/shared-repo"}
|
||||||
|
|
||||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
return SaveWorkflowGitConfig(tx, workflowID, func(a portainer.Artifact) bool {
|
return SaveWorkflowGitConfig(tx, adminUserContext, workflowID, func(a portainer.Artifact) bool {
|
||||||
return a.StackID == 1
|
return a.StackID == 1
|
||||||
}, oldSourceID, newCfg)
|
}, oldSourceID, newCfg)
|
||||||
})
|
})
|
||||||
@@ -659,46 +623,11 @@ func TestSaveWorkflowGitConfig_ReusesExistingSourceOnURLChange(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, existingSourceID, wf.Artifacts[0].Files[0].SourceID)
|
require.Equal(t, existingSourceID, wf.Artifacts[0].Files[0].SourceID)
|
||||||
|
|
||||||
sources, err := store.Source().ReadAll()
|
sources, err := store.Source().ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, sources, 2)
|
require.Len(t, sources, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSaveWorkflowGitConfig_NilGitConfigReturnsError(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
_, store := datastore.MustNewTestStore(t, false, true)
|
|
||||||
|
|
||||||
var workflowID portainer.WorkflowID
|
|
||||||
var sourceID portainer.SourceID
|
|
||||||
|
|
||||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
|
||||||
src := &portainer.Source{Type: portainer.SourceTypeGit}
|
|
||||||
err := tx.Source().Create(src)
|
|
||||||
require.NoError(t, err)
|
|
||||||
sourceID = src.ID
|
|
||||||
|
|
||||||
wf := &portainer.Workflow{
|
|
||||||
Artifacts: []portainer.Artifact{{
|
|
||||||
StackID: 1,
|
|
||||||
Files: []portainer.ArtifactFile{{SourceID: sourceID}},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
err = tx.Workflow().Create(wf)
|
|
||||||
require.NoError(t, err)
|
|
||||||
workflowID = wf.ID
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
|
||||||
return SaveWorkflowGitConfig(tx, workflowID, func(a portainer.Artifact) bool {
|
|
||||||
return a.StackID == 1
|
|
||||||
}, sourceID, &gittypes.RepoConfig{URL: "https://github.com/example/repo"})
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaveWorkflowGitConfig_OnlyMatchingArtifactUpdated(t *testing.T) {
|
func TestSaveWorkflowGitConfig_OnlyMatchingArtifactUpdated(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
_, store := datastore.MustNewTestStore(t, false, true)
|
_, store := datastore.MustNewTestStore(t, false, true)
|
||||||
@@ -711,7 +640,7 @@ func TestSaveWorkflowGitConfig_OnlyMatchingArtifactUpdated(t *testing.T) {
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sourceID = src.ID
|
sourceID = src.ID
|
||||||
|
|
||||||
@@ -736,7 +665,7 @@ func TestSaveWorkflowGitConfig_OnlyMatchingArtifactUpdated(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
return SaveWorkflowGitConfig(tx, workflowID, func(a portainer.Artifact) bool {
|
return SaveWorkflowGitConfig(tx, adminUserContext, workflowID, func(a portainer.Artifact) bool {
|
||||||
return a.StackID == 1
|
return a.StackID == 1
|
||||||
}, sourceID, &gittypes.RepoConfig{
|
}, sourceID, &gittypes.RepoConfig{
|
||||||
URL: "https://github.com/example/repo",
|
URL: "https://github.com/example/repo",
|
||||||
@@ -759,7 +688,7 @@ func TestUpdateArtifactFileForStack_MultipleArtifactsOnlyMatchingUpdated(t *test
|
|||||||
var srcID portainer.SourceID
|
var srcID portainer.SourceID
|
||||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -804,7 +733,7 @@ func TestSaveWorkflowArtifact_SwitchesSourceWithoutMutatingIt(t *testing.T) {
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(old)
|
err := tx.Source().Create(adminUserContext, old)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
oldSourceID = old.ID
|
oldSourceID = old.ID
|
||||||
|
|
||||||
@@ -818,7 +747,7 @@ func TestSaveWorkflowArtifact_SwitchesSourceWithoutMutatingIt(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = tx.Source().Create(selected)
|
err = tx.Source().Create(adminUserContext, selected)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
newSourceID = selected.ID
|
newSourceID = selected.ID
|
||||||
|
|
||||||
@@ -861,7 +790,7 @@ func TestSaveWorkflowArtifact_SwitchesSourceWithoutMutatingIt(t *testing.T) {
|
|||||||
require.Equal(t, "new-hash", wf.Artifacts[0].Files[0].Hash)
|
require.Equal(t, "new-hash", wf.Artifacts[0].Files[0].Hash)
|
||||||
|
|
||||||
// The selected source's git config must be left untouched.
|
// The selected source's git config must be left untouched.
|
||||||
selected, err := store.Source().Read(newSourceID)
|
selected, err := store.Source().Read(adminUserContext, newSourceID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "https://github.com/example/repo", selected.Git.URL)
|
require.Equal(t, "https://github.com/example/repo", selected.Git.URL)
|
||||||
require.Equal(t, "selected-user", selected.Git.Authentication.Username)
|
require.Equal(t, "selected-user", selected.Git.Authentication.Username)
|
||||||
@@ -876,7 +805,7 @@ func TestUpdateArtifactFileForEdgeStack_MultipleArtifactsOnlyMatchingUpdated(t *
|
|||||||
var srcID portainer.SourceID
|
var srcID portainer.SourceID
|
||||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -919,7 +848,7 @@ func TestSaveWorkflowArtifact_SameSourceUpdatesArtifactOnly(t *testing.T) {
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sourceID = src.ID
|
sourceID = src.ID
|
||||||
|
|
||||||
@@ -971,7 +900,7 @@ func TestGitSourceAndArtifactForStack_MultipleArtifactsReturnsCorrectOne(t *test
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/shared-repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/shared-repo"},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(gitSrc)
|
err := tx.Source().Create(adminUserContext, gitSrc)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
wf := &portainer.Workflow{
|
wf := &portainer.Workflow{
|
||||||
@@ -992,7 +921,7 @@ func TestGitSourceAndArtifactForStack_MultipleArtifactsReturnsCorrectOne(t *test
|
|||||||
var file *portainer.ArtifactFile
|
var file *portainer.ArtifactFile
|
||||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var txErr error
|
var txErr error
|
||||||
src, file, txErr = GitSourceAndArtifactForStack(tx, workflowID, 20)
|
src, file, txErr = GitSourceAndArtifactForStack(tx, adminUserContext, workflowID, 20)
|
||||||
return txErr
|
return txErr
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1012,7 +941,7 @@ func TestGitSourceAndArtifactForEdgeStack_MultipleArtifactsReturnsCorrectOne(t *
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/shared-edge-repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/shared-edge-repo"},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(gitSrc)
|
err := tx.Source().Create(adminUserContext, gitSrc)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
wf := &portainer.Workflow{
|
wf := &portainer.Workflow{
|
||||||
@@ -1033,7 +962,7 @@ func TestGitSourceAndArtifactForEdgeStack_MultipleArtifactsReturnsCorrectOne(t *
|
|||||||
var file *portainer.ArtifactFile
|
var file *portainer.ArtifactFile
|
||||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var txErr error
|
var txErr error
|
||||||
src, file, txErr = GitSourceAndArtifactForEdgeStack(tx, workflowID, 20)
|
src, file, txErr = GitSourceAndArtifactForEdgeStack(tx, adminUserContext, workflowID, 20)
|
||||||
return txErr
|
return txErr
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1070,7 +999,7 @@ func TestFindOrCreateGitSource_StripsEmbeddedCredentialsFromURL(t *testing.T) {
|
|||||||
var src *portainer.Source
|
var src *portainer.Source
|
||||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var txErr error
|
var txErr error
|
||||||
src, txErr = FindOrCreateGitSource(tx, &portainer.Source{
|
src, txErr = FindOrCreateGitSource(tx, adminUserContext, &portainer.Source{
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{
|
Git: &gittypes.RepoConfig{
|
||||||
URL: "https://user:secret@github.com/example/repo",
|
URL: "https://user:secret@github.com/example/repo",
|
||||||
@@ -1081,97 +1010,3 @@ func TestFindOrCreateGitSource_StripsEmbeddedCredentialsFromURL(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
|
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))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/gitops/sources"
|
"github.com/portainer/portainer/api/gitops/sources"
|
||||||
@@ -32,9 +33,9 @@ func (handler *Handler) customTemplateCreate(w http.ResponseWriter, r *http.Requ
|
|||||||
return httperror.BadRequest("Invalid query parameter: method", err)
|
return httperror.BadRequest("Invalid query parameter: method", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenData, err := security.RetrieveTokenData(r)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve user details from authentication token", err)
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
customTemplate, err := handler.createCustomTemplate(method, r)
|
customTemplate, err := handler.createCustomTemplate(method, r)
|
||||||
@@ -42,16 +43,16 @@ func (handler *Handler) customTemplateCreate(w http.ResponseWriter, r *http.Requ
|
|||||||
return httperror.InternalServerError("Unable to create custom template", err)
|
return httperror.InternalServerError("Unable to create custom template", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
customTemplate.CreatedByUserID = tokenData.ID
|
customTemplate.CreatedByUserID = securityContext.UserID
|
||||||
|
|
||||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
return createCustomTemplateTx(tx, customTemplate, tokenData.ID)
|
return createCustomTemplateTx(tx, customTemplate, securityContext)
|
||||||
})
|
})
|
||||||
|
|
||||||
return response.TxResponse(w, customTemplate, err)
|
return response.TxResponse(w, customTemplate, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createCustomTemplateTx(tx dataservices.DataStoreTx, customTemplate *portainer.CustomTemplate, userID portainer.UserID) error {
|
func createCustomTemplateTx(tx dataservices.DataStoreTx, customTemplate *portainer.CustomTemplate, sc *security.RestrictedRequestContext) error {
|
||||||
existingTemplates, err := tx.CustomTemplate().ReadAll()
|
existingTemplates, err := tx.CustomTemplate().ReadAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve custom templates from the database", err)
|
return httperror.InternalServerError("Unable to retrieve custom templates from the database", err)
|
||||||
@@ -67,14 +68,16 @@ func createCustomTemplateTx(tx dataservices.DataStoreTx, customTemplate *portain
|
|||||||
return httperror.InternalServerError("Unable to create custom template", err)
|
return httperror.InternalServerError("Unable to create custom template", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl := authorization.NewPrivateResourceControl(strconv.Itoa(int(customTemplate.ID)), portainer.CustomTemplateResourceControl, userID)
|
resourceControl := authorization.NewPrivateResourceControl(strconv.Itoa(int(customTemplate.ID)), portainer.CustomTemplateResourceControl, sc.UserID)
|
||||||
|
|
||||||
if err := tx.ResourceControl().Create(resourceControl); err != nil {
|
if err := tx.ResourceControl().Create(resourceControl); err != nil {
|
||||||
return httperror.InternalServerError("Unable to persist resource control inside the database", err)
|
return httperror.InternalServerError("Unable to persist resource control inside the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
customTemplate.ResourceControl = resourceControl
|
customTemplate.ResourceControl = resourceControl
|
||||||
populateGitConfig(tx, customTemplate)
|
|
||||||
|
userContext := source.NewUserContext(sc.User, sc.UserMemberships)
|
||||||
|
populateGitConfig(tx, userContext, customTemplate)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -282,6 +285,11 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
customTemplateID := handler.DataStore.CustomTemplate().GetNextIdentifier()
|
customTemplateID := handler.DataStore.CustomTemplate().GetNextIdentifier()
|
||||||
customTemplate := &portainer.CustomTemplate{
|
customTemplate := &portainer.CustomTemplate{
|
||||||
ID: portainer.CustomTemplateID(customTemplateID),
|
ID: portainer.CustomTemplateID(customTemplateID),
|
||||||
@@ -302,7 +310,9 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
|||||||
projectPath := getProjectPath()
|
projectPath := getProjectPath()
|
||||||
customTemplate.ProjectPath = projectPath
|
customTemplate.ProjectPath = projectPath
|
||||||
|
|
||||||
gitConfig, httpErr := sources.ResolveRepoConfig(handler.DataStore, sources.RepoConfigInput{
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
|
||||||
|
gitConfig, httpErr := sources.ResolveRepoConfig(handler.DataStore, userContext, sources.RepoConfigInput{
|
||||||
SourceID: payload.SourceID,
|
SourceID: payload.SourceID,
|
||||||
ReferenceName: payload.RepositoryReferenceName,
|
ReferenceName: payload.RepositoryReferenceName,
|
||||||
ConfigFilePath: payload.ComposeFilePathInRepository,
|
ConfigFilePath: payload.ComposeFilePathInRepository,
|
||||||
@@ -327,7 +337,7 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
|||||||
|
|
||||||
sourceID := payload.SourceID
|
sourceID := payload.SourceID
|
||||||
if sourceID == 0 {
|
if sourceID == 0 {
|
||||||
src, err := workflows.FindOrCreateGitSource(handler.DataStore, &portainer.Source{
|
src, err := workflows.FindOrCreateGitSource(handler.DataStore, userContext, &portainer.Source{
|
||||||
Name: gittypes.RepoName(gitConfig.URL),
|
Name: gittypes.RepoName(gitConfig.URL),
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{
|
Git: &gittypes.RepoConfig{
|
||||||
|
|||||||
@@ -30,7 +30,14 @@ func createTemplateRequest(t *testing.T, method string, payload any, userID port
|
|||||||
r.Header.Set("Content-Type", "application/json")
|
r.Header.Set("Content-Type", "application/json")
|
||||||
r = mux.SetURLVars(r, map[string]string{"method": method})
|
r = mux.SetURLVars(r, map[string]string{"method": method})
|
||||||
|
|
||||||
return r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: userID, Role: role}))
|
ctx := security.StoreTokenData(r, &portainer.TokenData{ID: userID, Role: role})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
ctx = security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{
|
||||||
|
UserID: userID,
|
||||||
|
IsAdmin: role == portainer.AdministratorRole,
|
||||||
|
User: &portainer.User{ID: userID, Role: role},
|
||||||
|
})
|
||||||
|
return r.WithContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCustomTemplateCreate_FromFileContent_Success(t *testing.T) {
|
func TestCustomTemplateCreate_FromFileContent_Success(t *testing.T) {
|
||||||
@@ -272,7 +279,13 @@ func TestCustomTemplateCreate_FromFileUpload_Success(t *testing.T) {
|
|||||||
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
||||||
r.Header.Set("Content-Type", writer.FormDataContentType())
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
||||||
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
ctx := security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{
|
||||||
|
UserID: 1,
|
||||||
|
IsAdmin: true,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
herr := handler.customTemplateCreate(rr, r)
|
herr := handler.customTemplateCreate(rr, r)
|
||||||
@@ -461,7 +474,13 @@ func TestCustomTemplateCreate_FromFileUpload_MissingTitle(t *testing.T) {
|
|||||||
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
||||||
r.Header.Set("Content-Type", writer.FormDataContentType())
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
||||||
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
ctx := security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{
|
||||||
|
UserID: 1,
|
||||||
|
IsAdmin: true,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
herr := handler.customTemplateCreate(rr, r)
|
herr := handler.customTemplateCreate(rr, r)
|
||||||
@@ -497,7 +516,13 @@ func TestCustomTemplateCreate_FromFileUpload_MissingDescription(t *testing.T) {
|
|||||||
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
||||||
r.Header.Set("Content-Type", writer.FormDataContentType())
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
||||||
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
ctx := security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{
|
||||||
|
UserID: 1,
|
||||||
|
IsAdmin: true,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
herr := handler.customTemplateCreate(rr, r)
|
herr := handler.customTemplateCreate(rr, r)
|
||||||
@@ -530,7 +555,13 @@ func TestCustomTemplateCreate_FromFileUpload_MissingFile(t *testing.T) {
|
|||||||
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
||||||
r.Header.Set("Content-Type", writer.FormDataContentType())
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
||||||
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
ctx := security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{
|
||||||
|
UserID: 1,
|
||||||
|
IsAdmin: true,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
herr := handler.customTemplateCreate(rr, r)
|
herr := handler.customTemplateCreate(rr, r)
|
||||||
@@ -569,7 +600,13 @@ func TestCustomTemplateCreate_FromFileUpload_InvalidType(t *testing.T) {
|
|||||||
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
||||||
r.Header.Set("Content-Type", writer.FormDataContentType())
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
||||||
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
ctx := security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{
|
||||||
|
UserID: 1,
|
||||||
|
IsAdmin: true,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
herr := handler.customTemplateCreate(rr, r)
|
herr := handler.customTemplateCreate(rr, r)
|
||||||
@@ -608,7 +645,13 @@ func TestCustomTemplateCreate_FromFileUpload_InvalidPlatform(t *testing.T) {
|
|||||||
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
||||||
r.Header.Set("Content-Type", writer.FormDataContentType())
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
||||||
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
ctx := security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{
|
||||||
|
UserID: 1,
|
||||||
|
IsAdmin: true,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
herr := handler.customTemplateCreate(rr, r)
|
herr := handler.customTemplateCreate(rr, r)
|
||||||
@@ -650,7 +693,13 @@ func TestCustomTemplateCreate_FromFileUpload_NoteWithImage(t *testing.T) {
|
|||||||
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
||||||
r.Header.Set("Content-Type", writer.FormDataContentType())
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
||||||
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
ctx := security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{
|
||||||
|
UserID: 1,
|
||||||
|
IsAdmin: true,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
herr := handler.customTemplateCreate(rr, r)
|
herr := handler.customTemplateCreate(rr, r)
|
||||||
@@ -689,7 +738,13 @@ func TestCustomTemplateCreate_FromFileUpload_KubernetesIgnoresPlatform(t *testin
|
|||||||
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
||||||
r.Header.Set("Content-Type", writer.FormDataContentType())
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
||||||
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
ctx := security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{
|
||||||
|
UserID: 1,
|
||||||
|
IsAdmin: true,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
herr := handler.customTemplateCreate(rr, r)
|
herr := handler.customTemplateCreate(rr, r)
|
||||||
@@ -748,7 +803,13 @@ func TestCustomTemplateCreate_FromFileUpload_Variables(t *testing.T) {
|
|||||||
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
||||||
r.Header.Set("Content-Type", writer.FormDataContentType())
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
||||||
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
ctx := security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{
|
||||||
|
UserID: 1,
|
||||||
|
IsAdmin: true,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
herr := handler.customTemplateCreate(rr, r)
|
herr := handler.customTemplateCreate(rr, r)
|
||||||
@@ -804,7 +865,13 @@ func TestCustomTemplateCreate_FromFileUpload_InvalidVariables(t *testing.T) {
|
|||||||
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
||||||
r.Header.Set("Content-Type", writer.FormDataContentType())
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
||||||
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
ctx := security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{
|
||||||
|
UserID: 1,
|
||||||
|
IsAdmin: true,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
herr := handler.customTemplateCreate(rr, r)
|
herr := handler.customTemplateCreate(rr, r)
|
||||||
@@ -866,7 +933,7 @@ func TestCustomTemplateCreate_FromRepository_Success(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, stored.Artifact)
|
require.NotNil(t, stored.Artifact)
|
||||||
|
|
||||||
src, err := tx.Source().Read(stored.Artifact.Files[0].SourceID)
|
src, err := tx.Source().Read(adminUserContext, stored.Artifact.Files[0].SourceID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
||||||
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
|
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
|
||||||
@@ -903,7 +970,7 @@ func TestCustomTemplateCreate_FromRepository_DeduplicatesSource(t *testing.T) {
|
|||||||
require.Nil(t, herr)
|
require.Nil(t, herr)
|
||||||
|
|
||||||
err := ds.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err := ds.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
sources, err := tx.Source().ReadAll()
|
sources, err := tx.Source().ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, sources, 1, "two templates with the same URL must share one Source")
|
require.Len(t, sources, 1, "two templates with the same URL must share one Source")
|
||||||
|
|
||||||
@@ -1052,7 +1119,7 @@ func TestCustomTemplateCreate_FromRepository_WithSourceID_Success(t *testing.T)
|
|||||||
URL: "https://github.com/example/repo",
|
URL: "https://github.com/example/repo",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ func TestCustomTemplateFile_GitTemplate(t *testing.T) {
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
path, err := fs.StoreCustomTemplateFileFromBytes("10", configFilePath, []byte(templateContent))
|
path, err := fs.StoreCustomTemplateFileFromBytes("10", configFilePath, []byte(templateContent))
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
@@ -49,10 +52,24 @@ func (handler *Handler) customTemplateGitFetch(w http.ResponseWriter, r *http.Re
|
|||||||
|
|
||||||
file := customTemplate.Artifact.Files[0]
|
file := customTemplate.Artifact.Files[0]
|
||||||
|
|
||||||
src, err := handler.DataStore.Source().Read(file.SourceID)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var src *portainer.Source
|
||||||
|
if err := handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
src, err = tx.Source().Read(userContext, file.SourceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve git source for custom template", err)
|
return httperror.InternalServerError("Unable to retrieve git source for custom template", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return response.TxErrorResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
if src.Git == nil {
|
if src.Git == nil {
|
||||||
return httperror.InternalServerError("Source has no git configuration", nil)
|
return httperror.InternalServerError("Source has no git configuration", nil)
|
||||||
|
|||||||
@@ -176,11 +176,12 @@ func Test_customTemplateGitFetch(t *testing.T) {
|
|||||||
src := &portainer.Source{
|
src := &portainer.Source{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
|
Public: true,
|
||||||
Git: &gittypes.RepoConfig{
|
Git: &gittypes.RepoConfig{
|
||||||
URL: "https://github.com/example/repo",
|
URL: "https://github.com/example/repo",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = store.Source().Create(src)
|
err = store.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err, "error creating source")
|
require.NoError(t, err, "error creating source")
|
||||||
|
|
||||||
const configFilePath = "test-config-path.txt"
|
const configFilePath = "test-config-path.txt"
|
||||||
@@ -336,31 +337,3 @@ func TestCustomTemplateGitFetch_EmptySourceIDsReturnsBadRequest(t *testing.T) {
|
|||||||
|
|
||||||
require.Equal(t, http.StatusBadRequest, rr.Code)
|
require.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCustomTemplateGitFetch_SourceWithNilGitConfigReturnsInternalError(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
_, store := datastore.MustNewTestStore(t, false, true)
|
|
||||||
|
|
||||||
src := &portainer.Source{Type: portainer.SourceTypeGit}
|
|
||||||
err := store.Source().Create(src)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
template := &portainer.CustomTemplate{
|
|
||||||
ID: 1,
|
|
||||||
Title: "nil-git-config",
|
|
||||||
Artifact: &portainer.Artifact{
|
|
||||||
Files: []portainer.ArtifactFile{{SourceID: src.ID}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err = store.CustomTemplateService.Create(template)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
h := NewHandler(testhelpers.NewTestRequestBouncer(), store, &TestFileService{}, &TestGitService{})
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPut, "/custom_templates/1/git_fetch", bytes.NewBufferString("{}"))
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusInternalServerError, rr.Code)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
@@ -70,7 +71,8 @@ func (handler *Handler) customTemplateInspect(w http.ResponseWriter, r *http.Req
|
|||||||
return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied)
|
return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied)
|
||||||
}
|
}
|
||||||
|
|
||||||
populateGitConfig(tx, customTemplate)
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
populateGitConfig(tx, userContext, customTemplate)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ func TestInspectHandler_GitConfigPopulatedFromSource(t *testing.T) {
|
|||||||
TLSSkipVerify: true,
|
TLSSkipVerify: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
@@ -194,7 +194,7 @@ func TestInspectHandler_GitConfigPopulatedFromSource(t *testing.T) {
|
|||||||
|
|
||||||
r := httptest.NewRequest(http.MethodGet, "/custom_templates/10", nil)
|
r := httptest.NewRequest(http.MethodGet, "/custom_templates/10", nil)
|
||||||
r = mux.SetURLVars(r, map[string]string{"id": "10"})
|
r = mux.SetURLVars(r, map[string]string{"id": "10"})
|
||||||
ctx := security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true})
|
ctx := security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true, User: &portainer.User{ID: 1, Role: portainer.AdministratorRole}})
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
herr := handler.customTemplateInspect(rr, r)
|
herr := handler.customTemplateInspect(rr, r)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
"github.com/portainer/portainer/api/slicesx"
|
"github.com/portainer/portainer/api/slicesx"
|
||||||
@@ -37,25 +39,28 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
edge := retrieveEdgeParam(r)
|
edge := retrieveEdgeParam(r)
|
||||||
|
|
||||||
customTemplates, err := handler.DataStore.CustomTemplate().ReadAll()
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var customTemplates []portainer.CustomTemplate
|
||||||
|
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
var err error
|
||||||
|
customTemplates, err = tx.CustomTemplate().ReadAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve custom templates from the database", err)
|
return httperror.InternalServerError("Unable to retrieve custom templates from the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControls, err := handler.DataStore.ResourceControl().ReadAll()
|
resourceControls, err := tx.ResourceControl().ReadAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve resource controls from the database", err)
|
return httperror.InternalServerError("Unable to retrieve resource controls from the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
customTemplates = authorization.DecorateCustomTemplates(customTemplates, resourceControls)
|
customTemplates = authorization.DecorateCustomTemplates(customTemplates, resourceControls)
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
if !securityContext.IsAdmin {
|
||||||
user, err := handler.DataStore.User().Read(securityContext.UserID)
|
user, err := tx.User().Read(securityContext.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve user information from the database", err)
|
return httperror.InternalServerError("Unable to retrieve user information from the database", err)
|
||||||
}
|
}
|
||||||
@@ -73,11 +78,15 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
for i := range customTemplates {
|
for i := range customTemplates {
|
||||||
populateGitConfig(handler.DataStore, &customTemplates[i])
|
populateGitConfig(tx, userContext, &customTemplates[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, customTemplates)
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.TxResponse(w, customTemplates, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveEdgeParam(r *http.Request) *bool {
|
func retrieveEdgeParam(r *http.Request) *bool {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func TestCustomTemplateList_PopulatesGitConfigFromSource(t *testing.T) {
|
|||||||
TLSSkipVerify: true,
|
TLSSkipVerify: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||||
@@ -48,7 +48,7 @@ func TestCustomTemplateList_PopulatesGitConfigFromSource(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
r := httptest.NewRequest(http.MethodGet, "/custom_templates", nil)
|
r := httptest.NewRequest(http.MethodGet, "/custom_templates", nil)
|
||||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}))
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true, User: &portainer.User{ID: 1, Role: portainer.AdministratorRole}}))
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(rr, r)
|
handler.ServeHTTP(rr, r)
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ func TestCustomTemplateList_StripsPasswordFromGitConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
||||||
@@ -111,7 +111,7 @@ func TestCustomTemplateList_StripsPasswordFromGitConfig(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
r := httptest.NewRequest(http.MethodGet, "/custom_templates", nil)
|
r := httptest.NewRequest(http.MethodGet, "/custom_templates", nil)
|
||||||
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}))
|
r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true, User: &portainer.User{ID: 1, Role: portainer.AdministratorRole}}))
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(rr, r)
|
handler.ServeHTTP(rr, r)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/git"
|
"github.com/portainer/portainer/api/git"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
@@ -182,8 +183,10 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
|||||||
customTemplate.IsComposeFormat = payload.IsComposeFormat
|
customTemplate.IsComposeFormat = payload.IsComposeFormat
|
||||||
customTemplate.EdgeTemplate = payload.EdgeTemplate
|
customTemplate.EdgeTemplate = payload.EdgeTemplate
|
||||||
|
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
|
||||||
if payload.SourceID != 0 || payload.RepositoryURL != "" {
|
if payload.SourceID != 0 || payload.RepositoryURL != "" {
|
||||||
gitConfig, httpErr := sources.ResolveRepoConfig(handler.DataStore, sources.RepoConfigInput{
|
gitConfig, httpErr := sources.ResolveRepoConfig(handler.DataStore, userContext, sources.RepoConfigInput{
|
||||||
SourceID: payload.SourceID,
|
SourceID: payload.SourceID,
|
||||||
ReferenceName: payload.RepositoryReferenceName,
|
ReferenceName: payload.RepositoryReferenceName,
|
||||||
ConfigFilePath: payload.ComposeFilePathInRepository,
|
ConfigFilePath: payload.ComposeFilePathInRepository,
|
||||||
@@ -231,7 +234,7 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
sourceID := payload.SourceID
|
sourceID := payload.SourceID
|
||||||
if sourceID == 0 {
|
if sourceID == 0 {
|
||||||
src, err := workflows.FindOrCreateGitSource(handler.DataStore, &portainer.Source{
|
src, err := workflows.FindOrCreateGitSource(handler.DataStore, userContext, &portainer.Source{
|
||||||
Name: gittypes.RepoName(gitConfig.URL),
|
Name: gittypes.RepoName(gitConfig.URL),
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{
|
Git: &gittypes.RepoConfig{
|
||||||
@@ -271,7 +274,8 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
|||||||
return httperror.InternalServerError("Unable to persist custom template changes inside the database", err)
|
return httperror.InternalServerError("Unable to persist custom template changes inside the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
populateGitConfig(tx, customTemplate)
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
populateGitConfig(tx, userContext, customTemplate)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ func updateTemplateRequest(t *testing.T, templateID string, payload any, ctx *se
|
|||||||
r.Header.Set("Content-Type", "application/json")
|
r.Header.Set("Content-Type", "application/json")
|
||||||
r = mux.SetURLVars(r, map[string]string{"id": templateID})
|
r = mux.SetURLVars(r, map[string]string{"id": templateID})
|
||||||
|
|
||||||
|
if ctx.User == nil {
|
||||||
|
role := portainer.StandardUserRole
|
||||||
|
if ctx.IsAdmin {
|
||||||
|
role = portainer.AdministratorRole
|
||||||
|
}
|
||||||
|
ctx.User = &portainer.User{ID: ctx.UserID, Role: role}
|
||||||
|
}
|
||||||
|
|
||||||
return r.WithContext(security.StoreRestrictedRequestContext(r, ctx))
|
return r.WithContext(security.StoreRestrictedRequestContext(r, ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +484,7 @@ func TestCustomTemplateUpdate_WithSourceID_Success(t *testing.T) {
|
|||||||
URL: "https://github.com/example/repo",
|
URL: "https://github.com/example/repo",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
return nil
|
return nil
|
||||||
@@ -630,7 +638,7 @@ func TestCustomTemplateUpdate_GitRepository_Success(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, stored.Artifact)
|
require.NotNil(t, stored.Artifact)
|
||||||
|
|
||||||
src, err := tx.Source().Read(stored.Artifact.Files[0].SourceID)
|
src, err := tx.Source().Read(adminUserContext, stored.Artifact.Files[0].SourceID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
||||||
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
|
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package customtemplates
|
||||||
|
|
||||||
|
import "github.com/portainer/portainer/api/dataservices/source"
|
||||||
|
|
||||||
|
var adminUserContext = source.InsecureNewAdminContext()
|
||||||
@@ -8,14 +8,14 @@ import (
|
|||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func populateGitConfig(tx dataservices.DataStoreTx, template *portainer.CustomTemplate) {
|
func populateGitConfig(tx dataservices.DataStoreTx, userContext *dataservices.SourceServiceUserContext, template *portainer.CustomTemplate) {
|
||||||
if template.Artifact == nil || len(template.Artifact.Files) == 0 {
|
if template.Artifact == nil || len(template.Artifact.Files) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file := template.Artifact.Files[0]
|
file := template.Artifact.Files[0]
|
||||||
|
|
||||||
src, err := tx.Source().Read(file.SourceID)
|
src, err := tx.Source().Read(userContext, file.SourceID)
|
||||||
if err != nil || src.Git == nil {
|
if err != nil || src.Git == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ func TestPopulateGitConfig_NilArtifactIsNoOp(t *testing.T) {
|
|||||||
template := &portainer.CustomTemplate{ID: 1}
|
template := &portainer.CustomTemplate{ID: 1}
|
||||||
|
|
||||||
err := store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err := store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
populateGitConfig(tx, template)
|
|
||||||
|
populateGitConfig(tx, adminUserContext, template)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -40,39 +41,7 @@ func TestPopulateGitConfig_EmptySourceIDsIsNoOp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err := store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
populateGitConfig(tx, template)
|
populateGitConfig(tx, adminUserContext, template)
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Nil(t, template.GitConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPopulateGitConfig_SourceWithNilGitConfigIsNoOp(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
_, store := datastore.MustNewTestStore(t, false, true)
|
|
||||||
|
|
||||||
var srcID portainer.SourceID
|
|
||||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
|
||||||
src := &portainer.Source{Type: portainer.SourceTypeGit}
|
|
||||||
err := tx.Source().Create(src)
|
|
||||||
require.NoError(t, err)
|
|
||||||
srcID = src.ID
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
template := &portainer.CustomTemplate{
|
|
||||||
ID: 1,
|
|
||||||
Artifact: &portainer.Artifact{
|
|
||||||
Files: []portainer.ArtifactFile{{SourceID: srcID}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
|
||||||
populateGitConfig(tx, template)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -94,7 +63,7 @@ func TestPopulateGitConfig_PopulatesFromSourceAndArtifact(t *testing.T) {
|
|||||||
TLSSkipVerify: true,
|
TLSSkipVerify: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -115,7 +84,7 @@ func TestPopulateGitConfig_PopulatesFromSourceAndArtifact(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
populateGitConfig(tx, template)
|
populateGitConfig(tx, adminUserContext, template)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -145,7 +114,7 @@ func TestPopulateGitConfig_StripsPassword(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -161,7 +130,7 @@ func TestPopulateGitConfig_StripsPassword(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
populateGitConfig(tx, template)
|
populateGitConfig(tx, adminUserContext, template)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/gitops/sources"
|
"github.com/portainer/portainer/api/gitops/sources"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
"github.com/portainer/portainer/pkg/edge"
|
"github.com/portainer/portainer/pkg/edge"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/ssrf"
|
"github.com/portainer/portainer/pkg/libhttp/ssrf"
|
||||||
"github.com/portainer/portainer/pkg/validate"
|
"github.com/portainer/portainer/pkg/validate"
|
||||||
@@ -124,7 +127,13 @@ func (handler *Handler) createEdgeStackFromGitRepository(r *http.Request, tx dat
|
|||||||
return stack, nil
|
return stack, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
repoConfig, httpErr := sources.ResolveRepoConfig(tx, sources.RepoConfigInput{
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, httperror.InternalServerError("Unable to retrieve user info from request context", err)
|
||||||
|
}
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
|
||||||
|
repoConfig, httpErr := sources.ResolveRepoConfig(tx, userContext, sources.RepoConfigInput{
|
||||||
SourceID: payload.SourceID,
|
SourceID: payload.SourceID,
|
||||||
ReferenceName: payload.RepositoryReferenceName,
|
ReferenceName: payload.RepositoryReferenceName,
|
||||||
ConfigFilePath: payload.FilePathInRepository,
|
ConfigFilePath: payload.FilePathInRepository,
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/gitops/sources"
|
"github.com/portainer/portainer/api/gitops/sources"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
@@ -87,8 +89,14 @@ func (handler *Handler) gitOperationRepoFilePreview(w http.ResponseWriter, r *ht
|
|||||||
password := payload.Password
|
password := payload.Password
|
||||||
tlsSkipVerify := payload.TLSSkipVerify
|
tlsSkipVerify := payload.TLSSkipVerify
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve user info from request context", err)
|
||||||
|
}
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
|
||||||
if payload.SourceID != 0 {
|
if payload.SourceID != 0 {
|
||||||
src, httpErr := sources.ValidateGitSourceAccess(handler.dataStore, payload.SourceID)
|
src, httpErr := sources.ValidateGitSourceAccess(handler.dataStore, userContext, payload.SourceID)
|
||||||
if httpErr != nil {
|
if httpErr != nil {
|
||||||
return httpErr
|
return httpErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/gitops/workflows"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
@@ -21,8 +22,16 @@ type GitAuthenticationPayload struct {
|
|||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SourceAccessControlPayload struct {
|
||||||
|
Public bool `json:"public" example:"true"`
|
||||||
|
AdministratorsOnly bool `json:"administratorsOnly" example:"true"`
|
||||||
|
UserAccesses []portainer.UserID `json:"userAccesses"`
|
||||||
|
TeamAccesses []portainer.TeamID `json:"teamAccesses"`
|
||||||
|
}
|
||||||
|
|
||||||
// GitSourceCreatePayload holds the parameters for creating a git-backed source
|
// GitSourceCreatePayload holds the parameters for creating a git-backed source
|
||||||
type GitSourceCreatePayload struct {
|
type GitSourceCreatePayload struct {
|
||||||
|
SourceAccessControlPayload
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
URL string `json:"url" validate:"required"`
|
URL string `json:"url" validate:"required"`
|
||||||
TLSSkipVerify bool `json:"tlsSkipVerify"`
|
TLSSkipVerify bool `json:"tlsSkipVerify"`
|
||||||
@@ -41,7 +50,7 @@ func (payload *GitSourceCreatePayload) Validate(_ *http.Request) error {
|
|||||||
// @id GitOpsSourcesCreateGit
|
// @id GitOpsSourcesCreateGit
|
||||||
// @summary Create a Git source
|
// @summary Create a Git source
|
||||||
// @description Creates a new GitOps source backed by a Git repository.
|
// @description Creates a new GitOps source backed by a Git repository.
|
||||||
// @description **Access policy**: administrator
|
// @description **Access policy**: authenticated
|
||||||
// @tags gitops
|
// @tags gitops
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @security jwt
|
// @security jwt
|
||||||
@@ -61,26 +70,20 @@ func (h *Handler) gitSourceCreate(w http.ResponseWriter, r *http.Request) *httpe
|
|||||||
return httperror.BadRequest("Invalid request payload", err)
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
src, err := BuildGitSource(payload)
|
src, err := BuildGitSource(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
username, password := "", ""
|
|
||||||
if payload.Authentication != nil {
|
|
||||||
username = payload.Authentication.Username
|
|
||||||
password = payload.Authentication.Password
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
if err := h.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
if isUnique, err := workflows.ValidateUniqueSource(tx, payload.URL, username, password, 0); err != nil {
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
return err
|
return tx.Source().Create(userContext, src)
|
||||||
} else if !isUnique {
|
}); errors.Is(err, source.ErrDuplicateSource) {
|
||||||
return ErrDuplicateSource
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Source().Create(src)
|
|
||||||
}); errors.Is(err, ErrDuplicateSource) {
|
|
||||||
return httperror.Conflict("A source with this URL and credentials already exists", err)
|
return httperror.Conflict("A source with this URL and credentials already exists", err)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return httperror.InternalServerError("Unable to create source", err)
|
return httperror.InternalServerError("Unable to create source", err)
|
||||||
@@ -99,8 +102,7 @@ func BuildGitSource(payload GitSourceCreatePayload) (*portainer.Source, error) {
|
|||||||
return src, nil
|
return src, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildBaseGitSource constructs the source skeleton (name, URL, TLS) without
|
// BuildBaseGitSource constructs the source skeleton (name, URL, TLS, accesses) without authentication.
|
||||||
// authentication.
|
|
||||||
func BuildBaseGitSource(payload GitSourceCreatePayload) *portainer.Source {
|
func BuildBaseGitSource(payload GitSourceCreatePayload) *portainer.Source {
|
||||||
name := payload.Name
|
name := payload.Name
|
||||||
if strings.TrimSpace(name) == "" {
|
if strings.TrimSpace(name) == "" {
|
||||||
@@ -114,6 +116,10 @@ func BuildBaseGitSource(payload GitSourceCreatePayload) *portainer.Source {
|
|||||||
URL: payload.URL,
|
URL: payload.URL,
|
||||||
TLSSkipVerify: payload.TLSSkipVerify,
|
TLSSkipVerify: payload.TLSSkipVerify,
|
||||||
},
|
},
|
||||||
|
UserAccesses: payload.UserAccesses,
|
||||||
|
TeamAccesses: payload.TeamAccesses,
|
||||||
|
Public: payload.Public,
|
||||||
|
AdministratorsOnly: payload.AdministratorsOnly,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func TestGitSourceCreate_Success(t *testing.T) {
|
|||||||
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
||||||
require.NotZero(t, src.ID)
|
require.NotZero(t, src.ID)
|
||||||
require.NotNil(t, src.Git)
|
require.NotNil(t, src.Git)
|
||||||
require.Equal(t, "https://github.com/org/repo.git", src.Git.URL)
|
require.Equal(t, "https://github.com/org/repo", src.Git.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGitSourceCreate_SanitizesCredentials(t *testing.T) {
|
func TestGitSourceCreate_SanitizesCredentials(t *testing.T) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
@@ -18,7 +20,7 @@ var ErrSourceInUse = errors.New("source is used by one or more workflows or cust
|
|||||||
// @id GitOpsSourcesDelete
|
// @id GitOpsSourcesDelete
|
||||||
// @summary Delete a source
|
// @summary Delete a source
|
||||||
// @description Deletes an existing GitOps source. Returns 409 if the source is referenced by any workflow or custom template.
|
// @description Deletes an existing GitOps source. Returns 409 if the source is referenced by any workflow or custom template.
|
||||||
// @description **Access policy**: admin
|
// @description **Access policy**: authenticated
|
||||||
// @tags gitops
|
// @tags gitops
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @security jwt
|
// @security jwt
|
||||||
@@ -36,8 +38,15 @@ func (h *Handler) sourceDelete(w http.ResponseWriter, r *http.Request) *httperro
|
|||||||
return httperror.BadRequest("Invalid source identifier route variable", err)
|
return httperror.BadRequest("Invalid source identifier route variable", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
if err := h.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
if exists, err := tx.Source().Exists(portainer.SourceID(sourceID)); err != nil {
|
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
if exists, err := tx.Source().Exists(userContext, portainer.SourceID(sourceID)); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !exists {
|
} else if !exists {
|
||||||
return dserrors.ErrObjectNotFound
|
return dserrors.ErrObjectNotFound
|
||||||
@@ -71,11 +80,13 @@ func (h *Handler) sourceDelete(w http.ResponseWriter, r *http.Request) *httperro
|
|||||||
return ErrSourceInUse
|
return ErrSourceInUse
|
||||||
}
|
}
|
||||||
|
|
||||||
return tx.Source().Delete(portainer.SourceID(sourceID))
|
return tx.Source().Delete(userContext, portainer.SourceID(sourceID))
|
||||||
}); h.dataStore.IsErrObjectNotFound(err) {
|
}); h.dataStore.IsErrObjectNotFound(err) {
|
||||||
return httperror.NotFound("Unable to find a source with the specified identifier", err)
|
return httperror.NotFound("Unable to find a source with the specified identifier", err)
|
||||||
} else if errors.Is(err, ErrSourceInUse) {
|
} else if errors.Is(err, ErrSourceInUse) {
|
||||||
return httperror.Conflict("Source is used by one or more workflows or custom templates", err)
|
return httperror.Conflict("Source is used by one or more workflows or custom templates", err)
|
||||||
|
} else if errors.Is(err, source.ErrNotEnoughPermission) {
|
||||||
|
return httperror.Forbidden("Not enough permissions to delete source", err)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return httperror.InternalServerError("Unable to delete source", err)
|
return httperror.InternalServerError("Unable to delete source", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -19,8 +20,8 @@ func TestSourceDelete_Success(t *testing.T) {
|
|||||||
|
|
||||||
var srcID portainer.SourceID
|
var srcID portainer.SourceID
|
||||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Name: "to-delete", Type: portainer.SourceTypeGit}
|
src := &portainer.Source{Name: "to-delete", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -57,8 +58,8 @@ func TestSourceDelete_InUse(t *testing.T) {
|
|||||||
|
|
||||||
var srcID portainer.SourceID
|
var srcID portainer.SourceID
|
||||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Name: "in-use", Type: portainer.SourceTypeGit}
|
src := &portainer.Source{Name: "in-use", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -99,8 +100,8 @@ func TestSourceDelete_InUseByCustomTemplate(t *testing.T) {
|
|||||||
|
|
||||||
var srcID portainer.SourceID
|
var srcID portainer.SourceID
|
||||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Name: "in-use-by-template", Type: portainer.SourceTypeGit}
|
src := &portainer.Source{Name: "in-use-by-template", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package sources
|
package sources
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
sourceDS "github.com/portainer/portainer/api/dataservices/source"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/gitops/workflows"
|
"github.com/portainer/portainer/api/gitops/workflows"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
@@ -27,12 +30,19 @@ type AutoUpdateInfo struct {
|
|||||||
FetchInterval string `json:"fetchInterval,omitempty"`
|
FetchInterval string `json:"fetchInterval,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SourceAccess struct {
|
||||||
|
Public bool `json:"public,omitempty"`
|
||||||
|
Users []portainer.UserID `json:"users,omitempty"`
|
||||||
|
Teams []portainer.TeamID `json:"teams,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// SourceDetail extends Source with connection settings and linked workflows.
|
// SourceDetail extends Source with connection settings and linked workflows.
|
||||||
type SourceDetail struct {
|
type SourceDetail struct {
|
||||||
Source
|
Source
|
||||||
Connection connectionInfo `json:"connection" validate:"required"`
|
Connection connectionInfo `json:"connection" validate:"required"`
|
||||||
AutoUpdate *AutoUpdateInfo `json:"autoUpdate,omitempty"`
|
AutoUpdate *AutoUpdateInfo `json:"autoUpdate,omitempty"`
|
||||||
Workflows []workflows.Workflow `json:"workflows"`
|
Workflows []workflows.Workflow `json:"workflows"`
|
||||||
|
Access SourceAccess `json:"access"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id GitOpsSourceGet
|
// @id GitOpsSourceGet
|
||||||
@@ -56,6 +66,11 @@ func (h *Handler) getSource(w http.ResponseWriter, r *http.Request) *httperror.H
|
|||||||
return httperror.BadRequest("Invalid source identifier route variable", err)
|
return httperror.BadRequest("Invalid source identifier route variable", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
sourceID := portainer.SourceID(srcID)
|
sourceID := portainer.SourceID(srcID)
|
||||||
|
|
||||||
var source *portainer.Source
|
var source *portainer.Source
|
||||||
@@ -64,7 +79,8 @@ func (h *Handler) getSource(w http.ResponseWriter, r *http.Request) *httperror.H
|
|||||||
|
|
||||||
err = h.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
err = h.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var err error
|
var err error
|
||||||
source, err = tx.Source().Read(sourceID)
|
userContext := sourceDS.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
source, err = tx.Source().Read(userContext, sourceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -75,15 +91,19 @@ func (h *Handler) getSource(w http.ResponseWriter, r *http.Request) *httperror.H
|
|||||||
|
|
||||||
if h.dataStore.IsErrObjectNotFound(err) {
|
if h.dataStore.IsErrObjectNotFound(err) {
|
||||||
return httperror.NotFound("Source not found", err)
|
return httperror.NotFound("Source not found", err)
|
||||||
|
} else if errors.Is(err, sourceDS.ErrNotEnoughPermission) {
|
||||||
|
return httperror.Forbidden("Not enough permissions to retrieve source", err)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve source", err)
|
return httperror.InternalServerError("Unable to retrieve source", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
detail := BuildSourceDetail(h.buildSource(r.Context(), source, stats), source.Git, sourceWfs)
|
access := BuildSourceAccess(source)
|
||||||
|
|
||||||
|
detail := BuildSourceDetail(h.buildSource(r.Context(), source, stats), source.Git, sourceWfs, access)
|
||||||
return response.JSON(w, detail)
|
return response.JSON(w, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildSourceDetail(baseSource Source, cfg *gittypes.RepoConfig, sourceWfs []workflows.Workflow) SourceDetail {
|
func BuildSourceDetail(baseSource Source, cfg *gittypes.RepoConfig, sourceWfs []workflows.Workflow, access SourceAccess) SourceDetail {
|
||||||
var autoUpdate *AutoUpdateInfo
|
var autoUpdate *AutoUpdateInfo
|
||||||
if len(sourceWfs) > 0 {
|
if len(sourceWfs) > 0 {
|
||||||
autoUpdate = BuildAutoUpdateInfo(sourceWfs[0].AutoUpdate)
|
autoUpdate = BuildAutoUpdateInfo(sourceWfs[0].AutoUpdate)
|
||||||
@@ -94,6 +114,29 @@ func BuildSourceDetail(baseSource Source, cfg *gittypes.RepoConfig, sourceWfs []
|
|||||||
Connection: buildConnectionInfo(cfg),
|
Connection: buildConnectionInfo(cfg),
|
||||||
AutoUpdate: autoUpdate,
|
AutoUpdate: autoUpdate,
|
||||||
Workflows: redactWorkflowCredentials(sourceWfs),
|
Workflows: redactWorkflowCredentials(sourceWfs),
|
||||||
|
Access: access,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildSourceAccess(source *portainer.Source) SourceAccess {
|
||||||
|
if source == nil {
|
||||||
|
return SourceAccess{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if source.AdministratorsOnly {
|
||||||
|
return SourceAccess{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if source.Public {
|
||||||
|
return SourceAccess{
|
||||||
|
Public: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SourceAccess{
|
||||||
|
Public: source.Public,
|
||||||
|
Users: source.UserAccesses,
|
||||||
|
Teams: source.TeamAccesses,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,13 +42,15 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
|
|||||||
authenticatedRouter.Handle("", httperror.LoggerHandler(h.list)).Methods(http.MethodGet)
|
authenticatedRouter.Handle("", httperror.LoggerHandler(h.list)).Methods(http.MethodGet)
|
||||||
authenticatedRouter.Handle("/summary", httperror.LoggerHandler(h.summary)).Methods(http.MethodGet)
|
authenticatedRouter.Handle("/summary", httperror.LoggerHandler(h.summary)).Methods(http.MethodGet)
|
||||||
authenticatedRouter.Handle("/{id}", httperror.LoggerHandler(h.getSource)).Methods(http.MethodGet)
|
authenticatedRouter.Handle("/{id}", httperror.LoggerHandler(h.getSource)).Methods(http.MethodGet)
|
||||||
|
authenticatedRouter.Handle("/git", httperror.LoggerHandler(h.gitSourceCreate)).Methods(http.MethodPost)
|
||||||
|
authenticatedRouter.Handle("/test", httperror.LoggerHandler(h.gitSourceTest)).Methods(http.MethodPost)
|
||||||
|
authenticatedRouter.Handle("/{id}", httperror.LoggerHandler(h.gitSourceUpdate)).Methods(http.MethodPut)
|
||||||
|
authenticatedRouter.Handle("/{id}", httperror.LoggerHandler(h.sourceDelete)).Methods(http.MethodDelete)
|
||||||
|
authenticatedRouter.Handle("/{id}/test", httperror.LoggerHandler(h.sourceTestConnection)).Methods(http.MethodPost)
|
||||||
|
|
||||||
adminRouter := h.PathPrefix("/gitops/sources").Subrouter()
|
adminRouter := h.PathPrefix("/gitops/sources").Subrouter()
|
||||||
adminRouter.Use(bouncer.AdminAccess)
|
adminRouter.Use(bouncer.AdminAccess)
|
||||||
adminRouter.Handle("/git", httperror.LoggerHandler(h.gitSourceCreate)).Methods(http.MethodPost)
|
adminRouter.Handle("/{id}/access", httperror.LoggerHandler(h.gitSourceUpdateAccess)).Methods(http.MethodPut)
|
||||||
adminRouter.Handle("/test", httperror.LoggerHandler(h.gitSourceTest)).Methods(http.MethodPost)
|
|
||||||
adminRouter.Handle("/{id}", httperror.LoggerHandler(h.gitSourceUpdate)).Methods(http.MethodPut)
|
|
||||||
adminRouter.Handle("/{id}", httperror.LoggerHandler(h.sourceDelete)).Methods(http.MethodDelete)
|
|
||||||
adminRouter.Handle("/{id}/test", httperror.LoggerHandler(h.sourceTestConnection)).Methods(http.MethodPost)
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
@@ -17,6 +18,8 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var adminUserContext = source.InsecureNewAdminContext()
|
||||||
|
|
||||||
// createGitWorkflow creates a Source and Workflow for the given config and
|
// createGitWorkflow creates a Source and Workflow for the given config and
|
||||||
// wires them up by setting stack.WorkflowID before creating the stack.
|
// wires them up by setting stack.WorkflowID before creating the stack.
|
||||||
func createGitWorkflow(t *testing.T, tx dataservices.DataStoreTx, stack *portainer.Stack, cfg *gittypes.RepoConfig) portainer.SourceID {
|
func createGitWorkflow(t *testing.T, tx dataservices.DataStoreTx, stack *portainer.Stack, cfg *gittypes.RepoConfig) portainer.SourceID {
|
||||||
@@ -27,7 +30,7 @@ func createGitWorkflow(t *testing.T, tx dataservices.DataStoreTx, stack *portain
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: cfg,
|
Git: cfg,
|
||||||
}
|
}
|
||||||
require.NoError(t, tx.Source().Create(src))
|
require.NoError(t, tx.Source().Create(adminUserContext, src))
|
||||||
|
|
||||||
wf := &portainer.Workflow{
|
wf := &portainer.Workflow{
|
||||||
Artifacts: []portainer.Artifact{{
|
Artifacts: []portainer.Artifact{{
|
||||||
@@ -51,13 +54,19 @@ func newTestHandler(t *testing.T, store dataservices.DataStore) *Handler {
|
|||||||
return NewHandler(testhelpers.NewTestRequestBouncer(), store, nil, nil)
|
return NewHandler(testhelpers.NewTestRequestBouncer(), store, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func adminRestrictedContext(userID portainer.UserID) *security.RestrictedRequestContext {
|
||||||
|
return &security.RestrictedRequestContext{
|
||||||
|
UserID: userID,
|
||||||
|
IsAdmin: true,
|
||||||
|
User: &portainer.User{ID: userID, Role: portainer.AdministratorRole},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func buildListReq(t *testing.T, userID portainer.UserID, query string) *http.Request {
|
func buildListReq(t *testing.T, userID portainer.UserID, query string) *http.Request {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
req := httptest.NewRequest(http.MethodGet, "/gitops/sources?"+query, nil)
|
req := httptest.NewRequest(http.MethodGet, "/gitops/sources?"+query, nil)
|
||||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||||
UserID: userID, IsAdmin: true,
|
|
||||||
}))
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,9 +74,7 @@ func buildGetReq(t *testing.T, userID portainer.UserID, id string) *http.Request
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
req := httptest.NewRequest(http.MethodGet, "/gitops/sources/"+id, nil)
|
req := httptest.NewRequest(http.MethodGet, "/gitops/sources/"+id, nil)
|
||||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||||
UserID: userID, IsAdmin: true,
|
|
||||||
}))
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,9 +107,7 @@ func buildCreateReq(t *testing.T, userID portainer.UserID, body []byte) *http.Re
|
|||||||
req := httptest.NewRequest(http.MethodPost, "/gitops/sources/git", bytes.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/gitops/sources/git", bytes.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||||
UserID: userID, IsAdmin: true,
|
|
||||||
}))
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,9 +116,7 @@ func buildUpdateReq(t *testing.T, userID portainer.UserID, id int, body []byte)
|
|||||||
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/gitops/sources/%d", id), bytes.NewReader(body))
|
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/gitops/sources/%d", id), bytes.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||||
UserID: userID, IsAdmin: true,
|
|
||||||
}))
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +124,7 @@ func buildDeleteReq(t *testing.T, userID portainer.UserID, id int) *http.Request
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/gitops/sources/%d", id), nil)
|
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/gitops/sources/%d", id), nil)
|
||||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||||
UserID: userID, IsAdmin: true,
|
|
||||||
}))
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +132,7 @@ func buildSummaryReq(t *testing.T, userID portainer.UserID) *http.Request {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
req := httptest.NewRequest(http.MethodGet, "/gitops/sources/summary", nil)
|
req := httptest.NewRequest(http.MethodGet, "/gitops/sources/summary", nil)
|
||||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||||
UserID: userID, IsAdmin: true,
|
|
||||||
}))
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,9 +141,7 @@ func buildUpdateReqWithRawID(t *testing.T, userID portainer.UserID, id string, b
|
|||||||
req := httptest.NewRequest(http.MethodPut, "/gitops/sources/"+id, bytes.NewReader(body))
|
req := httptest.NewRequest(http.MethodPut, "/gitops/sources/"+id, bytes.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||||
UserID: userID, IsAdmin: true,
|
|
||||||
}))
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,8 +149,6 @@ func buildDeleteReqWithRawID(t *testing.T, userID portainer.UserID, id string) *
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/gitops/sources/"+id, nil)
|
req := httptest.NewRequest(http.MethodDelete, "/gitops/sources/"+id, nil)
|
||||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{ID: userID}))
|
||||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
req = req.WithContext(security.StoreRestrictedRequestContext(req, adminRestrictedContext(userID)))
|
||||||
UserID: userID, IsAdmin: true,
|
|
||||||
}))
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
ceWorkflows "github.com/portainer/portainer/api/gitops/workflows"
|
"github.com/portainer/portainer/api/gitops/workflows"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/http/utils/filters"
|
"github.com/portainer/portainer/api/http/utils/filters"
|
||||||
"github.com/portainer/portainer/api/slicesx"
|
"github.com/portainer/portainer/api/slicesx"
|
||||||
@@ -56,7 +56,7 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request) *httperror.Handle
|
|||||||
}
|
}
|
||||||
|
|
||||||
if status, _ := request.RetrieveQueryParameter(r, "status", true); status != "" {
|
if status, _ := request.RetrieveQueryParameter(r, "status", true); status != "" {
|
||||||
s, err := ceWorkflows.ParseStatus(status)
|
s, err := workflows.ParseStatus(status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid status parameter", err)
|
return httperror.BadRequest("Invalid status parameter", err)
|
||||||
}
|
}
|
||||||
@@ -111,11 +111,11 @@ func cacheKey(sc *security.RestrictedRequestContext) string {
|
|||||||
|
|
||||||
func (h *Handler) fetchSources(ctx context.Context, sc *security.RestrictedRequestContext) ([]Source, error) {
|
func (h *Handler) fetchSources(ctx context.Context, sc *security.RestrictedRequestContext) ([]Source, error) {
|
||||||
var allSrcs []portainer.Source
|
var allSrcs []portainer.Source
|
||||||
var stats map[portainer.SourceID]ceWorkflows.SourceStats
|
var stats map[portainer.SourceID]workflows.SourceStats
|
||||||
|
|
||||||
if err := h.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
if err := h.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var err error
|
var err error
|
||||||
allSrcs, stats, err = ceWorkflows.FetchSourceStats(tx, h.k8sFactory, sc)
|
allSrcs, stats, err = workflows.FetchSourceStats(tx, h.k8sFactory, sc)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -123,12 +123,12 @@ func (h *Handler) fetchSources(ctx context.Context, sc *security.RestrictedReque
|
|||||||
|
|
||||||
result := make([]Source, 0, len(allSrcs))
|
result := make([]Source, 0, len(allSrcs))
|
||||||
for _, src := range allSrcs {
|
for _, src := range allSrcs {
|
||||||
s, accessible := stats[src.ID]
|
stat, ok := stats[src.ID]
|
||||||
if !accessible && !sc.IsAdmin {
|
if !ok {
|
||||||
continue
|
stat = workflows.SourceStats{}
|
||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, h.buildSource(ctx, &src, s))
|
result = append(result, h.buildSource(ctx, &src, stat))
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestSourcesList_GroupsByURLAndCredentials(t *testing.T) {
|
|||||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
cfg := gitCfg("https://github.com/org/repo")
|
cfg := gitCfg("https://github.com/org/repo")
|
||||||
src := &portainer.Source{Name: "repo", Type: portainer.SourceTypeGit, Git: cfg}
|
src := &portainer.Source{Name: "repo", Type: portainer.SourceTypeGit, Git: cfg}
|
||||||
require.NoError(t, tx.Source().Create(src))
|
require.NoError(t, tx.Source().Create(adminUserContext, src))
|
||||||
|
|
||||||
wfA := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}}
|
wfA := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}}
|
||||||
require.NoError(t, tx.Workflow().Create(wfA))
|
require.NoError(t, tx.Workflow().Create(wfA))
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
@@ -17,7 +19,7 @@ import (
|
|||||||
// @id GitOpsSourcesTestById
|
// @id GitOpsSourcesTestById
|
||||||
// @summary Test the connection of a stored source
|
// @summary Test the connection of a stored source
|
||||||
// @description Tests connectivity for a GitOps source, applying optional overrides to the stored configuration.
|
// @description Tests connectivity for a GitOps source, applying optional overrides to the stored configuration.
|
||||||
// @description **Access policy**: administrator
|
// @description **Access policy**: authenticated
|
||||||
// @tags gitops
|
// @tags gitops
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @security jwt
|
// @security jwt
|
||||||
@@ -37,6 +39,11 @@ func (h *Handler) sourceTestConnection(w http.ResponseWriter, r *http.Request) *
|
|||||||
return httperror.BadRequest("Invalid source identifier route variable", err)
|
return httperror.BadRequest("Invalid source identifier route variable", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
var payload GitSourceUpdatePayload
|
var payload GitSourceUpdatePayload
|
||||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil && !errors.Is(err, io.EOF) {
|
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil && !errors.Is(err, io.EOF) {
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
@@ -44,10 +51,13 @@ func (h *Handler) sourceTestConnection(w http.ResponseWriter, r *http.Request) *
|
|||||||
|
|
||||||
var src *portainer.Source
|
var src *portainer.Source
|
||||||
if err := h.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
if err := h.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src, err = tx.Source().Read(portainer.SourceID(sourceID))
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
src, err = tx.Source().Read(userContext, portainer.SourceID(sourceID))
|
||||||
return err
|
return err
|
||||||
}); h.dataStore.IsErrObjectNotFound(err) {
|
}); h.dataStore.IsErrObjectNotFound(err) {
|
||||||
return httperror.NotFound("Unable to find a source with the specified identifier", err)
|
return httperror.NotFound("Unable to find a source with the specified identifier", err)
|
||||||
|
} else if errors.Is(err, source.ErrNotEnoughPermission) {
|
||||||
|
return httperror.Forbidden("Not enough permissions to retrieve source", err)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return httperror.InternalServerError("Unable to find source", err)
|
return httperror.InternalServerError("Unable to find source", err)
|
||||||
}
|
}
|
||||||
@@ -75,7 +85,7 @@ type ConnectionTestResult struct {
|
|||||||
// @id GitOpsSourcesTest
|
// @id GitOpsSourcesTest
|
||||||
// @summary Test a Git source connection
|
// @summary Test a Git source connection
|
||||||
// @description Tests connectivity for Git connection details that have not been persisted yet.
|
// @description Tests connectivity for Git connection details that have not been persisted yet.
|
||||||
// @description **Access policy**: administrator
|
// @description **Access policy**: authenticated
|
||||||
// @tags gitops
|
// @tags gitops
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @security jwt
|
// @security jwt
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package sources
|
package sources
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -8,6 +9,7 @@ import (
|
|||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
ceWorkflows "github.com/portainer/portainer/api/gitops/workflows"
|
ceWorkflows "github.com/portainer/portainer/api/gitops/workflows"
|
||||||
|
|
||||||
"github.com/segmentio/encoding/json"
|
"github.com/segmentio/encoding/json"
|
||||||
@@ -38,10 +40,9 @@ func TestSourcesSummary_CountsByStatus(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
_, store := datastore.MustNewTestStore(t, false, true)
|
_, store := datastore.MustNewTestStore(t, false, true)
|
||||||
|
|
||||||
// With nil gitService and nil GitConfig, all sources get StatusUnknown.
|
|
||||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
for _, name := range []string{"source-a", "source-b", "source-c"} {
|
for idx, name := range []string{"source-a", "source-b", "source-c"} {
|
||||||
err := tx.Source().Create(&portainer.Source{Name: name, Type: portainer.SourceTypeGit})
|
err := tx.Source().Create(adminUserContext, &portainer.Source{Name: name, Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: fmt.Sprintf("http://github.com/org/repo%d", idx)}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package sources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SourceAccessUpdatePayload struct {
|
||||||
|
Public bool `json:"public"`
|
||||||
|
Users []portainer.UserID `json:"users,omitempty"`
|
||||||
|
Teams []portainer.TeamID `json:"teams,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GitOpsSourcesUpdateAccess
|
||||||
|
// @summary Update a GitOps source's access control
|
||||||
|
// @description Updates the access control settings for an existing GitOps source.
|
||||||
|
// @description **Access policy**: administrator
|
||||||
|
// @tags gitops
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Source identifier"
|
||||||
|
// @param body body SourceAccessUpdatePayload true "Source access control"
|
||||||
|
// @success 200 {object} portainer.Source
|
||||||
|
// @failure 400 "Invalid request payload"
|
||||||
|
// @failure 403 "Access denied"
|
||||||
|
// @failure 404 "Source not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /gitops/sources/{id}/access [put]
|
||||||
|
func (h *Handler) gitSourceUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return httperror.BadRequest("Invalid source identifier route variable", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload SourceAccessUpdatePayload
|
||||||
|
|
||||||
|
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||||
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceID := portainer.SourceID(id)
|
||||||
|
|
||||||
|
var src *portainer.Source
|
||||||
|
|
||||||
|
if err := h.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
if src, err = tx.Source().Read(userContext, sourceID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplySourceAccessChanges(src, payload)
|
||||||
|
|
||||||
|
return tx.Source().Update(userContext, src.ID, src)
|
||||||
|
}); h.dataStore.IsErrObjectNotFound(err) {
|
||||||
|
return httperror.NotFound("Unable to find a source with the specified identifier", err)
|
||||||
|
} else if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to update source access", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate implements the portainer.Validatable interface
|
||||||
|
func (payload *SourceAccessUpdatePayload) Validate(_ *http.Request) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplySourceAccessChanges applies the payload access changes to the source in place.
|
||||||
|
func ApplySourceAccessChanges(src *portainer.Source, payload SourceAccessUpdatePayload) {
|
||||||
|
src.Public = payload.Public
|
||||||
|
|
||||||
|
if payload.Public {
|
||||||
|
src.AdministratorsOnly = false
|
||||||
|
src.UserAccesses = []portainer.UserID{}
|
||||||
|
src.TeamAccesses = []portainer.TeamID{}
|
||||||
|
} else if len(payload.Users) == 0 && len(payload.Teams) == 0 {
|
||||||
|
src.AdministratorsOnly = true
|
||||||
|
src.UserAccesses = []portainer.UserID{}
|
||||||
|
src.TeamAccesses = []portainer.TeamID{}
|
||||||
|
} else {
|
||||||
|
src.AdministratorsOnly = false
|
||||||
|
src.UserAccesses = payload.Users
|
||||||
|
src.TeamAccesses = payload.Teams
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,9 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/gitops/workflows"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
@@ -17,7 +18,6 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotGitSource = errors.New("source is not a Git source")
|
ErrNotGitSource = errors.New("source is not a Git source")
|
||||||
ErrDuplicateSource = errors.New("a source with this URL and credentials already exists")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GitSourceUpdatePayload holds the parameters for creating a git-backed source
|
// GitSourceUpdatePayload holds the parameters for creating a git-backed source
|
||||||
@@ -46,7 +46,7 @@ func (payload *GitSourceUpdatePayload) Validate(_ *http.Request) error {
|
|||||||
// @id GitOpsSourcesUpdateGit
|
// @id GitOpsSourcesUpdateGit
|
||||||
// @summary Update a Git source
|
// @summary Update a Git source
|
||||||
// @description Updates an existing GitOps source backed by a Git repository.
|
// @description Updates an existing GitOps source backed by a Git repository.
|
||||||
// @description **Access policy**: administrator
|
// @description **Access policy**: authenticated
|
||||||
// @tags gitops
|
// @tags gitops
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @security jwt
|
// @security jwt
|
||||||
@@ -73,6 +73,11 @@ func (h *Handler) gitSourceUpdate(w http.ResponseWriter, r *http.Request) *httpe
|
|||||||
return httperror.BadRequest("Invalid request payload", err)
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
sourceID := portainer.SourceID(id)
|
sourceID := portainer.SourceID(id)
|
||||||
|
|
||||||
var src *portainer.Source
|
var src *portainer.Source
|
||||||
@@ -80,7 +85,8 @@ func (h *Handler) gitSourceUpdate(w http.ResponseWriter, r *http.Request) *httpe
|
|||||||
if err := h.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
if err := h.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if src, err = tx.Source().Read(sourceID); err != nil {
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
if src, err = tx.Source().Read(userContext, sourceID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,24 +94,14 @@ func (h *Handler) gitSourceUpdate(w http.ResponseWriter, r *http.Request) *httpe
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
username, password := "", ""
|
return tx.Source().Update(userContext, src.ID, src)
|
||||||
if src.Git != nil && src.Git.Authentication != nil {
|
|
||||||
username = src.Git.Authentication.Username
|
|
||||||
password = src.Git.Authentication.Password
|
|
||||||
}
|
|
||||||
|
|
||||||
if isUnique, err := workflows.ValidateUniqueSource(tx, src.Git.URL, username, password, sourceID); err != nil {
|
|
||||||
return err
|
|
||||||
} else if !isUnique {
|
|
||||||
return ErrDuplicateSource
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Source().Update(src.ID, src)
|
|
||||||
}); h.dataStore.IsErrObjectNotFound(err) {
|
}); h.dataStore.IsErrObjectNotFound(err) {
|
||||||
return httperror.NotFound("Unable to find a source with the specified identifier", err)
|
return httperror.NotFound("Unable to find a source with the specified identifier", err)
|
||||||
} else if errors.Is(err, ErrNotGitSource) {
|
} else if errors.Is(err, ErrNotGitSource) {
|
||||||
return httperror.BadRequest("Source is not a Git source", err)
|
return httperror.BadRequest("Source is not a Git source", err)
|
||||||
} else if errors.Is(err, ErrDuplicateSource) {
|
} else if errors.Is(err, source.ErrNotEnoughPermission) {
|
||||||
|
return httperror.Forbidden("Not enough permissions to update source", err)
|
||||||
|
} else if errors.Is(err, source.ErrDuplicateSource) {
|
||||||
return httperror.Conflict("A source with this URL and credentials already exists", err)
|
return httperror.Conflict("A source with this URL and credentials already exists", err)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return httperror.InternalServerError("Unable to update source", err)
|
return httperror.InternalServerError("Unable to update source", err)
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ func TestGitSourceUpdate_Success(t *testing.T) {
|
|||||||
|
|
||||||
var srcID portainer.SourceID
|
var srcID portainer.SourceID
|
||||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Name: "old-name", Type: portainer.SourceTypeGit}
|
src := &portainer.Source{Name: "old-name", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ func TestGitSourceUpdate_Success(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "new-name", src.Name)
|
require.Equal(t, "new-name", src.Name)
|
||||||
require.NotNil(t, src.Git)
|
require.NotNil(t, src.Git)
|
||||||
require.Equal(t, "https://github.com/org/new.git", src.Git.URL)
|
require.Equal(t, "https://github.com/org/new", src.Git.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGitSourceUpdate_PreservesAuthWhenNotProvided(t *testing.T) {
|
func TestGitSourceUpdate_PreservesAuthWhenNotProvided(t *testing.T) {
|
||||||
@@ -66,7 +66,7 @@ func TestGitSourceUpdate_PreservesAuthWhenNotProvided(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ func TestGitSourceUpdate_PreservesAuthWhenNotProvided(t *testing.T) {
|
|||||||
var stored *portainer.Source
|
var stored *portainer.Source
|
||||||
require.NoError(t, store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var err error
|
var err error
|
||||||
stored, err = tx.Source().Read(srcID)
|
stored, err = tx.Source().Read(adminUserContext, srcID)
|
||||||
return err
|
return err
|
||||||
}))
|
}))
|
||||||
require.NotNil(t, stored.Git)
|
require.NotNil(t, stored.Git)
|
||||||
@@ -115,7 +115,7 @@ func TestGitSourceUpdate_ClearsAuthWhenRequested(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ func TestGitSourceUpdate_ClearsAuthWhenRequested(t *testing.T) {
|
|||||||
var stored *portainer.Source
|
var stored *portainer.Source
|
||||||
require.NoError(t, store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var err error
|
var err error
|
||||||
stored, err = tx.Source().Read(srcID)
|
stored, err = tx.Source().Read(adminUserContext, srcID)
|
||||||
return err
|
return err
|
||||||
}))
|
}))
|
||||||
require.NotNil(t, stored.Git)
|
require.NotNil(t, stored.Git)
|
||||||
@@ -162,7 +162,7 @@ func TestGitSourceUpdate_ReplacesAuthWhenProvided(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ func TestGitSourceUpdate_ReplacesAuthWhenProvided(t *testing.T) {
|
|||||||
var stored *portainer.Source
|
var stored *portainer.Source
|
||||||
require.NoError(t, store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var err error
|
var err error
|
||||||
stored, err = tx.Source().Read(srcID)
|
stored, err = tx.Source().Read(adminUserContext, srcID)
|
||||||
return err
|
return err
|
||||||
}))
|
}))
|
||||||
require.NotNil(t, stored.Git)
|
require.NotNil(t, stored.Git)
|
||||||
@@ -229,11 +229,11 @@ func TestGitSourceUpdate_ConflictOnDuplicateURL(t *testing.T) {
|
|||||||
URL: "https://github.com/org/existing.git",
|
URL: "https://github.com/org/existing.git",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := tx.Source().Create(existing)
|
err := tx.Source().Create(adminUserContext, existing)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
src := &portainer.Source{Name: "other", Type: portainer.SourceTypeGit}
|
src := &portainer.Source{Name: "other", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||||
err = tx.Source().Create(src)
|
err = tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -253,39 +253,14 @@ func TestGitSourceUpdate_ConflictOnDuplicateURL(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusConflict, rr.Code)
|
require.Equal(t, http.StatusConflict, rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGitSourceUpdate_NotGitSource(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 := &portainer.Source{Name: "helm-source", Type: portainer.SourceTypeHelm}
|
|
||||||
err := tx.Source().Create(src)
|
|
||||||
require.NoError(t, err)
|
|
||||||
srcID = src.ID
|
|
||||||
|
|
||||||
return tx.User().Create(&portainer.User{ID: 1, Role: portainer.AdministratorRole})
|
|
||||||
}))
|
|
||||||
|
|
||||||
h := newTestHandler(t, store)
|
|
||||||
|
|
||||||
body, err := json.Marshal(GitSourceUpdatePayload{URL: new("https://github.com/org/repo.git")})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, buildUpdateReq(t, 1, int(srcID), body))
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusBadRequest, rr.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGitSourceUpdate_MalformedJSON(t *testing.T) {
|
func TestGitSourceUpdate_MalformedJSON(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
_, store := datastore.MustNewTestStore(t, false, true)
|
_, store := datastore.MustNewTestStore(t, false, true)
|
||||||
|
|
||||||
var srcID portainer.SourceID
|
var srcID portainer.SourceID
|
||||||
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
src := &portainer.Source{Name: "src", Type: portainer.SourceTypeGit}
|
src := &portainer.Source{Name: "src", Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "http://github.com/org/repo"}}
|
||||||
err := tx.Source().Create(src)
|
err := tx.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
srcID = src.ID
|
srcID = src.ID
|
||||||
|
|
||||||
@@ -317,7 +292,7 @@ func TestGitSourceUpdate_ConflictWhenAuthChangesMatchAnotherSource(t *testing.T)
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := tx.Source().Create(existing); err != nil {
|
if err := tx.Source().Create(adminUserContext, existing); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +301,7 @@ func TestGitSourceUpdate_ConflictWhenAuthChangesMatchAnotherSource(t *testing.T)
|
|||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/org/repo.git"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/org/repo.git"},
|
||||||
}
|
}
|
||||||
if err := tx.Source().Create(other); err != nil {
|
if err := tx.Source().Create(adminUserContext, other); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
srcID = other.ID
|
srcID = other.ID
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
ce "github.com/portainer/portainer/api/gitops/workflows"
|
ce "github.com/portainer/portainer/api/gitops/workflows"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
@@ -24,6 +25,7 @@ func buildWorkflowsReq(t *testing.T, userID portainer.UserID, role portainer.Use
|
|||||||
ctx = security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
ctx = security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
IsAdmin: security.IsAdminRole(role),
|
IsAdmin: security.IsAdminRole(role),
|
||||||
|
User: &portainer.User{ID: userID, Role: role},
|
||||||
})
|
})
|
||||||
return req.WithContext(ctx)
|
return req.WithContext(ctx)
|
||||||
}
|
}
|
||||||
@@ -48,7 +50,7 @@ func createGitStack(t *testing.T, tx dataservices.DataStoreTx, stack *portainer.
|
|||||||
|
|
||||||
if stack.GitConfig != nil {
|
if stack.GitConfig != nil {
|
||||||
src := &portainer.Source{Git: stack.GitConfig, Type: portainer.SourceTypeGit}
|
src := &portainer.Source{Git: stack.GitConfig, Type: portainer.SourceTypeGit}
|
||||||
require.NoError(t, tx.Source().Create(src))
|
require.NoError(t, tx.Source().Create(source.InsecureNewAdminContext(), src))
|
||||||
|
|
||||||
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
||||||
StackID: stack.ID,
|
StackID: stack.ID,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func TestWorkflowsList_Pagination(t *testing.T) {
|
|||||||
createGitStack(t, tx, &portainer.Stack{
|
createGitStack(t, tx, &portainer.Stack{
|
||||||
ID: portainer.StackID(i),
|
ID: portainer.StackID(i),
|
||||||
Name: fmt.Sprintf("stack-%d", i),
|
Name: fmt.Sprintf("stack-%d", i),
|
||||||
GitConfig: gitConfig("https://github.com/x/y"),
|
GitConfig: gitConfig(fmt.Sprintf("https://github.com/x/y-%d", i)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func buildSummaryReq(t *testing.T, userID portainer.UserID, role portainer.UserR
|
|||||||
ctx = security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
ctx = security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
IsAdmin: security.IsAdminRole(role),
|
IsAdmin: security.IsAdminRole(role),
|
||||||
|
User: &portainer.User{ID: userID, Role: role},
|
||||||
})
|
})
|
||||||
return req.WithContext(ctx)
|
return req.WithContext(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/git/update"
|
"github.com/portainer/portainer/api/git/update"
|
||||||
"github.com/portainer/portainer/api/gitops/sources"
|
"github.com/portainer/portainer/api/gitops/sources"
|
||||||
@@ -52,7 +53,7 @@ func createStackPayloadFromComposeFileContentPayload(name string, fileContent st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) checkAndCleanStackDupFromSwarm(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID, stack *portainer.Stack) error {
|
func (handler *Handler) checkAndCleanStackDupFromSwarm(_ http.ResponseWriter, _ *http.Request, _ *portainer.Endpoint, _ portainer.UserID, stack *portainer.Stack) error {
|
||||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -279,15 +280,16 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.SourceID != 0 {
|
|
||||||
if _, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, payload.SourceID); httpErr != nil {
|
|
||||||
return httpErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
return httperror.InternalServerError("Unable to retrieve user info from request context", err)
|
||||||
|
}
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
|
||||||
|
if payload.SourceID != 0 {
|
||||||
|
if _, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, userContext, payload.SourceID); httpErr != nil {
|
||||||
|
return httpErr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stackPayload := createStackPayloadFromComposeGitPayload(payload.Name,
|
stackPayload := createStackPayloadFromComposeGitPayload(payload.Name,
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/git/update"
|
"github.com/portainer/portainer/api/git/update"
|
||||||
"github.com/portainer/portainer/api/gitops/sources"
|
"github.com/portainer/portainer/api/gitops/sources"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
"github.com/portainer/portainer/api/internal/registryutils"
|
"github.com/portainer/portainer/api/internal/registryutils"
|
||||||
"github.com/portainer/portainer/api/stacks/stackbuilders"
|
"github.com/portainer/portainer/api/stacks/stackbuilders"
|
||||||
@@ -234,8 +236,13 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve user info from request context", err)
|
||||||
|
}
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
if payload.SourceID != 0 {
|
if payload.SourceID != 0 {
|
||||||
if _, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, payload.SourceID); httpErr != nil {
|
if _, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, userContext, payload.SourceID); httpErr != nil {
|
||||||
return httpErr
|
return httpErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/git/update"
|
"github.com/portainer/portainer/api/git/update"
|
||||||
"github.com/portainer/portainer/api/gitops/sources"
|
"github.com/portainer/portainer/api/gitops/sources"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
@@ -218,15 +219,15 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.SourceID != 0 {
|
|
||||||
if _, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, payload.SourceID); httpErr != nil {
|
|
||||||
return httpErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
return httperror.InternalServerError("Unable to retrieve user info from request context", err)
|
||||||
|
}
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
if payload.SourceID != 0 {
|
||||||
|
if _, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, userContext, payload.SourceID); httpErr != nil {
|
||||||
|
return httpErr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stackPayload := createStackPayloadFromSwarmGitPayload(payload.Name,
|
stackPayload := createStackPayloadFromSwarmGitPayload(payload.Name,
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ type stackResponse struct {
|
|||||||
|
|
||||||
// loadGitConfigForStack reads the merged GitConfig (Source URL/auth/TLS + Artifact ref/path/hash)
|
// loadGitConfigForStack reads the merged GitConfig (Source URL/auth/TLS + Artifact ref/path/hash)
|
||||||
// and the SourceID for the given stack.
|
// and the SourceID for the given stack.
|
||||||
func loadGitConfigForStack(tx dataservices.DataStoreTx, workflowID portainer.WorkflowID, stackID portainer.StackID) (*gittypes.RepoConfig, portainer.SourceID, error) {
|
func loadGitConfigForStack(tx dataservices.DataStoreTx, userContext *dataservices.SourceServiceUserContext, workflowID portainer.WorkflowID, stackID portainer.StackID) (*gittypes.RepoConfig, portainer.SourceID, error) {
|
||||||
src, file, err := workflows.GitSourceAndArtifactForStack(tx, workflowID, stackID)
|
src, file, err := workflows.GitSourceAndArtifactForStack(tx, userContext, workflowID, stackID)
|
||||||
if err != nil || src == nil {
|
if err != nil || src == nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ func loadGitConfigForStack(tx dataservices.DataStoreTx, workflowID portainer.Wor
|
|||||||
// saveStackGitConfig persists the stack's git settings. When newSourceID is non-zero the stack's
|
// saveStackGitConfig persists the stack's git settings. When newSourceID is non-zero the stack's
|
||||||
// artifact is repointed to that existing Source (selected by the caller) without modifying any
|
// artifact is repointed to that existing Source (selected by the caller) without modifying any
|
||||||
// Source's git config; otherwise the target Source is derived from cfg.URL.
|
// Source's git config; otherwise the target Source is derived from cfg.URL.
|
||||||
func saveStackGitConfig(tx dataservices.DataStoreTx, workflowID portainer.WorkflowID, stackID portainer.StackID, oldSourceID, newSourceID portainer.SourceID, cfg *gittypes.RepoConfig) error {
|
func saveStackGitConfig(tx dataservices.DataStoreTx, userContext *dataservices.SourceServiceUserContext, workflowID portainer.WorkflowID, stackID portainer.StackID, oldSourceID, newSourceID portainer.SourceID, cfg *gittypes.RepoConfig) error {
|
||||||
matchArtifact := func(a portainer.Artifact) bool {
|
matchArtifact := func(a portainer.Artifact) bool {
|
||||||
return a.StackID == stackID
|
return a.StackID == stackID
|
||||||
}
|
}
|
||||||
@@ -41,16 +41,16 @@ func saveStackGitConfig(tx dataservices.DataStoreTx, workflowID portainer.Workfl
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return workflows.SaveWorkflowGitConfig(tx, workflowID, matchArtifact, oldSourceID, cfg)
|
return workflows.SaveWorkflowGitConfig(tx, userContext, workflowID, matchArtifact, oldSourceID, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newStackResponse fills stack.GitConfig and returns a response that also includes GitSourceId.
|
// newStackResponse fills stack.GitConfig and returns a response that also includes GitSourceId.
|
||||||
func newStackResponse(tx dataservices.DataStoreTx, stack *portainer.Stack) (*stackResponse, error) {
|
func newStackResponse(tx dataservices.DataStoreTx, userContext *dataservices.SourceServiceUserContext, stack *portainer.Stack) (*stackResponse, error) {
|
||||||
if stack.WorkflowID == 0 {
|
if stack.WorkflowID == 0 {
|
||||||
return &stackResponse{Stack: *stack}, nil
|
return &stackResponse{Stack: *stack}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
gitConfig, gitSourceID, err := loadGitConfigForStack(tx, stack.WorkflowID, stack.ID)
|
gitConfig, gitSourceID, err := loadGitConfigForStack(tx, userContext, stack.WorkflowID, stack.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -61,12 +61,12 @@ func newStackResponse(tx dataservices.DataStoreTx, stack *portainer.Stack) (*sta
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fillStackGitConfig populates stack.GitConfig from the merged Source+Artifact for backwards-compatible responses.
|
// fillStackGitConfig populates stack.GitConfig from the merged Source+Artifact for backwards-compatible responses.
|
||||||
func fillStackGitConfig(tx dataservices.DataStoreTx, stack *portainer.Stack) error {
|
func fillStackGitConfig(tx dataservices.DataStoreTx, userContext *dataservices.SourceServiceUserContext, stack *portainer.Stack) error {
|
||||||
if stack.WorkflowID == 0 {
|
if stack.WorkflowID == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
gitConfig, _, err := loadGitConfigForStack(tx, stack.WorkflowID, stack.ID)
|
gitConfig, _, err := loadGitConfigForStack(tx, userContext, stack.WorkflowID, stack.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
@@ -120,9 +122,13 @@ func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *
|
|||||||
|
|
||||||
stack.ResourceControl = resourceControl
|
stack.ResourceControl = resourceControl
|
||||||
|
|
||||||
if err := fillStackGitConfig(handler.DataStore, stack); err != nil {
|
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
if err := fillStackGitConfig(tx, userContext, stack); err != nil {
|
||||||
return httperror.InternalServerError("Unable to load git config for stack", err)
|
return httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
return response.JSON(w, stack)
|
return response.TxResponse(w, stack, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
@@ -126,16 +128,29 @@ func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *port
|
|||||||
resourceControl = authorization.NewPrivateResourceControl(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl, userID)
|
resourceControl = authorization.NewPrivateResourceControl(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.DataStore.ResourceControl().Create(resourceControl)
|
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
err = tx.ResourceControl().Create(resourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to persist resource control inside the database", err)
|
return httperror.InternalServerError("Unable to persist resource control inside the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.ResourceControl = resourceControl
|
stack.ResourceControl = resourceControl
|
||||||
|
|
||||||
if err := fillStackGitConfig(handler.DataStore, stack); err != nil {
|
user, err := tx.User().Read(userID)
|
||||||
return httperror.InternalServerError("Unable to load git config for stack", err)
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to read user", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, stack)
|
userMemberships, err := tx.TeamMembership().TeamMembershipsByUserID(userID)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to read user's team memberships", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext := source.NewUserContext(user, userMemberships)
|
||||||
|
if err := fillStackGitConfig(tx, userContext, stack); err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.TxResponse(w, stack, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ func (handler *Handler) deleteStack(ctx context.Context, userID portainer.UserID
|
|||||||
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)
|
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)
|
||||||
|
|
||||||
if stackutils.IsRelativePathStack(stack) {
|
if stackutils.IsRelativePathStack(stack) {
|
||||||
return handler.StackDeployer.UndeployRemoteSwarmStack(ctx, stack, endpoint)
|
return handler.StackDeployer.UndeployRemoteSwarmStack(ctx, userID, stack, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.SwarmStackManager.Remove(ctx, stack, endpoint)
|
return handler.SwarmStackManager.Remove(ctx, stack, endpoint)
|
||||||
@@ -202,7 +202,7 @@ func (handler *Handler) deleteStack(ctx context.Context, userID portainer.UserID
|
|||||||
stack.Name = handler.ComposeStackManager.NormalizeStackName(stack.Name)
|
stack.Name = handler.ComposeStackManager.NormalizeStackName(stack.Name)
|
||||||
|
|
||||||
if stackutils.IsRelativePathStack(stack) {
|
if stackutils.IsRelativePathStack(stack) {
|
||||||
return handler.StackDeployer.UndeployRemoteComposeStack(ctx, stack, endpoint)
|
return handler.StackDeployer.UndeployRemoteComposeStack(ctx, userID, stack, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.StackDeployer.UndeployComposeStack(ctx, stack, endpoint)
|
return handler.StackDeployer.UndeployComposeStack(ctx, stack, endpoint)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
@@ -96,11 +98,17 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
|||||||
|
|
||||||
var gitConfig *gittypes.RepoConfig
|
var gitConfig *gittypes.RepoConfig
|
||||||
if stack.WorkflowID != 0 {
|
if stack.WorkflowID != 0 {
|
||||||
|
if err := handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
var err error
|
var err error
|
||||||
gitConfig, _, err = loadGitConfigForStack(handler.DataStore, stack.WorkflowID, stack.ID)
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
gitConfig, _, err = loadGitConfigForStack(tx, userContext, stack.WorkflowID, stack.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to load git config for stack", err)
|
return httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return response.TxErrorResponse(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if gitStackPendingRedeploy(stack, gitConfig) {
|
if gitStackPendingRedeploy(stack, gitConfig) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
@@ -45,7 +46,7 @@ func TestStackFile_GitPendingRedeploy_Returns409(t *testing.T) {
|
|||||||
ConfigFilePath: "docker-compose.yml",
|
ConfigFilePath: "docker-compose.yml",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
require.NoError(t, store.Source().Create(src))
|
require.NoError(t, store.Source().Create(source.InsecureNewAdminContext(), src))
|
||||||
|
|
||||||
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
||||||
StackID: stackID,
|
StackID: stackID,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
@@ -91,7 +92,8 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := newStackResponse(handler.DataStore, stack)
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
resp, err := newStackResponse(handler.DataStore, userContext, stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to load git config for stack", err)
|
return httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
@@ -79,13 +81,17 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
|
|||||||
stacks = authorization.FilterAuthorizedStacks(stacks, user.ID, userTeamIDs)
|
stacks = authorization.FilterAuthorizedStacks(stacks, user.ID, userTeamIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
for i := range stacks {
|
for i := range stacks {
|
||||||
if err := fillStackGitConfig(handler.DataStore, &stacks[i]); err != nil {
|
if err := fillStackGitConfig(tx, userContext, &stacks[i]); err != nil {
|
||||||
return httperror.InternalServerError("Unable to load git config for stack", err)
|
return httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
return response.JSON(w, stacks)
|
return response.TxResponse(w, stacks, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterStacks refines a collection of Stack instances using specified criteria.
|
// filterStacks refines a collection of Stack instances using specified criteria.
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
@@ -172,11 +174,17 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fillStackGitConfig(handler.DataStore, stack); err != nil {
|
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
|
||||||
|
if err := fillStackGitConfig(tx, userContext, stack); err != nil {
|
||||||
return httperror.InternalServerError("Unable to load git config for stack", err)
|
return httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, stack)
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.TxResponse(w, stack, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) migrateStack(r *http.Request, stack *portainer.Stack, next *portainer.Endpoint) *httperror.HandlerError {
|
func (handler *Handler) migrateStack(r *http.Request, stack *portainer.Stack, next *portainer.Endpoint) *httperror.HandlerError {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
@@ -136,7 +137,7 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
|
|||||||
stack.AutoUpdate.JobID = jobID
|
stack.AutoUpdate.JobID = jobID
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := handler.startStack(context.TODO(), stack, endpoint, securityContext); err != nil {
|
if err := handler.startStack(context.TODO(), securityContext.UserID, stack, endpoint, securityContext); err != nil {
|
||||||
stack.Status = portainer.StackStatusError
|
stack.Status = portainer.StackStatusError
|
||||||
stack.DeploymentStatus = append(stack.DeploymentStatus, portainer.StackDeploymentStatus{
|
stack.DeploymentStatus = append(stack.DeploymentStatus, portainer.StackDeploymentStatus{
|
||||||
Status: portainer.StackStatusError,
|
Status: portainer.StackStatusError,
|
||||||
@@ -156,21 +157,25 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
|
|||||||
stack.DeploymentStatus = []portainer.StackDeploymentStatus{
|
stack.DeploymentStatus = []portainer.StackDeploymentStatus{
|
||||||
{Status: portainer.StackStatusActive, Time: time.Now().Unix()},
|
{Status: portainer.StackStatusActive, Time: time.Now().Unix()},
|
||||||
}
|
}
|
||||||
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
return tx.Stack().Update(stack.ID, stack)
|
if err := tx.Stack().Update(stack.ID, stack); err != nil {
|
||||||
}); err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to update stack status", err)
|
return httperror.InternalServerError("Unable to update stack status", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fillStackGitConfig(handler.DataStore, stack); err != nil {
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
|
||||||
|
if err := fillStackGitConfig(tx, userContext, stack); err != nil {
|
||||||
return httperror.InternalServerError("Unable to load git config for stack", err)
|
return httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
return response.JSON(w, stack)
|
return response.TxResponse(w, stack, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) startStack(
|
func (handler *Handler) startStack(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
userID portainer.UserID,
|
||||||
stack *portainer.Stack,
|
stack *portainer.Stack,
|
||||||
endpoint *portainer.Endpoint,
|
endpoint *portainer.Endpoint,
|
||||||
securityContext *security.RestrictedRequestContext,
|
securityContext *security.RestrictedRequestContext,
|
||||||
@@ -192,7 +197,7 @@ func (handler *Handler) startStack(
|
|||||||
stack.Name = handler.ComposeStackManager.NormalizeStackName(stack.Name)
|
stack.Name = handler.ComposeStackManager.NormalizeStackName(stack.Name)
|
||||||
|
|
||||||
if stackutils.IsRelativePathStack(stack) {
|
if stackutils.IsRelativePathStack(stack) {
|
||||||
return handler.StackDeployer.StartRemoteComposeStack(ctx, stack, endpoint, filteredRegistries)
|
return handler.StackDeployer.StartRemoteComposeStack(ctx, userID, stack, endpoint, filteredRegistries)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.StackDeployer.DeployComposeStack(ctx, stack, endpoint, filteredRegistries, false, false, false)
|
return handler.StackDeployer.DeployComposeStack(ctx, stack, endpoint, filteredRegistries, false, false, false)
|
||||||
@@ -200,7 +205,7 @@ func (handler *Handler) startStack(
|
|||||||
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)
|
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)
|
||||||
|
|
||||||
if stackutils.IsRelativePathStack(stack) {
|
if stackutils.IsRelativePathStack(stack) {
|
||||||
return handler.StackDeployer.StartRemoteSwarmStack(ctx, stack, endpoint, filteredRegistries)
|
return handler.StackDeployer.StartRemoteSwarmStack(ctx, userID, stack, endpoint, filteredRegistries)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.StackDeployer.DeploySwarmStack(ctx, stack, endpoint, filteredRegistries, true, true)
|
return handler.StackDeployer.DeploySwarmStack(ctx, stack, endpoint, filteredRegistries, true, true)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
@@ -108,7 +109,7 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
|
|||||||
stack.AutoUpdate.JobID = ""
|
stack.AutoUpdate.JobID = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
stopErr := handler.stopStack(r.Context(), stack, endpoint)
|
stopErr := handler.stopStack(r.Context(), securityContext.UserID, stack, endpoint)
|
||||||
if stopErr != nil {
|
if stopErr != nil {
|
||||||
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
stackutils.UpdateStackStatusFromUndeploymentResult(stack, stopErr)
|
stackutils.UpdateStackStatusFromUndeploymentResult(stack, stopErr)
|
||||||
@@ -120,27 +121,29 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
|
|||||||
return httperror.InternalServerError("Unable to stop stack", stopErr)
|
return httperror.InternalServerError("Unable to stop stack", stopErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
stackutils.UpdateStackStatusFromUndeploymentResult(stack, nil)
|
stackutils.UpdateStackStatusFromUndeploymentResult(stack, nil)
|
||||||
return tx.Stack().Update(stack.ID, stack)
|
if err := tx.Stack().Update(stack.ID, stack); err != nil {
|
||||||
}); err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to update stack status", err)
|
return httperror.InternalServerError("Unable to update stack status", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fillStackGitConfig(handler.DataStore, stack); err != nil {
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
if err := fillStackGitConfig(tx, userContext, stack); err != nil {
|
||||||
return httperror.InternalServerError("Unable to load git config for stack", err)
|
return httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
return response.JSON(w, stack)
|
return response.TxResponse(w, stack, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) stopStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (handler *Handler) stopStack(ctx context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
switch stack.Type {
|
switch stack.Type {
|
||||||
case portainer.DockerComposeStack:
|
case portainer.DockerComposeStack:
|
||||||
stack.Name = handler.ComposeStackManager.NormalizeStackName(stack.Name)
|
stack.Name = handler.ComposeStackManager.NormalizeStackName(stack.Name)
|
||||||
|
|
||||||
if stackutils.IsRelativePathStack(stack) {
|
if stackutils.IsRelativePathStack(stack) {
|
||||||
return handler.StackDeployer.StopRemoteComposeStack(ctx, stack, endpoint)
|
return handler.StackDeployer.StopRemoteComposeStack(ctx, userId, stack, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.StackDeployer.UndeployComposeStack(ctx, stack, endpoint)
|
return handler.StackDeployer.UndeployComposeStack(ctx, stack, endpoint)
|
||||||
@@ -148,7 +151,7 @@ func (handler *Handler) stopStack(ctx context.Context, stack *portainer.Stack, e
|
|||||||
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)
|
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)
|
||||||
|
|
||||||
if stackutils.IsRelativePathStack(stack) {
|
if stackutils.IsRelativePathStack(stack) {
|
||||||
return handler.StackDeployer.StopRemoteSwarmStack(ctx, stack, endpoint)
|
return handler.StackDeployer.StopRemoteSwarmStack(ctx, userId, stack, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.SwarmStackManager.Remove(ctx, stack, endpoint)
|
return handler.SwarmStackManager.Remove(ctx, stack, endpoint)
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func mockCreateStackRequestWithSecurityContext(method, target string, body io.Re
|
|||||||
ctx := security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
ctx := security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{
|
||||||
IsAdmin: true,
|
IsAdmin: true,
|
||||||
UserID: portainer.UserID(1),
|
UserID: portainer.UserID(1),
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
})
|
})
|
||||||
|
|
||||||
return req.WithContext(ctx)
|
return req.WithContext(ctx)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
@@ -188,7 +189,8 @@ func (handler *Handler) updateStackInTx(tx dataservices.DataStoreTx, r *http.Req
|
|||||||
|
|
||||||
deployGate.startDeploy()
|
deployGate.startDeploy()
|
||||||
|
|
||||||
if err := fillStackGitConfig(tx, stack); err != nil {
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
if err := fillStackGitConfig(tx, userContext, stack); err != nil {
|
||||||
return nil, httperror.InternalServerError("Unable to load git config for stack", err)
|
return nil, httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/git/update"
|
"github.com/portainer/portainer/api/git/update"
|
||||||
"github.com/portainer/portainer/api/gitops/sources"
|
"github.com/portainer/portainer/api/gitops/sources"
|
||||||
@@ -87,15 +88,30 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
|||||||
return httperror.InternalServerError(msg, errors.New(msg))
|
return httperror.InternalServerError(msg, errors.New(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
gitConfig, sourceID, err := loadGitConfigForStack(handler.DataStore, stack.WorkflowID, stack.ID)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var gitConfig *gittypes.RepoConfig
|
||||||
|
var sourceID portainer.SourceID
|
||||||
|
if err := handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
gitConfig, sourceID, err = loadGitConfigForStack(tx, userContext, stack.WorkflowID, stack.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to load git config for stack", err)
|
return httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if gitConfig == nil {
|
if gitConfig == nil {
|
||||||
msg := "No Git config in the found stack source"
|
msg := "No Git config in the found stack source"
|
||||||
return httperror.InternalServerError(msg, errors.New(msg))
|
return httperror.InternalServerError(msg, errors.New(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return response.TxErrorResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
if payload.AutoUpdate != nil && payload.AutoUpdate.Webhook != "" &&
|
if payload.AutoUpdate != nil && payload.AutoUpdate.Webhook != "" &&
|
||||||
(stack.AutoUpdate == nil ||
|
(stack.AutoUpdate == nil ||
|
||||||
(stack.AutoUpdate != nil && stack.AutoUpdate.Webhook != payload.AutoUpdate.Webhook)) {
|
(stack.AutoUpdate != nil && stack.AutoUpdate.Webhook != payload.AutoUpdate.Webhook)) {
|
||||||
@@ -126,11 +142,6 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
|||||||
return httperror.Forbidden("Permission denied to access environment", err)
|
return httperror.Forbidden("Permission denied to access environment", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := handler.DataStore.User().Read(securityContext.UserID)
|
user, err := handler.DataStore.User().Read(securityContext.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Cannot find context user", errors.Wrap(err, "failed to fetch the user"))
|
return httperror.BadRequest("Cannot find context user", errors.Wrap(err, "failed to fetch the user"))
|
||||||
@@ -193,8 +204,10 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
|||||||
stack.Option = &portainer.StackOption{Prune: payload.Prune}
|
stack.Option = &portainer.StackOption{Prune: payload.Prune}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
|
||||||
if payload.SourceID != 0 {
|
if payload.SourceID != 0 {
|
||||||
src, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, payload.SourceID)
|
src, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, userContext, payload.SourceID)
|
||||||
if httpErr != nil {
|
if httpErr != nil {
|
||||||
return httpErr
|
return httpErr
|
||||||
}
|
}
|
||||||
@@ -250,11 +263,12 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
|||||||
if err := tx.Stack().Update(stack.ID, stack); err != nil {
|
if err := tx.Stack().Update(stack.ID, stack); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := saveStackGitConfig(tx, stack.WorkflowID, stack.ID, sourceID, payload.SourceID, gitConfig); err != nil {
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
if err := saveStackGitConfig(tx, userContext, stack.WorkflowID, stack.ID, sourceID, payload.SourceID, gitConfig); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
resp, err = newStackResponse(tx, stack)
|
resp, err = newStackResponse(tx, userContext, stack)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return httperror.InternalServerError("Unable to persist the stack changes inside the database", err)
|
return httperror.InternalServerError("Unable to persist the stack changes inside the database", err)
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/git"
|
"github.com/portainer/portainer/api/git"
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/gitops/workflows"
|
"github.com/portainer/portainer/api/gitops/workflows"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
@@ -68,8 +70,17 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
|||||||
return httperror.BadRequest("Invalid stack identifier route variable", err)
|
return httperror.BadRequest("Invalid stack identifier route variable", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stack, err := handler.DataStore.Stack().Read(portainer.StackID(stackID))
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stack *portainer.Stack
|
||||||
|
var gitConfig *gittypes.RepoConfig
|
||||||
|
var sourceID portainer.SourceID
|
||||||
|
if err := handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
stack, err = tx.Stack().Read(portainer.StackID(stackID))
|
||||||
|
if tx.IsErrObjectNotFound(err) {
|
||||||
return httperror.NotFound("Unable to find a stack with the specified identifier inside the database", err)
|
return httperror.NotFound("Unable to find a stack with the specified identifier inside the database", err)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return httperror.InternalServerError("Unable to find a stack with the specified identifier inside the database", err)
|
return httperror.InternalServerError("Unable to find a stack with the specified identifier inside the database", err)
|
||||||
@@ -79,7 +90,8 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
|||||||
return httperror.BadRequest("Stack is not created from git", errors.New("stack has no git workflow"))
|
return httperror.BadRequest("Stack is not created from git", errors.New("stack has no git workflow"))
|
||||||
}
|
}
|
||||||
|
|
||||||
gitConfig, sourceID, err := loadGitConfigForStack(handler.DataStore, stack.WorkflowID, stack.ID)
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
gitConfig, sourceID, err = loadGitConfigForStack(tx, userContext, stack.WorkflowID, stack.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to load git config for stack", err)
|
return httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
}
|
}
|
||||||
@@ -90,6 +102,10 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
|||||||
if stack.Status == portainer.StackStatusDeploying {
|
if stack.Status == portainer.StackStatusDeploying {
|
||||||
return httperror.Conflict("Unable to update stack", errors.New("Stack deployment is already in progress"))
|
return httperror.Conflict("Unable to update stack", errors.New("Stack deployment is already in progress"))
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return response.TxErrorResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||||
// The EndpointID property is not available for these stacks, this API environment(endpoint)
|
// The EndpointID property is not available for these stacks, this API environment(endpoint)
|
||||||
@@ -113,11 +129,6 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
|||||||
return httperror.Forbidden("Permission denied to access environment", err)
|
return httperror.Forbidden("Permission denied to access environment", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only check resource control when it is a DockerSwarmStack or a DockerComposeStack
|
// Only check resource control when it is a DockerSwarmStack or a DockerComposeStack
|
||||||
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
|
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
|
||||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||||
@@ -254,11 +265,12 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
|||||||
if err := tx.Stack().Update(stack.ID, stack); err != nil {
|
if err := tx.Stack().Update(stack.ID, stack); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := saveStackGitConfig(tx, stack.WorkflowID, stack.ID, sourceID, 0, gitConfig); err != nil {
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
|
if err := saveStackGitConfig(tx, userContext, stack.WorkflowID, stack.ID, sourceID, 0, gitConfig); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return fillStackGitConfig(tx, stack)
|
return fillStackGitConfig(tx, userContext, stack)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
deployGate.abortDeploy()
|
deployGate.abortDeploy()
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
@@ -36,30 +37,23 @@ func TestStackUpdateGitWebhookUniqueness(t *testing.T) {
|
|||||||
const stack1ID = portainer.StackID(456)
|
const stack1ID = portainer.StackID(456)
|
||||||
const stack2ID = portainer.StackID(457)
|
const stack2ID = portainer.StackID(457)
|
||||||
|
|
||||||
src1 := &portainer.Source{
|
sharedSrc := &portainer.Source{
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/portainer/portainer.git"},
|
Git: &gittypes.RepoConfig{URL: "https://github.com/portainer/portainer.git"},
|
||||||
}
|
}
|
||||||
err = store.Source().Create(src1)
|
err = store.Source().Create(source.InsecureNewAdminContext(), sharedSrc)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
wf1 := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
wf1 := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
||||||
StackID: stack1ID,
|
StackID: stack1ID,
|
||||||
Files: []portainer.ArtifactFile{{SourceID: src1.ID}},
|
Files: []portainer.ArtifactFile{{SourceID: sharedSrc.ID}},
|
||||||
}}}
|
}}}
|
||||||
err = store.Workflow().Create(wf1)
|
err = store.Workflow().Create(wf1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
src2 := &portainer.Source{
|
|
||||||
Type: portainer.SourceTypeGit,
|
|
||||||
Git: &gittypes.RepoConfig{URL: "https://github.com/portainer/portainer.git"},
|
|
||||||
}
|
|
||||||
err = store.Source().Create(src2)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
wf2 := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
wf2 := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
||||||
StackID: stack2ID,
|
StackID: stack2ID,
|
||||||
Files: []portainer.ArtifactFile{{SourceID: src2.ID}},
|
Files: []portainer.ArtifactFile{{SourceID: sharedSrc.ID}},
|
||||||
}}}
|
}}}
|
||||||
err = store.Workflow().Create(wf2)
|
err = store.Workflow().Create(wf2)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -99,7 +93,11 @@ func TestStackUpdateGitWebhookUniqueness(t *testing.T) {
|
|||||||
url := "/stacks/" + strconv.Itoa(int(stack2.ID)) + "/git?endpointId=" + strconv.Itoa(int(endpoint.ID))
|
url := "/stacks/" + strconv.Itoa(int(stack2.ID)) + "/git?endpointId=" + strconv.Itoa(int(endpoint.ID))
|
||||||
req := httptest.NewRequest(http.MethodPost, url, bytes.NewReader(jsonPayload))
|
req := httptest.NewRequest(http.MethodPost, url, bytes.NewReader(jsonPayload))
|
||||||
|
|
||||||
rrc := &security.RestrictedRequestContext{}
|
rrc := &security.RestrictedRequestContext{
|
||||||
|
IsAdmin: true,
|
||||||
|
UserID: 1,
|
||||||
|
User: &portainer.User{ID: 1, Role: portainer.AdministratorRole},
|
||||||
|
}
|
||||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, rrc))
|
req = req.WithContext(security.StoreRestrictedRequestContext(req, rrc))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/git/update"
|
"github.com/portainer/portainer/api/git/update"
|
||||||
@@ -54,8 +55,15 @@ func (payload *kubernetesGitStackUpdatePayload) Validate(r *http.Request) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) updateKubernetesStack(tx dataservices.DataStoreTx, r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, gate *deployGate) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesStack(tx dataservices.DataStoreTx, r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, gate *deployGate) *httperror.HandlerError {
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships)
|
||||||
if stack.WorkflowID != 0 {
|
if stack.WorkflowID != 0 {
|
||||||
gitConfig, sourceID, err := loadGitConfigForStack(tx, stack.WorkflowID, stack.ID)
|
gitConfig, sourceID, err := loadGitConfigForStack(tx, userContext, stack.WorkflowID, stack.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to load git config for stack", err)
|
return httperror.InternalServerError("Unable to load git config for stack", err)
|
||||||
}
|
}
|
||||||
@@ -111,7 +119,7 @@ func (handler *Handler) updateKubernetesStack(tx dataservices.DataStoreTx, r *ht
|
|||||||
stack.AutoUpdate.JobID = jobID
|
stack.AutoUpdate.JobID = jobID
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := saveStackGitConfig(tx, stack.WorkflowID, stack.ID, sourceID, 0, gitConfig); err != nil {
|
if err := saveStackGitConfig(tx, userContext, stack.WorkflowID, stack.ID, sourceID, 0, gitConfig); err != nil {
|
||||||
return httperror.InternalServerError("Unable to update source git config", err)
|
return httperror.InternalServerError("Unable to update source git config", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ type (
|
|||||||
IsTeamLeader bool
|
IsTeamLeader bool
|
||||||
UserID portainer.UserID
|
UserID portainer.UserID
|
||||||
UserMemberships []portainer.TeamMembership
|
UserMemberships []portainer.TeamMembership
|
||||||
|
User *portainer.User
|
||||||
}
|
}
|
||||||
|
|
||||||
// tokenLookup looks up a token in the request
|
// tokenLookup looks up a token in the request
|
||||||
@@ -274,7 +275,7 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
requestContext, err := bouncer.newRestrictedContextRequest(tokenData.ID, tokenData.Role)
|
requestContext, err := newRestrictedContextRequest(bouncer.dataStore, tokenData.ID, tokenData.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteError(w, http.StatusInternalServerError, "Unable to create restricted request context ", err)
|
httperror.WriteError(w, http.StatusInternalServerError, "Unable to create restricted request context ", err)
|
||||||
return
|
return
|
||||||
@@ -535,15 +536,21 @@ func MWSecureHeaders(next http.Handler, hsts, csp bool) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.UserID, userRole portainer.UserRole) (*RestrictedRequestContext, error) {
|
func newRestrictedContextRequest(tx dataservices.DataStoreTx, userID portainer.UserID, userRole portainer.UserRole) (*RestrictedRequestContext, error) {
|
||||||
|
user, err := tx.User().Read(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if userRole == portainer.AdministratorRole {
|
if userRole == portainer.AdministratorRole {
|
||||||
return &RestrictedRequestContext{
|
return &RestrictedRequestContext{
|
||||||
IsAdmin: true,
|
IsAdmin: true,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
|
User: user,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(userID)
|
memberships, err := tx.TeamMembership().TeamMembershipsByUserID(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -557,6 +564,7 @@ func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.User
|
|||||||
UserID: userID,
|
UserID: userID,
|
||||||
IsTeamLeader: isTeamLeader,
|
IsTeamLeader: isTeamLeader,
|
||||||
UserMemberships: memberships,
|
UserMemberships: memberships,
|
||||||
|
User: user,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,34 +36,34 @@ func (d *TestStackDeployer) DeployKubernetesStack(_ context.Context, stack *port
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *TestStackDeployer) DeployRemoteComposeStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, forcePullImage, forceRecreate bool) error {
|
func (d *TestStackDeployer) DeployRemoteComposeStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, forcePullImage, forceRecreate bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *TestStackDeployer) UndeployRemoteComposeStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (d *TestStackDeployer) UndeployRemoteComposeStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *TestStackDeployer) StartRemoteComposeStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
|
func (d *TestStackDeployer) StartRemoteComposeStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *TestStackDeployer) StopRemoteComposeStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (d *TestStackDeployer) StopRemoteComposeStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *TestStackDeployer) DeployRemoteSwarmStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, pullImage bool) error {
|
func (d *TestStackDeployer) DeployRemoteSwarmStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, pullImage bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *TestStackDeployer) UndeployRemoteSwarmStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (d *TestStackDeployer) UndeployRemoteSwarmStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *TestStackDeployer) StartRemoteSwarmStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
|
func (d *TestStackDeployer) StartRemoteSwarmStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *TestStackDeployer) StopRemoteSwarmStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (d *TestStackDeployer) StopRemoteSwarmStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1328,6 +1328,12 @@ type (
|
|||||||
Git *gittypes.RepoConfig `json:"git,omitempty"`
|
Git *gittypes.RepoConfig `json:"git,omitempty"`
|
||||||
Registry *Registry `json:"registry,omitempty"`
|
Registry *Registry `json:"registry,omitempty"`
|
||||||
Helm *HelmConfig `json:"helm,omitempty"`
|
Helm *HelmConfig `json:"helm,omitempty"`
|
||||||
|
|
||||||
|
Public bool `json:"public"`
|
||||||
|
AdministratorsOnly bool `json:"administratorsOnly"`
|
||||||
|
UserAccesses []UserID `json:"userAccesses"`
|
||||||
|
TeamAccesses []TeamID `json:"teamAccesses"`
|
||||||
|
OwnerID UserID `json:"ownerID,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SourceID represents a source identifier
|
// SourceID represents a source identifier
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/portainer/portainer/api/agent"
|
"github.com/portainer/portainer/api/agent"
|
||||||
"github.com/portainer/portainer/api/crypto"
|
"github.com/portainer/portainer/api/crypto"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/git/update"
|
"github.com/portainer/portainer/api/git/update"
|
||||||
"github.com/portainer/portainer/api/gitops/workflows"
|
"github.com/portainer/portainer/api/gitops/workflows"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
@@ -121,7 +122,17 @@ func redeployWhenChangedSecondStage(
|
|||||||
user *portainer.User,
|
user *portainer.User,
|
||||||
endpoint *portainer.Endpoint,
|
endpoint *portainer.Endpoint,
|
||||||
) error {
|
) error {
|
||||||
gitSrc, file, err := workflows.GitSourceAndArtifactForStack(datastore, stack.WorkflowID, stack.ID)
|
var teamMemberships []portainer.TeamMembership
|
||||||
|
if err := datastore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
var err error
|
||||||
|
teamMemberships, err = tx.TeamMembership().TeamMembershipsByUserID(user.ID)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext := source.NewUserContext(user, teamMemberships)
|
||||||
|
gitSrc, file, err := workflows.GitSourceAndArtifactForStack(datastore, userContext, stack.WorkflowID, stack.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithMessagef(err, "failed to load git config for stack %v", stack.ID)
|
return errors.WithMessagef(err, "failed to load git config for stack %v", stack.ID)
|
||||||
}
|
}
|
||||||
@@ -184,7 +195,7 @@ func redeployWhenChangedSecondStage(
|
|||||||
switch stack.Type {
|
switch stack.Type {
|
||||||
case portainer.DockerComposeStack:
|
case portainer.DockerComposeStack:
|
||||||
if stackutils.IsRelativePathStack(stack) {
|
if stackutils.IsRelativePathStack(stack) {
|
||||||
err = deployer.DeployRemoteComposeStack(ctx, stack, endpoint, registries, true, true, false)
|
err = deployer.DeployRemoteComposeStack(ctx, user.ID, stack, endpoint, registries, true, true, false)
|
||||||
} else {
|
} else {
|
||||||
err = deployer.DeployComposeStack(ctx, stack, endpoint, registries, true, true, false)
|
err = deployer.DeployComposeStack(ctx, stack, endpoint, registries, true, true, false)
|
||||||
}
|
}
|
||||||
@@ -194,7 +205,7 @@ func redeployWhenChangedSecondStage(
|
|||||||
}
|
}
|
||||||
case portainer.DockerSwarmStack:
|
case portainer.DockerSwarmStack:
|
||||||
if stackutils.IsRelativePathStack(stack) {
|
if stackutils.IsRelativePathStack(stack) {
|
||||||
err = deployer.DeployRemoteSwarmStack(ctx, stack, endpoint, registries, true, true)
|
err = deployer.DeployRemoteSwarmStack(ctx, user.ID, stack, endpoint, registries, true, true)
|
||||||
} else {
|
} else {
|
||||||
err = deployer.DeploySwarmStack(ctx, stack, endpoint, registries, true, true)
|
err = deployer.DeploySwarmStack(ctx, stack, endpoint, registries, true, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/crypto"
|
"github.com/portainer/portainer/api/crypto"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
@@ -77,6 +78,8 @@ L9x22ol5c5rToZa1qKSnSdSDCud298MyRujMUy2UcUKHeNs3MK9AT41sDv266I7b
|
|||||||
vJUUCFYm8+9p6gTVOcoMit+eGSwa81PCPEs1TnU1PV/PaDFeUhn/mg==
|
vJUUCFYm8+9p6gTVOcoMit+eGSwa81PCPEs1TnU1PV/PaDFeUhn/mg==
|
||||||
-----END RSA PRIVATE KEY-----`
|
-----END RSA PRIVATE KEY-----`
|
||||||
|
|
||||||
|
var adminUserContext = source.InsecureNewAdminContext()
|
||||||
|
|
||||||
type noopDeployer struct{}
|
type noopDeployer struct{}
|
||||||
|
|
||||||
// without unpacker
|
// without unpacker
|
||||||
@@ -97,28 +100,28 @@ func (s noopDeployer) DeployKubernetesStack(_ context.Context, stack *portainer.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// with unpacker
|
// with unpacker
|
||||||
func (s noopDeployer) DeployRemoteComposeStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, forcePullImage, forceRecreate bool) error {
|
func (s noopDeployer) DeployRemoteComposeStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, forcePullImage, forceRecreate bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s noopDeployer) UndeployRemoteComposeStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (s noopDeployer) UndeployRemoteComposeStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s noopDeployer) StartRemoteComposeStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
|
func (s noopDeployer) StartRemoteComposeStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s noopDeployer) StopRemoteComposeStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (s noopDeployer) StopRemoteComposeStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s noopDeployer) DeployRemoteSwarmStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, pullImage bool) error {
|
func (s noopDeployer) DeployRemoteSwarmStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, pullImage bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s noopDeployer) UndeployRemoteSwarmStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (s noopDeployer) UndeployRemoteSwarmStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s noopDeployer) StartRemoteSwarmStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
|
func (s noopDeployer) StartRemoteSwarmStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s noopDeployer) StopRemoteSwarmStack(_ context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (s noopDeployer) StopRemoteSwarmStack(_ context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +194,7 @@ func Test_redeployWhenChanged_DoesNothingWhenNoGitChanges(t *testing.T) {
|
|||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
admin := &portainer.User{ID: 1, Username: "admin"}
|
admin := &portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}
|
||||||
err := store.User().Create(admin)
|
err := store.User().Create(admin)
|
||||||
require.NoError(t, err, "error creating an admin")
|
require.NoError(t, err, "error creating an admin")
|
||||||
|
|
||||||
@@ -206,7 +209,7 @@ func Test_redeployWhenChanged_DoesNothingWhenNoGitChanges(t *testing.T) {
|
|||||||
ConfigHash: "oldHash",
|
ConfigHash: "oldHash",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = store.Source().Create(src)
|
err = store.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err, "failed to create source")
|
require.NoError(t, err, "failed to create source")
|
||||||
|
|
||||||
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}}
|
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}}
|
||||||
@@ -231,7 +234,7 @@ func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) {
|
|||||||
cloneErr := errors.New("failed to clone")
|
cloneErr := errors.New("failed to clone")
|
||||||
_, store := datastore.MustNewTestStore(t, false, true)
|
_, store := datastore.MustNewTestStore(t, false, true)
|
||||||
|
|
||||||
admin := &portainer.User{ID: 1, Username: "admin"}
|
admin := &portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}
|
||||||
err := store.User().Create(admin)
|
err := store.User().Create(admin)
|
||||||
require.NoError(t, err, "error creating an admin")
|
require.NoError(t, err, "error creating an admin")
|
||||||
|
|
||||||
@@ -253,7 +256,7 @@ func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) {
|
|||||||
ConfigHash: "oldHash",
|
ConfigHash: "oldHash",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = store.Source().Create(src)
|
err = store.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err, "failed to create source")
|
require.NoError(t, err, "failed to create source")
|
||||||
|
|
||||||
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
||||||
@@ -296,7 +299,7 @@ func setupRedeployStore(t *testing.T, stackType portainer.StackType) (dataservic
|
|||||||
ConfigHash: "oldHash",
|
ConfigHash: "oldHash",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = store.Source().Create(src)
|
err = store.Source().Create(adminUserContext, src)
|
||||||
require.NoError(t, err, "failed to create source")
|
require.NoError(t, err, "failed to create source")
|
||||||
|
|
||||||
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}}
|
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}}
|
||||||
@@ -359,7 +362,7 @@ func Test_getUserRegistries(t *testing.T) {
|
|||||||
err = store.User().Create(user)
|
err = store.User().Create(user)
|
||||||
require.NoError(t, err, "error creating a user")
|
require.NoError(t, err, "error creating a user")
|
||||||
|
|
||||||
team := portainer.Team{ID: 1, Name: "team"}
|
team := portainer.Team{ID: 1}
|
||||||
|
|
||||||
err = store.TeamMembership().Create(&portainer.TeamMembership{
|
err = store.TeamMembership().Create(&portainer.TeamMembership{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/gitops/workflows"
|
"github.com/portainer/portainer/api/gitops/workflows"
|
||||||
"github.com/portainer/portainer/api/logs"
|
"github.com/portainer/portainer/api/logs"
|
||||||
@@ -35,20 +37,21 @@ const (
|
|||||||
|
|
||||||
type RemoteStackDeployer interface {
|
type RemoteStackDeployer interface {
|
||||||
// compose
|
// compose
|
||||||
DeployRemoteComposeStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, forcePullImage bool, forceRecreate bool) error
|
DeployRemoteComposeStack(ctx context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, forcePullImage bool, forceRecreate bool) error
|
||||||
UndeployRemoteComposeStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
UndeployRemoteComposeStack(ctx context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
||||||
StartRemoteComposeStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
|
StartRemoteComposeStack(ctx context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
|
||||||
StopRemoteComposeStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
StopRemoteComposeStack(ctx context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
||||||
// swarm
|
// swarm
|
||||||
DeployRemoteSwarmStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error
|
DeployRemoteSwarmStack(ctx context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error
|
||||||
UndeployRemoteSwarmStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
UndeployRemoteSwarmStack(ctx context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
||||||
StartRemoteSwarmStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
|
StartRemoteSwarmStack(ctx context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
|
||||||
StopRemoteSwarmStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
StopRemoteSwarmStack(ctx context.Context, userId portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deploy a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
// Deploy a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||||
func (d *stackDeployer) DeployRemoteComposeStack(
|
func (d *stackDeployer) DeployRemoteComposeStack(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
userId portainer.UserID,
|
||||||
stack *portainer.Stack,
|
stack *portainer.Stack,
|
||||||
endpoint *portainer.Endpoint,
|
endpoint *portainer.Endpoint,
|
||||||
registries []portainer.Registry,
|
registries []portainer.Registry,
|
||||||
@@ -70,6 +73,7 @@ func (d *stackDeployer) DeployRemoteComposeStack(
|
|||||||
|
|
||||||
return d.remoteStack(
|
return d.remoteStack(
|
||||||
ctx,
|
ctx,
|
||||||
|
userId,
|
||||||
stack,
|
stack,
|
||||||
endpoint,
|
endpoint,
|
||||||
OperationDeploy,
|
OperationDeploy,
|
||||||
@@ -82,22 +86,29 @@ func (d *stackDeployer) DeployRemoteComposeStack(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Undeploy a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
// Undeploy a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||||
func (d *stackDeployer) UndeployRemoteComposeStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (d *stackDeployer) UndeployRemoteComposeStack(
|
||||||
|
ctx context.Context,
|
||||||
|
userId portainer.UserID,
|
||||||
|
stack *portainer.Stack,
|
||||||
|
endpoint *portainer.Endpoint,
|
||||||
|
) error {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
defer d.lock.Unlock()
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
return d.remoteStack(ctx, stack, endpoint, OperationUndeploy, unpackerCmdBuilderOptions{})
|
return d.remoteStack(ctx, userId, stack, endpoint, OperationUndeploy, unpackerCmdBuilderOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
// Start a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||||
func (d *stackDeployer) StartRemoteComposeStack(
|
func (d *stackDeployer) StartRemoteComposeStack(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
userId portainer.UserID,
|
||||||
stack *portainer.Stack,
|
stack *portainer.Stack,
|
||||||
endpoint *portainer.Endpoint,
|
endpoint *portainer.Endpoint,
|
||||||
registries []portainer.Registry,
|
registries []portainer.Registry,
|
||||||
) error {
|
) error {
|
||||||
return d.remoteStack(
|
return d.remoteStack(
|
||||||
ctx,
|
ctx,
|
||||||
|
userId,
|
||||||
stack,
|
stack,
|
||||||
endpoint,
|
endpoint,
|
||||||
OperationComposeStart,
|
OperationComposeStart,
|
||||||
@@ -108,13 +119,19 @@ func (d *stackDeployer) StartRemoteComposeStack(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
// Stop a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||||
func (d *stackDeployer) StopRemoteComposeStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (d *stackDeployer) StopRemoteComposeStack(
|
||||||
return d.remoteStack(ctx, stack, endpoint, OperationComposeStop, unpackerCmdBuilderOptions{})
|
ctx context.Context,
|
||||||
|
userId portainer.UserID,
|
||||||
|
stack *portainer.Stack,
|
||||||
|
endpoint *portainer.Endpoint,
|
||||||
|
) error {
|
||||||
|
return d.remoteStack(ctx, userId, stack, endpoint, OperationComposeStop, unpackerCmdBuilderOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deploy a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
// Deploy a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||||
func (d *stackDeployer) DeployRemoteSwarmStack(
|
func (d *stackDeployer) DeployRemoteSwarmStack(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
userId portainer.UserID,
|
||||||
stack *portainer.Stack,
|
stack *portainer.Stack,
|
||||||
endpoint *portainer.Endpoint,
|
endpoint *portainer.Endpoint,
|
||||||
registries []portainer.Registry,
|
registries []portainer.Registry,
|
||||||
@@ -124,7 +141,7 @@ func (d *stackDeployer) DeployRemoteSwarmStack(
|
|||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
defer d.lock.Unlock()
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
return d.remoteStack(ctx, stack, endpoint, OperationSwarmDeploy, unpackerCmdBuilderOptions{
|
return d.remoteStack(ctx, userId, stack, endpoint, OperationSwarmDeploy, unpackerCmdBuilderOptions{
|
||||||
pullImage: pullImage,
|
pullImage: pullImage,
|
||||||
prune: prune,
|
prune: prune,
|
||||||
forceRecreate: stack.AutoUpdate != nil && stack.AutoUpdate.ForceUpdate,
|
forceRecreate: stack.AutoUpdate != nil && stack.AutoUpdate.ForceUpdate,
|
||||||
@@ -133,22 +150,29 @@ func (d *stackDeployer) DeployRemoteSwarmStack(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Undeploy a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
// Undeploy a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||||
func (d *stackDeployer) UndeployRemoteSwarmStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (d *stackDeployer) UndeployRemoteSwarmStack(
|
||||||
|
ctx context.Context,
|
||||||
|
userId portainer.UserID,
|
||||||
|
stack *portainer.Stack,
|
||||||
|
endpoint *portainer.Endpoint,
|
||||||
|
) error {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
defer d.lock.Unlock()
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
return d.remoteStack(ctx, stack, endpoint, OperationSwarmUndeploy, unpackerCmdBuilderOptions{})
|
return d.remoteStack(ctx, userId, stack, endpoint, OperationSwarmUndeploy, unpackerCmdBuilderOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
// Start a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||||
func (d *stackDeployer) StartRemoteSwarmStack(
|
func (d *stackDeployer) StartRemoteSwarmStack(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
userId portainer.UserID,
|
||||||
stack *portainer.Stack,
|
stack *portainer.Stack,
|
||||||
endpoint *portainer.Endpoint,
|
endpoint *portainer.Endpoint,
|
||||||
registries []portainer.Registry,
|
registries []portainer.Registry,
|
||||||
) error {
|
) error {
|
||||||
return d.remoteStack(
|
return d.remoteStack(
|
||||||
ctx,
|
ctx,
|
||||||
|
userId,
|
||||||
stack,
|
stack,
|
||||||
endpoint,
|
endpoint,
|
||||||
OperationSwarmStart,
|
OperationSwarmStart,
|
||||||
@@ -157,8 +181,20 @@ func (d *stackDeployer) StartRemoteSwarmStack(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
// Stop a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||||
func (d *stackDeployer) StopRemoteSwarmStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (d *stackDeployer) StopRemoteSwarmStack(
|
||||||
return d.remoteStack(ctx, stack, endpoint, OperationSwarmStop, unpackerCmdBuilderOptions{})
|
ctx context.Context,
|
||||||
|
userId portainer.UserID,
|
||||||
|
stack *portainer.Stack,
|
||||||
|
endpoint *portainer.Endpoint,
|
||||||
|
) error {
|
||||||
|
return d.remoteStack(
|
||||||
|
ctx,
|
||||||
|
userId,
|
||||||
|
stack,
|
||||||
|
endpoint,
|
||||||
|
OperationSwarmStop,
|
||||||
|
unpackerCmdBuilderOptions{},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does all the heavy lifting:
|
// Does all the heavy lifting:
|
||||||
@@ -167,9 +203,21 @@ func (d *stackDeployer) StopRemoteSwarmStack(ctx context.Context, stack *portain
|
|||||||
// * deploy compose-unpacker container
|
// * deploy compose-unpacker container
|
||||||
// * wait for deployment to end
|
// * wait for deployment to end
|
||||||
// * gather deployment logs and bubble them up
|
// * gather deployment logs and bubble them up
|
||||||
func (d *stackDeployer) remoteStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, operation StackRemoteOperation, opts unpackerCmdBuilderOptions) error {
|
func (d *stackDeployer) remoteStack(ctx context.Context, userID portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint, operation StackRemoteOperation, opts unpackerCmdBuilderOptions) error {
|
||||||
if stack.WorkflowID != 0 && opts.gitConfig == nil {
|
if stack.WorkflowID != 0 && opts.gitConfig == nil {
|
||||||
src, file, err := workflows.GitSourceAndArtifactForStack(d.dataStore, stack.WorkflowID, stack.ID)
|
if err := d.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
user, err := tx.User().Read(userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
memberships, err := tx.TeamMembership().TeamMembershipsByUserID(userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext := source.NewUserContext(user, memberships)
|
||||||
|
src, file, err := workflows.GitSourceAndArtifactForStack(tx, userContext, stack.WorkflowID, stack.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to load git config for remote stack")
|
return errors.Wrap(err, "failed to load git config for remote stack")
|
||||||
}
|
}
|
||||||
@@ -177,6 +225,10 @@ func (d *stackDeployer) remoteStack(ctx context.Context, stack *portainer.Stack,
|
|||||||
if src != nil {
|
if src != nil {
|
||||||
opts.gitConfig = workflows.MergeSourceAndFile(src, file)
|
opts.gitConfig = workflows.MergeSourceAndFile(src, file)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := d.createDockerClient(ctx, endpoint)
|
cli, err := d.createDockerClient(ctx, endpoint)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func (config *ComposeStackDeploymentConfig) Deploy(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if stackutils.IsRelativePathStack(config.stack) {
|
if stackutils.IsRelativePathStack(config.stack) {
|
||||||
return config.StackDeployer.DeployRemoteComposeStack(ctx, config.stack, config.endpoint, config.registries, config.prune, config.forcePullImage, config.ForceCreate)
|
return config.StackDeployer.DeployRemoteComposeStack(ctx, config.user.ID, config.stack, config.endpoint, config.registries, config.prune, config.forcePullImage, config.ForceCreate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.StackDeployer.DeployComposeStack(ctx, config.stack, config.endpoint, config.registries, config.prune, config.forcePullImage, config.ForceCreate)
|
return config.StackDeployer.DeployComposeStack(ctx, config.stack, config.endpoint, config.registries, config.prune, config.forcePullImage, config.ForceCreate)
|
||||||
@@ -89,7 +89,7 @@ func (config *ComposeStackDeploymentConfig) Undeploy(ctx context.Context) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
if stackutils.IsRelativePathStack(config.stack) {
|
if stackutils.IsRelativePathStack(config.stack) {
|
||||||
return config.StackDeployer.UndeployRemoteComposeStack(ctx, config.stack, config.endpoint)
|
return config.StackDeployer.UndeployRemoteComposeStack(ctx, config.user.ID, config.stack, config.endpoint)
|
||||||
}
|
}
|
||||||
return config.StackDeployer.UndeployComposeStack(ctx, config.stack, config.endpoint)
|
return config.StackDeployer.UndeployComposeStack(ctx, config.stack, config.endpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func (config *SwarmStackDeploymentConfig) Deploy(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if stackutils.IsRelativePathStack(config.stack) {
|
if stackutils.IsRelativePathStack(config.stack) {
|
||||||
return config.StackDeployer.DeployRemoteSwarmStack(ctx, config.stack, config.endpoint, config.registries, config.prune, config.pullImage)
|
return config.StackDeployer.DeployRemoteSwarmStack(ctx, config.user.ID, config.stack, config.endpoint, config.registries, config.prune, config.pullImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.StackDeployer.DeploySwarmStack(ctx, config.stack, config.endpoint, config.registries, config.prune, config.pullImage)
|
return config.StackDeployer.DeploySwarmStack(ctx, config.stack, config.endpoint, config.registries, config.prune, config.pullImage)
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/gitops/workflows"
|
"github.com/portainer/portainer/api/gitops/workflows"
|
||||||
"github.com/portainer/portainer/api/scheduler"
|
"github.com/portainer/portainer/api/scheduler"
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/ssrf"
|
"github.com/portainer/portainer/pkg/libhttp/ssrf"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,11 +32,28 @@ func (b *GitMethodStackBuilder) prepare(ctx context.Context, payload *StackPaylo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userContext *dataservices.SourceServiceUserContext
|
||||||
|
if err := b.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
user, err := tx.User().Read(userID)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to read user", err)
|
||||||
|
}
|
||||||
|
memberships, err := tx.TeamMembership().TeamMembershipsByUserID(userID)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to read user team memberships", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userContext = source.NewUserContext(user, memberships)
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var repoConfig gittypes.RepoConfig
|
var repoConfig gittypes.RepoConfig
|
||||||
var sourceID portainer.SourceID
|
var sourceID portainer.SourceID
|
||||||
|
|
||||||
if payload.SourceID != 0 {
|
if payload.SourceID != 0 {
|
||||||
src, err := b.dataStore.Source().Read(payload.SourceID)
|
src, err := b.dataStore.Source().Read(userContext, payload.SourceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read source: %w", err)
|
return fmt.Errorf("failed to read source: %w", err)
|
||||||
}
|
}
|
||||||
@@ -105,7 +124,7 @@ func (b *GitMethodStackBuilder) prepare(ctx context.Context, payload *StackPaylo
|
|||||||
} else {
|
} else {
|
||||||
repoConfig.URL = gittypes.SanitizeURL(repoConfig.URL)
|
repoConfig.URL = gittypes.SanitizeURL(repoConfig.URL)
|
||||||
|
|
||||||
src, err := workflows.FindOrCreateGitSource(tx, &portainer.Source{
|
src, err := workflows.FindOrCreateGitSource(tx, userContext, &portainer.Source{
|
||||||
Name: gittypes.RepoName(repoConfig.URL),
|
Name: gittypes.RepoName(repoConfig.URL),
|
||||||
Type: portainer.SourceTypeGit,
|
Type: portainer.SourceTypeGit,
|
||||||
Git: &repoConfig,
|
Git: &repoConfig,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/source"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
"github.com/portainer/portainer/api/gitops/workflows"
|
"github.com/portainer/portainer/api/gitops/workflows"
|
||||||
@@ -13,6 +14,8 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var adminUserContext = source.InsecureNewAdminContext()
|
||||||
|
|
||||||
// stubFileService satisfies portainer.FileService for git builder tests.
|
// stubFileService satisfies portainer.FileService for git builder tests.
|
||||||
type stubFileService struct {
|
type stubFileService struct {
|
||||||
portainer.FileService
|
portainer.FileService
|
||||||
@@ -25,7 +28,7 @@ func (s *stubFileService) GetStackProjectPath(stackIdentifier string) string {
|
|||||||
func newGitMethodBuilder(t *testing.T, commitHash string) *GitMethodStackBuilder {
|
func newGitMethodBuilder(t *testing.T, commitHash string) *GitMethodStackBuilder {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
_, store := datastore.MustNewTestStore(t, false, false)
|
_, store := datastore.MustNewTestStore(t, false, false)
|
||||||
require.NoError(t, store.User().Create(&portainer.User{ID: 1, Username: "testuser"}))
|
require.NoError(t, store.User().Create(&portainer.User{ID: 1, Username: "testuser", Role: portainer.AdministratorRole}))
|
||||||
return &GitMethodStackBuilder{
|
return &GitMethodStackBuilder{
|
||||||
StackBuilder: StackBuilder{
|
StackBuilder: StackBuilder{
|
||||||
stack: &portainer.Stack{},
|
stack: &portainer.Stack{},
|
||||||
@@ -52,7 +55,7 @@ func TestGitMethodStackBuilder_WithSourceID_ReferencesExistingSource(t *testing.
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
require.NoError(t, builder.dataStore.Source().Create(src))
|
require.NoError(t, builder.dataStore.Source().Create(adminUserContext, src))
|
||||||
|
|
||||||
payload := &StackPayload{
|
payload := &StackPayload{
|
||||||
RepositoryConfigPayload: RepositoryConfigPayload{
|
RepositoryConfigPayload: RepositoryConfigPayload{
|
||||||
@@ -69,12 +72,12 @@ func TestGitMethodStackBuilder_WithSourceID_ReferencesExistingSource(t *testing.
|
|||||||
assert.Equal(t, src.ID, referencedSourceID)
|
assert.Equal(t, src.ID, referencedSourceID)
|
||||||
|
|
||||||
// Only one Source exists — no duplicate was created.
|
// Only one Source exists — no duplicate was created.
|
||||||
allSources, err := builder.dataStore.Source().ReadAll()
|
allSources, err := builder.dataStore.Source().ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, allSources, 1)
|
assert.Len(t, allSources, 1)
|
||||||
|
|
||||||
// The merged git config picks up the Source URL/auth.
|
// The merged git config picks up the Source URL/auth.
|
||||||
readSrc, artifact, err := workflows.GitSourceAndArtifactForStack(builder.dataStore, builder.stack.WorkflowID, builder.stack.ID)
|
readSrc, artifact, err := workflows.GitSourceAndArtifactForStack(builder.dataStore, adminUserContext, builder.stack.WorkflowID, builder.stack.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
merged := workflows.MergeSourceAndFile(readSrc, artifact)
|
merged := workflows.MergeSourceAndFile(readSrc, artifact)
|
||||||
assert.Equal(t, "https://github.com/org/private-repo", merged.URL)
|
assert.Equal(t, "https://github.com/org/private-repo", merged.URL)
|
||||||
@@ -114,7 +117,7 @@ func TestGitMethodStackBuilder_WithoutSourceID_InlinePathStillWorks(t *testing.T
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// A Source was created via the inline path.
|
// A Source was created via the inline path.
|
||||||
allSources, err := builder.dataStore.Source().ReadAll()
|
allSources, err := builder.dataStore.Source().ReadAll(adminUserContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, allSources, 1)
|
assert.Len(t, allSources, 1)
|
||||||
assert.Equal(t, "https://github.com/org/public-repo", allSources[0].Git.URL)
|
assert.Equal(t, "https://github.com/org/public-repo", allSources[0].Git.URL)
|
||||||
|
|||||||
@@ -329,9 +329,6 @@ angular
|
|||||||
component: 'sourcesListView',
|
component: 'sourcesListView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
|
||||||
access: AccessHeaders.Admin,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var gitopsSourceDetail = {
|
var gitopsSourceDetail = {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const accessControlModule = angular
|
|||||||
'resourceId',
|
'resourceId',
|
||||||
'resourceType',
|
'resourceType',
|
||||||
'environmentId',
|
'environmentId',
|
||||||
|
'resourceName',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
@@ -32,6 +33,7 @@ export const accessControlModule = angular
|
|||||||
'onChange',
|
'onChange',
|
||||||
'value',
|
'value',
|
||||||
'teams',
|
'teams',
|
||||||
|
'resourceName',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface Props {
|
|||||||
environmentId: EnvironmentId;
|
environmentId: EnvironmentId;
|
||||||
disableOwnershipChange?: boolean;
|
disableOwnershipChange?: boolean;
|
||||||
onUpdateSuccess(): Promise<void>;
|
onUpdateSuccess(): Promise<void>;
|
||||||
|
resourceName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccessControlPanel({
|
export function AccessControlPanel({
|
||||||
@@ -35,6 +36,7 @@ export function AccessControlPanel({
|
|||||||
resourceId,
|
resourceId,
|
||||||
environmentId,
|
environmentId,
|
||||||
onUpdateSuccess,
|
onUpdateSuccess,
|
||||||
|
resourceName = 'resource',
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [isEditMode, toggleEditMode] = useReducer((state) => !state, false);
|
const [isEditMode, toggleEditMode] = useReducer((state) => !state, false);
|
||||||
const isAdminQuery = useIsEdgeAdmin();
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
@@ -62,6 +64,7 @@ export function AccessControlPanel({
|
|||||||
<TableContainer>
|
<TableContainer>
|
||||||
<TableTitle label="Access control" icon={Eye} />
|
<TableTitle label="Access control" icon={Eye} />
|
||||||
<AccessControlPanelDetails
|
<AccessControlPanelDetails
|
||||||
|
resourceName={resourceName}
|
||||||
resourceType={resourceType}
|
resourceType={resourceType}
|
||||||
resourceControl={resourceControl}
|
resourceControl={resourceControl}
|
||||||
isAuthorisedToFetchUsers={isAdmin || isTeamLeader}
|
isAuthorisedToFetchUsers={isAdmin || isTeamLeader}
|
||||||
|
|||||||
+13
-8
@@ -24,14 +24,16 @@ import { ResourceControlViewModel } from '../models/ResourceControlViewModel';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
resourceControl?: ResourceControlViewModel;
|
resourceControl?: ResourceControlViewModel;
|
||||||
resourceType: ResourceControlType;
|
resourceType?: ResourceControlType;
|
||||||
isAuthorisedToFetchUsers?: boolean;
|
isAuthorisedToFetchUsers?: boolean;
|
||||||
|
resourceName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccessControlPanelDetails({
|
export function AccessControlPanelDetails({
|
||||||
resourceControl,
|
resourceControl,
|
||||||
resourceType,
|
resourceType,
|
||||||
isAuthorisedToFetchUsers = false,
|
isAuthorisedToFetchUsers = false,
|
||||||
|
resourceName = 'resource',
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const inheritanceMessage = getInheritanceMessage(
|
const inheritanceMessage = getInheritanceMessage(
|
||||||
resourceType,
|
resourceType,
|
||||||
@@ -81,7 +83,7 @@ export function AccessControlPanelDetails({
|
|||||||
aria-label="ownership-icon"
|
aria-label="ownership-icon"
|
||||||
/>
|
/>
|
||||||
<span aria-label="ownership">{ownership}</span>
|
<span aria-label="ownership">{ownership}</span>
|
||||||
<Tooltip message={getOwnershipTooltip(ownership)} />
|
<Tooltip message={getOwnershipTooltip(ownership, resourceName)} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{inheritanceMessage}
|
{inheritanceMessage}
|
||||||
@@ -102,22 +104,25 @@ export function AccessControlPanelDetails({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOwnershipTooltip(ownership: ResourceControlOwnership) {
|
function getOwnershipTooltip(
|
||||||
|
ownership: ResourceControlOwnership,
|
||||||
|
resourceName: string
|
||||||
|
) {
|
||||||
switch (ownership) {
|
switch (ownership) {
|
||||||
case ResourceControlOwnership.PRIVATE:
|
case ResourceControlOwnership.PRIVATE:
|
||||||
return 'Management of this resource is restricted to a single user.';
|
return `Management of this ${resourceName} is restricted to a single user.`;
|
||||||
case ResourceControlOwnership.RESTRICTED:
|
case ResourceControlOwnership.RESTRICTED:
|
||||||
return 'This resource can be managed by a restricted set of users and/or teams.';
|
return `This ${resourceName} can be managed by a restricted set of users and/or teams.`;
|
||||||
case ResourceControlOwnership.PUBLIC:
|
case ResourceControlOwnership.PUBLIC:
|
||||||
return 'This resource can be managed by any user with access to this environment.';
|
return `This ${resourceName} can be managed by any user with access to this environment.`;
|
||||||
case ResourceControlOwnership.ADMINISTRATORS:
|
case ResourceControlOwnership.ADMINISTRATORS:
|
||||||
default:
|
default:
|
||||||
return 'This resource can only be managed by administrators.';
|
return `This ${resourceName} can only be managed by administrators.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInheritanceMessage(
|
function getInheritanceMessage(
|
||||||
resourceType: ResourceControlType,
|
resourceType: ResourceControlType | undefined,
|
||||||
resourceControl?: ResourceControlViewModel
|
resourceControl?: ResourceControlViewModel
|
||||||
) {
|
) {
|
||||||
if (!resourceControl || resourceControl.Type === resourceType) {
|
if (!resourceControl || resourceControl.Type === resourceType) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export function AccessTypeSelector({
|
|||||||
teams,
|
teams,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
resourceName = 'resource',
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
@@ -19,8 +20,9 @@ export function AccessTypeSelector({
|
|||||||
isPublicVisible: boolean;
|
isPublicVisible: boolean;
|
||||||
value: ResourceControlOwnership;
|
value: ResourceControlOwnership;
|
||||||
onChange(value: ResourceControlOwnership): void;
|
onChange(value: ResourceControlOwnership): void;
|
||||||
|
resourceName?: string;
|
||||||
}) {
|
}) {
|
||||||
const options = useOptions(isAdmin, teams, isPublicVisible);
|
const options = useOptions(isAdmin, teams, isPublicVisible, resourceName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxSelector
|
<BoxSelector
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ interface Props {
|
|||||||
isPublicVisible?: boolean;
|
isPublicVisible?: boolean;
|
||||||
errors?: FormikErrors<AccessControlFormData>;
|
errors?: FormikErrors<AccessControlFormData>;
|
||||||
formNamespace?: string;
|
formNamespace?: string;
|
||||||
environmentId: EnvironmentId;
|
resourceName?: string;
|
||||||
|
environmentId?: EnvironmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditDetails({
|
export function EditDetails({
|
||||||
@@ -28,6 +29,7 @@ export function EditDetails({
|
|||||||
isPublicVisible = false,
|
isPublicVisible = false,
|
||||||
errors,
|
errors,
|
||||||
formNamespace,
|
formNamespace,
|
||||||
|
resourceName = 'resource',
|
||||||
environmentId,
|
environmentId,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { user, isPureAdmin } = useCurrentUser();
|
const { user, isPureAdmin } = useCurrentUser();
|
||||||
@@ -60,6 +62,7 @@ export function EditDetails({
|
|||||||
isAdmin={isPureAdmin}
|
isAdmin={isPureAdmin}
|
||||||
isPublicVisible={isPublicVisible}
|
isPublicVisible={isPublicVisible}
|
||||||
teams={teams}
|
teams={teams}
|
||||||
|
resourceName={resourceName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{values.ownership === ResourceControlOwnership.RESTRICTED && (
|
{values.ownership === ResourceControlOwnership.RESTRICTED && (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useUsers } from '@/portainer/users/queries';
|
|||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
|
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
export function useLoadState(environmentId: EnvironmentId) {
|
export function useLoadState(environmentId?: EnvironmentId) {
|
||||||
const isAdminQuery = useIsEdgeAdmin();
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
const teams = useTeams(false, environmentId);
|
const teams = useTeams(false, environmentId);
|
||||||
|
|
||||||
|
|||||||
@@ -10,34 +10,42 @@ import { BadgeIcon } from '@@/BadgeIcon';
|
|||||||
|
|
||||||
import { ResourceControlOwnership } from '../types';
|
import { ResourceControlOwnership } from '../types';
|
||||||
|
|
||||||
const publicOption: BoxSelectorOption<ResourceControlOwnership> = {
|
function publicOption(
|
||||||
|
resourceName: string
|
||||||
|
): BoxSelectorOption<ResourceControlOwnership> {
|
||||||
|
return {
|
||||||
value: ResourceControlOwnership.PUBLIC,
|
value: ResourceControlOwnership.PUBLIC,
|
||||||
label: 'Public',
|
label: 'Public',
|
||||||
id: 'access_public',
|
id: 'access_public',
|
||||||
description:
|
description: `I want any user with access to this ${resourceName} to be able to manage this ${resourceName}`,
|
||||||
'I want any user with access to this environment to be able to manage this resource',
|
|
||||||
icon: <BadgeIcon icon={ownershipIcon(ResourceControlOwnership.PUBLIC)} />,
|
icon: <BadgeIcon icon={ownershipIcon(ResourceControlOwnership.PUBLIC)} />,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function useOptions(
|
export function useOptions(
|
||||||
isAdmin: boolean,
|
isAdmin: boolean,
|
||||||
teams?: Team[],
|
teams?: Team[],
|
||||||
isPublicVisible = false
|
isPublicVisible = false,
|
||||||
|
resourceName = 'resource'
|
||||||
) {
|
) {
|
||||||
const [options, setOptions] = useState<
|
const [options, setOptions] = useState<
|
||||||
Array<BoxSelectorOption<ResourceControlOwnership>>
|
Array<BoxSelectorOption<ResourceControlOwnership>>
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const options = isAdmin ? adminOptions() : nonAdminOptions(teams);
|
const options = isAdmin
|
||||||
|
? adminOptions(resourceName)
|
||||||
|
: nonAdminOptions(teams, resourceName);
|
||||||
|
|
||||||
setOptions(isPublicVisible ? [...options, publicOption] : options);
|
setOptions(
|
||||||
}, [isAdmin, teams, isPublicVisible]);
|
isPublicVisible ? [...options, publicOption(resourceName)] : options
|
||||||
|
);
|
||||||
|
}, [isAdmin, teams, isPublicVisible, resourceName]);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
function adminOptions() {
|
function adminOptions(resourceName: string) {
|
||||||
return [
|
return [
|
||||||
buildOption(
|
buildOption(
|
||||||
'access_administrators',
|
'access_administrators',
|
||||||
@@ -45,25 +53,25 @@ function adminOptions() {
|
|||||||
icon={ownershipIcon(ResourceControlOwnership.ADMINISTRATORS)}
|
icon={ownershipIcon(ResourceControlOwnership.ADMINISTRATORS)}
|
||||||
/>,
|
/>,
|
||||||
'Administrators',
|
'Administrators',
|
||||||
'I want to restrict the management of this resource to administrators only',
|
`I want to restrict the management of this ${resourceName} to administrators only`,
|
||||||
ResourceControlOwnership.ADMINISTRATORS
|
ResourceControlOwnership.ADMINISTRATORS
|
||||||
),
|
),
|
||||||
buildOption(
|
buildOption(
|
||||||
'access_restricted',
|
'access_restricted',
|
||||||
<BadgeIcon icon={ownershipIcon(ResourceControlOwnership.RESTRICTED)} />,
|
<BadgeIcon icon={ownershipIcon(ResourceControlOwnership.RESTRICTED)} />,
|
||||||
'Restricted',
|
'Restricted',
|
||||||
'I want to restrict the management of this resource to a set of users and/or teams',
|
`I want to restrict the management of this ${resourceName} to a set of users and/or teams`,
|
||||||
ResourceControlOwnership.RESTRICTED
|
ResourceControlOwnership.RESTRICTED
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
function nonAdminOptions(teams?: Team[]) {
|
function nonAdminOptions(teams?: Team[], resourceName = 'resource') {
|
||||||
return _.compact([
|
return _.compact([
|
||||||
buildOption(
|
buildOption(
|
||||||
'access_private',
|
'access_private',
|
||||||
<BadgeIcon icon={ownershipIcon(ResourceControlOwnership.PRIVATE)} />,
|
<BadgeIcon icon={ownershipIcon(ResourceControlOwnership.PRIVATE)} />,
|
||||||
'Private',
|
'Private',
|
||||||
'I want to restrict this resource to be manageable by myself only',
|
`I want to restrict this ${resourceName} to be manageable by myself only`,
|
||||||
ResourceControlOwnership.PRIVATE
|
ResourceControlOwnership.PRIVATE
|
||||||
),
|
),
|
||||||
teams &&
|
teams &&
|
||||||
@@ -75,12 +83,12 @@ function nonAdminOptions(teams?: Team[]) {
|
|||||||
teams.length === 1 ? (
|
teams.length === 1 ? (
|
||||||
<>
|
<>
|
||||||
I want any member of my team (<b>{teams[0].Name}</b>) to be able to
|
I want any member of my team (<b>{teams[0].Name}</b>) to be able to
|
||||||
manage this resource
|
manage this {resourceName}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
I want to restrict the management of this resource to one or more of
|
I want to restrict the management of this {resourceName} to one or
|
||||||
my teams
|
more of my teams
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
ResourceControlOwnership.RESTRICTED
|
ResourceControlOwnership.RESTRICTED
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export enum ResourceControlType {
|
|||||||
ContainerGroup,
|
ContainerGroup,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ResourceAccessLevel {
|
export enum ResourceAccessLevel {
|
||||||
ReadWriteAccessLevel = 1,
|
ReadWriteAccessLevel = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -455,6 +455,9 @@ import type {
|
|||||||
GitOpsSourcesTestData,
|
GitOpsSourcesTestData,
|
||||||
GitOpsSourcesTestErrors,
|
GitOpsSourcesTestErrors,
|
||||||
GitOpsSourcesTestResponses,
|
GitOpsSourcesTestResponses,
|
||||||
|
GitOpsSourcesUpdateAccessData,
|
||||||
|
GitOpsSourcesUpdateAccessErrors,
|
||||||
|
GitOpsSourcesUpdateAccessResponses,
|
||||||
GitOpsSourcesUpdateGitData,
|
GitOpsSourcesUpdateGitData,
|
||||||
GitOpsSourcesUpdateGitErrors,
|
GitOpsSourcesUpdateGitErrors,
|
||||||
GitOpsSourcesUpdateGitResponses,
|
GitOpsSourcesUpdateGitResponses,
|
||||||
@@ -2557,7 +2560,7 @@ export const gitOpsSourcesList = <ThrowOnError extends boolean = true>(
|
|||||||
* Delete a source
|
* Delete a source
|
||||||
*
|
*
|
||||||
* Deletes an existing GitOps source. Returns 409 if the source is referenced by any workflow or custom template.
|
* Deletes an existing GitOps source. Returns 409 if the source is referenced by any workflow or custom template.
|
||||||
* **Access policy**: admin
|
* **Access policy**: authenticated
|
||||||
*/
|
*/
|
||||||
export const gitOpsSourcesDelete = <ThrowOnError extends boolean = true>(
|
export const gitOpsSourcesDelete = <ThrowOnError extends boolean = true>(
|
||||||
options: Options<GitOpsSourcesDeleteData, ThrowOnError>
|
options: Options<GitOpsSourcesDeleteData, ThrowOnError>
|
||||||
@@ -2602,7 +2605,7 @@ export const gitOpsSourceGet = <ThrowOnError extends boolean = true>(
|
|||||||
* Update a Git source
|
* Update a Git source
|
||||||
*
|
*
|
||||||
* Updates an existing GitOps source backed by a Git repository.
|
* Updates an existing GitOps source backed by a Git repository.
|
||||||
* **Access policy**: administrator
|
* **Access policy**: authenticated
|
||||||
*/
|
*/
|
||||||
export const gitOpsSourcesUpdateGit = <ThrowOnError extends boolean = true>(
|
export const gitOpsSourcesUpdateGit = <ThrowOnError extends boolean = true>(
|
||||||
options: Options<GitOpsSourcesUpdateGitData, ThrowOnError>
|
options: Options<GitOpsSourcesUpdateGitData, ThrowOnError>
|
||||||
@@ -2625,11 +2628,38 @@ export const gitOpsSourcesUpdateGit = <ThrowOnError extends boolean = true>(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a GitOps source's access control
|
||||||
|
*
|
||||||
|
* Updates the access control settings for an existing GitOps source.
|
||||||
|
* **Access policy**: admin
|
||||||
|
*/
|
||||||
|
export const gitOpsSourcesUpdateAccess = <ThrowOnError extends boolean = true>(
|
||||||
|
options: Options<GitOpsSourcesUpdateAccessData, ThrowOnError>
|
||||||
|
) =>
|
||||||
|
(options.client ?? client).put<
|
||||||
|
GitOpsSourcesUpdateAccessResponses,
|
||||||
|
GitOpsSourcesUpdateAccessErrors,
|
||||||
|
ThrowOnError
|
||||||
|
>({
|
||||||
|
responseType: 'json',
|
||||||
|
security: [
|
||||||
|
{ name: 'X-API-KEY', type: 'apiKey' },
|
||||||
|
{ name: 'Authorization', type: 'apiKey' },
|
||||||
|
],
|
||||||
|
url: '/gitops/sources/{id}/access',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the connection of a stored source
|
* Test the connection of a stored source
|
||||||
*
|
*
|
||||||
* Tests connectivity for a GitOps source, applying optional overrides to the stored configuration.
|
* Tests connectivity for a GitOps source, applying optional overrides to the stored configuration.
|
||||||
* **Access policy**: administrator
|
* **Access policy**: authenticated
|
||||||
*/
|
*/
|
||||||
export const gitOpsSourcesTestById = <ThrowOnError extends boolean = true>(
|
export const gitOpsSourcesTestById = <ThrowOnError extends boolean = true>(
|
||||||
options: Options<GitOpsSourcesTestByIdData, ThrowOnError>
|
options: Options<GitOpsSourcesTestByIdData, ThrowOnError>
|
||||||
@@ -2656,7 +2686,7 @@ export const gitOpsSourcesTestById = <ThrowOnError extends boolean = true>(
|
|||||||
* Create a Git source
|
* Create a Git source
|
||||||
*
|
*
|
||||||
* Creates a new GitOps source backed by a Git repository.
|
* Creates a new GitOps source backed by a Git repository.
|
||||||
* **Access policy**: administrator
|
* **Access policy**: authenticated
|
||||||
*/
|
*/
|
||||||
export const gitOpsSourcesCreateGit = <ThrowOnError extends boolean = true>(
|
export const gitOpsSourcesCreateGit = <ThrowOnError extends boolean = true>(
|
||||||
options: Options<GitOpsSourcesCreateGitData, ThrowOnError>
|
options: Options<GitOpsSourcesCreateGitData, ThrowOnError>
|
||||||
@@ -2706,7 +2736,7 @@ export const gitOpsSourcesSummary = <ThrowOnError extends boolean = true>(
|
|||||||
* Test a Git source connection
|
* Test a Git source connection
|
||||||
*
|
*
|
||||||
* Tests connectivity for Git connection details that have not been persisted yet.
|
* Tests connectivity for Git connection details that have not been persisted yet.
|
||||||
* **Access policy**: administrator
|
* **Access policy**: authenticated
|
||||||
*/
|
*/
|
||||||
export const gitOpsSourcesTest = <ThrowOnError extends boolean = true>(
|
export const gitOpsSourcesTest = <ThrowOnError extends boolean = true>(
|
||||||
options: Options<GitOpsSourcesTestData, ThrowOnError>
|
options: Options<GitOpsSourcesTestData, ThrowOnError>
|
||||||
|
|||||||
@@ -4603,6 +4603,7 @@ export type SourcesSourceType =
|
|||||||
(typeof SourcesSourceType)[keyof typeof SourcesSourceType];
|
(typeof SourcesSourceType)[keyof typeof SourcesSourceType];
|
||||||
|
|
||||||
export type SourcesSourceDetail = {
|
export type SourcesSourceDetail = {
|
||||||
|
access?: SourcesSourceAccess;
|
||||||
autoUpdate?: SourcesAutoUpdateInfo;
|
autoUpdate?: SourcesAutoUpdateInfo;
|
||||||
connection: SourcesConnectionInfo;
|
connection: SourcesConnectionInfo;
|
||||||
environments?: number;
|
environments?: number;
|
||||||
@@ -4622,6 +4623,18 @@ export type SourcesAutoUpdateInfo = {
|
|||||||
mechanism?: string;
|
mechanism?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SourcesSourceAccess = {
|
||||||
|
public?: boolean;
|
||||||
|
teams?: Array<number>;
|
||||||
|
users?: Array<number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SourcesSourceAccessUpdatePayload = {
|
||||||
|
public?: boolean;
|
||||||
|
teams?: Array<number>;
|
||||||
|
users?: Array<number>;
|
||||||
|
};
|
||||||
|
|
||||||
export type SourcesSource = {
|
export type SourcesSource = {
|
||||||
environments?: number;
|
environments?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -4648,10 +4661,14 @@ export type SourcesGitAuthenticationUpdatePayload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SourcesGitSourceCreatePayload = {
|
export type SourcesGitSourceCreatePayload = {
|
||||||
|
administratorsOnly?: boolean;
|
||||||
authentication?: SourcesGitAuthenticationPayload;
|
authentication?: SourcesGitAuthenticationPayload;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
public?: boolean;
|
||||||
|
teamAccesses?: Array<number>;
|
||||||
tlsSkipVerify?: boolean;
|
tlsSkipVerify?: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
|
userAccesses?: Array<number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SourcesGitAuthenticationPayload = {
|
export type SourcesGitAuthenticationPayload = {
|
||||||
@@ -5860,13 +5877,18 @@ export type PortainerSourceType =
|
|||||||
(typeof PortainerSourceType)[keyof typeof PortainerSourceType];
|
(typeof PortainerSourceType)[keyof typeof PortainerSourceType];
|
||||||
|
|
||||||
export type PortainerSource = {
|
export type PortainerSource = {
|
||||||
|
administratorsOnly?: boolean;
|
||||||
git?: GittypesRepoConfig;
|
git?: GittypesRepoConfig;
|
||||||
helm?: PortainerHelmConfig;
|
helm?: PortainerHelmConfig;
|
||||||
id?: number;
|
id?: number;
|
||||||
lastSync?: number;
|
lastSync?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
ownerID?: number;
|
||||||
|
public?: boolean;
|
||||||
registry?: PortainerRegistry;
|
registry?: PortainerRegistry;
|
||||||
|
teamAccesses?: Array<number>;
|
||||||
type?: PortainerSourceType;
|
type?: PortainerSourceType;
|
||||||
|
userAccesses?: Array<number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PortainerRegistryManagementConfiguration = {
|
export type PortainerRegistryManagementConfiguration = {
|
||||||
@@ -11484,6 +11506,50 @@ export type GitOpsSourcesUpdateGitResponses = {
|
|||||||
export type GitOpsSourcesUpdateGitResponse =
|
export type GitOpsSourcesUpdateGitResponse =
|
||||||
GitOpsSourcesUpdateGitResponses[keyof GitOpsSourcesUpdateGitResponses];
|
GitOpsSourcesUpdateGitResponses[keyof GitOpsSourcesUpdateGitResponses];
|
||||||
|
|
||||||
|
export type GitOpsSourcesUpdateAccessData = {
|
||||||
|
/**
|
||||||
|
* Source access control
|
||||||
|
*/
|
||||||
|
body: SourcesSourceAccessUpdatePayload;
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Source identifier
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/gitops/sources/{id}/access';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitOpsSourcesUpdateAccessErrors = {
|
||||||
|
/**
|
||||||
|
* Invalid request payload
|
||||||
|
*/
|
||||||
|
400: unknown;
|
||||||
|
/**
|
||||||
|
* Access denied
|
||||||
|
*/
|
||||||
|
403: unknown;
|
||||||
|
/**
|
||||||
|
* Source not found
|
||||||
|
*/
|
||||||
|
404: unknown;
|
||||||
|
/**
|
||||||
|
* Server error
|
||||||
|
*/
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitOpsSourcesUpdateAccessResponses = {
|
||||||
|
/**
|
||||||
|
* OK
|
||||||
|
*/
|
||||||
|
200: PortainerSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitOpsSourcesUpdateAccessResponse =
|
||||||
|
GitOpsSourcesUpdateAccessResponses[keyof GitOpsSourcesUpdateAccessResponses];
|
||||||
|
|
||||||
export type GitOpsSourcesTestByIdData = {
|
export type GitOpsSourcesTestByIdData = {
|
||||||
/**
|
/**
|
||||||
* Optional connection overrides; omitted fields fall back to stored values
|
* Optional connection overrides; omitted fields fall back to stored values
|
||||||
|
|||||||
@@ -1223,7 +1223,14 @@ export const zSourcesAutoUpdateInfo = z.object({
|
|||||||
mechanism: z.string().optional(),
|
mechanism: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const zSourcesSourceAccess = z.object({
|
||||||
|
public: z.boolean().optional(),
|
||||||
|
teams: z.array(z.int()).optional(),
|
||||||
|
users: z.array(z.int()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const zSourcesSourceDetail = z.object({
|
export const zSourcesSourceDetail = z.object({
|
||||||
|
access: zSourcesSourceAccess.optional(),
|
||||||
autoUpdate: zSourcesAutoUpdateInfo.optional(),
|
autoUpdate: zSourcesAutoUpdateInfo.optional(),
|
||||||
connection: zSourcesConnectionInfo,
|
connection: zSourcesConnectionInfo,
|
||||||
environments: z.int().optional(),
|
environments: z.int().optional(),
|
||||||
@@ -1238,6 +1245,12 @@ export const zSourcesSourceDetail = z.object({
|
|||||||
workflows: z.array(zWorkflowsWorkflow).optional(),
|
workflows: z.array(zWorkflowsWorkflow).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const zSourcesSourceAccessUpdatePayload = z.object({
|
||||||
|
public: z.boolean().optional(),
|
||||||
|
teams: z.array(z.int()).optional(),
|
||||||
|
users: z.array(z.int()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const zSourcesSource = z.object({
|
export const zSourcesSource = z.object({
|
||||||
environments: z.int().optional(),
|
environments: z.int().optional(),
|
||||||
error: z.string().optional(),
|
error: z.string().optional(),
|
||||||
@@ -1269,10 +1282,14 @@ export const zSourcesGitAuthenticationPayload = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const zSourcesGitSourceCreatePayload = z.object({
|
export const zSourcesGitSourceCreatePayload = z.object({
|
||||||
|
administratorsOnly: z.boolean().optional(),
|
||||||
authentication: zSourcesGitAuthenticationPayload.optional(),
|
authentication: zSourcesGitAuthenticationPayload.optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
|
public: z.boolean().optional(),
|
||||||
|
teamAccesses: z.array(z.int()).optional(),
|
||||||
tlsSkipVerify: z.boolean().optional(),
|
tlsSkipVerify: z.boolean().optional(),
|
||||||
url: z.string(),
|
url: z.string(),
|
||||||
|
userAccesses: z.array(z.int()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const zSourcesConnectionTestResult = z.object({
|
export const zSourcesConnectionTestResult = z.object({
|
||||||
@@ -1817,13 +1834,18 @@ export const zPortainerHelmConfig = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const zPortainerSource = z.object({
|
export const zPortainerSource = z.object({
|
||||||
|
administratorsOnly: z.boolean().optional(),
|
||||||
git: zGittypesRepoConfig.optional(),
|
git: zGittypesRepoConfig.optional(),
|
||||||
helm: zPortainerHelmConfig.optional(),
|
helm: zPortainerHelmConfig.optional(),
|
||||||
id: z.int().optional(),
|
id: z.int().optional(),
|
||||||
lastSync: z.int().optional(),
|
lastSync: z.int().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
|
ownerID: z.int().optional(),
|
||||||
|
public: z.boolean().optional(),
|
||||||
registry: zPortainerRegistry.optional(),
|
registry: zPortainerRegistry.optional(),
|
||||||
|
teamAccesses: z.array(z.int()).optional(),
|
||||||
type: zPortainerSourceType.optional(),
|
type: zPortainerSourceType.optional(),
|
||||||
|
userAccesses: z.array(z.int()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const zPortainerEdge = z.object({
|
export const zPortainerEdge = z.object({
|
||||||
@@ -4087,6 +4109,20 @@ export const zGitOpsSourcesUpdateGitPath = z.object({
|
|||||||
*/
|
*/
|
||||||
export const zGitOpsSourcesUpdateGitResponse = zPortainerSource;
|
export const zGitOpsSourcesUpdateGitResponse = zPortainerSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source access control
|
||||||
|
*/
|
||||||
|
export const zGitOpsSourcesUpdateAccessBody = zSourcesSourceAccessUpdatePayload;
|
||||||
|
|
||||||
|
export const zGitOpsSourcesUpdateAccessPath = z.object({
|
||||||
|
id: z.int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OK
|
||||||
|
*/
|
||||||
|
export const zGitOpsSourcesUpdateAccessResponse = zPortainerSource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional connection overrides; omitted fields fall back to stored values
|
* Optional connection overrides; omitted fields fall back to stored values
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Form, Formik, FormikHelpers } from 'formik';
|
|||||||
import { useRouter } from '@uirouter/react';
|
import { useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
import { notifySuccess } from '@/portainer/services/notifications';
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
import { ResourceControlOwnership } from '@/react/portainer/access-control/types';
|
||||||
|
import { useIsPureAdmin } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { Widget } from '@@/Widget';
|
import { Widget } from '@@/Widget';
|
||||||
|
|
||||||
@@ -11,7 +13,17 @@ import { WizardStep, useWizardContext } from './WizardContext';
|
|||||||
import { WizardHeader } from './WizardHeader';
|
import { WizardHeader } from './WizardHeader';
|
||||||
import { WizardFooter } from './WizardFooter';
|
import { WizardFooter } from './WizardFooter';
|
||||||
|
|
||||||
const initialFormValues: FormValues = {
|
type Props = {
|
||||||
|
steps: WizardStep[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CreateForm({ steps }: Props) {
|
||||||
|
const isAdmin = useIsPureAdmin();
|
||||||
|
const mutation = useCreateSourceMutation();
|
||||||
|
const router = useRouter();
|
||||||
|
const { currentStep, isLastStep, goToNextStep } = useWizardContext();
|
||||||
|
|
||||||
|
const initialFormValues: FormValues = {
|
||||||
name: '',
|
name: '',
|
||||||
type: 'git',
|
type: 'git',
|
||||||
git: {
|
git: {
|
||||||
@@ -21,16 +33,12 @@ const initialFormValues: FormValues = {
|
|||||||
},
|
},
|
||||||
connectionOk: false,
|
connectionOk: false,
|
||||||
},
|
},
|
||||||
};
|
authorizedTeams: [],
|
||||||
|
authorizedUsers: [],
|
||||||
type Props = {
|
ownership: isAdmin
|
||||||
steps: WizardStep[];
|
? ResourceControlOwnership.ADMINISTRATORS
|
||||||
};
|
: ResourceControlOwnership.PRIVATE,
|
||||||
|
};
|
||||||
export function CreateForm({ steps }: Props) {
|
|
||||||
const mutation = useCreateSourceMutation();
|
|
||||||
const router = useRouter();
|
|
||||||
const { currentStep, isLastStep, goToNextStep } = useWizardContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { TypeSelectStep, validateTypeSelectStep } from './steps/TypeSelectStep';
|
|||||||
import { ConfigureStep, validateConfigureStep } from './steps/ConfigureStep';
|
import { ConfigureStep, validateConfigureStep } from './steps/ConfigureStep';
|
||||||
import { WizardStep, WizardProvider } from './WizardContext';
|
import { WizardStep, WizardProvider } from './WizardContext';
|
||||||
import { CreateForm } from './CreateForm';
|
import { CreateForm } from './CreateForm';
|
||||||
|
import {
|
||||||
|
AccessControlStep,
|
||||||
|
validateAccessControlStep,
|
||||||
|
} from './steps/AccessControlStep';
|
||||||
|
|
||||||
const steps: WizardStep[] = [
|
const steps: WizardStep[] = [
|
||||||
{
|
{
|
||||||
@@ -19,6 +23,12 @@ const steps: WizardStep[] = [
|
|||||||
component: ConfigureStep,
|
component: ConfigureStep,
|
||||||
validateStep: validateConfigureStep,
|
validateStep: validateConfigureStep,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'access',
|
||||||
|
label: 'Access control',
|
||||||
|
component: AccessControlStep,
|
||||||
|
validateStep: validateAccessControlStep,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function CreateView() {
|
export function CreateView() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user