package source import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" gittypes "github.com/portainer/portainer/api/git/types" ) type ServiceTx struct { base dataservices.BaseDataServiceTx[portainer.Source, portainer.SourceID] } // Create creates a new source. func (service ServiceTx) Create(context UserContext, source *portainer.Source) error { 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, func(id uint64) (int, any) { source.ID = portainer.SourceID(id) return int(source.ID), source }, ) } 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.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.GitSource{ 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 }