@@ -5,18 +5,22 @@ import (
55 "context"
66 "encoding/json"
77 "fmt"
8- "github.com/docker/docker/api/types/container"
9- tc "github.com/testcontainers/testcontainers-go"
10- "golang.org/x/sync/errgroup"
118 "io"
129 "net/url"
1310 "os"
11+ "os/exec"
1412 "path/filepath"
1513 "regexp"
1614 "strings"
1715 "time"
1816
17+ "github.com/docker/docker/api/types/container"
1918 "github.com/pkg/errors"
19+ tc "github.com/testcontainers/testcontainers-go"
20+ "golang.org/x/sync/errgroup"
21+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+ "k8s.io/client-go/kubernetes"
23+ "k8s.io/client-go/rest"
2024
2125 "github.com/smartcontractkit/chainlink-testing-framework/lib/client"
2226 "github.com/smartcontractkit/chainlink-testing-framework/wasp"
@@ -38,14 +42,13 @@ type Report interface {
3842 Fetch () error
3943 // IsComparable checks whether both reports can be compared (e.g. test config is the same, app's resources are the same, queries or metrics used are the same, etc.), and returns a map of the differences and an error (if any difference is found)
4044 IsComparable (otherReport Report ) (bool , map [string ]string , error )
41- //// Compare compares two reports and returns a map of the differences and an error (if any difference is found). You should call it only after IsComparable returns true
42- //Compare(other Report) (bool, map[string]string, error)
4345}
4446
4547var directory = "performance_reports"
4648
4749type BasicReport struct {
48- TestName string `json:"test_name"`
50+ TestName string `json:"test_name"`
51+ // either k8s or docker
4952 ExecutionEnvironment ExecutionEnvironment `json:"execution_environment"`
5053
5154 // Test metrics
@@ -57,12 +60,13 @@ type BasicReport struct {
5760 GeneratorConfigs map [string ]* wasp.Config `json:"generator_configs"`
5861
5962 // AUT metrics
60- PodsResources map [string ]* PodResources `json:"pods_resources"`
61- ContainerResources map [string ]* DockerResources `json:"container_resources"`
62- ResourceSelectionPattern string `json:"resource_selection_pattern"`
63+ PodsResources map [string ]* PodResources `json:"pods_resources"`
64+ ContainerResources map [string ]* DockerResources `json:"container_resources"`
65+ // regex pattern to select the resources we want to fetch
66+ ResourceSelectionPattern string `json:"resource_selection_pattern"`
6367
6468 // Performance queries
65- // a map of name to query template, ex: avg(rate(cpu_usage_seconds_total[5m])) @ 1663915200
69+ // a map of name to query template, ex: "average cpu usage": " avg(rate(cpu_usage_seconds_total[5m]))"
6670 LokiQueries map [string ]string `json:"loki_queries"`
6771 // Performance queries results
6872 // can be anything, avg RPS, amount of errors, 95th percentile of CPU utilization, etc
@@ -81,8 +85,10 @@ type DockerResources struct {
8185}
8286
8387type PodResources struct {
84- CPU int64
85- Memory int64
88+ RequestsCPU int64
89+ RequestsMemory int64
90+ LimitsCPU int64
91+ LimitsMemory int64
8692}
8793
8894func (b * BasicReport ) Store () (string , error ) {
@@ -123,14 +129,19 @@ func (b *BasicReport) Load() error {
123129 return errors .New ("test name is empty. Please set it and try again" )
124130 }
125131
126- var reportFilePath string
127132 if b .CommitOrTag == "" {
128- // TODO load the latest one, but we need to think how to know which one is the latest one
129- // with tags it is easy, just use semver, but with commits not so straightforward, although we could probably comb the repo to find the commit and then its commit date
130- panic ("not implemented" )
131- } else {
132- reportFilePath = filepath .Join (directory , fmt .Sprintf ("%s-%s.json" , b .TestName , b .CommitOrTag ))
133+ tagsOrCommits , tagErr := extractTagsOrCommits (directory )
134+ if tagErr != nil {
135+ return tagErr
136+ }
137+
138+ latestCommit , commitErr := findLatestCommit (tagsOrCommits )
139+ if commitErr != nil {
140+ return commitErr
141+ }
142+ b .CommitOrTag = latestCommit
133143 }
144+ reportFilePath := filepath .Join (directory , fmt .Sprintf ("%s-%s.json" , b .TestName , b .CommitOrTag ))
134145
135146 reportFile , err := os .Open (reportFilePath )
136147 if err != nil {
@@ -145,6 +156,50 @@ func (b *BasicReport) Load() error {
145156 return nil
146157}
147158
159+ func extractTagsOrCommits (directory string ) ([]string , error ) {
160+ pattern := regexp .MustCompile (`.+-(.+)\.json$` )
161+
162+ files , err := os .ReadDir (directory )
163+ if err != nil {
164+ return nil , errors .Wrapf (err , "failed to read directory %s" , directory )
165+ }
166+
167+ var tagsOrCommits []string
168+
169+ for _ , file := range files {
170+ if file .IsDir () {
171+ continue
172+ }
173+
174+ matches := pattern .FindStringSubmatch (file .Name ())
175+ if len (matches ) == 2 {
176+ tagsOrCommits = append (tagsOrCommits , matches [1 ])
177+ }
178+ }
179+
180+ return tagsOrCommits , nil
181+ }
182+
183+ func findLatestCommit (references []string ) (string , error ) {
184+ refList := strings .Join (references , " " )
185+
186+ cmd := exec .Command ("git" , "rev-list" , "--topo-order" , "--date-order" , "-n" , "1" , refList )
187+
188+ var stdout , stderr bytes.Buffer
189+ cmd .Stdout = & stdout
190+ cmd .Stderr = & stderr
191+ if err := cmd .Run (); err != nil {
192+ return "" , fmt .Errorf ("failed to run git rev-list: %s, error: %v" , stderr .String (), err )
193+ }
194+
195+ latestCommit := strings .TrimSpace (stdout .String ())
196+ if latestCommit == "" {
197+ return "" , fmt .Errorf ("no latest commit found" )
198+ }
199+
200+ return latestCommit , nil
201+ }
202+
148203func (b * BasicReport ) Fetch () error {
149204 if len (b .LokiQueries ) == 0 {
150205 return errors .New ("there are no Loki queries, there's nothing to fetch. Please set them and try again" )
@@ -216,8 +271,10 @@ func (b *BasicReport) Fetch() error {
216271func (b * BasicReport ) fetchResources () error {
217272 //TODO in both cases we'd need to know some mask to filter out the resources we need
218273 if b .ExecutionEnvironment == ExecutionEnvironment_Docker {
219- // fetch docker resources
220- // get all containers and their resources
274+ err := b .fetchDockerResources ()
275+ if err != nil {
276+ return err
277+ }
221278 } else {
222279 // fetch k8s resources
223280 // get all pods and their resources
@@ -226,6 +283,42 @@ func (b *BasicReport) fetchResources() error {
226283 return nil
227284}
228285
286+ func (b * BasicReport ) fetchK8sResources () error {
287+ config , err := rest .InClusterConfig ()
288+ if err != nil {
289+ return errors .Wrapf (err , "failed to get in-cluster config, are you sure this is running in a k8s cluster?" )
290+ }
291+
292+ clientset , err := kubernetes .NewForConfig (config )
293+ if err != nil {
294+ return errors .Wrapf (err , "failed to create k8s clientset" )
295+ }
296+
297+ namespaceFile := "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
298+ namespace , err := os .ReadFile (namespaceFile )
299+ if err != nil {
300+ return errors .Wrapf (err , "failed to read namespace file %s" , namespaceFile )
301+ }
302+
303+ pods , err := clientset .CoreV1 ().Pods (string (namespace )).List (context .TODO (), metav1.ListOptions {})
304+ if err != nil {
305+ panic (err )
306+ }
307+
308+ b .PodsResources = make (map [string ]* PodResources )
309+
310+ for _ , pod := range pods .Items {
311+ b .PodsResources [pod .Name ] = & PodResources {
312+ RequestsCPU : pod .Spec .Containers [0 ].Resources .Requests .Cpu ().MilliValue (),
313+ RequestsMemory : pod .Spec .Containers [0 ].Resources .Requests .Memory ().Value (),
314+ LimitsCPU : pod .Spec .Containers [0 ].Resources .Limits .Cpu ().MilliValue (),
315+ LimitsMemory : pod .Spec .Containers [0 ].Resources .Limits .Memory ().Value (),
316+ }
317+ }
318+
319+ return nil
320+ }
321+
229322func (b * BasicReport ) fetchDockerResources () error {
230323 provider , err := tc .NewDockerProvider ()
231324 if err != nil {
@@ -457,64 +550,3 @@ func mustMarshallSegment(segment *wasp.Segment) string {
457550
458551 return string (segmentBytes )
459552}
460-
461- //type DifferentResult struct {
462- // Expected []string
463- // Actual []string
464- //}
465- //
466- //// this could also just be a generic var, so we don't couple Result with this struct
467- //type ComparisonReport struct {
468- // missingResults []string
469- // unexpectedResults []string
470- // differentResults map[string]DifferentResult
471- //}
472- //
473- //func (c *ComparisonReport) HasDifferences() bool {
474- // return len(c.missingResults) > 0 || len(c.unexpectedResults) > 0 || len(c.differentResults) > 0
475- //}
476- //
477- //func (c *ComparisonReport) MissingResults() []string {
478- // return c.missingResults
479- //}
480- //
481- //func (c *ComparisonReport) UnexpectedResults() []string {
482- // return c.unexpectedResults
483- //}
484- //
485- //func (c *ComparisonReport) DifferentResults() map[string]DifferentResult {
486- // return c.differentResults
487- //}
488- //
489- //func (b *BasicReport) Compare(other BasicReport) (bool, ComparisonReport, error) {
490- // // check if there are no errors
491- //
492- // // check if results are the same
493- // if len(b.Results) != len(other.Results) {
494- // return false, ComparisonReport{}, fmt.Errorf("result count is different. Expected %d, got %d", len(b.Results), len(other.Results))
495- // }
496- //
497- // comparisonReport := ComparisonReport{
498- // differentResults: make(map[string]DifferentResult),
499- // }
500- //
501- // for name, result := range b.Results {
502- // if otherResult, ok := other.Results[name]; !ok {
503- // comparisonReport.missingResults = append(comparisonReport.missingResults, name)
504- // } else {
505- //
506- // comparisonReport.differentResults[name] = DifferentResult{
507- // Expected: result,
508- // Actual: otherResult,
509- // }
510- // }
511- // }
512- //
513- // for name, otherResult := range other.Results {
514- // if _, ok := b.Results[name]; !ok {
515- // comparisonReport.unexpectedResults[name] = otherResult
516- // }
517- // }
518- //
519- // return comparisonReport.HasDifferences(), comparisonReport, nil
520- //}
0 commit comments