feat(gitops): show live git validity status in workflow overview [BE-12885] (#2447)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Chaim Lev-Ari
2026-04-27 13:11:55 +03:00
committed by GitHub
parent da36002d37
commit ae1b6b8a71
26 changed files with 967 additions and 243 deletions
+59
View File
@@ -0,0 +1,59 @@
package workflows
import (
"context"
"fmt"
"path"
"slices"
)
// ListRefsFunc lists all git refs for a repository.
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)
// 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) {
source = computeSourcePhase(ctx, referenceName, listRefs)
if source.Status == StatusError {
return source, WorkflowPhaseStatus{Status: StatusUnknown}
}
return source, computeArtifactPhase(ctx, configFilePath, listFiles)
}
func computeSourcePhase(ctx context.Context, referenceName string, listRefs ListRefsFunc) WorkflowPhaseStatus {
refs, err := listRefs(ctx)
if err != nil {
return WorkflowPhaseStatus{Status: StatusError, Error: err.Error()}
}
if referenceName == "" {
return WorkflowPhaseStatus{Status: StatusHealthy}
}
if !slices.Contains(refs, referenceName) {
return WorkflowPhaseStatus{Status: StatusError, Error: fmt.Sprintf("ref %q not found", referenceName)}
}
return WorkflowPhaseStatus{Status: StatusHealthy}
}
func computeArtifactPhase(ctx context.Context, configFilePath string, listFiles ListFilesFunc) WorkflowPhaseStatus {
if configFilePath == "" {
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}
}
files, err := listFiles(ctx, exts)
if err != nil {
return WorkflowPhaseStatus{Status: StatusError, Error: err.Error()}
}
if !slices.Contains(files, configFilePath) {
return WorkflowPhaseStatus{Status: StatusError, Error: fmt.Sprintf("file %q not found", configFilePath)}
}
return WorkflowPhaseStatus{Status: StatusHealthy}
}
+162
View File
@@ -0,0 +1,162 @@
package workflows
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestComputeGitPhases(t *testing.T) {
t.Parallel()
okRefs := func(_ context.Context) ([]string, error) {
return []string{"refs/heads/main"}, nil
}
okFiles := func(_ context.Context, _ []string) ([]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) {
return nil, errors.New("connection refused")
}
cases := []struct {
name string
referenceName string
configFilePath string
listRefs ListRefsFunc
listFiles ListFilesFunc
expectedSource Status
expectedArtifact Status
}{
{
name: "listRefs errors → source error, artifact unknown",
referenceName: "refs/heads/main",
configFilePath: "docker-compose.yml",
listRefs: errRefs,
listFiles: okFiles,
expectedSource: StatusError,
expectedArtifact: StatusUnknown,
},
{
name: "ref not in list → source error, artifact unknown",
referenceName: "refs/heads/missing",
configFilePath: "docker-compose.yml",
listRefs: func(_ context.Context) ([]string, error) {
return []string{"refs/heads/main"}, nil
},
listFiles: okFiles,
expectedSource: StatusError,
expectedArtifact: StatusUnknown,
},
{
name: "empty configFilePath → artifact error",
referenceName: "refs/heads/main",
configFilePath: "",
listRefs: okRefs,
listFiles: okFiles,
expectedSource: StatusHealthy,
expectedArtifact: StatusError,
},
{
name: "listFiles errors → artifact error",
referenceName: "refs/heads/main",
configFilePath: "docker-compose.yml",
listRefs: okRefs,
listFiles: errFiles,
expectedSource: StatusHealthy,
expectedArtifact: StatusError,
},
{
name: "file not in list → artifact error",
referenceName: "refs/heads/main",
configFilePath: "docker-compose.yml",
listRefs: okRefs,
listFiles: func(_ context.Context, _ []string) ([]string, error) {
return []string{"other.yml"}, nil
},
expectedSource: StatusHealthy,
expectedArtifact: StatusError,
},
{
name: "both healthy",
referenceName: "refs/heads/main",
configFilePath: "docker-compose.yml",
listRefs: okRefs,
listFiles: okFiles,
expectedSource: StatusHealthy,
expectedArtifact: StatusHealthy,
},
{
name: "empty referenceName → source healthy (default HEAD)",
referenceName: "",
configFilePath: "docker-compose.yml",
listRefs: okRefs,
listFiles: okFiles,
expectedSource: StatusHealthy,
expectedArtifact: StatusHealthy,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
source, artifact := ComputeGitPhases(t.Context(), tc.referenceName, tc.configFilePath, tc.listRefs, tc.listFiles)
assert.Equal(t, tc.expectedSource, source.Status)
assert.Equal(t, tc.expectedArtifact, artifact.Status)
})
}
}
func TestComputeArtifactPhase_ExtensionFilter(t *testing.T) {
t.Parallel()
cases := []struct {
configPath string
wantExts []string
}{
{"docker-compose.yml", []string{"yml"}},
{"stack.yaml", []string{"yaml"}},
{"subdir/compose.yml", []string{"yml"}},
{"Makefile", nil},
{"archive.tar.gz", []string{"gz"}},
}
for _, tc := range cases {
t.Run(tc.configPath, func(t *testing.T) {
t.Parallel()
var capturedExts []string
ComputeGitPhases(
t.Context(),
"",
tc.configPath,
func(_ context.Context) ([]string, error) { return nil, nil },
func(_ context.Context, exts []string) ([]string, error) {
capturedExts = exts
return []string{tc.configPath}, nil
},
)
assert.Equal(t, tc.wantExts, capturedExts)
})
}
}
func TestComputeGitPhases_ArtifactNotCalledOnSourceError(t *testing.T) {
t.Parallel()
listFilesCalled := false
listRefs := func(_ context.Context) ([]string, error) {
return nil, errors.New("repo unreachable")
}
listFiles := func(_ context.Context, _ []string) ([]string, error) {
listFilesCalled = true
return nil, nil
}
ComputeGitPhases(t.Context(), "refs/heads/main", "docker-compose.yml", listRefs, listFiles)
assert.False(t, listFilesCalled, "listFiles must not be called when source fails")
}
+24 -18
View File
@@ -7,16 +7,19 @@ import (
// MapStackToWorkflow converts a stack to a Workflow. gitConfig is passed separately
// because EE embeds a different GitConfig type that shadows the CE field.
func MapStackToWorkflow(s portainer.Stack, gitConfig *gittypes.RepoConfig) Workflow {
status, msg := deriveStackStatus(s)
// source and artifact are the pre-computed git phase statuses from the caller.
func MapStackToWorkflow(s portainer.Stack, gitConfig *gittypes.RepoConfig, source, artifact WorkflowPhaseStatus) Workflow {
return Workflow{
ID: int(s.ID),
Name: s.Name,
Type: TypeStack,
Platform: platformFromStackType(s.Type),
Status: status,
StatusMessage: msg,
GitConfig: gitConfig,
ID: int(s.ID),
Name: s.Name,
Type: TypeStack,
Platform: platformFromStackType(s.Type),
Status: WorkflowStatusObject{
Source: source,
Artifact: artifact,
Target: deriveStackTargetState(s),
},
GitConfig: gitConfig,
Target: Target{
EndpointID: s.EndpointID,
Namespace: s.Namespace,
@@ -28,20 +31,23 @@ func MapStackToWorkflow(s portainer.Stack, gitConfig *gittypes.RepoConfig) Workf
// MapEdgeStackToWorkflow converts an edge stack to a Workflow. gitConfig is passed separately
// because EE embeds a different GitConfig type that shadows the CE field.
func MapEdgeStackToWorkflow(es portainer.EdgeStack, gitConfig *gittypes.RepoConfig, statuses []portainer.EdgeStackStatusForEnv, groupEndpoints map[portainer.EdgeGroupID][]portainer.EndpointID) Workflow {
status, msg := deriveEdgeStackStatus(statuses)
// source and artifact are the pre-computed git phase statuses from the caller.
func MapEdgeStackToWorkflow(es portainer.EdgeStack, gitConfig *gittypes.RepoConfig, statuses []portainer.EdgeStackStatusForEnv, groupEndpoints map[portainer.EdgeGroupID][]portainer.EndpointID, source, artifact WorkflowPhaseStatus) Workflow {
platform := DeploymentPlatformDockerStandalone
if es.DeploymentType == portainer.EdgeStackDeploymentKubernetes {
platform = DeploymentPlatformKubernetes
}
return Workflow{
ID: int(es.ID),
Name: es.Name,
Type: TypeEdgeStack,
Platform: platform,
Status: status,
StatusMessage: msg,
GitConfig: gitConfig,
ID: int(es.ID),
Name: es.Name,
Type: TypeEdgeStack,
Platform: platform,
Status: WorkflowStatusObject{
Source: source,
Artifact: artifact,
Target: deriveEdgeStackTargetState(statuses),
},
GitConfig: gitConfig,
Target: Target{
EdgeGroupIDs: es.EdgeGroups,
GroupStatus: edgeStackTargetStatuses(es.EdgeGroups, statuses, groupEndpoints),
+149
View File
@@ -0,0 +1,149 @@
package workflows
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func TestStackLastSyncDate(t *testing.T) {
t.Parallel()
t.Run("no deployment status", func(t *testing.T) {
t.Parallel()
assert.Equal(t, int64(0), stackLastSyncDate(portainer.Stack{}))
})
t.Run("no active entry", func(t *testing.T) {
t.Parallel()
s := portainer.Stack{DeploymentStatus: []portainer.StackDeploymentStatus{
{Status: portainer.StackStatusDeploying, Time: 100},
}}
assert.Equal(t, int64(0), stackLastSyncDate(s))
})
t.Run("last entry is active", func(t *testing.T) {
t.Parallel()
s := portainer.Stack{DeploymentStatus: []portainer.StackDeploymentStatus{
{Status: portainer.StackStatusDeploying, Time: 50},
{Status: portainer.StackStatusActive, Time: 100},
}}
assert.Equal(t, int64(100), stackLastSyncDate(s))
})
t.Run("active followed by non-active returns the active time", func(t *testing.T) {
t.Parallel()
s := portainer.Stack{DeploymentStatus: []portainer.StackDeploymentStatus{
{Status: portainer.StackStatusActive, Time: 100},
{Status: portainer.StackStatusDeploying, Time: 200},
}}
assert.Equal(t, int64(100), stackLastSyncDate(s))
})
}
func TestEdgeStackLastSyncDate(t *testing.T) {
t.Parallel()
t.Run("empty statuses", func(t *testing.T) {
t.Parallel()
assert.Equal(t, int64(0), edgeStackLastSyncDate(nil))
})
t.Run("no healthy status for endpoint", func(t *testing.T) {
t.Parallel()
statuses := []portainer.EdgeStackStatusForEnv{
{EndpointID: 1, Status: []portainer.EdgeStackDeploymentStatus{
{Type: portainer.EdgeStackStatusDeploying, Time: 100},
}},
}
assert.Equal(t, int64(0), edgeStackLastSyncDate(statuses))
})
t.Run("single endpoint with healthy status", func(t *testing.T) {
t.Parallel()
statuses := []portainer.EdgeStackStatusForEnv{
{EndpointID: 1, Status: []portainer.EdgeStackDeploymentStatus{
{Type: portainer.EdgeStackStatusRunning, Time: 200},
}},
}
assert.Equal(t, int64(200), edgeStackLastSyncDate(statuses))
})
t.Run("returns minimum healthy time across endpoints", func(t *testing.T) {
t.Parallel()
statuses := []portainer.EdgeStackStatusForEnv{
{EndpointID: 1, Status: []portainer.EdgeStackDeploymentStatus{
{Type: portainer.EdgeStackStatusRunning, Time: 300},
}},
{EndpointID: 2, Status: []portainer.EdgeStackDeploymentStatus{
{Type: portainer.EdgeStackStatusRunning, Time: 100},
}},
}
assert.Equal(t, int64(100), edgeStackLastSyncDate(statuses))
})
t.Run("one endpoint not yet synced returns 0", func(t *testing.T) {
t.Parallel()
statuses := []portainer.EdgeStackStatusForEnv{
{EndpointID: 1, Status: []portainer.EdgeStackDeploymentStatus{
{Type: portainer.EdgeStackStatusRunning, Time: 200},
}},
{EndpointID: 2, Status: []portainer.EdgeStackDeploymentStatus{
{Type: portainer.EdgeStackStatusDeploying, Time: 100},
}},
}
assert.Equal(t, int64(0), edgeStackLastSyncDate(statuses))
})
}
func TestEdgeStackTargetStatuses(t *testing.T) {
t.Parallel()
ep := func(id portainer.EndpointID, typ portainer.EdgeStackStatusType) portainer.EdgeStackStatusForEnv {
return portainer.EdgeStackStatusForEnv{
EndpointID: id,
Status: []portainer.EdgeStackDeploymentStatus{{Type: typ}},
}
}
t.Run("group with no endpoints is unknown", func(t *testing.T) {
t.Parallel()
result := edgeStackTargetStatuses(
[]portainer.EdgeGroupID{1},
nil,
map[portainer.EdgeGroupID][]portainer.EndpointID{1: {}},
)
assert.Equal(t, StatusUnknown, result[portainer.EdgeGroupID(1)])
})
t.Run("group inherits highest-priority endpoint status", func(t *testing.T) {
t.Parallel()
result := edgeStackTargetStatuses(
[]portainer.EdgeGroupID{1},
[]portainer.EdgeStackStatusForEnv{
ep(1, portainer.EdgeStackStatusRunning),
ep(2, portainer.EdgeStackStatusDeploying),
},
map[portainer.EdgeGroupID][]portainer.EndpointID{1: {1, 2}},
)
assert.Equal(t, StatusSyncing, result[portainer.EdgeGroupID(1)])
})
t.Run("multiple groups tracked separately", func(t *testing.T) {
t.Parallel()
result := edgeStackTargetStatuses(
[]portainer.EdgeGroupID{10, 20},
[]portainer.EdgeStackStatusForEnv{
ep(1, portainer.EdgeStackStatusRunning),
ep(2, portainer.EdgeStackStatusError),
},
map[portainer.EdgeGroupID][]portainer.EndpointID{
10: {1},
20: {2},
},
)
assert.Equal(t, StatusHealthy, result[portainer.EdgeGroupID(10)])
assert.Equal(t, StatusError, result[portainer.EdgeGroupID(20)])
})
}
+24 -12
View File
@@ -2,37 +2,37 @@ package workflows
import portainer "github.com/portainer/portainer/api"
func deriveStackStatus(s portainer.Stack) (Status, string) {
func deriveStackTargetState(s portainer.Stack) WorkflowPhaseStatus {
if len(s.DeploymentStatus) == 0 {
return StatusHealthy, ""
return WorkflowPhaseStatus{Status: StatusHealthy}
}
last := s.DeploymentStatus[len(s.DeploymentStatus)-1]
switch last.Status {
case portainer.StackStatusActive:
return StatusHealthy, ""
return WorkflowPhaseStatus{Status: StatusHealthy}
case portainer.StackStatusError:
return StatusError, last.Message
return WorkflowPhaseStatus{Status: StatusError, Error: last.Message}
case portainer.StackStatusDeploying:
return StatusSyncing, ""
return WorkflowPhaseStatus{Status: StatusSyncing}
case portainer.StackStatusInactive:
return StatusPaused, ""
return WorkflowPhaseStatus{Status: StatusPaused}
default:
return StatusUnknown, ""
return WorkflowPhaseStatus{Status: StatusUnknown}
}
}
func deriveEdgeStackStatus(statuses []portainer.EdgeStackStatusForEnv) (Status, string) {
func deriveEdgeStackTargetState(statuses []portainer.EdgeStackStatusForEnv) WorkflowPhaseStatus {
result := StatusUnknown
for _, epStatus := range statuses {
ws, msg := endpointWorkflowStatus(epStatus)
if ws == StatusError {
return ws, msg
return WorkflowPhaseStatus{Status: ws, Error: msg}
}
if statusPriority(ws) > statusPriority(result) {
result = ws
}
}
return result, ""
return WorkflowPhaseStatus{Status: result}
}
func endpointWorkflowStatus(epStatus portainer.EdgeStackStatusForEnv) (Status, string) {
@@ -64,11 +64,23 @@ func endpointWorkflowStatus(epStatus portainer.EdgeStackStatusForEnv) (Status, s
}
}
// CountByStatus counts workflows per status and returns a StatusSummary.
// EffectiveStatus returns the highest-priority status across all three phases of a workflow.
func EffectiveStatus(w Workflow) Status {
s := w.Status.Target.Status
if statusPriority(w.Status.Source.Status) > statusPriority(s) {
s = w.Status.Source.Status
}
if statusPriority(w.Status.Artifact.Status) > statusPriority(s) {
s = w.Status.Artifact.Status
}
return s
}
// CountByStatus counts workflows per effective status and returns a StatusSummary.
func CountByStatus(workflows []Workflow) StatusSummary {
var s StatusSummary
for _, w := range workflows {
switch w.Status {
switch EffectiveStatus(w) {
case StatusHealthy:
s.Healthy++
case StatusSyncing:
+151
View File
@@ -0,0 +1,151 @@
package workflows
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func TestEffectiveStatus(t *testing.T) {
t.Parallel()
makeWorkflow := func(source, artifact, target Status) Workflow {
return Workflow{
Status: WorkflowStatusObject{
Source: WorkflowPhaseStatus{Status: source},
Artifact: WorkflowPhaseStatus{Status: artifact},
Target: WorkflowPhaseStatus{Status: target},
},
}
}
cases := []struct {
name string
w Workflow
want Status
}{
{"all healthy", makeWorkflow(StatusHealthy, StatusHealthy, StatusHealthy), StatusHealthy},
{"all unknown", makeWorkflow(StatusUnknown, StatusUnknown, StatusUnknown), StatusUnknown},
{"source error wins over syncing target", makeWorkflow(StatusError, StatusSyncing, StatusHealthy), StatusError},
{"artifact error wins over syncing target", makeWorkflow(StatusHealthy, StatusError, StatusSyncing), StatusError},
{"target error wins over healthy phases", makeWorkflow(StatusHealthy, StatusHealthy, StatusError), StatusError},
{"syncing beats paused and healthy", makeWorkflow(StatusPaused, StatusSyncing, StatusHealthy), StatusSyncing},
{"paused beats healthy", makeWorkflow(StatusHealthy, StatusPaused, StatusHealthy), StatusPaused},
{"healthy beats unknown", makeWorkflow(StatusUnknown, StatusHealthy, StatusUnknown), StatusHealthy},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tc.want, EffectiveStatus(tc.w))
})
}
}
func TestCountByStatus(t *testing.T) {
t.Parallel()
makeW := func(s Status) Workflow {
return Workflow{
Status: WorkflowStatusObject{
Source: WorkflowPhaseStatus{Status: s},
Artifact: WorkflowPhaseStatus{Status: s},
Target: WorkflowPhaseStatus{Status: s},
},
}
}
t.Run("empty list", func(t *testing.T) {
t.Parallel()
assert.Equal(t, StatusSummary{}, CountByStatus(nil))
})
t.Run("single healthy", func(t *testing.T) {
t.Parallel()
assert.Equal(t, StatusSummary{Healthy: 1}, CountByStatus([]Workflow{makeW(StatusHealthy)}))
})
t.Run("mixed statuses", func(t *testing.T) {
t.Parallel()
workflows := []Workflow{
makeW(StatusHealthy),
makeW(StatusError),
makeW(StatusSyncing),
makeW(StatusPaused),
makeW(StatusUnknown),
makeW(StatusError),
}
assert.Equal(t, StatusSummary{Healthy: 1, Error: 2, Syncing: 1, Paused: 1, Unknown: 1}, CountByStatus(workflows))
})
t.Run("error phase overrides healthy target", func(t *testing.T) {
t.Parallel()
w := Workflow{
Status: WorkflowStatusObject{
Source: WorkflowPhaseStatus{Status: StatusError},
Artifact: WorkflowPhaseStatus{Status: StatusUnknown},
Target: WorkflowPhaseStatus{Status: StatusHealthy},
},
}
s := CountByStatus([]Workflow{w})
assert.Equal(t, 1, s.Error)
assert.Equal(t, 0, s.Healthy)
})
}
func TestDeriveEdgeStackTargetState(t *testing.T) {
t.Parallel()
ep := func(id portainer.EndpointID, typ portainer.EdgeStackStatusType) portainer.EdgeStackStatusForEnv {
return portainer.EdgeStackStatusForEnv{
EndpointID: id,
Status: []portainer.EdgeStackDeploymentStatus{{Type: typ}},
}
}
cases := []struct {
name string
statuses []portainer.EdgeStackStatusForEnv
want Status
}{
{"empty", nil, StatusUnknown},
{"all per-env status slices empty", []portainer.EdgeStackStatusForEnv{{EndpointID: 1}}, StatusUnknown},
{"running → healthy", []portainer.EdgeStackStatusForEnv{ep(1, portainer.EdgeStackStatusRunning)}, StatusHealthy},
{"deploying → syncing", []portainer.EdgeStackStatusForEnv{ep(1, portainer.EdgeStackStatusDeploying)}, StatusSyncing},
{"paused deploying → paused", []portainer.EdgeStackStatusForEnv{ep(1, portainer.EdgeStackStatusPausedDeploying)}, StatusPaused},
{"error short-circuits", []portainer.EdgeStackStatusForEnv{ep(1, portainer.EdgeStackStatusError)}, StatusError},
{
"error + running → error (short-circuit, order matters)",
[]portainer.EdgeStackStatusForEnv{
ep(1, portainer.EdgeStackStatusError),
ep(2, portainer.EdgeStackStatusRunning),
},
StatusError,
},
{
"syncing beats paused",
[]portainer.EdgeStackStatusForEnv{
ep(1, portainer.EdgeStackStatusPausedDeploying),
ep(2, portainer.EdgeStackStatusDeploying),
},
StatusSyncing,
},
{
"healthy does not downgrade syncing",
[]portainer.EdgeStackStatusForEnv{
ep(1, portainer.EdgeStackStatusDeploying),
ep(2, portainer.EdgeStackStatusRunning),
},
StatusSyncing,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result := deriveEdgeStackTargetState(tc.statuses)
assert.Equal(t, tc.want, result.Status)
})
}
}
+23 -10
View File
@@ -63,17 +63,30 @@ type Target struct {
GroupStatus map[portainer.EdgeGroupID]Status `json:"groupStatus,omitempty"`
}
// WorkflowPhaseStatus represents the status of one phase (source, artifact, or target) of a workflow.
// All three phases share the Status type; source and artifact only ever emit healthy, error, or unknown.
type WorkflowPhaseStatus struct {
Status Status `json:"status"`
Error string `json:"error,omitempty"`
}
// WorkflowStatusObject is the structured status reported for a workflow.
type WorkflowStatusObject struct {
Source WorkflowPhaseStatus `json:"source"`
Artifact WorkflowPhaseStatus `json:"artifact"`
Target WorkflowPhaseStatus `json:"target"`
}
type Workflow struct {
ID int `json:"id"`
Name string `json:"name"`
Type Type `json:"type"`
Platform DeploymentPlatform `json:"platform"`
Status Status `json:"status"`
StatusMessage string `json:"statusMessage,omitempty"`
GitConfig *gittypes.RepoConfig `json:"gitConfig,omitempty"`
Target Target `json:"target"`
CreationDate int64 `json:"creationDate"`
LastSyncDate int64 `json:"lastSyncDate"`
ID int `json:"id"`
Name string `json:"name"`
Type Type `json:"type"`
Platform DeploymentPlatform `json:"platform"`
Status WorkflowStatusObject `json:"status"`
GitConfig *gittypes.RepoConfig `json:"gitConfig,omitempty"`
Target Target `json:"target"`
CreationDate int64 `json:"creationDate"`
LastSyncDate int64 `json:"lastSyncDate"`
}
type StatusSummary struct {
+65
View File
@@ -0,0 +1,65 @@
package workflows
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseStatus(t *testing.T) {
t.Parallel()
for _, valid := range []string{"healthy", "error", "syncing", "paused", "unknown"} {
t.Run(valid, func(t *testing.T) {
t.Parallel()
s, err := ParseStatus(valid)
require.NoError(t, err)
assert.Equal(t, Status(valid), s)
})
}
t.Run("invalid returns error", func(t *testing.T) {
t.Parallel()
_, err := ParseStatus("garbage")
assert.Error(t, err)
})
}
func TestParseType(t *testing.T) {
t.Parallel()
for _, valid := range []string{"stack", "edgeStack"} {
t.Run(valid, func(t *testing.T) {
t.Parallel()
tp, err := ParseType(valid)
require.NoError(t, err)
assert.Equal(t, Type(valid), tp)
})
}
t.Run("invalid returns error", func(t *testing.T) {
t.Parallel()
_, err := ParseType("garbage")
assert.Error(t, err)
})
}
func TestParsePlatform(t *testing.T) {
t.Parallel()
for _, valid := range []string{"dockerStandalone", "dockerSwarm", "kubernetes"} {
t.Run(valid, func(t *testing.T) {
t.Parallel()
p, err := ParsePlatform(valid)
require.NoError(t, err)
assert.Equal(t, DeploymentPlatform(valid), p)
})
}
t.Run("invalid returns error", func(t *testing.T) {
t.Parallel()
_, err := ParsePlatform("garbage")
assert.Error(t, err)
})
}