From 1a7bf9821cdefa16bca83f307af9e514fc914e39 Mon Sep 17 00:00:00 2001 From: Sergiy Kulanov Date: Wed, 4 Oct 2023 21:53:31 +0300 Subject: [PATCH] refactor: First refactor iteration Move logic out of main.go Signed-off-by: Sergiy Kulanov --- .goreleaser.yaml | 25 ++++++- cmd/graph/main.go | 59 +++++------------ go.mod | 1 + pkg/client/tekton.go | 76 +++++++++++++++++++++ pkg/client/tekton_test.go | 134 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 249 insertions(+), 46 deletions(-) create mode 100644 pkg/client/tekton.go create mode 100644 pkg/client/tekton_test.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index ce98204..1107306 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -13,6 +13,11 @@ builds: - linux - windows - darwin + goarch: + - amd64 + - arm64 + - s390x + - ppc64le mod_timestamp: '{{ .CommitTimestamp }}' flags: # trims path @@ -22,8 +27,21 @@ builds: # only needed if you actually use those things in your main package, otherwise can be ignored. - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} +archives: +- name_template: >- + {{- .Binary }}_ + {{- .Version }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else if eq .Arch "arm64" }}aarch64 + {{- else }}{{ .Arch }}{{ end }} + format_overrides: + - goos: windows + format: zip + checksum: - name_template: "{{ .ProjectName }}_checksums.txt" + name_template: "checksums.txt" snapshot: name_template: "{{ incpatch .Version }}-snapshot" @@ -53,9 +71,10 @@ changelog: order: 999 filters: exclude: + - '^docs:' - '^test:' - - '^.*?Bump(\([[:word:]]+\))?.+$' - - '^.*?[Bot](\([[:word:]]+\))?.+$' + - Merge pull request + - Merge branch release: name_template: 'v{{ .Version }}' diff --git a/cmd/graph/main.go b/cmd/graph/main.go index a6f93bc..39a051d 100644 --- a/cmd/graph/main.go +++ b/cmd/graph/main.go @@ -1,23 +1,19 @@ package main import ( - "context" "fmt" "log" "os" "path/filepath" + "github.com/sergk/tkn-graph/pkg/client" "github.com/sergk/tkn-graph/pkg/taskgraph" "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" ) type Options struct { Namespace string - ObjectKind string + TektonKind string OutputFormat string OutputDir string WithTaskRef bool @@ -28,41 +24,24 @@ func main() { // Define the root command rootCmd := &cobra.Command{ - Use: "graph", + Use: "tkn-graph", Short: "Generate a graph of a Tekton object", - Long: "graph is a command-line tool for generating graphs from Tekton kind: Pipelines and kind: PipelineRuns.", + Long: "tkn-graph is a command-line tool for generating graphs from Tekton kind: Pipelines and kind: PipelineRuns.", Example: ` graph --namespace my-namespace --kind Pipeline --output-format dot graph --namespace my-namespace --kind PipelineRun --output-format puml graph --namespace my-namespace --kind Pipeline --output-format mmd --output-dir /tmp/output`, 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) + tektonClient, err := client.NewClient() 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() + namespace, err := tektonClient.GetNamespace() if err != nil { - log.Fatalf("Failed to get namespace from kubeconfig: %v", err) - } - if namespace == "" { - namespace = "default" + log.Fatalf("Failed to get namespace: %v", err) } options.Namespace = namespace } @@ -70,39 +49,33 @@ func main() { // Build the list of task graphs var graphs []*taskgraph.TaskGraph - switch options.ObjectKind { + switch options.TektonKind { case "Pipeline": - pipelines, err := tektonClient.TektonV1().Pipelines(options.Namespace).List(context.TODO(), v1.ListOptions{}) + pipelines, err := tektonClient.GetPipelines(options.Namespace) if err != nil { log.Fatalf("Failed to get Pipelines: %v", err) } - if len(pipelines.Items) == 0 { - log.Fatalf("No Pipelines found in namespace %s", options.Namespace) - } - for i := range pipelines.Items { - pipeline := &pipelines.Items[i] + for i := range pipelines { + pipeline := &pipelines[i] graph := taskgraph.BuildTaskGraph(pipeline.Spec.Tasks) graph.PipelineName = pipeline.Name graphs = append(graphs, graph) } case "PipelineRun": - pipelineRuns, err := tektonClient.TektonV1().PipelineRuns(options.Namespace).List(context.TODO(), v1.ListOptions{}) + pipelineRuns, err := tektonClient.GetPipelineRuns(options.Namespace) if err != nil { log.Fatalf("Failed to get PipelineRuns: %v", err) } - if len(pipelineRuns.Items) == 0 { - log.Fatalf("No PipelineRuns found in namespace %s", options.Namespace) - } - for i := range pipelineRuns.Items { - pipelineRun := &pipelineRuns.Items[i] + for i := range pipelineRuns { + pipelineRun := &pipelineRuns[i] graph := taskgraph.BuildTaskGraph(pipelineRun.Status.PipelineSpec.Tasks) graph.PipelineName = pipelineRun.Name graphs = append(graphs, graph) } default: - log.Fatalf("Invalid kind type: %s", options.ObjectKind) + log.Fatalf("Invalid kind type: %s", options.TektonKind) } // Generate graph for each object @@ -137,7 +110,7 @@ func main() { rootCmd.Flags().StringVar( &options.Namespace, "namespace", "", "the Kubernetes namespace to use. Will try to get namespace from KUBECONFIG if not specified then fallback to 'default'") rootCmd.Flags().StringVar( - &options.ObjectKind, "kind", "Pipeline", "the kind of the Tekton object to parse (Pipeline or PipelineRun)") + &options.TektonKind, "kind", "Pipeline", "the kind of the Tekton object to parse (Pipeline or PipelineRun)") rootCmd.Flags().StringVar( &options.OutputFormat, "output-format", "dot", "the output format (dot - DOT, puml - PlantUML or mmd - Mermaid)") rootCmd.Flags().StringVar( diff --git a/go.mod b/go.mod index 784198c..ddaeaa1 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/blendle/zapdriver v1.3.1 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/go-kit/log v0.2.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect diff --git a/pkg/client/tekton.go b/pkg/client/tekton.go new file mode 100644 index 0000000..af2dba6 --- /dev/null +++ b/pkg/client/tekton.go @@ -0,0 +1,76 @@ +package client + +import ( + "context" + "fmt" + "os" + + v1pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "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" +) + +type Client struct { + tektonClient versioned.Interface +} + +func NewClient() (*Client, error) { + // 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 { + return nil, fmt.Errorf("failed to get Kubernetes configuration: %w", err) + } + } + tektonClient, err := versioned.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create Tekton client: %w", err) + } + + return &Client{ + tektonClient: tektonClient, + }, nil +} + +func (c *Client) GetNamespace() (string, error) { + namespace, _, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), + &clientcmd.ConfigOverrides{}, + ).Namespace() + if err != nil { + return "", fmt.Errorf("failed to get namespace from kubeconfig: %w", err) + } + if namespace == "" { + namespace = "default" + } + return namespace, nil +} + +func (c *Client) GetPipelines(namespace string) ([]v1pipeline.Pipeline, error) { + pipelines, err := c.tektonClient.TektonV1().Pipelines(namespace).List(context.TODO(), v1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get Pipelines: %w", err) + } + if len(pipelines.Items) == 0 { + return nil, fmt.Errorf("no Pipelines found in namespace %s", namespace) + } + return pipelines.Items, nil +} + +func (c *Client) GetPipelineRuns(namespace string) ([]v1pipeline.PipelineRun, error) { + pipelineRuns, err := c.tektonClient.TektonV1().PipelineRuns(namespace).List(context.TODO(), v1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get PipelineRuns: %w", err) + } + if len(pipelineRuns.Items) == 0 { + return nil, fmt.Errorf("no PipelineRuns found in namespace %s", namespace) + } + return pipelineRuns.Items, nil +} diff --git a/pkg/client/tekton_test.go b/pkg/client/tekton_test.go new file mode 100644 index 0000000..28fa58c --- /dev/null +++ b/pkg/client/tekton_test.go @@ -0,0 +1,134 @@ +package client + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + v1pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + fakeclient "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + namespace = "test-namespace" + pipelineName = "test-pipeline" +) + +func TestClient_GetPipelinesNoError(t *testing.T) { + // Create a fake Tekton clientset for testing + fakeClient := fakeclient.NewSimpleClientset() + + expectedPipelines := []v1pipeline.Pipeline{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "pipeline-1", + Namespace: namespace, + }, + }, + { + ObjectMeta: v1.ObjectMeta{ + Name: "pipeline-2", + Namespace: namespace, + }, + }, + } + + // Add the test pipelines to the fake clientset + for i := range expectedPipelines { + _, err := fakeClient.TektonV1().Pipelines(namespace).Create(context.TODO(), &expectedPipelines[i], v1.CreateOptions{}) + if err != nil { + t.Fatalf("failed to create Pipeline: %v", err) + } + } + + // Create a client instance with the fake clientset + client := &Client{ + tektonClient: fakeClient, + } + + pipelines, err := client.GetPipelines(namespace) + assert.NoError(t, err) + // assert number of pipelines + assert.Equal(t, 2, len(pipelines)) + // Assert that the returned pipelines are the same as the expected pipelines + assert.Equal(t, expectedPipelines, pipelines) +} + +func TestClient_GetPipelinesError(t *testing.T) { + // Create a fake Tekton clientset for testing + fakeClient := fakeclient.NewSimpleClientset() + + // Create a client instance with the fake clientset + client := &Client{ + tektonClient: fakeClient, + } + + // Call the GetPipelines function + _, err := client.GetPipelines(namespace) + + // Assert that error occurred + assert.Error(t, err) + + // Assert that the error message is as expected + assert.Equal(t, "no Pipelines found in namespace test-namespace", err.Error()) +} + +func TestClient_GetPipelineRunsNoError(t *testing.T) { + // Create a fake Tekton clientset for testing + fakeClient := fakeclient.NewSimpleClientset() + + expectedPipelineRuns := []v1pipeline.PipelineRun{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "pipeline-1", + Namespace: namespace, + }, + }, + { + ObjectMeta: v1.ObjectMeta{ + Name: "pipeline-2", + Namespace: namespace, + }, + }, + } + + // Add the test pipelineruns to the fake clientset + for i := range expectedPipelineRuns { + _, err := fakeClient.TektonV1().PipelineRuns(namespace).Create(context.TODO(), &expectedPipelineRuns[i], v1.CreateOptions{}) + if err != nil { + t.Fatalf("failed to create PipelineRun: %v", err) + } + } + + // Create a client instance with the fake clientset + client := &Client{ + tektonClient: fakeClient, + } + + pipelineruns, err := client.GetPipelineRuns(namespace) + assert.NoError(t, err) + // assert number of pipelineruns + assert.Equal(t, 2, len(pipelineruns)) + // Assert that the returned pipelineruns are the same as the expected pipelineruns + assert.Equal(t, expectedPipelineRuns, pipelineruns) +} + +func TestClient_GetPipelineRunsError(t *testing.T) { + // Create a fake Tekton clientset for testing + fakeClient := fakeclient.NewSimpleClientset() + + // Create a client instance with the fake clientset + client := &Client{ + tektonClient: fakeClient, + } + + // Call the GetPipelines function + _, err := client.GetPipelineRuns(namespace) + + // Assert that error occurred + assert.Error(t, err) + + // Assert that the error message is as expected + assert.Equal(t, "no PipelineRuns found in namespace test-namespace", err.Error()) +}