Skip to content

Commit

Permalink
feat(cmd): initial implementation of entity validation
Browse files Browse the repository at this point in the history
POC using versioned entity JSON schemas from the Backstage repo.

Validation error output and overall configurability to be improved.
  • Loading branch information
odsod committed Apr 17, 2023
1 parent b980d47 commit 99fcf3b
Show file tree
Hide file tree
Showing 20 changed files with 1,192 additions and 2 deletions.
13 changes: 13 additions & 0 deletions .backstage/backstage-go.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-go
description: Backstage SDK for Go
links:
- url: https://pkg.go.dev/go.einride.tech/backstage
title: GoDoc
icon: docs
spec:
type: go-library
lifecycle: production
owner: team-cloud-control
9 changes: 8 additions & 1 deletion .sage/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func Default(ctx context.Context) error {
sg.Deps(ctx, GoLint, GoReview)
sg.Deps(ctx, GoTest)
sg.Deps(ctx, GoModTidy)
sg.Deps(ctx, GoLicenses, GitVerifyNoDiff)
sg.Deps(ctx, BackstageCatalogValidate, GoLicenses, GitVerifyNoDiff)
return nil
}

Expand Down Expand Up @@ -86,6 +86,13 @@ func GitVerifyNoDiff(ctx context.Context) error {
return sggit.VerifyNoDiff(ctx)
}

func BackstageCatalogValidate(ctx context.Context) error {
sg.Logger(ctx).Println("validating Backstage catalog entities...")
cmd := sg.Command(ctx, "go", "run", ".", "catalog", "entities", "validate", sg.FromGitRoot(".backstage"))
cmd.Dir = sg.FromGitRoot("cmd", "backstage")
return cmd.Run()
}

