Skip to content

Commit

Permalink
Copy pkg/config to pkg/configv1
Browse files Browse the repository at this point in the history
Signed-off-by: Shinnosuke Sawada-Dazai <[email protected]>
  • Loading branch information
Warashi committed Sep 27, 2024
1 parent 1689c63 commit e75d3e1
Show file tree
Hide file tree
Showing 74 changed files with 9,548 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/labeler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ area/tool:

area/pipedv1:
- pkg/app/pipedv1/**/*
- pkg/configv1/*
- pkg/configv1/**/*
178 changes: 178 additions & 0 deletions pkg/configv1/analysis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright 2024 The PipeCD Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package config

import (
"fmt"
"strconv"
"strings"
)

const (
AnalysisStrategyThreshold = "THRESHOLD"
AnalysisStrategyPrevious = "PREVIOUS"
AnalysisStrategyCanaryBaseline = "CANARY_BASELINE"
AnalysisStrategyCanaryPrimary = "CANARY_PRIMARY"

AnalysisDeviationEither = "EITHER"
AnalysisDeviationHigh = "HIGH"
AnalysisDeviationLow = "LOW"
)

// AnalysisMetrics contains common configurable values for deployment analysis with metrics.
type AnalysisMetrics struct {
// The strategy name. One of THRESHOLD or PREVIOUS or CANARY_BASELINE or CANARY_PRIMARY is available.
// Defaults to THRESHOLD.
Strategy string `json:"strategy" default:"THRESHOLD"`
// The unique name of provider defined in the Piped Configuration.
// Required field.
Provider string `json:"provider"`
// A query performed against the Analysis Provider.
// Required field.
Query string `json:"query"`
// The expected query result.
// Required field for the THRESHOLD strategy.
Expected AnalysisExpected `json:"expected"`
// Run a query at this intervals.
// Required field.
Interval Duration `json:"interval"`
// Acceptable number of failures. For instance, If 1 is set,
// the analysis will be considered a failure after 2 failures.
// Default is 0.
FailureLimit int `json:"failureLimit"`
// If true, it considers as a success when no data returned from the analysis provider.
// Default is false.
SkipOnNoData bool `json:"skipOnNoData"`
// How long after which the query times out.
// Default is 30s.
Timeout Duration `json:"timeout" default:"30s"`

// The stage fails on deviation in the specified direction. One of LOW or HIGH or EITHER is available.
// This can be used only for PREVIOUS, CANARY_BASELINE or CANARY_PRIMARY. Defaults to EITHER.
Deviation string `json:"deviation" default:"EITHER"`
// The custom arguments to be populated for the Canary query.
// They can be referred as {{ .VariantArgs.xxx }}.
CanaryArgs map[string]string `json:"canaryArgs"`
// The custom arguments to be populated for the Baseline query.
// They can be referred as {{ .VariantArgs.xxx }}.
BaselineArgs map[string]string `json:"baselineArgs"`
// The custom arguments to be populated for the Primary query.
// They can be referred as {{ .VariantArgs.xxx }}.
PrimaryArgs map[string]string `json:"primaryArgs"`
}

func (m *AnalysisMetrics) Validate() error {
if m.Provider == "" {
return fmt.Errorf("missing \"provider\" field")
}

Check warning on line 79 in pkg/configv1/analysis.go

View check run for this annotation

Codecov / codecov/patch

pkg/configv1/analysis.go#L78-L79

Added lines #L78 - L79 were not covered by tests
if m.Query == "" {
return fmt.Errorf("missing \"query\" field")
}

Check warning on line 82 in pkg/configv1/analysis.go

View check run for this annotation

Codecov / codecov/patch

pkg/configv1/analysis.go#L81-L82

Added lines #L81 - L82 were not covered by tests
if m.Interval == 0 {
return fmt.Errorf("missing \"interval\" field")
}

Check warning on line 85 in pkg/configv1/analysis.go

View check run for this annotation

Codecov / codecov/patch

pkg/configv1/analysis.go#L84-L85

Added lines #L84 - L85 were not covered by tests
if m.Deviation != AnalysisDeviationEither && m.Deviation != AnalysisDeviationHigh && m.Deviation != AnalysisDeviationLow {
return fmt.Errorf("\"deviation\" have to be one of %s, %s or %s", AnalysisDeviationEither, AnalysisDeviationHigh, AnalysisDeviationLow)
}

Check warning on line 88 in pkg/configv1/analysis.go

View check run for this annotation

Codecov / codecov/patch

pkg/configv1/analysis.go#L87-L88

Added lines #L87 - L88 were not covered by tests
return nil
}

