Skip to content

Commit 531055e

Browse files
authored
feat: Add objects inlining and separating utilities (#14)
Many of our objects support two flavors of dependencies, we can either inline the dependency object OR we can define it as a separate entity and reuse it freely, referencing it by name. While this feature gives freedom to the end users it comes with a toil for implementations as they need to take care of the object reference resolution. The goal of this PR is to introduce utility functions which would allow both inlining of referenced objects and separating inlined objects definitions.
1 parent b326672 commit 531055e

25 files changed

+1351
-18
lines changed

internal/assert/assert.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package assert
33
import (
44
"fmt"
55
"reflect"
6+
"strings"
67
"testing"
78
)
89

@@ -19,9 +20,13 @@ func Require(t *testing.T, isPassing bool) {
1920
}
2021

2122
// Equal fails the test if the expected and actual values are not equal.
22-
func Equal(t *testing.T, expected, actual interface{}) bool {
23+
func Equal(t *testing.T, expected, actual any) bool {
2324
t.Helper()
2425
if !areEqual(expected, actual) {
26+
isMultiline := strings.Contains(fmt.Sprint(expected), "\n")
27+
if isMultiline {
28+
return fail(t, "Expected:\n%v\n%s\nActual:\n%v", expected, strings.Repeat("-", 30), actual)
29+
}
2530
return fail(t, "Expected: %v, actual: %v", expected, actual)
2631
}
2732
return true
@@ -46,7 +51,7 @@ func False(t *testing.T, actual bool) bool {
4651
}
4752

4853
// Len fails the test if the value is not of the expected length.
49-
func Len(t *testing.T, v interface{}, length int) bool {
54+
func Len(t *testing.T, v any, length int) bool {
5055
t.Helper()
5156
actual, err := getLen(v)
5257
if err != nil {
@@ -85,7 +90,7 @@ func NotEmpty(t *testing.T, v any) bool {
8590
return true
8691
}
8792

88-
func areEqual(expected, actual interface{}) bool {
93+
func areEqual(expected, actual any) bool {
8994
if expected == nil || actual == nil {
9095
return expected == actual
9196
}
@@ -95,7 +100,7 @@ func areEqual(expected, actual interface{}) bool {
95100
return true
96101
}
97102

98-
func getLen(v interface{}) (int, error) {
103+
func getLen(v any) (int, error) {
99104
rv := reflect.ValueOf(v)
100105
switch rv.Kind() {
101106
case reflect.Slice, reflect.Map, reflect.String:
@@ -105,7 +110,7 @@ func getLen(v interface{}) (int, error) {
105110
}
106111
}
107112

108-
func isEmpty(v interface{}) bool {
113+
func isEmpty(v any) bool {
109114
if v == nil {
110115
return true
111116
}
@@ -125,7 +130,7 @@ func isEmpty(v interface{}) bool {
125130
}
126131
}
127132

128-
func fail(t *testing.T, msg string, a ...interface{}) bool {
133+
func fail(t *testing.T, msg string, a ...any) bool {
129134
t.Helper()
130135
t.Errorf(msg, a...)
131136
return false

pkg/openslo/v1/alert_policy.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ type AlertPolicySpec struct {
5151
AlertWhenNoData bool `json:"alertWhenNoData,omitempty"`
5252
AlertWhenBreaching bool `json:"alertWhenBreaching,omitempty"`
5353
AlertWhenResolved bool `json:"alertWhenResolved,omitempty"`
54-
Conditions []AlertPolicyCondition `json:"conditions"`
55-
NotificationTargets []AlertPolicyNotificationTarget `json:"notificationTargets"`
54+
Conditions []AlertPolicyCondition `json:"conditions,omitempty"`
55+
NotificationTargets []AlertPolicyNotificationTarget `json:"notificationTargets,omitempty"`
5656
}
5757

5858
type AlertPolicyCondition struct {

pkg/openslo/v1/slo.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ type SLOAlertPolicyInline struct {
128128
}
129129

130130
type SLOAlertPolicyRef struct {
131-
Ref string `json:"alertPolicyRef"`
131+
AlertPolicyRef string `json:"alertPolicyRef"`
132132
}
133133

134134
var sloValidation = govy.New(
@@ -245,7 +245,7 @@ var sloAlertPolicyValidation = govy.New(
245245
return a.SLOAlertPolicyRef
246246
}).
247247
Include(govy.New(
248-
govy.For(func(ref SLOAlertPolicyRef) string { return ref.Ref }).
248+
govy.For(func(ref SLOAlertPolicyRef) string { return ref.AlertPolicyRef }).
249249
WithName("alertPolicyRef").
250250
Required().
251251
Rules(rules.StringDNSLabel()),

pkg/openslo/v1/slo_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ func TestSLO_Validate_Spec_AlertPolicies(t *testing.T) {
421421
t.Run("invalid condition ref", func(t *testing.T) {
422422
slo := validSLO()
423423
slo.Spec.AlertPolicies[0].SLOAlertPolicyRef = &SLOAlertPolicyRef{
424-
Ref: "invalid ref",
424+
AlertPolicyRef: "invalid ref",
425425
}
426426
err := slo.Validate()
427427
govytest.AssertError(t, err, govytest.ExpectedRuleError{
@@ -571,7 +571,7 @@ func validSLO() SLO {
571571
},
572572
},
573573
AlertPolicies: []SLOAlertPolicy{
574-
{SLOAlertPolicyRef: &SLOAlertPolicyRef{Ref: "alert-policy-1"}},
574+
{SLOAlertPolicyRef: &SLOAlertPolicyRef{AlertPolicyRef: "alert-policy-1"}},
575575
},
576576
},
577577
)

pkg/openslo/v2alpha/alert_policy.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ type AlertPolicySpec struct {
5151
AlertWhenNoData bool `json:"alertWhenNoData,omitempty"`
5252
AlertWhenBreaching bool `json:"alertWhenBreaching,omitempty"`
5353
AlertWhenResolved bool `json:"alertWhenResolved,omitempty"`
54-
Conditions []AlertPolicyCondition `json:"conditions"`
55-
NotificationTargets []AlertPolicyNotificationTarget `json:"notificationTargets"`
54+
Conditions []AlertPolicyCondition `json:"conditions,omitempty"`
55+
NotificationTargets []AlertPolicyNotificationTarget `json:"notificationTargets,omitempty"`
5656
}
5757

5858
type AlertPolicyCondition struct {

pkg/openslosdk/reference_exporter.go

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package openslosdk
2+
3+
import (
4+
"sync"
5+
6+
"github.com/OpenSLO/go-sdk/pkg/openslo"
7+
v1 "github.com/OpenSLO/go-sdk/pkg/openslo/v1"
8+
)
9+
10+
func NewReferenceExporter(objects ...openslo.Object) *ReferenceExporter {
11+
return &ReferenceExporter{
12+
objects: objects,
13+
exported: make([]openslo.Object, 0, len(objects)),
14+
}
15+
}
16+
17+
type ReferenceExporter struct {
18+
objects []openslo.Object
19+
exported []openslo.Object
20+
once sync.Once
21+
}
22+
23+
// Export replaces all the inlined objects with references and returns
24+
// the original objects along with the exported, previously inlined, objects.
25+
func (r *ReferenceExporter) Export() []openslo.Object {
26+
r.once.Do(func() {
27+
r.exported = r.exportObjects()
28+
})
29+
return r.exported
30+
}
31+
32+
func (r *ReferenceExporter) exportObjects() []openslo.Object {
33+
for _, object := range r.objects {
34+
r.exportObject(object)
35+
}
36+
return r.exported
37+
}
38+
39+
func (r *ReferenceExporter) exportObject(object openslo.Object) {
40+
version := object.GetVersion()
41+
switch version {
42+
case openslo.VersionV1:
43+
r.addResult(r.exportV1Object(object)...)
44+
default:
45+
r.addResult(object)
46+
}
47+
}
48+
49+
func (r *ReferenceExporter) exportV1Object(object openslo.Object) []openslo.Object {
50+
switch v := object.(type) {
51+
case v1.AlertPolicy:
52+
return r.exportV1AlertPolicy(v)
53+
case v1.SLO:
54+
return r.exportV1SLO(v)
55+
default:
56+
return []openslo.Object{object}
57+
}
58+
}
59+
60+
func (r *ReferenceExporter) exportV1AlertPolicy(alertPolicy v1.AlertPolicy) []openslo.Object {
61+
exported := make([]openslo.Object, 0)
62+
for i, target := range alertPolicy.Spec.NotificationTargets {
63+
if target.AlertPolicyNotificationTargetInline == nil {
64+
continue
65+
}
66+
exported = append(exported, v1.NewAlertNotificationTarget(target.Metadata, target.Spec))
67+
target.AlertPolicyNotificationTargetRef = &v1.AlertPolicyNotificationTargetRef{
68+
TargetRef: target.Metadata.Name,
69+
}
70+
target.AlertPolicyNotificationTargetInline = nil
71+
alertPolicy.Spec.NotificationTargets[i] = target
72+
}
73+
for i, condition := range alertPolicy.Spec.Conditions {
74+
if condition.AlertPolicyConditionInline == nil {
75+
continue
76+
}
77+
exported = append(exported, v1.NewAlertCondition(condition.Metadata, condition.Spec))
78+
condition.AlertPolicyConditionRef = &v1.AlertPolicyConditionRef{
79+
ConditionRef: condition.Metadata.Name,
80+
}
81+
condition.AlertPolicyConditionInline = nil
82+
alertPolicy.Spec.Conditions[i] = condition
83+
}
84+
return append([]openslo.Object{alertPolicy}, exported...)
85+
}
86+
87+
func (r *ReferenceExporter) exportV1SLO(slo v1.SLO) []openslo.Object {
88+
exported := make([]openslo.Object, 0)
89+
for i, ap := range slo.Spec.AlertPolicies {
90+
if ap.SLOAlertPolicyInline == nil {
91+
continue
92+
}
93+
alertPolicy := v1.NewAlertPolicy(ap.Metadata, ap.Spec)
94+
exported = append(exported, r.exportV1AlertPolicy(alertPolicy)...)
95+
ap.SLOAlertPolicyRef = &v1.SLOAlertPolicyRef{
96+
AlertPolicyRef: alertPolicy.Metadata.Name,
97+
}
98+
ap.SLOAlertPolicyInline = nil
99+
slo.Spec.AlertPolicies[i] = ap
100+
}
101+
if slo.Spec.Indicator != nil {
102+
exported = append(exported, v1.NewSLI(slo.Spec.Indicator.Metadata, slo.Spec.Indicator.Spec))
103+
slo.Spec.IndicatorRef = &slo.Spec.Indicator.Metadata.Name
104+
slo.Spec.Indicator = nil
105+
}
106+
return append([]openslo.Object{slo}, exported...)
107+
}
108+
109+
func (r *ReferenceExporter) addResult(objects ...openslo.Object) {
110+
r.exported = append(r.exported, objects...)
111+
}
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package openslosdk
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/OpenSLO/go-sdk/internal"
10+
"github.com/OpenSLO/go-sdk/internal/assert"
11+
)
12+
13+
func TestReferenceExporter_Export(t *testing.T) {
14+
root := internal.FindModuleRoot()
15+
testDataPath := filepath.Join(root, "pkg", "openslosdk", "test_data", "export")
16+
17+
tests := map[string]struct {
18+
filename string
19+
}{
20+
"v1: Alert Policies": {
21+
filename: "v1_alert_policies.yaml",
22+
},
23+
"v1: SLO": {
24+
filename: "v1_slo.yaml",
25+
},
26+
}
27+
28+
for name, test := range tests {
29+
t.Run(name, func(t *testing.T) {
30+
// Read input.
31+
inputPath := filepath.Join(testDataPath, "inputs", test.filename)
32+
inputFileData, err := os.ReadFile(inputPath)
33+
assert.Require(t, assert.NoError(t, err))
34+
inputObjects, err := Decode(bytes.NewReader(inputFileData), FormatYAML)
35+
assert.Require(t, assert.NoError(t, err))
36+
err = Validate(inputObjects...)
37+
assert.Require(t, assert.NoError(t, err))
38+
39+
// Inline objects.
40+
exporter := NewReferenceExporter(inputObjects...)
41+
inlinedObjects := exporter.Export()
42+
assert.Require(t, assert.NotEmpty(t, inlinedObjects))
43+
44+
// Read output.
45+
outputPath := filepath.Join(testDataPath, "outputs", test.filename)
46+
outputsFileData, err := os.ReadFile(outputPath)
47+
assert.Require(t, assert.NoError(t, err))
48+
outputObjects, err := Decode(bytes.NewReader(outputsFileData), FormatYAML)
49+
assert.Require(t, assert.NoError(t, err))
50+
err = Validate(outputObjects...)
51+
assert.Require(t, assert.NoError(t, err))
52+
53+
// Check.
54+
err = Validate(inlinedObjects...)
55+
assert.Require(t, assert.NoError(t, err))
56+
var buf bytes.Buffer
57+
err = Encode(&buf, FormatYAML, inlinedObjects...)
58+
assert.Require(t, assert.NoError(t, err))
59+
assert.Equal(t, string(outputsFileData), buf.String())
60+
})
61+
}
62+
}

0 commit comments

Comments
 (0)