mirror of
https://github.com/portainer/portainer.git
synced 2026-06-23 04:30:16 +00:00
fix(gitops): incorrect workflow status for git-based helm edge stack [BE-12978] (#2678)
This commit is contained in:
@@ -68,25 +68,32 @@ func FetchWorkflows(
|
||||
|
||||
items := make([]Workflow, 0, len(entries))
|
||||
for _, s := range entries {
|
||||
source, artifact := computePhases(ctx, gitService, s.GitConfig)
|
||||
gitEntries := []GitEntries{
|
||||
{Name: s.GitConfig.ConfigFilePath, IsFile: true},
|
||||
}
|
||||
for _, additionalPath := range s.AdditionalFiles {
|
||||
gitEntries = append(gitEntries, GitEntries{Name: additionalPath, IsFile: true})
|
||||
}
|
||||
|
||||
source, artifact := computePhases(ctx, gitService, s.GitConfig, gitEntries)
|
||||
items = append(items, MapStackToWorkflow(s, s.GitConfig, source, artifact))
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func computePhases(ctx context.Context, gitSvc portainer.GitService, cfg *gittypes.RepoConfig) (source, artifact WorkflowPhaseStatus) {
|
||||
func computePhases(ctx context.Context, gitSvc portainer.GitService, cfg *gittypes.RepoConfig, gitEntries []GitEntries) (source, artifact WorkflowPhaseStatus) {
|
||||
if gitSvc == nil || cfg == nil {
|
||||
return WorkflowPhaseStatus{Status: StatusUnknown}, WorkflowPhaseStatus{Status: StatusUnknown}
|
||||
}
|
||||
|
||||
username, password := gitCredentials(cfg)
|
||||
return ComputeGitPhases(ctx, cfg.ReferenceName, cfg.ConfigFilePath,
|
||||
return ComputeGitPhases(ctx, cfg.ReferenceName, gitEntries,
|
||||
func(ctx context.Context) ([]string, error) {
|
||||
return gitSvc.ListRefs(ctx, cfg.URL, username, password, false, cfg.TLSSkipVerify)
|
||||
},
|
||||
func(ctx context.Context, exts []string) ([]string, error) {
|
||||
return gitSvc.ListFiles(ctx, cfg.URL, cfg.ReferenceName, username, password, false, false, exts, cfg.TLSSkipVerify)
|
||||
func(ctx context.Context, exts []string, dirOnly bool) ([]string, error) {
|
||||
return gitSvc.ListFiles(ctx, cfg.URL, cfg.ReferenceName, username, password, dirOnly, false, exts, cfg.TLSSkipVerify)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,11 +11,17 @@ import (
|
||||
type ListRefsFunc func(ctx context.Context) ([]string, error)
|
||||
|
||||
// ListFilesFunc lists files in a repository branch filtered by extension.
|
||||
type ListFilesFunc func(ctx context.Context, exts []string) ([]string, error)
|
||||
type ListFilesFunc func(ctx context.Context, exts []string, dirOnly bool) ([]string, error)
|
||||
|
||||
// GitEntries represents a git entry which can be either a file or a directory.
|
||||
type GitEntries struct {
|
||||
Name string
|
||||
IsFile bool
|
||||
}
|
||||
|
||||
// ComputeGitPhases checks source (ref reachability) and artifact (config file presence).
|
||||
// If source fails, artifact is returned as unknown without making a network call.
|
||||
func ComputeGitPhases(ctx context.Context, referenceName, configFilePath string, listRefs ListRefsFunc, listFiles ListFilesFunc) (source, artifact WorkflowPhaseStatus) {
|
||||
func ComputeGitPhases(ctx context.Context, referenceName string, configFilePath []GitEntries, listRefs ListRefsFunc, listFiles ListFilesFunc) (source, artifact WorkflowPhaseStatus) {
|
||||
source = computeSourcePhase(ctx, referenceName, listRefs)
|
||||
if source.Status == StatusError {
|
||||
return source, WorkflowPhaseStatus{Status: StatusUnknown}
|
||||
@@ -37,23 +43,58 @@ func computeSourcePhase(ctx context.Context, referenceName string, listRefs List
|
||||
return WorkflowPhaseStatus{Status: StatusHealthy}
|
||||
}
|
||||
|
||||
func computeArtifactPhase(ctx context.Context, configFilePath string, listFiles ListFilesFunc) WorkflowPhaseStatus {
|
||||
if configFilePath == "" {
|
||||
func computeArtifactPhase(ctx context.Context, gitEntries []GitEntries, listFiles ListFilesFunc) WorkflowPhaseStatus {
|
||||
if len(gitEntries) == 0 {
|
||||
return WorkflowPhaseStatus{Status: StatusError, Error: "no config file path specified"}
|
||||
}
|
||||
ext := path.Ext(configFilePath)
|
||||
var exts []string
|
||||
if len(ext) > 0 {
|
||||
ext = ext[1:]
|
||||
exts = []string{ext}
|
||||
|
||||
var (
|
||||
exts []string
|
||||
fileEntries []string
|
||||
dirEntries []string
|
||||
)
|
||||
for _, gitEntry := range gitEntries {
|
||||
if gitEntry.IsFile {
|
||||
ext := path.Ext(gitEntry.Name)
|
||||
if len(ext) > 0 {
|
||||
ext = ext[1:]
|
||||
exts = append(exts, ext)
|
||||
}
|
||||
|
||||
fileEntries = append(fileEntries, gitEntry.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
dirEntries = append(dirEntries, gitEntry.Name)
|
||||
}
|
||||
|
||||
files, err := listFiles(ctx, exts)
|
||||
if err != nil {
|
||||
return WorkflowPhaseStatus{Status: StatusError, Error: err.Error()}
|
||||
// Check file entries
|
||||
if len(fileEntries) > 0 {
|
||||
files, err := listFiles(ctx, exts, false)
|
||||
if err != nil {
|
||||
return WorkflowPhaseStatus{Status: StatusError, Error: err.Error()}
|
||||
}
|
||||
|
||||
for _, fileEntry := range fileEntries {
|
||||
if !slices.Contains(files, fileEntry) {
|
||||
return WorkflowPhaseStatus{Status: StatusError, Error: fmt.Sprintf("file %q not found", fileEntry)}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !slices.Contains(files, configFilePath) {
|
||||
return WorkflowPhaseStatus{Status: StatusError, Error: fmt.Sprintf("file %q not found", configFilePath)}
|
||||
|
||||
// Check directory entries
|
||||
if len(dirEntries) > 0 {
|
||||
dirs, err := listFiles(ctx, nil, true)
|
||||
if err != nil {
|
||||
return WorkflowPhaseStatus{Status: StatusError, Error: err.Error()}
|
||||
}
|
||||
|
||||
for _, dirEntry := range dirEntries {
|
||||
if !slices.Contains(dirs, dirEntry) {
|
||||
return WorkflowPhaseStatus{Status: StatusError, Error: fmt.Sprintf("directory %q not found", dirEntry)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return WorkflowPhaseStatus{Status: StatusHealthy}
|
||||
}
|
||||
|
||||
@@ -14,20 +14,20 @@ func TestComputeGitPhases(t *testing.T) {
|
||||
okRefs := func(_ context.Context) ([]string, error) {
|
||||
return []string{"refs/heads/main"}, nil
|
||||
}
|
||||
okFiles := func(_ context.Context, _ []string) ([]string, error) {
|
||||
okFiles := func(_ context.Context, _ []string, _ bool) ([]string, error) {
|
||||
return []string{"docker-compose.yml"}, nil
|
||||
}
|
||||
errRefs := func(_ context.Context) ([]string, error) {
|
||||
return nil, errors.New("connection refused")
|
||||
}
|
||||
errFiles := func(_ context.Context, _ []string) ([]string, error) {
|
||||
errFiles := func(_ context.Context, _ []string, _ bool) ([]string, error) {
|
||||
return nil, errors.New("connection refused")
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
referenceName string
|
||||
configFilePath string
|
||||
configFilePath []GitEntries
|
||||
listRefs ListRefsFunc
|
||||
listFiles ListFilesFunc
|
||||
expectedSource Status
|
||||
@@ -36,7 +36,7 @@ func TestComputeGitPhases(t *testing.T) {
|
||||
{
|
||||
name: "listRefs errors → source error, artifact unknown",
|
||||
referenceName: "refs/heads/main",
|
||||
configFilePath: "docker-compose.yml",
|
||||
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
|
||||
listRefs: errRefs,
|
||||
listFiles: okFiles,
|
||||
expectedSource: StatusError,
|
||||
@@ -45,7 +45,7 @@ func TestComputeGitPhases(t *testing.T) {
|
||||
{
|
||||
name: "ref not in list → source error, artifact unknown",
|
||||
referenceName: "refs/heads/missing",
|
||||
configFilePath: "docker-compose.yml",
|
||||
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
|
||||
listRefs: func(_ context.Context) ([]string, error) {
|
||||
return []string{"refs/heads/main"}, nil
|
||||
},
|
||||
@@ -56,7 +56,7 @@ func TestComputeGitPhases(t *testing.T) {
|
||||
{
|
||||
name: "empty configFilePath → artifact error",
|
||||
referenceName: "refs/heads/main",
|
||||
configFilePath: "",
|
||||
configFilePath: []GitEntries{},
|
||||
listRefs: okRefs,
|
||||
listFiles: okFiles,
|
||||
expectedSource: StatusHealthy,
|
||||
@@ -65,7 +65,7 @@ func TestComputeGitPhases(t *testing.T) {
|
||||
{
|
||||
name: "listFiles errors → artifact error",
|
||||
referenceName: "refs/heads/main",
|
||||
configFilePath: "docker-compose.yml",
|
||||
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
|
||||
listRefs: okRefs,
|
||||
listFiles: errFiles,
|
||||
expectedSource: StatusHealthy,
|
||||
@@ -74,9 +74,9 @@ func TestComputeGitPhases(t *testing.T) {
|
||||
{
|
||||
name: "file not in list → artifact error",
|
||||
referenceName: "refs/heads/main",
|
||||
configFilePath: "docker-compose.yml",
|
||||
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
|
||||
listRefs: okRefs,
|
||||
listFiles: func(_ context.Context, _ []string) ([]string, error) {
|
||||
listFiles: func(_ context.Context, _ []string, _ bool) ([]string, error) {
|
||||
return []string{"other.yml"}, nil
|
||||
},
|
||||
expectedSource: StatusHealthy,
|
||||
@@ -85,7 +85,7 @@ func TestComputeGitPhases(t *testing.T) {
|
||||
{
|
||||
name: "both healthy",
|
||||
referenceName: "refs/heads/main",
|
||||
configFilePath: "docker-compose.yml",
|
||||
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
|
||||
listRefs: okRefs,
|
||||
listFiles: okFiles,
|
||||
expectedSource: StatusHealthy,
|
||||
@@ -94,7 +94,7 @@ func TestComputeGitPhases(t *testing.T) {
|
||||
{
|
||||
name: "empty referenceName → source healthy (default HEAD)",
|
||||
referenceName: "",
|
||||
configFilePath: "docker-compose.yml",
|
||||
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
|
||||
listRefs: okRefs,
|
||||
listFiles: okFiles,
|
||||
expectedSource: StatusHealthy,
|
||||
@@ -132,9 +132,9 @@ func TestComputeArtifactPhase_ExtensionFilter(t *testing.T) {
|
||||
ComputeGitPhases(
|
||||
t.Context(),
|
||||
"",
|
||||
tc.configPath,
|
||||
[]GitEntries{{Name: tc.configPath, IsFile: true}},
|
||||
func(_ context.Context) ([]string, error) { return nil, nil },
|
||||
func(_ context.Context, exts []string) ([]string, error) {
|
||||
func(_ context.Context, exts []string, dirOnly bool) ([]string, error) {
|
||||
capturedExts = exts
|
||||
return []string{tc.configPath}, nil
|
||||
},
|
||||
@@ -151,12 +151,12 @@ func TestComputeGitPhases_ArtifactNotCalledOnSourceError(t *testing.T) {
|
||||
listRefs := func(_ context.Context) ([]string, error) {
|
||||
return nil, errors.New("repo unreachable")
|
||||
}
|
||||
listFiles := func(_ context.Context, _ []string) ([]string, error) {
|
||||
listFiles := func(_ context.Context, _ []string, _ bool) ([]string, error) {
|
||||
listFilesCalled = true
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ComputeGitPhases(t.Context(), "refs/heads/main", "docker-compose.yml", listRefs, listFiles)
|
||||
ComputeGitPhases(t.Context(), "refs/heads/main", []GitEntries{{Name: "docker-compose.yml", IsFile: true}}, listRefs, listFiles)
|
||||
|
||||
assert.False(t, listFilesCalled, "listFiles must not be called when source fails")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user