Files
lukaszraczylo 2b36071647 Multiple fixes (#29)
* Multiple fixes

- add goreleaser to the build / release process
- add kubectl plugin for job graphs visualization
- add installation scripts
- update dependencies

* Update the release & CRD content.

* Next set of improvements.

  Code Quality

  - Label constants: Added LabelWorkflowName, LabelGroupName, LabelJobName, LabelJobID in controllers/definitions.go
  - Removed commented debug code: Cleaned up dead code from multiple files
  - Removed unused dependencyTree field: Cleaned connPackage struct
  - Fixed snake_case variables: Changed to camelCase (runGroup, groupDep, runJob, jobDep, k8sJob)

  Kubernetes Best Practices

  - Finalizers: Implemented handleDeletion() and deleteChildJobs() for proper cleanup
  - Status enum validation: Added +kubebuilder:validation:Enum=pending;running;succeeded;failed;aborted
  - ImagePullPolicy default: Created getImagePullPolicy() helper that defaults to IfNotPresent
  - Resource limits support: Added Resources *corev1.ResourceRequirements to ManagedJobParameters

  Observability

  - Prometheus metrics: Created controllers/metrics.go with counters (jobs created/succeeded/failed), histogram (reconciliation duration), and gauge (active jobs)
  - Structured logging: Added logger field to connPackage, used context-based logging throughout

  Configuration

  - Leader election ID: Made configurable via --leader-election-id flag
  - Development mode: Made configurable via --dev-mode flag and LOG_LEVEL env var

  Performance

  - Dependency lookup optimization: Changed from O(n*m) to O(1) using lookup maps (jobDepMap, groupDepMap)
  - Reconciliation backoff: Added RequeueAfter: 30*time.Second when workflow is running

  Documentation & Testing

  - Godoc documentation: Added comprehensive comments to API types and controller
  - Unit tests: Added helpers_test.go with tests for all helper functions
  - Integration tests: Added managedjob_controller_test.go with Ginkgo/Gomega tests

* Add the helm chart release.

* Add reasonable test coverage.
2025-12-17 22:33:23 +00:00

151 lines
3.6 KiB
Go

package visualization
import (
"strings"
"github.com/fatih/color"
)
// Box-drawing characters for tree rendering
const (
newLine = "\n"
emptySpace = " "
middleItem = "\u251c\u2500\u2500 " // ├──
continueItem = "\u2502 " // │
lastItem = "\u2514\u2500\u2500 " // └──
)
// Status constants
const (
StatusPending = "pending"
StatusRunning = "running"
StatusSucceeded = "succeeded"
StatusFailed = "failed"
StatusAborted = "aborted"
StatusUnknown = "unknown"
)
// Renderer handles tree rendering with optional color support
type Renderer struct {
useColor bool
green *color.Color
yellow *color.Color
red *color.Color
gray *color.Color
magenta *color.Color
cyan *color.Color
}
// NewRenderer creates a new Renderer
func NewRenderer(useColor bool) *Renderer {
return &Renderer{
useColor: useColor,
green: color.New(color.FgGreen),
yellow: color.New(color.FgYellow),
red: color.New(color.FgRed),
gray: color.New(color.FgHiBlack),
magenta: color.New(color.FgMagenta),
cyan: color.New(color.FgCyan),
}
}
// Render renders a StatusTree to a string
func (r *Renderer) Render(t *StatusTree) string {
var sb strings.Builder
r.renderNode(&sb, t, []bool{})
return sb.String()
}
// renderNode renders a single node and its children
func (r *Renderer) renderNode(sb *strings.Builder, t *StatusTree, spaces []bool) {
// Render current node
r.renderText(sb, t.Text(), t.Status(), spaces, true)
// Render children
items := t.Items()
for i, child := range items {
isLast := i == len(items)-1
r.renderChild(sb, child, spaces, isLast)
}
}
// renderChild renders a child node with proper indentation
func (r *Renderer) renderChild(sb *strings.Builder, t *StatusTree, spaces []bool, isLast bool) {
// Add prefix based on whether this is the last item
for _, space := range spaces {
if space {
sb.WriteString(emptySpace)
} else {
sb.WriteString(continueItem)
}
}
if isLast {
sb.WriteString(lastItem)
} else {
sb.WriteString(middleItem)
}
// Render the text with status
r.renderTextInline(sb, t.Text(), t.Status())
sb.WriteString(newLine)
// Render children with updated spaces
newSpaces := append(spaces, isLast)
items := t.Items()
for i, child := range items {
childIsLast := i == len(items)-1
r.renderChild(sb, child, newSpaces, childIsLast)
}
}
// renderText renders the root node text
func (r *Renderer) renderText(sb *strings.Builder, text, status string, spaces []bool, isRoot bool) {
if isRoot {
r.renderTextInline(sb, text, status)
sb.WriteString(newLine)
}
}
// renderTextInline renders text with status inline
func (r *Renderer) renderTextInline(sb *strings.Builder, text, status string) {
sb.WriteString(text)
if status != "" {
sb.WriteString(" ")
r.renderStatus(sb, status)
}
}
// renderStatus renders the status with appropriate color
func (r *Renderer) renderStatus(sb *strings.Builder, status string) {
statusText := "[" + status + "]"
if !r.useColor {
sb.WriteString(statusText)
return
}
switch status {
case StatusSucceeded:
sb.WriteString(r.green.Sprint(statusText))
case StatusRunning:
sb.WriteString(r.yellow.Sprint(statusText))
case StatusFailed:
sb.WriteString(r.red.Sprint(statusText))
case StatusPending:
sb.WriteString(r.gray.Sprint(statusText))
case StatusAborted:
sb.WriteString(r.magenta.Sprint(statusText))
default:
sb.WriteString(r.cyan.Sprint(statusText))
}
}
// RenderDependency formats a dependency reference
func RenderDependency(name string, isGroup bool) string {
if isGroup {
return "depends on group: " + name
}
return "depends on: " + name
}