mirror of
https://github.com/lukaszraczylo/jobs-manager-operator.git
synced 2026-06-13 02:51:32 +00:00
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.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user