-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PC-10133: Move Project to separate pkg and add validation POC (#138)
* add validation poc * further refine the validation example * current progress * simplify rule logic * remove v1alpha aliases * add errors tests * add rules tests * add strings tests * make sure labels testing is deterministic * invert dependency between project and v1alpha * facilitate recent suggestions and feedback * rename object validation to struct * fix error handling * change labels validation rule name * let compiler infer the type * declare the function once * fix acccidental typo
- Loading branch information
1 parent
6be298d
commit a001f6e
Showing
48 changed files
with
1,054 additions
and
107 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package examples | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"net/http/httptest" | ||
"net/url" | ||
|
||
"github.com/goccy/go-yaml" | ||
|
||
"github.com/nobl9/nobl9-go/sdk" | ||
) | ||
|
||
// GetOfflineEchoClient creates an offline (local mock server) sdk.Client without auth (DisableOkta option). | ||
// It is used exclusively for running code examples without internet connection or valid Nobl9 credentials. | ||
// The body received by the server is decoded to JSON, converted to YAML and printed to stdout. | ||
func GetOfflineEchoClient() *sdk.Client { | ||
// Offline server: | ||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
objects, err := sdk.ReadObjectsFromSources(r.Context(), sdk.NewObjectSourceReader(r.Body, "")) | ||
if err != nil { | ||
panic(err) | ||
} | ||
data, err := yaml.Marshal(objects[0]) | ||
if err != nil { | ||
panic(err) | ||
} | ||
fmt.Println(string(data)) | ||
})) | ||
// Create sdk.Client: | ||
u, _ := url.Parse(srv.URL) | ||
config := &sdk.Config{DisableOkta: true, URL: u} | ||
client, err := sdk.NewClient(config) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return client | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
package v1alpha | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/pkg/errors" | ||
|
||
"github.com/nobl9/nobl9-go/manifest" | ||
"github.com/nobl9/nobl9-go/validation" | ||
) | ||
|
||
func NewObjectError(object manifest.Object, errs []error) error { | ||
oErr := &ObjectError{ | ||
Object: ObjectMetadata{ | ||
Kind: object.GetKind(), | ||
Name: object.GetName(), | ||
}, | ||
Errors: errs, | ||
} | ||
if v, ok := object.(ObjectContext); ok { | ||
oErr.Object.Source = v.GetManifestSource() | ||
} | ||
if v, ok := object.(manifest.ProjectScopedObject); ok { | ||
oErr.Object.IsProjectScoped = true | ||
oErr.Object.Project = v.GetProject() | ||
} | ||
return oErr | ||
} | ||
|
||
type ObjectError struct { | ||
Object ObjectMetadata `json:"object"` | ||
Errors []error `json:"errors"` | ||
} | ||
|
||
type ObjectMetadata struct { | ||
Kind manifest.Kind `json:"kind"` | ||
Name string `json:"name"` | ||
Source string `json:"source"` | ||
IsProjectScoped bool `json:"isProjectScoped"` | ||
Project string `json:"project,omitempty"` | ||
} | ||
|
||
func (o *ObjectError) Error() string { | ||
b := new(strings.Builder) | ||
b.WriteString(fmt.Sprintf("Validation for %s '%s'", o.Object.Kind, o.Object.Name)) | ||
if o.Object.IsProjectScoped { | ||
b.WriteString(" in project '" + o.Object.Project + "'") | ||
} | ||
b.WriteString(" has failed for the following fields:\n") | ||
validation.JoinErrors(b, o.Errors, strings.Repeat(" ", 2)) | ||
if o.Object.Source != "" { | ||
b.WriteString("\nManifest source: ") | ||
b.WriteString(o.Object.Source) | ||
} | ||
return b.String() | ||
} | ||
|
||
func (o *ObjectError) MarshalJSON() ([]byte, error) { | ||
var errs []json.RawMessage | ||
for _, oErr := range o.Errors { | ||
switch v := oErr.(type) { | ||
case validation.FieldError, *validation.FieldError: | ||
data, err := json.Marshal(v) | ||
if err != nil { | ||
return nil, err | ||
} | ||
errs = append(errs, data) | ||
default: | ||
data, err := json.Marshal(oErr.Error()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
errs = append(errs, data) | ||
} | ||
} | ||
return json.Marshal(struct { | ||
Object ObjectMetadata `json:"object"` | ||
Errors []json.RawMessage `json:"errors"` | ||
}{ | ||
Object: o.Object, | ||
Errors: errs, | ||
}) | ||
} | ||
|
||
func (o *ObjectError) UnmarshalJSON(bytes []byte) error { | ||
var intermediate struct { | ||
Object ObjectMetadata `json:"object"` | ||
Errors []json.RawMessage `json:"errors"` | ||
} | ||
if err := json.Unmarshal(bytes, &intermediate); err != nil { | ||
return err | ||
} | ||
o.Object = intermediate.Object | ||
for _, rawErr := range intermediate.Errors { | ||
if len(rawErr) > 0 && rawErr[0] == '{' { | ||
var fErr validation.FieldError | ||
if err := json.Unmarshal(rawErr, &fErr); err != nil { | ||
return err | ||
} | ||
o.Errors = append(o.Errors, fErr) | ||
} else { | ||
var stringErr string | ||
if err := json.Unmarshal(rawErr, &stringErr); err != nil { | ||
return err | ||
} | ||
o.Errors = append(o.Errors, errors.New(stringErr)) | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package v1alpha | ||
|
||
import ( | ||
"embed" | ||
"encoding/json" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/nobl9/nobl9-go/manifest" | ||
"github.com/nobl9/nobl9-go/validation" | ||
) | ||
|
||
//go:embed test_data/errors | ||
var errorsTestData embed.FS | ||
|
||
func TestObjectError(t *testing.T) { | ||
errs := []error{ | ||
validation.FieldError{ | ||
FieldPath: "metadata.name", | ||
FieldValue: "default", | ||
Errors: []string{"here's an error"}, | ||
}, | ||
validation.FieldError{ | ||
FieldPath: "spec.description", | ||
FieldValue: "some long description", | ||
Errors: []string{"here's another error"}, | ||
}, | ||
} | ||
|
||
t.Run("non project scoped object", func(t *testing.T) { | ||
err := &ObjectError{ | ||
Object: ObjectMetadata{ | ||
Kind: manifest.KindProject, | ||
Name: "default", | ||
Source: "/home/me/project.yaml", | ||
}, | ||
Errors: errs, | ||
} | ||
assert.EqualError(t, err, expectedErrorOutput(t, "object_error.txt")) | ||
}) | ||
|
||
t.Run("project scoped object", func(t *testing.T) { | ||
err := &ObjectError{ | ||
Object: ObjectMetadata{ | ||
IsProjectScoped: true, | ||
Kind: manifest.KindService, | ||
Name: "my-service", | ||
Project: "default", | ||
Source: "/home/me/service.yaml", | ||
}, | ||
Errors: errs, | ||
} | ||
assert.EqualError(t, err, expectedErrorOutput(t, "object_error_project_scoped.txt")) | ||
}) | ||
} | ||
|
||
func TestObjectError_UnmarshalJSON(t *testing.T) { | ||
expected := &ObjectError{ | ||
Object: ObjectMetadata{ | ||
Kind: manifest.KindService, | ||
Name: "test-service", | ||
Source: "/home/me/service.yaml", | ||
IsProjectScoped: true, | ||
Project: "default", | ||
}, | ||
Errors: []error{ | ||
validation.FieldError{ | ||
FieldPath: "metadata.project", | ||
FieldValue: "default", | ||
Errors: []string{"nested"}, | ||
}, | ||
errors.New("some error"), | ||
&validation.FieldError{ | ||
FieldPath: "metadata.name", | ||
FieldValue: "my-project", | ||
}, | ||
}, | ||
} | ||
data, err := json.Marshal(expected) | ||
require.NoError(t, err) | ||
|
||
var actual ObjectError | ||
err = json.Unmarshal(data, &actual) | ||
require.NoError(t, err) | ||
|
||
assert.Equal(t, expected.Object, actual.Object) | ||
assert.Equal(t, expected.Errors[0], actual.Errors[0]) | ||
assert.Equal(t, expected.Errors[1].Error(), actual.Errors[1].Error()) | ||
assert.Equal(t, *expected.Errors[2].(*validation.FieldError), actual.Errors[2]) | ||
} | ||
|
||
func expectedErrorOutput(t *testing.T, name string) string { | ||
t.Helper() | ||
data, err := errorsTestData.ReadFile(filepath.Join("test_data", "errors", name)) | ||
require.NoError(t, err) | ||
return string(data) | ||
} |
Oops, something went wrong.