diff --git a/manifest/v1alpha/alert.go b/manifest/v1alpha/alert.go index 48230fe29..9193fba89 100644 --- a/manifest/v1alpha/alert.go +++ b/manifest/v1alpha/alert.go @@ -1,6 +1,8 @@ package v1alpha -import "github.com/nobl9/nobl9-go/manifest" +import ( + "github.com/nobl9/nobl9-go/manifest" +) //go:generate go run ../../scripts/generate-object-impl.go Alert @@ -22,9 +24,9 @@ type AlertMetadata struct { // AlertSpec represents content of Alert's Spec type AlertSpec struct { - AlertPolicy AlertPolicyMetadata `json:"alertPolicy"` - SLO SLOMetadata `json:"slo"` - Service ServiceMetadata `json:"service"` + AlertPolicy AlertObjectMetadata `json:"alertPolicy"` + SLO AlertObjectMetadata `json:"slo"` + Service AlertObjectMetadata `json:"service"` Objective AlertObjective `json:"objective"` Severity string `json:"severity" validate:"required,severity" example:"High"` Status string `json:"status" example:"Resolved"` @@ -41,3 +43,10 @@ type AlertObjective struct { Name string `json:"name" validate:"omitempty"` DisplayName string `json:"displayName" validate:"omitempty"` } + +type AlertObjectMetadata struct { + Name string `json:"name"` + DisplayName string `json:"displayName,omitempty"` + Project string `json:"project,omitempty"` + Labels Labels `json:"labels,omitempty"` +} diff --git a/manifest/v1alpha/parser/parser.go b/manifest/v1alpha/parser/parser.go index 421992311..1afe7b7fa 100644 --- a/manifest/v1alpha/parser/parser.go +++ b/manifest/v1alpha/parser/parser.go @@ -11,6 +11,7 @@ import ( "github.com/nobl9/nobl9-go/manifest" "github.com/nobl9/nobl9-go/manifest/v1alpha" "github.com/nobl9/nobl9-go/manifest/v1alpha/project" + "github.com/nobl9/nobl9-go/manifest/v1alpha/service" ) type unmarshalFunc func(v interface{}) error @@ -45,7 +46,7 @@ func parseObject(kind manifest.Kind, unmarshal unmarshalFunc) (manifest.Object, //exhaustive:enforce switch kind { case manifest.KindService: - return genericParseObject[v1alpha.Service](unmarshal) + return genericParseObject[service.Service](unmarshal) case manifest.KindSLO: return genericParseObject[v1alpha.SLO](unmarshal) case manifest.KindProject: diff --git a/manifest/v1alpha/project/project.go b/manifest/v1alpha/project/project.go index 0d1b75a26..90bb09fe6 100644 --- a/manifest/v1alpha/project/project.go +++ b/manifest/v1alpha/project/project.go @@ -7,6 +7,16 @@ import ( //go:generate go run ../../../scripts/generate-object-impl.go Project +// New creates a new Project based on provided Metadata nad Spec. +func New(metadata Metadata, spec Spec) Project { + return Project{ + APIVersion: manifest.VersionV1alpha.String(), + Kind: manifest.KindProject, + Metadata: metadata, + Spec: spec, + } +} + // Project is the primary grouping primitive for manifest.Object. // Most objects are scoped to a certain Project. type Project struct { @@ -19,16 +29,6 @@ type Project struct { ManifestSource string `json:"manifestSrc,omitempty"` } -// New creates a new Project based on provided Metadata nad Spec. -func New(metadata Metadata, spec Spec) Project { - return Project{ - APIVersion: manifest.VersionV1alpha.String(), - Kind: manifest.KindProject, - Metadata: metadata, - Spec: spec, - } -} - // Metadata provides identity information for Project. type Metadata struct { Name string `json:"name" validate:"required,objectName" example:"name"` diff --git a/manifest/v1alpha/service.go b/manifest/v1alpha/service.go deleted file mode 100644 index b8fa3b3c0..000000000 --- a/manifest/v1alpha/service.go +++ /dev/null @@ -1,42 +0,0 @@ -package v1alpha - -import ( - "github.com/nobl9/nobl9-go/manifest" -) - -//go:generate go run ../../scripts/generate-object-impl.go Service - -// Service struct which mapped one to one with kind: service yaml definition -type Service struct { - APIVersion string `json:"apiVersion"` - Kind manifest.Kind `json:"kind"` - Metadata ServiceMetadata `json:"metadata"` - Spec ServiceSpec `json:"spec"` - Status *ServiceStatus `json:"status,omitempty"` - - Organization string `json:"organization,omitempty"` - ManifestSource string `json:"manifestSrc,omitempty"` -} - -type ServiceMetadata struct { - Name string `json:"name" validate:"required,objectName"` - DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63"` - Project string `json:"project,omitempty" validate:"objectName"` - Labels Labels `json:"labels,omitempty" validate:"omitempty,labels"` -} - -// ServiceStatus represents content of Status optional for Service Object. -type ServiceStatus struct { - SloCount int `json:"sloCount"` -} - -// ServiceSpec represents content of Spec typical for Service Object. -type ServiceSpec struct { - Description string `json:"description" validate:"description" example:"Bleeding edge web app"` -} - -// ServiceWithSLOs struct which mapped one to one with kind: service and slo yaml definition. -type ServiceWithSLOs struct { - Service Service `json:"service"` - SLOs []SLO `json:"slos"` -} diff --git a/manifest/v1alpha/service/doc.go b/manifest/v1alpha/service/doc.go new file mode 100644 index 000000000..1e72836d1 --- /dev/null +++ b/manifest/v1alpha/service/doc.go @@ -0,0 +1,2 @@ +// Package service defines Service object definitions. +package service diff --git a/manifest/v1alpha/service/example.yaml b/manifest/v1alpha/service/example.yaml new file mode 100644 index 000000000..78acb5173 --- /dev/null +++ b/manifest/v1alpha/service/example.yaml @@ -0,0 +1,11 @@ +apiVersion: n9/v1alpha +kind: Service +metadata: + name: my-service + displayName: My Service + project: default + labels: + team: [ green, orange ] + region: [ eu-central-1 ] +spec: + description: Example Service diff --git a/manifest/v1alpha/service/example_test.go b/manifest/v1alpha/service/example_test.go new file mode 100644 index 000000000..064fcce2a --- /dev/null +++ b/manifest/v1alpha/service/example_test.go @@ -0,0 +1,53 @@ +package service_test + +import ( + "context" + "log" + + "github.com/nobl9/nobl9-go/internal/examples" + "github.com/nobl9/nobl9-go/manifest" + "github.com/nobl9/nobl9-go/manifest/v1alpha" + "github.com/nobl9/nobl9-go/manifest/v1alpha/service" +) + +func ExampleService() { + // Create the object: + myService := service.New( + service.Metadata{ + Name: "my-service", + DisplayName: "My Service", + Project: "default", + Labels: v1alpha.Labels{ + "team": []string{"green", "orange"}, + "region": []string{"eu-central-1"}, + }, + }, + service.Spec{ + Description: "Example service", + }, + ) + // Verify the object: + if err := myService.Validate(); err != nil { + log.Fatal("service validation failed, err: %w", err) + } + // Apply the object: + client := examples.GetOfflineEchoClient() + if err := client.ApplyObjects(context.Background(), []manifest.Object{myService}, false); err != nil { + log.Fatal("failed to apply service, err: %w", err) + } + // Output: + // apiVersion: n9/v1alpha + // kind: Service + // metadata: + // name: my-service + // displayName: My Service + // project: default + // labels: + // region: + // - eu-central-1 + // team: + // - green + // - orange + // spec: + // description: Example service +} diff --git a/manifest/v1alpha/service/service.go b/manifest/v1alpha/service/service.go new file mode 100644 index 000000000..fd13e9bfb --- /dev/null +++ b/manifest/v1alpha/service/service.go @@ -0,0 +1,49 @@ +package service + +import ( + "github.com/nobl9/nobl9-go/manifest" + "github.com/nobl9/nobl9-go/manifest/v1alpha" +) + +//go:generate go run ../../../scripts/generate-object-impl.go Service + +// New creates a new Service based on provided Metadata nad Spec. +func New(metadata Metadata, spec Spec) Service { + return Service{ + APIVersion: manifest.VersionV1alpha.String(), + Kind: manifest.KindService, + Metadata: metadata, + Spec: spec, + } +} + +// Service struct which mapped one to one with kind: service yaml definition +type Service struct { + APIVersion string `json:"apiVersion"` + Kind manifest.Kind `json:"kind"` + Metadata Metadata `json:"metadata"` + Spec Spec `json:"spec"` + Status *Status `json:"status,omitempty"` + + Organization string `json:"organization,omitempty"` + ManifestSource string `json:"manifestSrc,omitempty"` +} + +// Metadata provides identity information for Service. +type Metadata struct { + Name string `json:"name" validate:"required,objectName"` + DisplayName string `json:"displayName,omitempty" validate:"omitempty,min=0,max=63"` + Project string `json:"project,omitempty" validate:"objectName"` + Labels v1alpha.Labels `json:"labels,omitempty" validate:"omitempty,labels"` +} + +// Status holds dynamic fields returned when the Service is fetched from Nobl9 platform. +// Status is not part of the static object definition. +type Status struct { + SloCount int `json:"sloCount"` +} + +// Spec holds detailed information specific to Service. +type Spec struct { + Description string `json:"description" validate:"description" example:"Bleeding edge web app"` +} diff --git a/manifest/v1alpha/service_object.go b/manifest/v1alpha/service/service_object.go similarity index 92% rename from manifest/v1alpha/service_object.go rename to manifest/v1alpha/service/service_object.go index 77e933684..3d941d3d6 100644 --- a/manifest/v1alpha/service_object.go +++ b/manifest/v1alpha/service/service_object.go @@ -1,13 +1,12 @@ // Code generated by "generate-object-impl Service"; DO NOT EDIT. -package v1alpha +package service import "github.com/nobl9/nobl9-go/manifest" // Ensure interfaces are implemented. var _ manifest.Object = Service{} var _ manifest.ProjectScopedObject = Service{} -var _ ObjectContext = Service{} func (s Service) GetVersion() string { return s.APIVersion @@ -22,7 +21,7 @@ func (s Service) GetName() string { } func (s Service) Validate() error { - return validator.Check(s) + return validate(s) } func (s Service) GetProject() string { diff --git a/manifest/v1alpha/service/test_data/expected_error.txt b/manifest/v1alpha/service/test_data/expected_error.txt new file mode 100644 index 000000000..326049c17 --- /dev/null +++ b/manifest/v1alpha/service/test_data/expected_error.txt @@ -0,0 +1,14 @@ +Validation for Service 'MY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICE' in project 'MY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECT' has failed for the following fields: + - 'metadata.name' with value 'MY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICEMY SERVICE...': + - length must be between 1 and 63 + - a DNS-1123 compliant name must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$') + - 'metadata.displayName' with value 'my-servicemy-servicemy-servicemy-servicemy-servicemy-servicemy-servicemy-servicemy-servicemy-service': + - length must be between 0 and 63 + - 'metadata.project' with value 'MY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECTMY PROJECT...': + - length must be between 1 and 63 + - a DNS-1123 compliant name must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$') + - 'metadata.labels' with value '{"L O L":["dip","dip"]}': + - label key 'L O L' does not match the regex: ^\p{L}([_\-0-9\p{L}]*[0-9\p{L}])?$ + - 'spec.description' with value 'llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll...': + - length must be between 0 and 1050 +Manifest source: /home/me/service.yaml \ No newline at end of file diff --git a/manifest/v1alpha/service/validation.go b/manifest/v1alpha/service/validation.go new file mode 100644 index 000000000..04153a315 --- /dev/null +++ b/manifest/v1alpha/service/validation.go @@ -0,0 +1,44 @@ +package service + +import ( + "github.com/nobl9/nobl9-go/manifest/v1alpha" + "github.com/nobl9/nobl9-go/validation" +) + +func validate(s Service) error { + v := validation.RulesForStruct( + validation.RulesForField[string]( + "metadata.name", + func() string { return s.Metadata.Name }, + ). + With( + validation.StringRequired(), + validation.StringIsDNSSubdomain()), + validation.RulesForField[string]( + "metadata.displayName", + func() string { return s.Metadata.DisplayName }, + ). + With(validation.StringLength(0, 63)), + validation.RulesForField[string]( + "metadata.project", + func() string { return s.Metadata.Project }, + ). + With( + validation.StringRequired(), + validation.StringIsDNSSubdomain()), + validation.RulesForField[v1alpha.Labels]( + "metadata.labels", + func() v1alpha.Labels { return s.Metadata.Labels }, + ). + With(v1alpha.ValidationRule()), + validation.RulesForField[string]( + "spec.description", + func() string { return s.Spec.Description }, + ). + With(validation.StringDescription()), + ) + if errs := v.Validate(); len(errs) > 0 { + return v1alpha.NewObjectError(s, errs) + } + return nil +} diff --git a/manifest/v1alpha/service/validation_test.go b/manifest/v1alpha/service/validation_test.go new file mode 100644 index 000000000..199934de6 --- /dev/null +++ b/manifest/v1alpha/service/validation_test.go @@ -0,0 +1,34 @@ +package service + +import ( + _ "embed" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/nobl9/nobl9-go/manifest" + "github.com/nobl9/nobl9-go/manifest/v1alpha" +) + +//go:embed test_data/expected_error.txt +var expectedError string + +func TestValidate_AllErrors(t *testing.T) { + err := validate(Service{ + Kind: manifest.KindService, + Metadata: Metadata{ + Name: strings.Repeat("MY SERVICE", 20), + DisplayName: strings.Repeat("my-service", 10), + Project: strings.Repeat("MY PROJECT", 20), + Labels: v1alpha.Labels{ + "L O L": []string{"dip", "dip"}, + }, + }, + Spec: Spec{ + Description: strings.Repeat("l", 2000), + }, + ManifestSource: "/home/me/service.yaml", + }) + assert.Equal(t, expectedError, err.Error()) +} diff --git a/manifest/v1alpha/validator.go b/manifest/v1alpha/validator.go index 485b852de..a19510444 100644 --- a/manifest/v1alpha/validator.go +++ b/manifest/v1alpha/validator.go @@ -1404,6 +1404,7 @@ func areSumoLogicTimesliceValuesEqual(sloSpec SLOSpec) bool { // haveAzureMonitorCountMetricSpecTheSameResourceIDAndMetricNamespace checks if good/bad query has the same resourceID // and metricNamespace as total query +// nolint: gocognit func haveAzureMonitorCountMetricSpecTheSameResourceIDAndMetricNamespace(sloSpec SLOSpec) bool { for _, objective := range sloSpec.Objectives { if objective.CountMetrics == nil { diff --git a/sdk/client_test.go b/sdk/client_test.go index b0ba36f00..a37307ea4 100644 --- a/sdk/client_test.go +++ b/sdk/client_test.go @@ -24,22 +24,23 @@ import ( "github.com/nobl9/nobl9-go/manifest" "github.com/nobl9/nobl9-go/manifest/v1alpha" + v1alphaService "github.com/nobl9/nobl9-go/manifest/v1alpha/service" ) func TestClient_GetObjects(t *testing.T) { responsePayload := []manifest.Object{ - v1alpha.Service{ + v1alphaService.Service{ APIVersion: v1alpha.APIVersion, Kind: manifest.KindService, - Metadata: v1alpha.ServiceMetadata{ + Metadata: v1alphaService.Metadata{ Name: "service1", Project: "default", }, }, - v1alpha.Service{ + v1alphaService.Service{ APIVersion: v1alpha.APIVersion, Kind: manifest.KindService, - Metadata: v1alpha.ServiceMetadata{ + Metadata: v1alphaService.Metadata{ Name: "service2", Project: "default", }, @@ -146,10 +147,10 @@ func TestClient_GetObjects_UserGroupsEndpoint(t *testing.T) { func TestClient_ApplyObjects(t *testing.T) { requestPayload := []manifest.Object{ - v1alpha.Service{ + v1alphaService.Service{ APIVersion: v1alpha.APIVersion, Kind: manifest.KindService, - Metadata: v1alpha.ServiceMetadata{ + Metadata: v1alphaService.Metadata{ Name: "service1", Project: "default", }, @@ -186,10 +187,10 @@ func TestClient_ApplyObjects(t *testing.T) { func TestClient_DeleteObjects(t *testing.T) { requestPayload := []manifest.Object{ - v1alpha.Service{ + v1alphaService.Service{ APIVersion: v1alpha.APIVersion, Kind: manifest.KindService, - Metadata: v1alpha.ServiceMetadata{ + Metadata: v1alphaService.Metadata{ Name: "service1", Project: "default", }, diff --git a/sdk/parser_test.go b/sdk/parser_test.go index 769d4e83f..0d8c808c4 100644 --- a/sdk/parser_test.go +++ b/sdk/parser_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/nobl9/nobl9-go/manifest" - "github.com/nobl9/nobl9-go/manifest/v1alpha" "github.com/nobl9/nobl9-go/manifest/v1alpha/project" + v1alphaService "github.com/nobl9/nobl9-go/manifest/v1alpha/service" ) //go:embed test_data/parser @@ -132,9 +132,9 @@ func TestDecodeSingle(t *testing.T) { }) t.Run("invalid type, return error", func(t *testing.T) { - _, err := DecodeObject[v1alpha.Service](readInputFile(t, "single_project.yaml")) + _, err := DecodeObject[v1alphaService.Service](readInputFile(t, "single_project.yaml")) require.Error(t, err) - assert.EqualError(t, err, "object of type project.Project is not of type v1alpha.Service") + assert.EqualError(t, err, "object of type project.Project is not of type service.Service") }) }