Skip to content

Commit

Permalink
Refactor and add plantUml support
Browse files Browse the repository at this point in the history
Signed-off-by: Sergiy Kulanov <sergiy_kulanov@epam.com>
  • Loading branch information
SergK committed Oct 2, 2023
1 parent fc442a2 commit 240ab13
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 100 deletions.
3 changes: 2 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}"
"program": "${fileDirname}",
"args": ["--namespace", "edp-delivery-tekton-dev", "--kind", "PipelineRun"]
}
]
}
92 changes: 92 additions & 0 deletions dot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"bytes"
"fmt"
"strings"

v1pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
)

type TaskGraph struct {
PipelineName string
Nodes map[string]*TaskNode
}

type TaskNode struct {
Name string
Dependencies []*TaskNode
}

type DOT struct {
Name string
Edges []string
Format string
}

func BuildTaskGraph(tasks []v1pipeline.PipelineTask) *TaskGraph {
graph := &TaskGraph{
Nodes: make(map[string]*TaskNode),
}

// Create a node for each task
for _, task := range tasks {
node := &TaskNode{
Name: task.Name,
}
graph.Nodes[task.Name] = node
}

// Add dependencies to each node
for _, task := range tasks {
node := graph.Nodes[task.Name]
for _, dep := range task.RunAfter {
depNode := graph.Nodes[dep]
node.Dependencies = append(node.Dependencies, depNode)
}
}

return graph
}

func (g *TaskGraph) ToDOT() *DOT {
dot := &DOT{
Name: g.PipelineName,
Format: "digraph",
}

for _, node := range g.Nodes {
for _, dep := range node.Dependencies {
dot.Edges = append(dot.Edges, fmt.Sprintf(" \"%s\" -> \"%s\"", dep.Name, node.Name))
}
}

return dot
}

func (d *DOT) String() string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("%s \"%s\" {\n", d.Format, d.Name))
for _, edge := range d.Edges {
buf.WriteString(fmt.Sprintf("%s\n", edge))
}
buf.WriteString("}\n")
return buf.String()
}

