Files
jobs-manager-operator/pkg/visualization/client.go
T
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

136 lines
3.5 KiB
Go

package visualization
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
)
// Client wraps the Kubernetes client for ManagedJob operations
type Client struct {
client client.Client
}
// NewClient creates a new Client for ManagedJob operations
func NewClient() (*Client, error) {
cfg, err := config.GetConfig()
if err != nil {
return nil, fmt.Errorf("failed to get kubeconfig: %w", err)
}
scheme := runtime.NewScheme()
if err := clientgoscheme.AddToScheme(scheme); err != nil {
return nil, fmt.Errorf("failed to add client-go scheme: %w", err)
}
if err := jobsmanagerv1beta1.AddToScheme(scheme); err != nil {
return nil, fmt.Errorf("failed to add jobsmanager scheme: %w", err)
}
cl, err := client.New(cfg, client.Options{Scheme: scheme})
if err != nil {
return nil, fmt.Errorf("failed to create client: %w", err)
}
return &Client{client: cl}, nil
}
// GetManagedJob retrieves a ManagedJob by name and namespace
func (c *Client) GetManagedJob(ctx context.Context, name, namespace string) (*jobsmanagerv1beta1.ManagedJob, error) {
mj := &jobsmanagerv1beta1.ManagedJob{}
err := c.client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, mj)
if err != nil {
return nil, fmt.Errorf("failed to get ManagedJob %s/%s: %w", namespace, name, err)
}
return mj, nil
}
// ListManagedJobs lists all ManagedJobs in a namespace
func (c *Client) ListManagedJobs(ctx context.Context, namespace string) (*jobsmanagerv1beta1.ManagedJobList, error) {
mjList := &jobsmanagerv1beta1.ManagedJobList{}
opts := []client.ListOption{}
if namespace != "" {
opts = append(opts, client.InNamespace(namespace))
}
err := c.client.List(ctx, mjList, opts...)
if err != nil {
return nil, fmt.Errorf("failed to list ManagedJobs: %w", err)
}
return mjList, nil
}
// BuildTree builds a StatusTree from a ManagedJob
func BuildTree(mj *jobsmanagerv1beta1.ManagedJob) *StatusTree {
root := NewStatusTreeWithStatus(mj.Name, mj.Status)
for _, group := range mj.Spec.Groups {
groupNode := root.AddWithStatus(group.Name, group.Status)
// Add group dependencies
for _, dep := range group.Dependencies {
groupNode.Add(RenderDependency(dep.Name, true))
}
// Add jobs
for _, job := range group.Jobs {
jobNode := groupNode.AddWithStatus(job.Name, job.Status)
// Add job dependencies
for _, dep := range job.Dependencies {
jobNode.Add(RenderDependency(dep.Name, false))
}
}
}
return root
}
// GetStatusSummary returns a summary of job statuses
type StatusSummary struct {
Name string
Namespace string
Status string
Groups int
Jobs int
Pending int
Running int
Succeeded int
Failed int
Aborted int
}
// GetStatusSummary builds a summary of the ManagedJob status
func GetStatusSummary(mj *jobsmanagerv1beta1.ManagedJob) StatusSummary {
summary := StatusSummary{
Name: mj.Name,
Namespace: mj.Namespace,
Status: mj.Status,
Groups: len(mj.Spec.Groups),
}
for _, group := range mj.Spec.Groups {
for _, job := range group.Jobs {
summary.Jobs++
switch job.Status {
case StatusPending:
summary.Pending++
case StatusRunning:
summary.Running++
case StatusSucceeded:
summary.Succeeded++
case StatusFailed:
summary.Failed++
case StatusAborted:
summary.Aborted++
}
}
}
return summary
}