Update community branch (#13208)
Co-authored-by: Hannah Cooper <hannah.cooper@portainer.io> Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Ali <83188384+testA113@users.noreply.github.com> Co-authored-by: Steven Kang <skan070@gmail.com> Co-authored-by: Josiah Clumont <josiah.clumont@portainer.io> Co-authored-by: nickl-portainer <nicholas.loomans@portainer.io> Co-authored-by: andres-portainer <91705312+andres-portainer@users.noreply.github.com> Co-authored-by: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Co-authored-by: ferreiraborgesaxel-design <ferreiraborgesaxel-design@users.noreply.github.com>
This commit is contained in:
@@ -9,8 +9,6 @@ import (
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/set"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// FetchWorkflows returns all GitOps workflows visible to the given user.
|
||||
@@ -43,69 +41,35 @@ func FetchWorkflows(
|
||||
|
||||
// First pass: filter by endpoint/stack-type match and collect workflow IDs.
|
||||
preFiltered := make([]portainer.Stack, 0, len(stacks))
|
||||
workflowIDSet := make(map[portainer.WorkflowID]struct{}, len(stacks))
|
||||
workflowIDSet := make(set.Set[portainer.WorkflowID], len(stacks))
|
||||
for _, stack := range stacks {
|
||||
if ep, ok := endpointMap[stack.EndpointID]; ok && !EndpointMatchesStackType(ep, stack.Type) {
|
||||
continue
|
||||
}
|
||||
preFiltered = append(preFiltered, stack)
|
||||
workflowIDSet[stack.WorkflowID] = struct{}{}
|
||||
workflowIDSet.Add(stack.WorkflowID)
|
||||
}
|
||||
|
||||
// Batch-load all needed workflows in one scan.
|
||||
wfs, err := tx.Workflow().ReadAll(func(wf portainer.Workflow) bool {
|
||||
_, ok := workflowIDSet[wf.ID]
|
||||
return ok
|
||||
})
|
||||
workflowMap, sourceMap, err := LoadWorkflowAndSourceMaps(tx, workflowIDSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workflowMap := make(map[portainer.WorkflowID]portainer.Workflow, len(wfs))
|
||||
var allArtifacts []portainer.ArtifactSources
|
||||
for _, wf := range wfs {
|
||||
workflowMap[wf.ID] = wf
|
||||
allArtifacts = append(allArtifacts, wf.Artifacts...)
|
||||
}
|
||||
sourceSet := ArtifactsToSourceSet(allArtifacts...)
|
||||
|
||||
// Batch-load all needed sources in one scan.
|
||||
srcs, err := tx.Source().ReadAll(func(src portainer.Source) bool {
|
||||
return sourceSet.Contains(src.ID)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sourceMap := make(map[portainer.SourceID]portainer.Source, len(srcs))
|
||||
for _, src := range srcs {
|
||||
sourceMap[src.ID] = src
|
||||
}
|
||||
|
||||
// Second pass: build filtered list using in-memory lookups.
|
||||
var filtered []portainer.Stack
|
||||
for _, stack := range preFiltered {
|
||||
wf, ok := workflowMap[stack.WorkflowID]
|
||||
if !ok {
|
||||
log.Warn().Int("stackID", int(stack.ID)).Msg("workflow record missing for stack, skipping")
|
||||
continue
|
||||
}
|
||||
wf := workflowMap[stack.WorkflowID]
|
||||
|
||||
outer:
|
||||
for _, as := range wf.Artifacts {
|
||||
if as.Artifact.StackID != stack.ID {
|
||||
if as.StackID != stack.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, srcID := range as.SourceIDs {
|
||||
src, ok := sourceMap[srcID]
|
||||
if !ok {
|
||||
log.Warn().Int("stackID", int(stack.ID)).Msg("source record missing for stack, skipping")
|
||||
break outer
|
||||
}
|
||||
|
||||
for _, f := range as.Files {
|
||||
src := sourceMap[f.SourceID]
|
||||
if src.Type == portainer.SourceTypeGit {
|
||||
gitConfigs[stack.ID] = MergeSourceAndArtifact(&src, &as.Artifact)
|
||||
gitConfigs[stack.ID] = MergeSourceAndFile(&src, &f)
|
||||
break outer
|
||||
}
|
||||
}
|
||||
@@ -169,28 +133,27 @@ func FetchSourceStats(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
workflowIDSet := make(map[portainer.WorkflowID]struct{}, len(allStacks))
|
||||
workflowIDSet := make(set.Set[portainer.WorkflowID], len(allStacks))
|
||||
preFiltered := make([]portainer.Stack, 0, len(allStacks))
|
||||
for _, stack := range allStacks {
|
||||
if ep, ok := endpointMap[stack.EndpointID]; ok && !EndpointMatchesStackType(ep, stack.Type) {
|
||||
continue
|
||||
}
|
||||
preFiltered = append(preFiltered, stack)
|
||||
workflowIDSet[stack.WorkflowID] = struct{}{}
|
||||
workflowIDSet.Add(stack.WorkflowID)
|
||||
}
|
||||
|
||||
wfs, err := tx.Workflow().ReadAll(func(wf portainer.Workflow) bool {
|
||||
_, ok := workflowIDSet[wf.ID]
|
||||
return ok
|
||||
})
|
||||
wfMap, err := LoadWorkflowMap(tx, workflowIDSet)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
wfSources := make(map[portainer.WorkflowID][]portainer.SourceID, len(wfs))
|
||||
for _, wf := range wfs {
|
||||
wfSources := make(map[portainer.WorkflowID][]portainer.SourceID, len(wfMap))
|
||||
for id, wf := range wfMap {
|
||||
for _, as := range wf.Artifacts {
|
||||
wfSources[wf.ID] = append(wfSources[wf.ID], as.SourceIDs...)
|
||||
for _, f := range as.Files {
|
||||
wfSources[id] = append(wfSources[id], f.SourceID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ func mustCreateGitWorkflow(t *testing.T, tx dataservices.DataStoreTx, stack *por
|
||||
|
||||
cfg := stack.GitConfig
|
||||
|
||||
src := &portainer.Source{Type: portainer.SourceTypeGit, GitConfig: cfg}
|
||||
src := &portainer.Source{Type: portainer.SourceTypeGit, Git: cfg}
|
||||
require.NoError(t, tx.Source().Create(src))
|
||||
|
||||
wf := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{StackID: stack.ID},
|
||||
SourceIDs: []portainer.SourceID{src.ID},
|
||||
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{
|
||||
StackID: stack.ID,
|
||||
Files: []portainer.ArtifactFile{{SourceID: src.ID}},
|
||||
}}}
|
||||
require.NoError(t, tx.Workflow().Create(wf))
|
||||
|
||||
@@ -228,7 +228,7 @@ func TestFetchSourceStats_TracksWorkflowCountAndEndpoints(t *testing.T) {
|
||||
srcID = src.ID
|
||||
|
||||
for i := 1; i <= 2; i++ {
|
||||
wf := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{SourceIDs: []portainer.SourceID{srcID}}}}
|
||||
wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: srcID}}}}}
|
||||
require.NoError(t, tx.Workflow().Create(wf))
|
||||
require.NoError(t, tx.Stack().Create(&portainer.Stack{
|
||||
ID: portainer.StackID(i),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package workflows
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/set"
|
||||
@@ -61,9 +63,9 @@ func MapEdgeStackToWorkflow(es portainer.EdgeStack, gitConfig *gittypes.RepoConf
|
||||
}
|
||||
|
||||
func StackLastSyncDate(s portainer.Stack) int64 {
|
||||
for i := len(s.DeploymentStatus) - 1; i >= 0; i-- {
|
||||
if s.DeploymentStatus[i].Status == portainer.StackStatusActive {
|
||||
return s.DeploymentStatus[i].Time
|
||||
for _, ds := range slices.Backward(s.DeploymentStatus) {
|
||||
if ds.Status == portainer.StackStatusActive {
|
||||
return ds.Time
|
||||
}
|
||||
}
|
||||
return 0
|
||||
@@ -84,9 +86,9 @@ func edgeStackLastSyncDate(statuses []portainer.EdgeStackStatusForEnv) int64 {
|
||||
}
|
||||
|
||||
func endpointLastSyncDate(epStatus portainer.EdgeStackStatusForEnv) int64 {
|
||||
for i := len(epStatus.Status) - 1; i >= 0; i-- {
|
||||
if isEdgeStackHealthyStatus(epStatus.Status[i].Type) {
|
||||
return epStatus.Status[i].Time
|
||||
for _, s := range slices.Backward(epStatus.Status) {
|
||||
if isEdgeStackHealthyStatus(s.Type) {
|
||||
return s.Time
|
||||
}
|
||||
}
|
||||
return 0
|
||||
@@ -115,18 +117,6 @@ func isEdgeStackHealthyStatus(t portainer.EdgeStackStatusType) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ArtifactsToSourceSet returns the set of all SourceIDs referenced by the given artifact-source mappings
|
||||
func ArtifactsToSourceSet(artifacts ...portainer.ArtifactSources) set.Set[portainer.SourceID] {
|
||||
s := make(set.Set[portainer.SourceID])
|
||||
for _, a := range artifacts {
|
||||
for _, sid := range a.SourceIDs {
|
||||
s.Add(sid)
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func resolveEdgeGroupEndpoints(groups []portainer.EdgeGroupID, groupEndpoints map[portainer.EdgeGroupID][]portainer.EndpointID) []portainer.EndpointID {
|
||||
seen := set.Set[portainer.EndpointID]{}
|
||||
for _, gid := range groups {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/set"
|
||||
)
|
||||
|
||||
// gitSourceStore is the minimal intersection of CE and EE DataStoreTx that these functions need.
|
||||
@@ -14,11 +15,11 @@ type gitSourceStore interface {
|
||||
Source() dataservices.SourceService
|
||||
}
|
||||
|
||||
// GitSourceAndArtifactForStack returns the git Source and the Artifact matching stackID
|
||||
// GitSourceAndArtifactForStack returns the git Source and the ArtifactFile matching stackID
|
||||
// from the workflow identified by workflowID.
|
||||
// Source carries the shared fields (URL, auth, TLS); Artifact carries the stack-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.
|
||||
func GitSourceAndArtifactForStack(tx gitSourceStore, workflowID portainer.WorkflowID, stackID portainer.StackID) (*portainer.Source, *portainer.Artifact, error) {
|
||||
func GitSourceAndArtifactForStack(tx gitSourceStore, workflowID portainer.WorkflowID, stackID portainer.StackID) (*portainer.Source, *portainer.ArtifactFile, error) {
|
||||
if workflowID == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
@@ -28,19 +29,24 @@ func GitSourceAndArtifactForStack(tx gitSourceStore, workflowID portainer.Workfl
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sourceMap, err := loadWorkflowSources(tx, wf)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for i, as := range wf.Artifacts {
|
||||
if as.Artifact.StackID != stackID {
|
||||
if as.StackID != stackID {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, srcID := range as.SourceIDs {
|
||||
src, err := tx.Source().Read(srcID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
for j, file := range as.Files {
|
||||
src, ok := sourceMap[file.SourceID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if src.Type == portainer.SourceTypeGit {
|
||||
return src, &wf.Artifacts[i].Artifact, nil
|
||||
return &src, &wf.Artifacts[i].Files[j], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,9 +54,9 @@ func GitSourceAndArtifactForStack(tx gitSourceStore, workflowID portainer.Workfl
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// GitSourceAndArtifactForEdgeStack returns the git Source and the Artifact 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.
|
||||
func GitSourceAndArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.WorkflowID, edgeStackID portainer.EdgeStackID) (*portainer.Source, *portainer.Artifact, error) {
|
||||
func GitSourceAndArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.WorkflowID, edgeStackID portainer.EdgeStackID) (*portainer.Source, *portainer.ArtifactFile, error) {
|
||||
if workflowID == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
@@ -60,19 +66,24 @@ func GitSourceAndArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.Wo
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sourceMap, err := loadWorkflowSources(tx, wf)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for i, as := range wf.Artifacts {
|
||||
if as.Artifact.EdgeStackID != edgeStackID {
|
||||
if as.EdgeStackID != edgeStackID {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, srcID := range as.SourceIDs {
|
||||
src, err := tx.Source().Read(srcID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
for j, file := range as.Files {
|
||||
src, ok := sourceMap[file.SourceID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if src.Type == portainer.SourceTypeGit {
|
||||
return src, &wf.Artifacts[i].Artifact, nil
|
||||
return &src, &wf.Artifacts[i].Files[j], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,60 +91,74 @@ func GitSourceAndArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.Wo
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// MergeSourceAndArtifact builds a RepoConfig by combining shared fields from src (URL, auth, TLS)
|
||||
// with stack-specific fields from artifact (ref, path, hash).
|
||||
func MergeSourceAndArtifact(src *portainer.Source, artifact *portainer.Artifact) *gittypes.RepoConfig {
|
||||
if src == nil || src.GitConfig == nil {
|
||||
// MergeSourceAndFile builds a RepoConfig by combining shared fields from src (URL, auth, TLS)
|
||||
// with file-specific fields from file (ref, path, hash).
|
||||
func MergeSourceAndFile(src *portainer.Source, file *portainer.ArtifactFile) *gittypes.RepoConfig {
|
||||
if src == nil || src.Git == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := &gittypes.RepoConfig{
|
||||
URL: src.GitConfig.URL,
|
||||
Authentication: src.GitConfig.Authentication,
|
||||
TLSSkipVerify: src.GitConfig.TLSSkipVerify,
|
||||
URL: src.Git.URL,
|
||||
Authentication: src.Git.Authentication,
|
||||
TLSSkipVerify: src.Git.TLSSkipVerify,
|
||||
}
|
||||
|
||||
if artifact != nil {
|
||||
cfg.ReferenceName = artifact.ReferenceName
|
||||
cfg.ConfigFilePath = artifact.ConfigFilePath
|
||||
cfg.ConfigHash = artifact.ConfigHash
|
||||
if file != nil {
|
||||
cfg.ReferenceName = file.Ref
|
||||
cfg.ConfigFilePath = file.Path
|
||||
cfg.ConfigHash = file.Hash
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// UpdateArtifactForStack finds the Artifact matching stackID in the workflow and applies fn to it,
|
||||
// then persists the updated Workflow. A no-op if no matching Artifact is found.
|
||||
func UpdateArtifactForStack(tx gitSourceStore, workflowID portainer.WorkflowID, stackID portainer.StackID, fn func(*portainer.Artifact)) error {
|
||||
// UpdateArtifactFileForStack finds the ArtifactFile matching stackID and sourceID in the workflow
|
||||
// and applies fn to it, then persists the updated Workflow.
|
||||
// A no-op if no matching artifact or file is found.
|
||||
func UpdateArtifactFileForStack(tx gitSourceStore, workflowID portainer.WorkflowID, stackID portainer.StackID, sourceID portainer.SourceID, fn func(*portainer.ArtifactFile)) error {
|
||||
wf, err := tx.Workflow().Read(workflowID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, as := range wf.Artifacts {
|
||||
if as.Artifact.StackID == stackID {
|
||||
fn(&wf.Artifacts[i].Artifact)
|
||||
if as.StackID != stackID {
|
||||
continue
|
||||
}
|
||||
|
||||
return tx.Workflow().Update(workflowID, wf)
|
||||
for j, file := range as.Files {
|
||||
if file.SourceID == sourceID {
|
||||
fn(&wf.Artifacts[i].Files[j])
|
||||
|
||||
return tx.Workflow().Update(workflowID, wf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateArtifactForEdgeStack finds the Artifact matching edgeStackID in the workflow and applies fn to it,
|
||||
// then persists the updated Workflow. A no-op if no matching Artifact is found.
|
||||
func UpdateArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.WorkflowID, edgeStackID portainer.EdgeStackID, fn func(*portainer.Artifact)) error {
|
||||
// UpdateArtifactFileForEdgeStack finds the ArtifactFile matching edgeStackID and sourceID in the workflow
|
||||
// and applies fn to it, then persists the updated Workflow.
|
||||
// A no-op if no matching artifact or file is found.
|
||||
func UpdateArtifactFileForEdgeStack(tx gitSourceStore, workflowID portainer.WorkflowID, edgeStackID portainer.EdgeStackID, sourceID portainer.SourceID, fn func(*portainer.ArtifactFile)) error {
|
||||
wf, err := tx.Workflow().Read(workflowID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, as := range wf.Artifacts {
|
||||
if as.Artifact.EdgeStackID == edgeStackID {
|
||||
fn(&wf.Artifacts[i].Artifact)
|
||||
if as.EdgeStackID != edgeStackID {
|
||||
continue
|
||||
}
|
||||
|
||||
return tx.Workflow().Update(workflowID, wf)
|
||||
for j, file := range as.Files {
|
||||
if file.SourceID == sourceID {
|
||||
fn(&wf.Artifacts[i].Files[j])
|
||||
|
||||
return tx.Workflow().Update(workflowID, wf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,13 +169,13 @@ func UpdateArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.Workflow
|
||||
// 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.
|
||||
func FindOrCreateGitSource(tx gitSourceStore, src *portainer.Source) (*portainer.Source, error) {
|
||||
src.GitConfig.URL = gittypes.SanitizeURL(src.GitConfig.URL)
|
||||
src.Git.URL = gittypes.SanitizeURL(src.Git.URL)
|
||||
|
||||
existing, err := tx.Source().ReadAll(func(s portainer.Source) bool {
|
||||
return s.Type == portainer.SourceTypeGit &&
|
||||
s.GitConfig != nil &&
|
||||
s.GitConfig.URL == src.GitConfig.URL &&
|
||||
gitAuthMatches(s.GitConfig.Authentication, src.GitConfig.Authentication)
|
||||
s.Git != nil &&
|
||||
s.Git.URL == src.Git.URL &&
|
||||
gitAuthMatches(s.Git.Authentication, src.Git.Authentication)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -163,10 +188,10 @@ func FindOrCreateGitSource(tx gitSourceStore, src *portainer.Source) (*portainer
|
||||
toCreate := &portainer.Source{
|
||||
Name: src.Name,
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: src.GitConfig.URL,
|
||||
Authentication: src.GitConfig.Authentication,
|
||||
TLSSkipVerify: src.GitConfig.TLSSkipVerify,
|
||||
Git: &gittypes.RepoConfig{
|
||||
URL: src.Git.URL,
|
||||
Authentication: src.Git.Authentication,
|
||||
TLSSkipVerify: src.Git.TLSSkipVerify,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -186,17 +211,17 @@ func SaveWorkflowGitConfig(tx gitSourceStore, workflowID portainer.WorkflowID, m
|
||||
return fmt.Errorf("failed to read source: %w", err)
|
||||
}
|
||||
|
||||
if src.GitConfig == nil {
|
||||
if src.Git == nil {
|
||||
return fmt.Errorf("source %d has no git configuration", oldSourceID)
|
||||
}
|
||||
|
||||
newSourceID := oldSourceID
|
||||
|
||||
if cfg.URL != src.GitConfig.URL {
|
||||
if cfg.URL != src.Git.URL {
|
||||
newSrc, err := FindOrCreateGitSource(tx, &portainer.Source{
|
||||
Name: gittypes.RepoName(cfg.URL),
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: cfg,
|
||||
Name: gittypes.RepoName(cfg.URL),
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: cfg,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find or create source: %w", err)
|
||||
@@ -204,8 +229,8 @@ func SaveWorkflowGitConfig(tx gitSourceStore, workflowID portainer.WorkflowID, m
|
||||
|
||||
newSourceID = newSrc.ID
|
||||
} else {
|
||||
src.GitConfig.Authentication = cfg.Authentication
|
||||
src.GitConfig.TLSSkipVerify = cfg.TLSSkipVerify
|
||||
src.Git.Authentication = cfg.Authentication
|
||||
src.Git.TLSSkipVerify = cfg.TLSSkipVerify
|
||||
|
||||
if err := tx.Source().Update(src.ID, src); err != nil {
|
||||
return fmt.Errorf("failed to update source: %w", err)
|
||||
@@ -218,20 +243,24 @@ func SaveWorkflowGitConfig(tx gitSourceStore, workflowID portainer.WorkflowID, m
|
||||
}
|
||||
|
||||
for i, as := range wf.Artifacts {
|
||||
if !matchArtifact(as.Artifact) {
|
||||
if !matchArtifact(as) {
|
||||
continue
|
||||
}
|
||||
|
||||
wf.Artifacts[i].Artifact.ReferenceName = cfg.ReferenceName
|
||||
wf.Artifacts[i].Artifact.ConfigFilePath = cfg.ConfigFilePath
|
||||
wf.Artifacts[i].Artifact.ConfigHash = cfg.ConfigHash
|
||||
|
||||
if newSourceID != oldSourceID {
|
||||
for j, sID := range as.SourceIDs {
|
||||
if sID == oldSourceID {
|
||||
wf.Artifacts[i].SourceIDs[j] = newSourceID
|
||||
}
|
||||
for j, file := range as.Files {
|
||||
if file.SourceID != oldSourceID {
|
||||
continue
|
||||
}
|
||||
|
||||
wf.Artifacts[i].Files[j].Ref = cfg.ReferenceName
|
||||
wf.Artifacts[i].Files[j].Path = cfg.ConfigFilePath
|
||||
wf.Artifacts[i].Files[j].Hash = cfg.ConfigHash
|
||||
|
||||
if newSourceID != oldSourceID {
|
||||
wf.Artifacts[i].Files[j].SourceID = newSourceID
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
break
|
||||
@@ -240,6 +269,73 @@ func SaveWorkflowGitConfig(tx gitSourceStore, workflowID portainer.WorkflowID, m
|
||||
return tx.Workflow().Update(workflowID, wf)
|
||||
}
|
||||
|
||||
// LoadWorkflowMap fetches workflows by their IDs and returns them keyed by ID.
|
||||
func LoadWorkflowMap(tx gitSourceStore, ids set.Set[portainer.WorkflowID]) (map[portainer.WorkflowID]portainer.Workflow, error) {
|
||||
result := make(map[portainer.WorkflowID]portainer.Workflow, len(ids))
|
||||
for id := range ids {
|
||||
wf, err := tx.Workflow().Read(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[id] = *wf
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// LoadWorkflowAndSourceMaps fetches workflows by their IDs and the sources they reference,
|
||||
// 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) {
|
||||
wfMap := make(map[portainer.WorkflowID]portainer.Workflow, len(ids))
|
||||
sourceIDs := make(set.Set[portainer.SourceID])
|
||||
for id := range ids {
|
||||
wf, err := tx.Workflow().Read(id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
wfMap[id] = *wf
|
||||
for _, as := range wf.Artifacts {
|
||||
for _, f := range as.Files {
|
||||
sourceIDs.Add(f.SourceID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
srcMap, err := LoadSourceMap(tx, sourceIDs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return wfMap, srcMap, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func loadWorkflowSources(tx gitSourceStore, wf *portainer.Workflow) (map[portainer.SourceID]portainer.Source, error) {
|
||||
ids := make(set.Set[portainer.SourceID])
|
||||
for _, as := range wf.Artifacts {
|
||||
for _, f := range as.Files {
|
||||
ids.Add(f.SourceID)
|
||||
}
|
||||
}
|
||||
|
||||
return LoadSourceMap(tx, ids)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
result := make(map[portainer.SourceID]portainer.Source, len(ids))
|
||||
for id := range ids {
|
||||
src, err := tx.Source().Read(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[id] = *src
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func gitAuthMatches(a, b *gittypes.GitAuthentication) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
@@ -260,11 +356,11 @@ func ValidateUniqueSourceURL(tx gitSourceStore, url string, sourceID portainer.S
|
||||
}
|
||||
|
||||
existing, err := tx.Source().ReadAll(func(s portainer.Source) bool {
|
||||
if s.ID == sourceID || s.Type != portainer.SourceTypeGit || s.GitConfig == nil {
|
||||
if s.ID == sourceID || s.Type != portainer.SourceTypeGit || s.Git == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
normalized, err := gittypes.NormalizeURL(gittypes.SanitizeURL(s.GitConfig.URL))
|
||||
normalized, err := gittypes.NormalizeURL(gittypes.SanitizeURL(s.Git.URL))
|
||||
|
||||
return err == nil && normalized == normalizedURL
|
||||
})
|
||||
|
||||
@@ -11,24 +11,24 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMergeSourceAndArtifact_NilSourceReturnsNil(t *testing.T) {
|
||||
func TestMergeSourceAndFile_NilSourceReturnsNil(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.Nil(t, MergeSourceAndArtifact(nil, nil))
|
||||
require.Nil(t, MergeSourceAndFile(nil, nil))
|
||||
}
|
||||
|
||||
func TestMergeSourceAndArtifact_NilGitConfigReturnsNil(t *testing.T) {
|
||||
func TestMergeSourceAndFile_NilGitConfigReturnsNil(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
src := &portainer.Source{Type: portainer.SourceTypeGit}
|
||||
require.Nil(t, MergeSourceAndArtifact(src, nil))
|
||||
require.Nil(t, MergeSourceAndFile(src, nil))
|
||||
}
|
||||
|
||||
func TestMergeSourceAndArtifact_NilArtifactLeavesPerStackFieldsEmpty(t *testing.T) {
|
||||
func TestMergeSourceAndFile_NilFileLeaveFileFieldsEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
src := &portainer.Source{
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
Git: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
TLSSkipVerify: true,
|
||||
Authentication: &gittypes.GitAuthentication{
|
||||
@@ -38,7 +38,7 @@ func TestMergeSourceAndArtifact_NilArtifactLeavesPerStackFieldsEmpty(t *testing.
|
||||
},
|
||||
}
|
||||
|
||||
cfg := MergeSourceAndArtifact(src, nil)
|
||||
cfg := MergeSourceAndFile(src, nil)
|
||||
require.NotNil(t, cfg)
|
||||
require.Equal(t, "https://github.com/example/repo", cfg.URL)
|
||||
require.True(t, cfg.TLSSkipVerify)
|
||||
@@ -48,22 +48,22 @@ func TestMergeSourceAndArtifact_NilArtifactLeavesPerStackFieldsEmpty(t *testing.
|
||||
require.Empty(t, cfg.ConfigHash)
|
||||
}
|
||||
|
||||
func TestMergeSourceAndArtifact_MergesAllFieldsFromArtifact(t *testing.T) {
|
||||
func TestMergeSourceAndFile_MergesAllFieldsFromFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
src := &portainer.Source{
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
Git: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
TLSSkipVerify: true,
|
||||
},
|
||||
}
|
||||
artifact := &portainer.Artifact{
|
||||
ReferenceName: "refs/heads/main",
|
||||
ConfigFilePath: "docker-compose.yml",
|
||||
ConfigHash: "abc123",
|
||||
file := &portainer.ArtifactFile{
|
||||
Path: "docker-compose.yml",
|
||||
Ref: "refs/heads/main",
|
||||
Hash: "abc123",
|
||||
}
|
||||
|
||||
cfg := MergeSourceAndArtifact(src, artifact)
|
||||
cfg := MergeSourceAndFile(src, file)
|
||||
require.NotNil(t, cfg)
|
||||
require.Equal(t, "https://github.com/example/repo", cfg.URL)
|
||||
require.True(t, cfg.TLSSkipVerify)
|
||||
@@ -77,39 +77,39 @@ func TestGitSourceAndArtifactForStack_ZeroWorkflowIDReturnsNil(t *testing.T) {
|
||||
_, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
var src *portainer.Source
|
||||
var artifact *portainer.Artifact
|
||||
var file *portainer.ArtifactFile
|
||||
err := store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var txErr error
|
||||
src, artifact, txErr = GitSourceAndArtifactForStack(tx, 0, 1)
|
||||
src, file, txErr = GitSourceAndArtifactForStack(tx, 0, 1)
|
||||
return txErr
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, src)
|
||||
require.Nil(t, artifact)
|
||||
require.Nil(t, file)
|
||||
}
|
||||
|
||||
func TestGitSourceAndArtifactForStack_ReturnsMatchingSourceAndArtifact(t *testing.T) {
|
||||
func TestGitSourceAndArtifactForStack_ReturnsMatchingSourceAndFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
var workflowID portainer.WorkflowID
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
gitSrc := &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||
}
|
||||
err := tx.Source().Create(gitSrc)
|
||||
require.NoError(t, err)
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{
|
||||
StackID: 42,
|
||||
ReferenceName: "refs/heads/main",
|
||||
ConfigFilePath: "docker-compose.yml",
|
||||
ConfigHash: "abc123",
|
||||
},
|
||||
SourceIDs: []portainer.SourceID{gitSrc.ID},
|
||||
Artifacts: []portainer.Artifact{{
|
||||
StackID: 42,
|
||||
Files: []portainer.ArtifactFile{{
|
||||
SourceID: gitSrc.ID,
|
||||
Path: "docker-compose.yml",
|
||||
Ref: "refs/heads/main",
|
||||
Hash: "abc123",
|
||||
}},
|
||||
}},
|
||||
}
|
||||
err = tx.Workflow().Create(wf)
|
||||
@@ -121,20 +121,19 @@ func TestGitSourceAndArtifactForStack_ReturnsMatchingSourceAndArtifact(t *testin
|
||||
require.NoError(t, err)
|
||||
|
||||
var src *portainer.Source
|
||||
var artifact *portainer.Artifact
|
||||
var file *portainer.ArtifactFile
|
||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var txErr error
|
||||
src, artifact, txErr = GitSourceAndArtifactForStack(tx, workflowID, 42)
|
||||
src, file, txErr = GitSourceAndArtifactForStack(tx, workflowID, 42)
|
||||
return txErr
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, src)
|
||||
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
||||
require.NotNil(t, artifact)
|
||||
require.Equal(t, portainer.StackID(42), artifact.StackID)
|
||||
require.Equal(t, "refs/heads/main", artifact.ReferenceName)
|
||||
require.Equal(t, "docker-compose.yml", artifact.ConfigFilePath)
|
||||
require.Equal(t, "abc123", artifact.ConfigHash)
|
||||
require.NotNil(t, file)
|
||||
require.Equal(t, "refs/heads/main", file.Ref)
|
||||
require.Equal(t, "docker-compose.yml", file.Path)
|
||||
require.Equal(t, "abc123", file.Hash)
|
||||
}
|
||||
|
||||
func TestGitSourceAndArtifactForStack_NoMatchingArtifactReturnsNil(t *testing.T) {
|
||||
@@ -144,16 +143,16 @@ func TestGitSourceAndArtifactForStack_NoMatchingArtifactReturnsNil(t *testing.T)
|
||||
var workflowID portainer.WorkflowID
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
src := &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||
}
|
||||
err := tx.Source().Create(src)
|
||||
require.NoError(t, err)
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{StackID: 1},
|
||||
SourceIDs: []portainer.SourceID{src.ID},
|
||||
Artifacts: []portainer.Artifact{{
|
||||
StackID: 1,
|
||||
Files: []portainer.ArtifactFile{{SourceID: src.ID}},
|
||||
}},
|
||||
}
|
||||
err = tx.Workflow().Create(wf)
|
||||
@@ -165,15 +164,15 @@ func TestGitSourceAndArtifactForStack_NoMatchingArtifactReturnsNil(t *testing.T)
|
||||
require.NoError(t, err)
|
||||
|
||||
var src *portainer.Source
|
||||
var artifact *portainer.Artifact
|
||||
var file *portainer.ArtifactFile
|
||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var txErr error
|
||||
src, artifact, txErr = GitSourceAndArtifactForStack(tx, workflowID, 99)
|
||||
src, file, txErr = GitSourceAndArtifactForStack(tx, workflowID, 99)
|
||||
return txErr
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, src)
|
||||
require.Nil(t, artifact)
|
||||
require.Nil(t, file)
|
||||
}
|
||||
|
||||
func TestGitSourceAndArtifactForStack_NonGitSourceSkipped(t *testing.T) {
|
||||
@@ -187,9 +186,9 @@ func TestGitSourceAndArtifactForStack_NonGitSourceSkipped(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{StackID: 1},
|
||||
SourceIDs: []portainer.SourceID{nonGitSrc.ID},
|
||||
Artifacts: []portainer.Artifact{{
|
||||
StackID: 1,
|
||||
Files: []portainer.ArtifactFile{{SourceID: nonGitSrc.ID}},
|
||||
}},
|
||||
}
|
||||
err = tx.Workflow().Create(wf)
|
||||
@@ -201,15 +200,15 @@ func TestGitSourceAndArtifactForStack_NonGitSourceSkipped(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var src *portainer.Source
|
||||
var artifact *portainer.Artifact
|
||||
var file *portainer.ArtifactFile
|
||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var txErr error
|
||||
src, artifact, txErr = GitSourceAndArtifactForStack(tx, workflowID, 1)
|
||||
src, file, txErr = GitSourceAndArtifactForStack(tx, workflowID, 1)
|
||||
return txErr
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, src)
|
||||
require.Nil(t, artifact)
|
||||
require.Nil(t, file)
|
||||
}
|
||||
|
||||
func TestGitSourceAndArtifactForEdgeStack_ZeroWorkflowIDReturnsNil(t *testing.T) {
|
||||
@@ -217,38 +216,38 @@ func TestGitSourceAndArtifactForEdgeStack_ZeroWorkflowIDReturnsNil(t *testing.T)
|
||||
_, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
var src *portainer.Source
|
||||
var artifact *portainer.Artifact
|
||||
var file *portainer.ArtifactFile
|
||||
err := store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var txErr error
|
||||
src, artifact, txErr = GitSourceAndArtifactForEdgeStack(tx, 0, 1)
|
||||
src, file, txErr = GitSourceAndArtifactForEdgeStack(tx, 0, 1)
|
||||
return txErr
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, src)
|
||||
require.Nil(t, artifact)
|
||||
require.Nil(t, file)
|
||||
}
|
||||
|
||||
func TestGitSourceAndArtifactForEdgeStack_ReturnsMatchingSourceAndArtifact(t *testing.T) {
|
||||
func TestGitSourceAndArtifactForEdgeStack_ReturnsMatchingSourceAndFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
var workflowID portainer.WorkflowID
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
gitSrc := &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/edge-repo"},
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/edge-repo"},
|
||||
}
|
||||
err := tx.Source().Create(gitSrc)
|
||||
require.NoError(t, err)
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{
|
||||
EdgeStackID: 5,
|
||||
ReferenceName: "refs/heads/edge",
|
||||
ConfigFilePath: "edge.yml",
|
||||
},
|
||||
SourceIDs: []portainer.SourceID{gitSrc.ID},
|
||||
Artifacts: []portainer.Artifact{{
|
||||
EdgeStackID: 5,
|
||||
Files: []portainer.ArtifactFile{{
|
||||
SourceID: gitSrc.ID,
|
||||
Path: "edge.yml",
|
||||
Ref: "refs/heads/edge",
|
||||
}},
|
||||
}},
|
||||
}
|
||||
err = tx.Workflow().Create(wf)
|
||||
@@ -260,32 +259,38 @@ func TestGitSourceAndArtifactForEdgeStack_ReturnsMatchingSourceAndArtifact(t *te
|
||||
require.NoError(t, err)
|
||||
|
||||
var src *portainer.Source
|
||||
var artifact *portainer.Artifact
|
||||
var file *portainer.ArtifactFile
|
||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var txErr error
|
||||
src, artifact, txErr = GitSourceAndArtifactForEdgeStack(tx, workflowID, 5)
|
||||
src, file, txErr = GitSourceAndArtifactForEdgeStack(tx, workflowID, 5)
|
||||
return txErr
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, src)
|
||||
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
||||
require.NotNil(t, artifact)
|
||||
require.Equal(t, portainer.EdgeStackID(5), artifact.EdgeStackID)
|
||||
require.Equal(t, "refs/heads/edge", artifact.ReferenceName)
|
||||
require.NotNil(t, file)
|
||||
require.Equal(t, "refs/heads/edge", file.Ref)
|
||||
}
|
||||
|
||||
func TestUpdateArtifactForStack_NoMatchingArtifactIsNoOp(t *testing.T) {
|
||||
func TestUpdateArtifactFileForStack_NoMatchingArtifactIsNoOp(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, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
||||
err := tx.Source().Create(src)
|
||||
require.NoError(t, err)
|
||||
sourceID = src.ID
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{StackID: 1, ConfigHash: "original"},
|
||||
Artifacts: []portainer.Artifact{{
|
||||
StackID: 1,
|
||||
Files: []portainer.ArtifactFile{{SourceID: sourceID, Hash: "original"}},
|
||||
}},
|
||||
}
|
||||
err := tx.Workflow().Create(wf)
|
||||
err = tx.Workflow().Create(wf)
|
||||
require.NoError(t, err)
|
||||
workflowID = wf.ID
|
||||
|
||||
@@ -294,29 +299,36 @@ func TestUpdateArtifactForStack_NoMatchingArtifactIsNoOp(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return UpdateArtifactForStack(tx, workflowID, 99, func(a *portainer.Artifact) {
|
||||
a.ConfigHash = "changed"
|
||||
return UpdateArtifactFileForStack(tx, workflowID, 99, sourceID, func(a *portainer.ArtifactFile) {
|
||||
a.Hash = "changed"
|
||||
})
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
wf, err := store.Workflow().Read(workflowID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "original", wf.Artifacts[0].Artifact.ConfigHash)
|
||||
require.Equal(t, "original", wf.Artifacts[0].Files[0].Hash)
|
||||
}
|
||||
|
||||
func TestUpdateArtifactForStack_AppliesFnAndPersists(t *testing.T) {
|
||||
func TestUpdateArtifactFileForStack_AppliesFnAndPersists(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, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
||||
err := tx.Source().Create(src)
|
||||
require.NoError(t, err)
|
||||
sourceID = src.ID
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{StackID: 1, ConfigHash: "old-hash"},
|
||||
Artifacts: []portainer.Artifact{{
|
||||
StackID: 1,
|
||||
Files: []portainer.ArtifactFile{{SourceID: sourceID, Hash: "old-hash"}},
|
||||
}},
|
||||
}
|
||||
err := tx.Workflow().Create(wf)
|
||||
err = tx.Workflow().Create(wf)
|
||||
require.NoError(t, err)
|
||||
workflowID = wf.ID
|
||||
|
||||
@@ -325,29 +337,36 @@ func TestUpdateArtifactForStack_AppliesFnAndPersists(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return UpdateArtifactForStack(tx, workflowID, 1, func(a *portainer.Artifact) {
|
||||
a.ConfigHash = "new-hash"
|
||||
return UpdateArtifactFileForStack(tx, workflowID, 1, sourceID, func(a *portainer.ArtifactFile) {
|
||||
a.Hash = "new-hash"
|
||||
})
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
wf, err := store.Workflow().Read(workflowID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "new-hash", wf.Artifacts[0].Artifact.ConfigHash)
|
||||
require.Equal(t, "new-hash", wf.Artifacts[0].Files[0].Hash)
|
||||
}
|
||||
|
||||
func TestUpdateArtifactForEdgeStack_AppliesFnAndPersists(t *testing.T) {
|
||||
func TestUpdateArtifactFileForEdgeStack_AppliesFnAndPersists(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, Git: &gittypes.RepoConfig{URL: "https://example.com"}}
|
||||
err := tx.Source().Create(src)
|
||||
require.NoError(t, err)
|
||||
sourceID = src.ID
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{EdgeStackID: 7, ConfigHash: "old-hash"},
|
||||
Artifacts: []portainer.Artifact{{
|
||||
EdgeStackID: 7,
|
||||
Files: []portainer.ArtifactFile{{SourceID: sourceID, Hash: "old-hash"}},
|
||||
}},
|
||||
}
|
||||
err := tx.Workflow().Create(wf)
|
||||
err = tx.Workflow().Create(wf)
|
||||
require.NoError(t, err)
|
||||
workflowID = wf.ID
|
||||
|
||||
@@ -356,15 +375,15 @@ func TestUpdateArtifactForEdgeStack_AppliesFnAndPersists(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return UpdateArtifactForEdgeStack(tx, workflowID, 7, func(a *portainer.Artifact) {
|
||||
a.ConfigHash = "new-hash"
|
||||
return UpdateArtifactFileForEdgeStack(tx, workflowID, 7, sourceID, func(a *portainer.ArtifactFile) {
|
||||
a.Hash = "new-hash"
|
||||
})
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
wf, err := store.Workflow().Read(workflowID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "new-hash", wf.Artifacts[0].Artifact.ConfigHash)
|
||||
require.Equal(t, "new-hash", wf.Artifacts[0].Files[0].Hash)
|
||||
}
|
||||
|
||||
func TestFindOrCreateGitSource_CreatesNewSource(t *testing.T) {
|
||||
@@ -377,7 +396,7 @@ func TestFindOrCreateGitSource_CreatesNewSource(t *testing.T) {
|
||||
src, txErr = FindOrCreateGitSource(tx, &portainer.Source{
|
||||
Name: "my-repo",
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
Git: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
},
|
||||
})
|
||||
@@ -386,7 +405,7 @@ func TestFindOrCreateGitSource_CreatesNewSource(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, src)
|
||||
require.NotZero(t, src.ID)
|
||||
require.Equal(t, "https://github.com/example/repo", src.GitConfig.URL)
|
||||
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
|
||||
}
|
||||
|
||||
func TestFindOrCreateGitSource_ReusesExistingSourceForSameURLAndAuth(t *testing.T) {
|
||||
@@ -396,7 +415,7 @@ func TestFindOrCreateGitSource_ReusesExistingSourceForSameURLAndAuth(t *testing.
|
||||
makeSource := func(tx dataservices.DataStoreTx) (*portainer.Source, error) {
|
||||
return FindOrCreateGitSource(tx, &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
Git: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
},
|
||||
})
|
||||
@@ -439,7 +458,7 @@ func TestFindOrCreateGitSource_DifferentAuthCreatesNewSource(t *testing.T) {
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
_, txErr := FindOrCreateGitSource(tx, &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
Git: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
Authentication: &gittypes.GitAuthentication{Username: "alice", Password: "pass1"},
|
||||
},
|
||||
@@ -451,7 +470,7 @@ func TestFindOrCreateGitSource_DifferentAuthCreatesNewSource(t *testing.T) {
|
||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
_, txErr := FindOrCreateGitSource(tx, &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
Git: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
Authentication: &gittypes.GitAuthentication{Username: "bob", Password: "pass2"},
|
||||
},
|
||||
@@ -465,7 +484,7 @@ func TestFindOrCreateGitSource_DifferentAuthCreatesNewSource(t *testing.T) {
|
||||
require.Len(t, sources, 2)
|
||||
}
|
||||
|
||||
func TestSaveWorkflowGitConfig_UpdatesArtifactAndSourceWhenURLUnchanged(t *testing.T) {
|
||||
func TestSaveWorkflowGitConfig_UpdatesFileAndSourceWhenURLUnchanged(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := datastore.MustNewTestStore(t, false, true)
|
||||
|
||||
@@ -475,7 +494,7 @@ func TestSaveWorkflowGitConfig_UpdatesArtifactAndSourceWhenURLUnchanged(t *testi
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
src := &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
Git: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
TLSSkipVerify: false,
|
||||
Authentication: &gittypes.GitAuthentication{
|
||||
@@ -489,14 +508,14 @@ func TestSaveWorkflowGitConfig_UpdatesArtifactAndSourceWhenURLUnchanged(t *testi
|
||||
sourceID = src.ID
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{
|
||||
StackID: 1,
|
||||
ReferenceName: "refs/heads/main",
|
||||
ConfigFilePath: "docker-compose.yml",
|
||||
ConfigHash: "old-hash",
|
||||
},
|
||||
SourceIDs: []portainer.SourceID{sourceID},
|
||||
Artifacts: []portainer.Artifact{{
|
||||
StackID: 1,
|
||||
Files: []portainer.ArtifactFile{{
|
||||
SourceID: sourceID,
|
||||
Path: "docker-compose.yml",
|
||||
Ref: "refs/heads/main",
|
||||
Hash: "old-hash",
|
||||
}},
|
||||
}},
|
||||
}
|
||||
err = tx.Workflow().Create(wf)
|
||||
@@ -528,16 +547,16 @@ func TestSaveWorkflowGitConfig_UpdatesArtifactAndSourceWhenURLUnchanged(t *testi
|
||||
|
||||
wf, err := store.Workflow().Read(workflowID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "refs/heads/dev", wf.Artifacts[0].Artifact.ReferenceName)
|
||||
require.Equal(t, "compose.yml", wf.Artifacts[0].Artifact.ConfigFilePath)
|
||||
require.Equal(t, "new-hash", wf.Artifacts[0].Artifact.ConfigHash)
|
||||
require.Equal(t, sourceID, wf.Artifacts[0].SourceIDs[0])
|
||||
require.Equal(t, "refs/heads/dev", wf.Artifacts[0].Files[0].Ref)
|
||||
require.Equal(t, "compose.yml", wf.Artifacts[0].Files[0].Path)
|
||||
require.Equal(t, "new-hash", wf.Artifacts[0].Files[0].Hash)
|
||||
require.Equal(t, sourceID, wf.Artifacts[0].Files[0].SourceID)
|
||||
|
||||
src, err := store.Source().Read(sourceID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "new-user", src.GitConfig.Authentication.Username)
|
||||
require.Equal(t, "new-pass", src.GitConfig.Authentication.Password)
|
||||
require.True(t, src.GitConfig.TLSSkipVerify)
|
||||
require.Equal(t, "new-user", src.Git.Authentication.Username)
|
||||
require.Equal(t, "new-pass", src.Git.Authentication.Password)
|
||||
require.True(t, src.Git.TLSSkipVerify)
|
||||
}
|
||||
|
||||
func TestSaveWorkflowGitConfig_CreatesNewSourceOnURLChange(t *testing.T) {
|
||||
@@ -549,17 +568,17 @@ func TestSaveWorkflowGitConfig_CreatesNewSourceOnURLChange(t *testing.T) {
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
src := &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"},
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"},
|
||||
}
|
||||
err := tx.Source().Create(src)
|
||||
require.NoError(t, err)
|
||||
oldSourceID = src.ID
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{StackID: 1},
|
||||
SourceIDs: []portainer.SourceID{oldSourceID},
|
||||
Artifacts: []portainer.Artifact{{
|
||||
StackID: 1,
|
||||
Files: []portainer.ArtifactFile{{SourceID: oldSourceID}},
|
||||
}},
|
||||
}
|
||||
err = tx.Workflow().Create(wf)
|
||||
@@ -581,12 +600,12 @@ func TestSaveWorkflowGitConfig_CreatesNewSourceOnURLChange(t *testing.T) {
|
||||
|
||||
wf, err := store.Workflow().Read(workflowID)
|
||||
require.NoError(t, err)
|
||||
newSourceID := wf.Artifacts[0].SourceIDs[0]
|
||||
newSourceID := wf.Artifacts[0].Files[0].SourceID
|
||||
require.NotEqual(t, oldSourceID, newSourceID)
|
||||
|
||||
newSrc, err := store.Source().Read(newSourceID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "https://github.com/example/new-repo", newSrc.GitConfig.URL)
|
||||
require.Equal(t, "https://github.com/example/new-repo", newSrc.Git.URL)
|
||||
}
|
||||
|
||||
func TestSaveWorkflowGitConfig_ReusesExistingSourceOnURLChange(t *testing.T) {
|
||||
@@ -598,25 +617,25 @@ func TestSaveWorkflowGitConfig_ReusesExistingSourceOnURLChange(t *testing.T) {
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
old := &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"},
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"},
|
||||
}
|
||||
err := tx.Source().Create(old)
|
||||
require.NoError(t, err)
|
||||
oldSourceID = old.ID
|
||||
|
||||
existing := &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/shared-repo"},
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/shared-repo"},
|
||||
}
|
||||
err = tx.Source().Create(existing)
|
||||
require.NoError(t, err)
|
||||
existingSourceID = existing.ID
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{StackID: 1},
|
||||
SourceIDs: []portainer.SourceID{oldSourceID},
|
||||
Artifacts: []portainer.Artifact{{
|
||||
StackID: 1,
|
||||
Files: []portainer.ArtifactFile{{SourceID: oldSourceID}},
|
||||
}},
|
||||
}
|
||||
err = tx.Workflow().Create(wf)
|
||||
@@ -638,7 +657,7 @@ func TestSaveWorkflowGitConfig_ReusesExistingSourceOnURLChange(t *testing.T) {
|
||||
|
||||
wf, err := store.Workflow().Read(workflowID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, existingSourceID, wf.Artifacts[0].SourceIDs[0])
|
||||
require.Equal(t, existingSourceID, wf.Artifacts[0].Files[0].SourceID)
|
||||
|
||||
sources, err := store.Source().ReadAll()
|
||||
require.NoError(t, err)
|
||||
@@ -659,9 +678,9 @@ func TestSaveWorkflowGitConfig_NilGitConfigReturnsError(t *testing.T) {
|
||||
sourceID = src.ID
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{{
|
||||
Artifact: portainer.Artifact{StackID: 1},
|
||||
SourceIDs: []portainer.SourceID{sourceID},
|
||||
Artifacts: []portainer.Artifact{{
|
||||
StackID: 1,
|
||||
Files: []portainer.ArtifactFile{{SourceID: sourceID}},
|
||||
}},
|
||||
}
|
||||
err = tx.Workflow().Create(wf)
|
||||
@@ -689,22 +708,22 @@ func TestSaveWorkflowGitConfig_OnlyMatchingArtifactUpdated(t *testing.T) {
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
src := &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"},
|
||||
}
|
||||
err := tx.Source().Create(src)
|
||||
require.NoError(t, err)
|
||||
sourceID = src.ID
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Artifacts: []portainer.ArtifactSources{
|
||||
Artifacts: []portainer.Artifact{
|
||||
{
|
||||
Artifact: portainer.Artifact{StackID: 1, ConfigHash: "hash-1"},
|
||||
SourceIDs: []portainer.SourceID{sourceID},
|
||||
StackID: 1,
|
||||
Files: []portainer.ArtifactFile{{SourceID: sourceID, Hash: "hash-1"}},
|
||||
},
|
||||
{
|
||||
Artifact: portainer.Artifact{StackID: 2, ConfigHash: "hash-2"},
|
||||
SourceIDs: []portainer.SourceID{sourceID},
|
||||
StackID: 2,
|
||||
Files: []portainer.ArtifactFile{{SourceID: sourceID, Hash: "hash-2"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -728,8 +747,8 @@ func TestSaveWorkflowGitConfig_OnlyMatchingArtifactUpdated(t *testing.T) {
|
||||
|
||||
wf, err := store.Workflow().Read(workflowID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "updated-hash", wf.Artifacts[0].Artifact.ConfigHash)
|
||||
require.Equal(t, "hash-2", wf.Artifacts[1].Artifact.ConfigHash)
|
||||
require.Equal(t, "updated-hash", wf.Artifacts[0].Files[0].Hash)
|
||||
require.Equal(t, "hash-2", wf.Artifacts[1].Files[0].Hash)
|
||||
}
|
||||
|
||||
func TestFindOrCreateGitSource_StripsEmbeddedCredentialsFromURL(t *testing.T) {
|
||||
@@ -741,12 +760,12 @@ func TestFindOrCreateGitSource_StripsEmbeddedCredentialsFromURL(t *testing.T) {
|
||||
var txErr error
|
||||
src, txErr = FindOrCreateGitSource(tx, &portainer.Source{
|
||||
Type: portainer.SourceTypeGit,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
Git: &gittypes.RepoConfig{
|
||||
URL: "https://user:secret@github.com/example/repo",
|
||||
},
|
||||
})
|
||||
return txErr
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "https://github.com/example/repo", src.GitConfig.URL)
|
||||
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user