func SemanticRelease(ctx context.Context, repo string, dry bool) error {
sg.Logger(ctx).Println("triggering release...")
args := []string{
Expand Down
61 changes: 61 additions & 0 deletions .sage/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"

"go.einride.tech/sage/sg"
)

func Schema(ctx context.Context) error {
sg.Deps(
ctx,
sg.Fn(downloadSchema, "Entity.schema.json"),
sg.Fn(downloadSchema, "EntityEnvelope.schema.json"),
sg.Fn(downloadSchema, "EntityMeta.schema.json"),
sg.Fn(downloadSchema, "shared/common.schema.json"),
sg.Fn(downloadSchema, "kinds/API.v1alpha1.schema.json"),
sg.Fn(downloadSchema, "kinds/Component.v1alpha1.schema.json"),
sg.Fn(downloadSchema, "kinds/Domain.v1alpha1.schema.json"),
sg.Fn(downloadSchema, "kinds/Group.v1alpha1.schema.json"),
sg.Fn(downloadSchema, "kinds/Location.v1alpha1.schema.json"),
sg.Fn(downloadSchema, "kinds/Resource.v1alpha1.schema.json"),
sg.Fn(downloadSchema, "kinds/System.v1alpha1.schema.json"),
sg.Fn(downloadSchema, "kinds/User.v1alpha1.schema.json"),
)
return nil
}

func downloadSchema(ctx context.Context, path string) error {
const version = "v1.12.1"
url := fmt.Sprintf(
"https://raw.githubusercontent.com/backstage/backstage/%s/packages/catalog-model/src/schema/%s",
version,
path,
)
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
response, err := http.DefaultClient.Do(request)
if err != nil {
return err
}
defer func() {
_ = response.Body.Close()
}()
data, err := io.ReadAll(response.Body)
if err != nil {
return err
}
out := sg.FromGitRoot("cmd", "backstage", "internal", "schema", filepath.Base(path))
if err := os.WriteFile(out, data, 0o600); err != nil {
return err
}
sg.Logger(ctx).Println("wrote schema", out)
return nil
}
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ update-sage: $(go)
clean-sage:
@git clean -fdx .sage/tools .sage/bin .sage/build

.PHONY: backstage-catalog-validate
backstage-catalog-validate: $(sagefile)
@$(sagefile) BackstageCatalogValidate

.PHONY: convco-check
convco-check: $(sagefile)
@$(sagefile) ConvcoCheck
Expand Down Expand Up @@ -90,6 +94,10 @@ go-review: $(sagefile)
go-test: $(sagefile)
@$(sagefile) GoTest

.PHONY: schema
schema: $(sagefile)
@$(sagefile) Schema

.PHONY: semantic-release
semantic-release: $(sagefile)
ifndef repo
Expand Down
2 changes: 2 additions & 0 deletions cmd/backstage/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ go 1.20

require (
github.com/adrg/xdg v0.4.0
github.com/santhosh-tekuri/jsonschema v1.2.4
github.com/spf13/cobra v1.6.1
go.einride.tech/backstage v0.0.0-00010101000000-000000000000
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand Down
3 changes: 3 additions & 0 deletions cmd/backstage/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
Expand All @@ -18,6 +20,7 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
79 changes: 79 additions & 0 deletions cmd/backstage/internal/schema/API.v1alpha1.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "ApiV1alpha1",
"description": "An API describes an interface that can be exposed by a component. The API can be defined in different formats, like OpenAPI, AsyncAPI, GraphQL, gRPC, or other formats.",
"examples": [
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "API",
"metadata": {
"name": "artist-api",
"description": "Retrieve artist details",
"labels": {
"product_name": "Random value Generator"
},
"annotations": {
"docs": "https://github.com/..../tree/develop/doc"
}
},
"spec": {
"type": "openapi",
"lifecycle": "production",
"owner": "artist-relations-team",
"system": "artist-engagement-portal",
"definition": "openapi: \"3.0.0\"\ninfo:..."
}
}
],
"allOf": [
{
"$ref": "Entity"
},
{
"type": "object",
"required": ["spec"],
"properties": {
"apiVersion": {
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
},
"kind": {
"enum": ["API"]
},
"spec": {
"type": "object",
"required": ["type", "lifecycle", "owner", "definition"],
"properties": {
"type": {
"type": "string",
"description": "The type of the API definition.",
"examples": ["openapi", "asyncapi", "graphql", "grpc", "trpc"],
"minLength": 1
},
"lifecycle": {
"type": "string",
"description": "The lifecycle state of the API.",
"examples": ["experimental", "production", "deprecated"],
"minLength": 1
},
"owner": {
"type": "string",
"description": "An entity reference to the owner of the API.",
"examples": ["artist-relations-team", "user:john.johnson"],
"minLength": 1
},
"system": {
"type": "string",
"description": "An entity reference to the system that the API belongs to.",
"minLength": 1
},
"definition": {
"type": "string",
"description": "The definition of the API, based on the format defined by the type.",
"minLength": 1
}
}
}
}
}
]
}
101 changes: 101 additions & 0 deletions cmd/backstage/internal/schema/Component.v1alpha1.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "ComponentV1alpha1",
"description": "A Component describes a software component. It is typically intimately linked to the source code that constitutes the component, and should be what a developer may regard a \"unit of software\", usually with a distinct deployable or linkable artifact.",
"examples": [
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "Component",
"metadata": {
"name": "LoremService",
"description": "Creates Lorems like a pro.",
"labels": {
"product_name": "Random value Generator"
},
"annotations": {
"docs": "https://github.com/..../tree/develop/doc"
}
},
"spec": {
"type": "service",
"lifecycle": "production",
"owner": "tools"
}
}
],
"allOf": [
{
"$ref": "Entity"
},
{
"type": "object",
"required": ["spec"],
"properties": {
"apiVersion": {
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
},
"kind": {
"enum": ["Component"]
},
"spec": {
"type": "object",
"required": ["type", "lifecycle", "owner"],
"properties": {
"type": {
"type": "string",
"description": "The type of component.",
"examples": ["service", "website", "library"],
"minLength": 1
},
"lifecycle": {
"type": "string",
"description": "The lifecycle state of the component.",
"examples": ["experimental", "production", "deprecated"],
"minLength": 1
},
"owner": {
"type": "string",
"description": "An entity reference to the owner of the component.",
"examples": ["artist-relations-team", "user:john.johnson"],
"minLength": 1
},
"system": {
"type": "string",
"description": "An entity reference to the system that the component belongs to.",
"minLength": 1
},
"subcomponentOf": {
"type": "string",
"description": "An entity reference to another component of which the component is a part.",
"minLength": 1
},
"providesApis": {
"type": "array",
"description": "An array of entity references to the APIs that are provided by the component.",
"items": {
"type": "string",
"minLength": 1
}
},
"consumesApis": {
"type": "array",
"description": "An array of entity references to the APIs that are consumed by the component.",
"items": {
"type": "string",
"minLength": 1
}
},
"dependsOn": {
"type": "array",
"description": "An array of references to other entities that the component depends on to function.",
"items": {
"type": "string",
"minLength": 1
}
}
}
}
}
}
]
}
47 changes: 47 additions & 0 deletions cmd/backstage/internal/schema/Domain.v1alpha1.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "DomainV1alpha1",
"description": "A Domain groups a collection of systems that share terminology, domain models, business purpose, or documentation, i.e. form a bounded context.",
"examples": [
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "Domain",
"metadata": {
"name": "artists",
"description": "Everything about artists"
},
"spec": {
"owner": "artist-relations-team"
}
}
],
"allOf": [
{
"$ref": "Entity"
},
{
"type": "object",
"required": ["spec"],
"properties": {
"apiVersion": {
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
},
"kind": {
"enum": ["Domain"]
},
"spec": {
"type": "object",
"required": ["owner"],
"properties": {
"owner": {
"type": "string",
"description": "An entity reference to the owner of the component.",
"examples": ["artist-relations-team", "user:john.johnson"],
"minLength": 1
}
}
}
}
}
]
}
Loading

0 comments on commit 99fcf3b

Please sign in to comment.