feat(catalog): add entity to describe APIs

This commit is contained in:
Dominik Henneke
2020-07-23 14:42:56 +02:00
parent 611201ee9f
commit b5f8aa89b8
8 changed files with 642 additions and 0 deletions
@@ -0,0 +1,46 @@
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: hello-world
description: Hello World example for gRPC
spec:
type: grpc
definition: |
// Copyright 2015 gRPC 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.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
@@ -0,0 +1,119 @@
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: petstore
description: The petstore API
spec:
type: openapi
definition: |
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
summary: Create a pet
operationId: createPets
tags:
- pets
responses:
'201':
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/pets/{petId}:
get:
summary: Info for a specific pet
operationId: showPetById
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
items:
$ref: "#/components/schemas/Pet"
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
@@ -0,0 +1,217 @@
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: streetlights
description: The Smartylighting Streetlights API allows you to remotely manage the city lights.
spec:
type: asyncapi
definition: |
asyncapi: 2.0.0
info:
title: Streetlights API
version: '1.0.0'
description: |
The Smartylighting Streetlights API allows you to remotely manage the city lights.
### Check out its awesome features:
* Turn a specific streetlight on/off 🌃
* Dim a specific streetlight 😎
* Receive real-time information about environmental lighting conditions 📈
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0
servers:
production:
url: api.streetlights.smartylighting.com:{port}
protocol: mqtt
description: Test broker
variables:
port:
description: Secure connection (TLS) is available through port 8883.
default: '1883'
enum:
- '1883'
- '8883'
security:
- apiKey: []
- supportedOauthFlows:
- streetlights:on
- streetlights:off
- streetlights:dim
- openIdConnectWellKnown: []
defaultContentType: application/json
channels:
smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured:
description: The topic on which measured values may be produced and consumed.
parameters:
streetlightId:
$ref: '#/components/parameters/streetlightId'
subscribe:
summary: Receive information about environmental lighting conditions of a particular streetlight.
operationId: receiveLightMeasurement
traits:
- $ref: '#/components/operationTraits/kafka'
message:
$ref: '#/components/messages/lightMeasured'
smartylighting/streetlights/1/0/action/{streetlightId}/turn/on:
parameters:
streetlightId:
$ref: '#/components/parameters/streetlightId'
publish:
operationId: turnOn
traits:
- $ref: '#/components/operationTraits/kafka'
message:
$ref: '#/components/messages/turnOnOff'
smartylighting/streetlights/1/0/action/{streetlightId}/turn/off:
parameters:
streetlightId:
$ref: '#/components/parameters/streetlightId'
publish:
operationId: turnOff
traits:
- $ref: '#/components/operationTraits/kafka'
message:
$ref: '#/components/messages/turnOnOff'
smartylighting/streetlights/1/0/action/{streetlightId}/dim:
parameters:
streetlightId:
$ref: '#/components/parameters/streetlightId'
publish:
operationId: dimLight
traits:
- $ref: '#/components/operationTraits/kafka'
message:
$ref: '#/components/messages/dimLight'
components:
messages:
lightMeasured:
name: lightMeasured
title: Light measured
summary: Inform about environmental lighting conditions for a particular streetlight.
contentType: application/json
traits:
- $ref: '#/components/messageTraits/commonHeaders'
payload:
$ref: "#/components/schemas/lightMeasuredPayload"
turnOnOff:
name: turnOnOff
title: Turn on/off
summary: Command a particular streetlight to turn the lights on or off.
traits:
- $ref: '#/components/messageTraits/commonHeaders'
payload:
$ref: "#/components/schemas/turnOnOffPayload"
dimLight:
name: dimLight
title: Dim light
summary: Command a particular streetlight to dim the lights.
traits:
- $ref: '#/components/messageTraits/commonHeaders'
payload:
$ref: "#/components/schemas/dimLightPayload"
schemas:
lightMeasuredPayload:
type: object
properties:
lumens:
type: integer
minimum: 0
description: Light intensity measured in lumens.
sentAt:
$ref: "#/components/schemas/sentAt"
turnOnOffPayload:
type: object
properties:
command:
type: string
enum:
- on
- off
description: Whether to turn on or off the light.
sentAt:
$ref: "#/components/schemas/sentAt"
dimLightPayload:
type: object
properties:
percentage:
type: integer
description: Percentage to which the light should be dimmed to.
minimum: 0
maximum: 100
sentAt:
$ref: "#/components/schemas/sentAt"
sentAt:
type: string
format: date-time
description: Date and time when the message was sent.
securitySchemes:
apiKey:
type: apiKey
in: user
description: Provide your API key as the user and leave the password empty.
supportedOauthFlows:
type: oauth2
description: Flows to support OAuth 2.0
flows:
implicit:
authorizationUrl: 'https://authserver.example/auth'
scopes:
'streetlights:on': Ability to switch lights on
'streetlights:off': Ability to switch lights off
'streetlights:dim': Ability to dim the lights
password:
tokenUrl: 'https://authserver.example/token'
scopes:
'streetlights:on': Ability to switch lights on
'streetlights:off': Ability to switch lights off
'streetlights:dim': Ability to dim the lights
clientCredentials:
tokenUrl: 'https://authserver.example/token'
scopes:
'streetlights:on': Ability to switch lights on
'streetlights:off': Ability to switch lights off
'streetlights:dim': Ability to dim the lights
authorizationCode:
authorizationUrl: 'https://authserver.example/auth'
tokenUrl: 'https://authserver.example/token'
refreshUrl: 'https://authserver.example/refresh'
scopes:
'streetlights:on': Ability to switch lights on
'streetlights:off': Ability to switch lights off
'streetlights:dim': Ability to dim the lights
openIdConnectWellKnown:
type: openIdConnect
openIdConnectUrl: 'https://authserver.example/.well-known'
parameters:
streetlightId:
description: The ID of the streetlight.
schema:
type: string
messageTraits:
commonHeaders:
headers:
type: object
properties:
my-app-header:
type: integer
minimum: 0
maximum: 100
operationTraits:
kafka:
bindings:
kafka:
clientId: my-app-id
@@ -23,6 +23,7 @@ import {
SchemaValidEntityPolicy,
} from './entity';
import {
ApiEntityV1alpha1Policy,
ComponentEntityV1alpha1Policy,
LocationEntityV1alpha1Policy,
TemplateEntityV1alpha1Policy,
@@ -76,6 +77,7 @@ export class EntityPolicies implements EntityPolicy {
new ComponentEntityV1alpha1Policy(),
new LocationEntityV1alpha1Policy(),
new TemplateEntityV1alpha1Policy(),
new ApiEntityV1alpha1Policy(),
]),
]);
}
@@ -0,0 +1,126 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
import { EntityPolicy } from '../types';
import {
ApiEntityV1alpha1,
ApiEntityV1alpha1Policy,
} from './ApiEntityV1alpha1';
describe('ApiV1alpha1Policy', () => {
let entity: ApiEntityV1alpha1;
let policy: EntityPolicy;
beforeEach(() => {
entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'test',
},
spec: {
type: 'openapi',
definition: `
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
paths:
/pets:
get:
summary: List all pets
operationId: listPets
responses:
'200':
description: A paged array of pets
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
components:
schemas:
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
items:
$ref: "#/components/schemas/Pet"
`,
},
};
policy = new ApiEntityV1alpha1Policy();
});
it('happy path: accepts valid data', async () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('silently accepts v1beta1 as well', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta1';
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
});
it('rejects unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
});
it('rejects missing type', async () => {
delete (entity as any).spec.type;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
it('rejects wrong type', async () => {
(entity as any).spec.type = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
it('rejects empty type', async () => {
(entity as any).spec.type = '';
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
it('rejects missing definition', async () => {
delete (entity as any).spec.definition;
await expect(policy.enforce(entity)).rejects.toThrow(/definition/);
});
it('rejects wrong definition', async () => {
(entity as any).spec.definition = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/definition/);
});
it('rejects empty definition', async () => {
(entity as any).spec.definition = '';
await expect(policy.enforce(entity)).rejects.toThrow(/definition/);
});
});
@@ -0,0 +1,52 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import type { EntityPolicy } from '../types';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'API' as const;
export interface ApiEntityV1alpha1 extends Entity {
apiVersion: typeof API_VERSION[number];
kind: typeof KIND;
spec: {
type: string;
definition: string;
};
}
export class ApiEntityV1alpha1Policy implements EntityPolicy {
private schema: yup.Schema<any>;
constructor() {
this.schema = yup.object<Partial<ApiEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
definition: yup.string().required().min(1),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity> {
return await this.schema.validate(envelope, { strict: true });
}
}
@@ -29,3 +29,8 @@ export type {
TemplateEntityV1alpha1 as TemplateEntity,
TemplateEntityV1alpha1,
} from './TemplateEntityV1alpha1';
export { ApiEntityV1alpha1Policy } from './ApiEntityV1alpha1';
export type {
ApiEntityV1alpha1 as ApiEntity,
ApiEntityV1alpha1,
} from './ApiEntityV1alpha1';