// AnalysisExpected defines the range used for metrics analysis.
type AnalysisExpected struct {
Min *float64 `json:"min"`
Max *float64 `json:"max"`
}

func (e *AnalysisExpected) Validate() error {
if e.Min == nil && e.Max == nil {
return fmt.Errorf("expected range is undefined")
}
return nil

Check warning on line 102 in pkg/configv1/analysis.go

View check run for this annotation

Codecov / codecov/patch

pkg/configv1/analysis.go#L98-L102

Added lines #L98 - L102 were not covered by tests
}

// InRange returns true if the given value is within the range.
func (e *AnalysisExpected) InRange(value float64) bool {
if e.Min != nil && *e.Min > value {
return false
}
if e.Max != nil && *e.Max < value {
return false
}
return true

Check warning on line 113 in pkg/configv1/analysis.go

View check run for this annotation

Codecov / codecov/patch

pkg/configv1/analysis.go#L106-L113

Added lines #L106 - L113 were not covered by tests
}

func (e *AnalysisExpected) String() string {
if e.Min == nil && e.Max == nil {
return ""
}

var b strings.Builder
if e.Min != nil {
min := strconv.FormatFloat(*e.Min, 'f', -1, 64)
b.WriteString(min + " ")
}

b.WriteString("<=")

if e.Max != nil {
max := strconv.FormatFloat(*e.Max, 'f', -1, 64)
b.WriteString(" " + max)
}
return b.String()
}

// AnalysisLog contains common configurable values for deployment analysis with log.
type AnalysisLog struct {
Query string `json:"query"`
Interval Duration `json:"interval"`
// Maximum number of failed checks before the query result is considered as failure.
FailureLimit int `json:"failureLimit"`
// If true, it considers as success when no data returned from the analysis provider.
// Default is false.
SkipOnNoData bool `json:"skipOnNoData"`
// How long after which the query times out.
Timeout Duration `json:"timeout"`
Provider string `json:"provider"`
}

func (a *AnalysisLog) Validate() error {
return nil
}

// AnalysisHTTP contains common configurable values for deployment analysis with http.
type AnalysisHTTP struct {
URL string `json:"url"`
Method string `json:"method"`
// Custom headers to set in the request. HTTP allows repeated headers.
Headers []AnalysisHTTPHeader `json:"headers"`
ExpectedCode int `json:"expectedCode"`
ExpectedResponse string `json:"expectedResponse"`
Interval Duration `json:"interval"`
// Maximum number of failed checks before the response is considered as failure.
FailureLimit int `json:"failureLimit"`
// If true, it considers as success when no data returned from the analysis provider.
// Default is false.
SkipOnNoData bool `json:"skipOnNoData"`
Timeout Duration `json:"timeout"`
}

func (a *AnalysisHTTP) Validate() error {
return nil
}

type AnalysisHTTPHeader struct {
Key string `json:"key"`
Value string `json:"value"`
}
63 changes: 63 additions & 0 deletions pkg/configv1/analysis_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2024 The PipeCD Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package config

import (
"fmt"
"os"
"path/filepath"
)

type AnalysisTemplateSpec struct {
Metrics map[string]AnalysisMetrics `json:"metrics"`
Logs map[string]AnalysisLog `json:"logs"`
HTTPS map[string]AnalysisHTTP `json:"https"`
}