func (g *TaskGraph) ToPlantUML() string {
plantuml := "@startuml\n\n"
for _, node := range g.Nodes {
nodeName := strings.ReplaceAll(node.Name, "-", "_")
if len(node.Dependencies) == 0 {
plantuml += fmt.Sprintf("[*] --> %s\n", nodeName)
}
for _, dep := range node.Dependencies {
depName := strings.ReplaceAll(dep.Name, "-", "_")
plantuml += fmt.Sprintf("%s <-down- %s\n", nodeName, depName)
}
plantuml += "\n"
}
plantuml += "@enduml\n"
return plantuml
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
Expand Down Expand Up @@ -65,6 +66,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5 // indirect
github.com/tektoncd/pipeline v0.52.0
golang.org/x/net v0.14.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cloudevents/sdk-go/v2 v2.14.0 h1:Nrob4FwVgi5L4tV9lhjzZcjYqFVyJzsA56CwPaPfv6s=
github.com/cloudevents/sdk-go/v2 v2.14.0/go.mod h1:xDmKfzNjM8gBvjaF8ijFjM1VYOVUEeUfapHMUX1T5To=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down Expand Up @@ -195,6 +196,8 @@ github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
Expand Down Expand Up @@ -284,9 +287,12 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
187 changes: 88 additions & 99 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,121 +1,110 @@
package main

import (
"bytes"
"context"
"fmt"
"log"
"os"

v1pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/spf13/cobra"
"github.com/tektoncd/pipeline/pkg/client/clientset/versioned"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

const (
namespace = "edp-delivery-tekton-dev"
)

type TaskGraph struct {
PipelineName string
Nodes map[string]*TaskNode
}

type TaskNode struct {
Name string
Dependencies []*TaskNode
type Options struct {
Namespace string
ObjectKind string
}

func main() {
// Get the Kubernetes configuration
config, err := rest.InClusterConfig()
if err != nil {
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
kubeconfig = clientcmd.RecommendedHomeFile
}
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
log.Fatalf("Failed to get Kubernetes configuration: %v", err)
}
}

// Create a Kubernetes clientset for the Tekton API group
tektonClient, err := versioned.NewForConfig(config)
if err != nil {
log.Fatalf("Failed to create Tekton clientset: %v", err)
}

// Get all Tekton Pipelines
pipelines, err := tektonClient.TektonV1().Pipelines(namespace).List(context.TODO(), v1.ListOptions{})
if err != nil {
log.Fatalf("Failed to get Pipelines: %v", err)
}
for _, pipeline := range pipelines.Items {
// Build the task graph
graph := BuildTaskGraph(pipeline.Spec.Tasks)
graph.PipelineName = pipeline.Name

// Generate the DOT format string
dot := graph.ToDOT()

// Print the DOT format string
fmt.Println(dot)
}

// Get all Tekton PipelineRuns
pipelineRuns, err := tektonClient.TektonV1().PipelineRuns(namespace).List(context.TODO(), v1.ListOptions{})
if err != nil {
log.Fatalf("Failed to get PipelineRuns: %v", err)
}
for _, pipelineRun := range pipelineRuns.Items {
// Build the task graph
graph := BuildTaskGraph(pipelineRun.Status.PipelineSpec.Tasks)
graph.PipelineName = pipelineRun.Name
// Generate the DOT format string
dot := graph.ToDOT()

// Print the DOT format string
fmt.Println(dot)
}

}

func BuildTaskGraph(tasks []v1pipeline.PipelineTask) *TaskGraph {
graph := &TaskGraph{
Nodes: make(map[string]*TaskNode),
}

// Create a node for each task
for _, task := range tasks {
node := &TaskNode{
Name: task.Name,
}
graph.Nodes[task.Name] = node
var options Options

// Define the root command
rootCmd := &cobra.Command{
Use: "graph",
Run: func(cmd *cobra.Command, args []string) {
// Create the Kubernetes client
config, err := rest.InClusterConfig()
if err != nil {
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
kubeconfig = clientcmd.RecommendedHomeFile
}
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
log.Fatalf("Failed to get Kubernetes configuration: %v", err)
}
}
tektonClient, err := versioned.NewForConfig(config)
if err != nil {
log.Fatalf("Failed to create Tekton client: %v", err)
}

// Get the namespace to use
if options.Namespace == "" {
namespace, _, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&clientcmd.ConfigOverrides{},
).Namespace()
if err != nil {
log.Fatalf("Failed to get namespace from kubeconfig: %v", err)
}
if namespace == "" {
namespace = "default"
}
options.Namespace = namespace
}

switch options.ObjectKind {
case "Pipeline":
pipelines, err := tektonClient.TektonV1().Pipelines(options.Namespace).List(context.TODO(), v1.ListOptions{})
if err != nil {
log.Fatalf("Failed to get Pipelines: %v", err)
}
for _, pipeline := range pipelines.Items {
// Build the task graph
graph := BuildTaskGraph(pipeline.Spec.Tasks)
graph.PipelineName = pipeline.Name

// Generate the DOT format string
dot := graph.ToDOT()

// Print the DOT format string
fmt.Println(dot)
}
case "PipelineRun":
pipelineRuns, err := tektonClient.TektonV1().PipelineRuns(options.Namespace).List(context.TODO(), v1.ListOptions{})
if err != nil {
log.Fatalf("Failed to get PipelineRuns: %v", err)
}
for _, pipelineRun := range pipelineRuns.Items {
// Build the task graph
graph := BuildTaskGraph(pipelineRun.Status.PipelineSpec.Tasks)
plantuml := graph.ToPlantUML()
fmt.Println(plantuml)
// graph.PipelineName = pipelineRun.Name

// Generate the DOT format string
// dot := graph.ToDOT()

// Print the DOT format string
// fmt.Println(dot)
}
default:
log.Fatalf("Invalid kind type: %s", options.ObjectKind)
}
},
}

// Add dependencies to each node
for _, task := range tasks {
node := graph.Nodes[task.Name]
for _, dep := range task.RunAfter {
depNode := graph.Nodes[dep]
node.Dependencies = append(node.Dependencies, depNode)
}
}

return graph
}
// Define the command-line options
rootCmd.Flags().StringVar(&options.Namespace, "namespace", "", "the Kubernetes namespace to use")
rootCmd.Flags().StringVar(&options.ObjectKind, "kind", "Pipeline", "the kind of the Tekton object to parse (Pipeline or PipelineRun)")

func (g *TaskGraph) ToDOT() string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("digraph \"%s\" {\n", g.PipelineName))
for _, node := range g.Nodes {
for _, dep := range node.Dependencies {
buf.WriteString(fmt.Sprintf(" \"%s\" -> \"%s\"\n", dep.Name, node.Name))
}
// Parse the command-line options
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
buf.WriteString("}\n")
return buf.String()
}

0 comments on commit 240ab13

Please sign in to comment.