// LoadAnalysisTemplate finds the config file for the analysis template in the .pipe
// directory first up. And returns parsed config, ErrNotFound is returned if not found.
func LoadAnalysisTemplate(repoRoot string) (*AnalysisTemplateSpec, error) {
dir := filepath.Join(repoRoot, SharedConfigurationDirName)
files, err := os.ReadDir(dir)
if os.IsNotExist(err) {
return nil, ErrNotFound
}

Check warning on line 36 in pkg/configv1/analysis_template.go

View check run for this annotation

Codecov / codecov/patch

pkg/configv1/analysis_template.go#L35-L36

Added lines #L35 - L36 were not covered by tests
if err != nil {
return nil, fmt.Errorf("failed to read %s: %w", dir, err)
}

Check warning on line 39 in pkg/configv1/analysis_template.go

View check run for this annotation

Codecov / codecov/patch

pkg/configv1/analysis_template.go#L38-L39

Added lines #L38 - L39 were not covered by tests

for _, f := range files {
if f.IsDir() {
continue

Check warning on line 43 in pkg/configv1/analysis_template.go

View check run for this annotation

Codecov / codecov/patch

pkg/configv1/analysis_template.go#L43

Added line #L43 was not covered by tests
}
ext := filepath.Ext(f.Name())
if ext != ".yaml" && ext != ".yml" && ext != ".json" {
continue
}
path := filepath.Join(dir, f.Name())
cfg, err := LoadFromYAML(path)
if err != nil {
return nil, fmt.Errorf("failed to load config file %s: %w", path, err)
}

Check warning on line 53 in pkg/configv1/analysis_template.go

View check run for this annotation

Codecov / codecov/patch

pkg/configv1/analysis_template.go#L52-L53

Added lines #L52 - L53 were not covered by tests
if cfg.Kind == KindAnalysisTemplate {
return cfg.AnalysisTemplateSpec, nil
}
}
return nil, ErrNotFound

Check warning on line 58 in pkg/configv1/analysis_template.go

View check run for this annotation

Codecov / codecov/patch

pkg/configv1/analysis_template.go#L58

Added line #L58 was not covered by tests
}

func (s *AnalysisTemplateSpec) Validate() error {
return nil
}
110 changes: 110 additions & 0 deletions pkg/configv1/analysis_template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2024 The PipeCD Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package config

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLoadAnalysisTemplate(t *testing.T) {
testcases := []struct {
name string
repoDir string
expectedSpec interface{}
expectedError error
}{
{
name: "Load analysis template successfully",
repoDir: "testdata",
expectedSpec: &AnalysisTemplateSpec{
Metrics: map[string]AnalysisMetrics{
"app_http_error_percentage": {
Strategy: AnalysisStrategyThreshold,
Query: "http_error_percentage{env={{ .App.Env }}, app={{ .App.Name }}}",
Expected: AnalysisExpected{Max: floatPointer(0.1)},
Interval: Duration(time.Minute),
Timeout: Duration(30 * time.Second),
Provider: "datadog-dev",
Deviation: AnalysisDeviationEither,
},
"container_cpu_usage_seconds_total": {
Strategy: AnalysisStrategyThreshold,
Query: `sum(
max(kube_pod_labels{label_app=~"{{ .App.Name }}", label_pipecd_dev_variant=~"canary"}) by (label_app, label_pipecd_dev_variant, pod)
*
on(pod)
group_right(label_app, label_pipecd_dev_variant)
label_replace(
sum by(pod_name) (
rate(container_cpu_usage_seconds_total{namespace="default"}[5m])
), "pod", "$1", "pod_name", "(.+)"
)
) by (label_app, label_pipecd_dev_variant)
`,
Expected: AnalysisExpected{Max: floatPointer(0.0001)},
FailureLimit: 2,
Interval: Duration(10 * time.Second),
Timeout: Duration(30 * time.Second),
Provider: "prometheus-dev",
Deviation: AnalysisDeviationEither,
},
"grpc_error_rate-percentage": {
Strategy: AnalysisStrategyThreshold,
Query: `100 - sum(
rate(
grpc_server_handled_total{
grpc_code!="OK",
kubernetes_namespace="{{ .Args.namespace }}",
kubernetes_pod_name=~"{{ .App.Name }}-[0-9a-zA-Z]+(-[0-9a-zA-Z]+)"
}[{{ .Args.interval }}]
)
)
/
sum(
rate(
grpc_server_started_total{
kubernetes_namespace="{{ .Args.namespace }}",
kubernetes_pod_name=~"{{ .App.Name }}-[0-9a-zA-Z]+(-[0-9a-zA-Z]+)"
}[{{ .Args.interval }}]
)
) * 100
`,
Expected: AnalysisExpected{Max: floatPointer(10)},
FailureLimit: 1,
Interval: Duration(time.Minute),
Timeout: Duration(30 * time.Second),
Provider: "prometheus-dev",
Deviation: AnalysisDeviationEither,
},
},
},
expectedError: nil,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
spec, err := LoadAnalysisTemplate(tc.repoDir)
require.Equal(t, tc.expectedError, err)
if err == nil {
assert.Equal(t, tc.expectedSpec, spec)
}
})
}
}
Loading

0 comments on commit e75d3e1

Please sign in to comment.