From 5c4236057793bb32990e6c4a8628e4c3e3fb76a6 Mon Sep 17 00:00:00 2001 From: Tomasz Szuba Date: Mon, 4 Oct 2021 16:23:13 +0200 Subject: [PATCH 01/37] Documentation and better type safety of DependencyGraph Signed-off-by: Tomasz Szuba --- .changeset/mean-eggs-knock.md | 6 + packages/core-components/api-report.md | 133 +++++++---------- .../DependencyGraph/DependencyGraph.tsx | 139 ++++++++++++++++-- .../components/DependencyGraph/Edge.test.tsx | 8 +- .../src/components/DependencyGraph/Edge.tsx | 35 ++++- .../components/DependencyGraph/Node.test.tsx | 13 +- .../src/components/DependencyGraph/Node.tsx | 16 +- .../src/components/DependencyGraph/types.ts | 121 +++++++++++---- plugins/catalog-graph/api-report.md | 18 ++- .../EntityRelationsGraph/CustomLabel.test.tsx | 12 -- .../EntityRelationsGraph/CustomLabel.tsx | 4 +- .../EntityRelationsGraph/CustomNode.test.tsx | 16 -- .../EntityRelationsGraph/CustomNode.tsx | 4 +- .../components/EntityRelationsGraph/index.ts | 7 +- .../components/EntityRelationsGraph/types.ts | 28 ++-- 15 files changed, 363 insertions(+), 197 deletions(-) create mode 100644 .changeset/mean-eggs-knock.md diff --git a/.changeset/mean-eggs-knock.md b/.changeset/mean-eggs-knock.md new file mode 100644 index 0000000000..dd9336a0e7 --- /dev/null +++ b/.changeset/mean-eggs-knock.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': minor +'@backstage/plugin-catalog-graph': minor +--- + +Add documentation and more type safety around DependencyGraph diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 4a94135b4a..1067cd3d02 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -17,7 +17,6 @@ import { ComponentProps } from 'react'; import { Context } from 'react'; import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; -import { default as dagre_2 } from 'dagre'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; import { IconComponent } from '@backstage/core-plugin-api'; @@ -53,15 +52,11 @@ export function AlertDisplay(_props: {}): JSX.Element | null; // Warning: (ae-missing-release-tag) "Alignment" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public enum Alignment { - // (undocumented) DOWN_LEFT = 'DL', - // (undocumented) DOWN_RIGHT = 'DR', - // (undocumented) UP_LEFT = 'UL', - // (undocumented) UP_RIGHT = 'UR', } @@ -264,11 +259,10 @@ export type CustomProviderClassKey = 'form' | 'button'; // @public (undocumented) export function DashboardIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-forgotten-export) The symbol "CustomType" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "DependencyEdge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -type DependencyEdge = T & { +// @public +type DependencyEdge = T & { from: string; to: string; label?: string; @@ -276,8 +270,10 @@ type DependencyEdge = T & { // Warning: (ae-missing-release-tag) "DependencyGraph" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export function DependencyGraph(props: DependencyGraphProps): JSX.Element; +// @public +export function DependencyGraph( + props: DependencyGraphProps, +): JSX.Element; // Warning: (ae-missing-release-tag) "DependencyGraphDefaultLabelClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -301,40 +297,47 @@ export type DependencyGraphNodeClassKey = 'node'; // Warning: (ae-missing-release-tag) "DependencyGraphProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export type DependencyGraphProps = React_2.SVGProps & { - edges: DependencyEdge[]; - nodes: DependencyNode[]; - direction?: Direction; +// @public +export interface DependencyGraphProps + extends React_2.SVGProps { + acyclicer?: 'greedy'; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver align?: Alignment; - nodeMargin?: number; + defs?: SVGDefsElement | SVGDefsElement[]; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver + direction?: Direction; edgeMargin?: number; - rankMargin?: number; + edgeRanks?: number; + edges: DependencyEdge[]; + edgeWeight?: number; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver + labelOffset?: number; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver + labelPosition?: LabelPosition; + nodeMargin?: number; + nodes: DependencyNode[]; paddingX?: number; paddingY?: number; - acyclicer?: 'greedy'; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver ranker?: Ranker; - labelPosition?: LabelPosition; - labelOffset?: number; - edgeRanks?: number; - edgeWeight?: number; - renderNode?: RenderNodeFunction; - renderLabel?: RenderLabelFunction; - defs?: SVGDefsElement | SVGDefsElement[]; + rankMargin?: number; + renderLabel?: RenderLabelFunction; + renderNode?: RenderNodeFunction; zoom?: 'enabled' | 'disabled' | 'enable-on-click'; -}; +} declare namespace DependencyGraphTypes { export { DependencyEdge, - GraphEdge, RenderLabelProps, RenderLabelFunction, DependencyNode, - GraphNode, RenderNodeProps, RenderNodeFunction, - EdgeProperties, Direction, Alignment, Ranker, @@ -345,22 +348,18 @@ export { DependencyGraphTypes }; // Warning: (ae-missing-release-tag) "DependencyNode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -type DependencyNode = T & { +// @public +type DependencyNode = T & { id: string; }; // Warning: (ae-missing-release-tag) "Direction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public enum Direction { - // (undocumented) BOTTOM_TOP = 'BT', - // (undocumented) LEFT_RIGHT = 'LR', - // (undocumented) RIGHT_LEFT = 'RL', - // (undocumented) TOP_BOTTOM = 'TB', } @@ -387,20 +386,6 @@ export type DismissbleBannerClassKey = // @public (undocumented) export function DocsIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-missing-release-tag) "EdgeProperties" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -type EdgeProperties = { - label?: string; - width?: number; - height?: number; - labeloffset?: number; - labelpos?: LabelPosition; - minlen?: number; - weight?: number; - [customKey: string]: any; -}; - // Warning: (ae-missing-release-tag) "EmailIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -520,18 +505,6 @@ export type GaugeClassKey = 'root' | 'overlay' | 'circle' | 'colorUnknown'; // @public (undocumented) export function GitHubIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-missing-release-tag) "GraphEdge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -type GraphEdge = DependencyEdge & - dagre_2.GraphEdge & - EdgeProperties; - -// Warning: (ae-missing-release-tag) "GraphNode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -type GraphNode = dagre_2.Node>; - // Warning: (ae-missing-release-tag) "GroupIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -738,7 +711,7 @@ export type ItemCardHeaderProps = Partial> & { // Warning: (ae-missing-release-tag) "LabelPosition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public enum LabelPosition { // (undocumented) CENTER = 'c', @@ -888,37 +861,43 @@ export function Progress( // Warning: (ae-missing-release-tag) "Ranker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public enum Ranker { - // (undocumented) LONGEST_PATH = 'longest-path', - // (undocumented) NETWORK_SIMPLEX = 'network-simplex', - // (undocumented) TIGHT_TREE = 'tight-tree', } // Warning: (ae-missing-release-tag) "RenderLabelFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -type RenderLabelFunction = (props: RenderLabelProps) => React.ReactNode; +// @public +type RenderLabelFunction = ( + props: RenderLabelProps, +) => React_2.ReactNode; // Warning: (ae-missing-release-tag) "RenderLabelProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver +// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver // -// @public (undocumented) -type RenderLabelProps = { +// @public +type RenderLabelProps = { edge: DependencyEdge; }; // Warning: (ae-missing-release-tag) "RenderNodeFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver // -// @public (undocumented) -type RenderNodeFunction = (props: RenderNodeProps) => React.ReactNode; +// @public +type RenderNodeFunction = ( + props: RenderNodeProps, +) => React_2.ReactNode; // Warning: (ae-missing-release-tag) "RenderNodeProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver +// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver // -// @public (undocumented) -type RenderNodeProps = { +// @public +type RenderNodeProps = { node: DependencyNode; }; @@ -2498,6 +2477,8 @@ export type WarningPanelClassKey = // Warnings were encountered during analysis: // +// src/components/DependencyGraph/types.d.ts:14:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" +// src/components/DependencyGraph/types.d.ts:18:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" // src/components/TabbedLayout/RoutedTabs.d.ts:9:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts // src/components/Table/Table.d.ts:19:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts // src/layout/ErrorBoundary/ErrorBoundary.d.ts:7:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index abc98dd60a..7a9ef9a9c7 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -29,39 +29,148 @@ import { Ranker, RenderNodeFunction, RenderLabelFunction, - GraphEdge, - GraphNode, LabelPosition, } from './types'; import { Node } from './Node'; -import { Edge } from './Edge'; +import { Edge, GraphEdge } from './Edge'; import { ARROW_MARKER_ID } from './constants'; -export type DependencyGraphProps = React.SVGProps & { - edges: DependencyEdge[]; - nodes: DependencyNode[]; +/** + * Properties of {@link DependencyGraph} + * + * @remarks + * and are useful when rendering custom or edge labels + */ +export interface DependencyGraphProps + extends React.SVGProps { + /** + * Edges of graph + */ + edges: DependencyEdge[]; + /** + * Nodes of Graph + */ + nodes: DependencyNode[]; + /** + * Graph {@link DependencyGraphTypes.Direction | direction} + * + * @remarks + * + * Default: {@link DependencyGraphTypes.Direction.TOP_BOTTOM} + */ direction?: Direction; + /** + * Node {@link DependencyGraphTypes.Alignment | alignment} + */ align?: Alignment; + /** + * Margin between nodes on each rank + * + * @remarks + * + * Default: 50 + */ nodeMargin?: number; + /** + * Margin between edges + * + * @remarks + * + * Default: 10 + */ edgeMargin?: number; + /** + * Margin between each rank + * + * @remarks + * + * Default: 50 + */ rankMargin?: number; + /** + * Margin on left and right of whole graph + * + * @remarks + * + * Default: 0 + */ paddingX?: number; + /** + * Margin on top and bottom of whole graph + * + * @remarks + * + * Default: 0 + */ paddingY?: number; + /** + * Heuristic used to find set of edges that will make graph acyclic + */ acyclicer?: 'greedy'; + /** + * {@link DependencyGraphTypes.Ranker | Algorithm} used to rank nodes + * + * @remarks + * + * Default: {@link DependencyGraphTypes.Ranker.NETWORK_SIMPLEX} + */ ranker?: Ranker; + /** + * {@link DependencyGraphTypes.LabelPosition | Position} of label in relation to edge + * + * @remarks + * + * Default: {@link DependencyGraphTypes.LabelPosition.RIGHT} + */ labelPosition?: LabelPosition; + /** + * How much to move label away from edge + * + * @remarks + * + * Applies only when {@link DependencyGraphProps.labelPosition} is {@link DependencyGraphTypes.LabelPosition.LEFT} or + * {@link DependencyGraphTypes.LabelPosition.RIGHT} + */ labelOffset?: number; + /** + * Minimum number of ranks to keep between connected nodes + */ edgeRanks?: number; + /** + * Weight applied to edges in graph + */ edgeWeight?: number; - renderNode?: RenderNodeFunction; - renderLabel?: RenderLabelFunction; + /** + * Custom node rendering component + */ + renderNode?: RenderNodeFunction; + /** + * Custom label rendering component + */ + renderLabel?: RenderLabelFunction; + /** + * {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs | Defs} shared by rendered SVG to be used by + * {@link DependencyGraphProps.renderNode} and/or {@link DependencyGraphProps.renderLabel} + */ defs?: SVGDefsElement | SVGDefsElement[]; + /** + * Controls zoom behavior of graph + * + * @remarks + * + * Default: `enabled` + */ zoom?: 'enabled' | 'disabled' | 'enable-on-click'; -}; +} const WORKSPACE_ID = 'workspace'; -export function DependencyGraph(props: DependencyGraphProps) { +/** + * Graph component used to visualize relations between entities + */ +export function DependencyGraph( + props: DependencyGraphProps, +) { const { edges, nodes, @@ -88,7 +197,7 @@ export function DependencyGraph(props: DependencyGraphProps) { const [containerWidth, setContainerWidth] = React.useState(100); const [containerHeight, setContainerHeight] = React.useState(100); - const graph = React.useRef>( + const graph = React.useRef>>( new dagre.graphlib.Graph(), ); const [graphWidth, setGraphWidth] = React.useState( @@ -256,13 +365,13 @@ export function DependencyGraph(props: DependencyGraphProps) { updateGraph, ]); - function setNode(id: string, node: DependencyNode) { + function setNode(id: string, node: DependencyNode) { graph.current.setNode(id, node); updateGraph(); return graph.current; } - function setEdge(id: dagre.Edge, edge: DependencyEdge) { + function setEdge(id: dagre.Edge, edge: DependencyEdge) { graph.current.setEdge(id, edge); updateGraph(); return graph.current; @@ -303,7 +412,7 @@ export function DependencyGraph(props: DependencyGraphProps) { viewBox={`0 0 ${graphWidth} ${graphHeight}`} > {graphEdges.map(e => { - const edge = graph.current.edge(e) as GraphEdge; + const edge = graph.current.edge(e) as GraphEdge; if (!edge) return null; return ( { - const node = graph.current.node(id) as GraphNode; + const node = graph.current.node(id); if (!node) return null; return ( ( )); const minProps = { - points: [ - { x: 10, y: 20 }, - { x: 20, y: 20 }, - ], id, setEdge, renderElement, diff --git a/packages/core-components/src/components/DependencyGraph/Edge.tsx b/packages/core-components/src/components/DependencyGraph/Edge.tsx index a82c5d4813..fd4d92b834 100644 --- a/packages/core-components/src/components/DependencyGraph/Edge.tsx +++ b/packages/core-components/src/components/DependencyGraph/Edge.tsx @@ -20,13 +20,26 @@ import isFinite from 'lodash/isFinite'; import makeStyles from '@material-ui/core/styles/makeStyles'; import { BackstageTheme } from '@backstage/theme'; import { - GraphEdge, RenderLabelProps, RenderLabelFunction, DependencyEdge, + LabelPosition, } from './types'; import { ARROW_MARKER_ID, EDGE_TEST_ID, LABEL_TEST_ID } from './constants'; import { DefaultLabel } from './DefaultLabel'; +import dagre from 'dagre'; + +/* Based on: https://github.com/dagrejs/dagre/wiki#configuring-the-layout */ +export type EdgeProperties = { + label?: string; + width?: number; + height?: number; + labeloffset?: number; + labelpos?: LabelPosition; + minlen?: number; + weight?: number; +}; +export type GraphEdge = DependencyEdge & dagre.GraphEdge & EdgeProperties; export type DependencyGraphEdgeClassKey = 'path' | 'label'; @@ -47,14 +60,19 @@ const useStyles = makeStyles( type EdgePoint = dagre.GraphEdge['points'][0]; -export type EdgeComponentProps = { +export type EdgeComponentProps = { id: dagre.Edge; edge: GraphEdge; - render?: RenderLabelFunction; - setEdge: (id: dagre.Edge, edge: DependencyEdge) => dagre.graphlib.Graph<{}>; + render?: RenderLabelFunction; + setEdge: ( + id: dagre.Edge, + edge: DependencyEdge, + ) => dagre.graphlib.Graph<{}>; }; -const renderDefault = (props: RenderLabelProps) => ; +const renderDefault = (props: RenderLabelProps) => ( + +); const createPath = d3Shape .line() @@ -62,13 +80,14 @@ const createPath = d3Shape .y(d => d.y) .curve(d3Shape.curveMonotoneX); -export function Edge({ +export function Edge({ render = renderDefault, setEdge, id, edge, -}: EdgeComponentProps) { - const { x = 0, y = 0, width, height, points, ...labelProps } = edge; +}: EdgeComponentProps) { + const { x = 0, y = 0, width, height, points } = edge; + const labelProps: DependencyEdge = edge; const classes = useStyles(); const labelRef = React.useRef(null); diff --git a/packages/core-components/src/components/DependencyGraph/Node.test.tsx b/packages/core-components/src/components/DependencyGraph/Node.test.tsx index ebd6478db9..aaba09c004 100644 --- a/packages/core-components/src/components/DependencyGraph/Node.test.tsx +++ b/packages/core-components/src/components/DependencyGraph/Node.test.tsx @@ -20,21 +20,16 @@ import { render } from '@testing-library/react'; import { Node } from './Node'; import { RenderNodeProps } from './types'; -const node = { id: 'abc' }; +const node = { id: 'abc', x: 0, y: 0, width: 0, height: 0 }; const setNode = jest.fn(() => new dagre.graphlib.Graph()); const renderElement = jest.fn((props: RenderNodeProps) => ( {props.node.id} )); const minProps = { - id: node.id, node, setNode, render: renderElement, - x: 0, - y: 0, - width: 0, - height: 0, }; describe('', () => { @@ -50,7 +45,7 @@ describe('', () => { it('renders the supplied element', () => { const { getByText } = render(); - expect(getByText(minProps.id)).toBeInTheDocument(); + expect(getByText(minProps.node.id)).toBeInTheDocument(); }); it('passes down node properties to the render method', () => { @@ -62,13 +57,13 @@ describe('', () => { it('calls setNode with node ID and actual size after rendering', () => { const { getByText } = render(); - expect(getByText(minProps.id)).toBeInTheDocument(); + expect(getByText(minProps.node.id)).toBeInTheDocument(); // Updates the node in the graph expect(setNode).toHaveBeenCalledWith(node.id, { + ...node, height: 100, width: 100, - ...node, }); // Does not pass down width/height to node diff --git a/packages/core-components/src/components/DependencyGraph/Node.tsx b/packages/core-components/src/components/DependencyGraph/Node.tsx index e37f7a6e37..01d518b554 100644 --- a/packages/core-components/src/components/DependencyGraph/Node.tsx +++ b/packages/core-components/src/components/DependencyGraph/Node.tsx @@ -17,8 +17,9 @@ import React from 'react'; import makeStyles from '@material-ui/core/styles/makeStyles'; import { DefaultNode } from './DefaultNode'; -import { RenderNodeFunction, RenderNodeProps, GraphNode } from './types'; +import { RenderNodeFunction, RenderNodeProps, DependencyNode } from './types'; import { NODE_TEST_ID } from './constants'; +import dagre from 'dagre'; export type DependencyGraphNodeClassKey = 'node'; @@ -31,20 +32,23 @@ const useStyles = makeStyles( { name: 'BackstageDependencyGraphNode' }, ); -export type NodeComponentProps = { +export type GraphNode = dagre.Node>; + +export type NodeComponentProps = { node: GraphNode; - render?: RenderNodeFunction; + render?: RenderNodeFunction; setNode: dagre.graphlib.Graph['setNode']; }; const renderDefault = (props: RenderNodeProps) => ; -export function Node({ +export function Node({ render = renderDefault, setNode, node, -}: NodeComponentProps) { - const { width, height, x = 0, y = 0, ...nodeProps } = node; +}: NodeComponentProps) { + const { width, height, x = 0, y = 0 } = node; + const nodeProps: DependencyNode = node; const classes = useStyles(); const nodeRef = React.useRef(null); diff --git a/packages/core-components/src/components/DependencyGraph/types.ts b/packages/core-components/src/components/DependencyGraph/types.ts index ed22007f9a..eab793685b 100644 --- a/packages/core-components/src/components/DependencyGraph/types.ts +++ b/packages/core-components/src/components/DependencyGraph/types.ts @@ -14,73 +14,132 @@ * limitations under the License. */ -import dagre from 'dagre'; +/** + * Types used to customize and provide data to {@link DependencyGraph} + * + * @packageDocumentation + */ -type CustomType = { [customKey: string]: any }; +import React from 'react'; -/* Edges */ -export type DependencyEdge = T & { +/** + * Edge of {@link DependencyGraph} + */ +export type DependencyEdge = T & { + /** + * ID of {@link DependencyNode} from where the Edge start + */ from: string; + /** + * ID of {@link DependencyNode} to where the Edge goes to + */ to: string; + /** + * Label assigned and rendered with the Edge + */ label?: string; }; -export type GraphEdge = DependencyEdge & - dagre.GraphEdge & - EdgeProperties; +/** + * Properties of {@link DependencyGraphTypes.RenderLabelFunction} for {@link DependencyGraphTypes.DependencyEdge} + */ +export type RenderLabelProps = { edge: DependencyEdge }; -export type RenderLabelProps = { edge: DependencyEdge }; - -export type RenderLabelFunction = ( - props: RenderLabelProps, +/** + * Custom React component for edge labels + */ +export type RenderLabelFunction = ( + props: RenderLabelProps, ) => React.ReactNode; -/* Nodes */ -export type DependencyNode = T & { +/** + * Node of {@link DependencyGraph} + */ +export type DependencyNode = T & { id: string; }; -export type GraphNode = dagre.Node>; +/** + * Properties of {@link DependencyGraphTypes.RenderNodeFunction} for {@link DependencyGraphTypes.DependencyNode} + */ +export type RenderNodeProps = { node: DependencyNode }; -export type RenderNodeProps = { node: DependencyNode }; - -export type RenderNodeFunction = ( - props: RenderNodeProps, +/** + * Custom React component for graph {@link DependencyGraphTypes.DependencyNode} + */ +export type RenderNodeFunction = ( + props: RenderNodeProps, ) => React.ReactNode; -/* Based on: https://github.com/dagrejs/dagre/wiki#configuring-the-layout */ - -export type EdgeProperties = { - label?: string; - width?: number; - height?: number; - labeloffset?: number; - labelpos?: LabelPosition; - minlen?: number; - weight?: number; - [customKey: string]: any; -}; - +/** + * Graph direction + */ export enum Direction { + /** + * Top to Bottom + */ TOP_BOTTOM = 'TB', + /** + * Bottom to Top + */ BOTTOM_TOP = 'BT', + /** + * Left to Right + */ LEFT_RIGHT = 'LR', + /** + * Right to Left + */ RIGHT_LEFT = 'RL', } +/** + * Node alignment + */ export enum Alignment { + /** + * Up Left + */ UP_LEFT = 'UL', + /** + * Up Right + */ UP_RIGHT = 'UR', + /** + * Down Left + */ DOWN_LEFT = 'DL', + /** + * Down Right + */ DOWN_RIGHT = 'DR', } +/** + * Algorithm used to rand nodes in graph + */ export enum Ranker { + /** + * {@link https://en.wikipedia.org/wiki/Network_simplex_algorithm | Network Simplex} algorithm + */ NETWORK_SIMPLEX = 'network-simplex', + /** + * Tight Tree algorithm + */ TIGHT_TREE = 'tight-tree', + /** + * Longest path algorithm + * + * @remarks + * + * Simplest and fastest + */ LONGEST_PATH = 'longest-path', } +/** + * Position of label in relation to the edge + */ export enum LabelPosition { LEFT = 'l', RIGHT = 'r', diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index b3694b9fe8..815f3427f2 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -93,13 +93,23 @@ export const EntityCatalogGraphCard: ({ }) => JSX.Element; // @public -export type EntityEdge = DependencyGraphTypes.DependencyEdge<{ +export type EntityEdge = DependencyGraphTypes.DependencyEdge; + +// Warning: (ae-missing-release-tag) "EntityEdgeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type EntityEdgeData = { relations: string[]; label: 'visible'; -}>; +}; // @public -export type EntityNode = DependencyGraphTypes.DependencyNode<{ +export type EntityNode = DependencyGraphTypes.DependencyNode; + +// Warning: (ae-missing-release-tag) "EntityNodeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type EntityNodeData = { name: string; kind?: string; title?: string; @@ -107,7 +117,7 @@ export type EntityNode = DependencyGraphTypes.DependencyNode<{ focused?: boolean; color?: 'primary' | 'secondary' | 'default'; onClick?: MouseEventHandler; -}>; +}; // @public export const EntityRelationsGraph: ({ diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.test.tsx index 2a48600941..5b8f50b983 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.test.tsx @@ -31,12 +31,6 @@ describe('', () => { relations: [RELATION_PARENT_OF], from: 'from-id', to: 'to-id', - id: 'id', - x: 111, - y: 222, - width: 100, - height: 25, - points: [], }} /> , @@ -54,12 +48,6 @@ describe('', () => { relations: [RELATION_PARENT_OF, RELATION_CHILD_OF], from: 'from-id', to: 'to-id', - id: 'id', - x: 111, - y: 222, - width: 100, - height: 25, - points: [], }} /> , diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.tsx index 9016891efa..60a858eee6 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.tsx @@ -17,7 +17,7 @@ import { DependencyGraphTypes } from '@backstage/core-components'; import { BackstageTheme } from '@backstage/theme'; import makeStyles from '@material-ui/core/styles/makeStyles'; import React from 'react'; -import { GraphEdge } from './types'; +import { EntityEdgeData } from './types'; import classNames from 'classnames'; const useStyles = makeStyles((theme: BackstageTheme) => ({ @@ -31,7 +31,7 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({ export function CustomLabel({ edge: { relations }, -}: DependencyGraphTypes.RenderLabelProps) { +}: DependencyGraphTypes.RenderLabelProps) { const classes = useStyles(); return ( diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.test.tsx index f411f8255a..48272b8d1e 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.test.tsx @@ -36,10 +36,6 @@ describe('', () => { name: 'name', namespace: 'namespace', id: 'kind:namespace/name', - x: 111, - y: 222, - width: 100, - height: 25, color: 'primary', }} /> @@ -59,10 +55,6 @@ describe('', () => { name: 'name', namespace: 'default', id: 'kind:default/name', - x: 111, - y: 222, - width: 100, - height: 25, }} /> , @@ -83,10 +75,6 @@ describe('', () => { namespace: 'namespace', onClick, id: 'kind:namespace/name', - x: 111, - y: 222, - width: 100, - height: 25, }} /> , @@ -108,10 +96,6 @@ describe('', () => { namespace: 'namespace', title: 'Custom Title', id: 'kind:namespace/name', - x: 111, - y: 222, - width: 100, - height: 25, }} /> , diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx index 53542cfbd6..9cd752a5d7 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx @@ -20,7 +20,7 @@ import { makeStyles } from '@material-ui/core/styles'; import classNames from 'classnames'; import React, { useLayoutEffect, useRef, useState } from 'react'; import { EntityKindIcon } from './EntityKindIcon'; -import { GraphNode } from './types'; +import { EntityNodeData } from './types'; const useStyles = makeStyles((theme: BackstageTheme) => ({ node: { @@ -65,7 +65,7 @@ export function CustomNode({ title, onClick, }, -}: DependencyGraphTypes.RenderNodeProps) { +}: DependencyGraphTypes.RenderNodeProps) { const classes = useStyles(); const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts index 63ac9c2eac..b86c8f05ae 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts @@ -17,4 +17,9 @@ export { EntityRelationsGraph } from './EntityRelationsGraph'; export { ALL_RELATION_PAIRS } from './relations'; export type { RelationPairs } from './relations'; export { Direction } from './types'; -export type { EntityEdge, EntityNode } from './types'; +export type { + EntityEdgeData, + EntityEdge, + EntityNodeData, + EntityNode, +} from './types'; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts index d6bda71a33..830032d527 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts @@ -17,11 +17,9 @@ import { DependencyGraphTypes } from '@backstage/core-components'; import { MouseEventHandler } from 'react'; /** - * Edge between two entities. - * - * @public + * Additional Data for entities */ -export type EntityEdge = DependencyGraphTypes.DependencyEdge<{ +export type EntityEdgeData = { /** * Up to two relations that are connecting an entity. */ @@ -31,14 +29,19 @@ export type EntityEdge = DependencyGraphTypes.DependencyEdge<{ */ // Not used, but has to be non empty to draw a label at all! label: 'visible'; -}>; +}; /** - * Node representing an entity. + * Edge between two entities. * * @public */ -export type EntityNode = DependencyGraphTypes.DependencyNode<{ +export type EntityEdge = DependencyGraphTypes.DependencyEdge; + +/** + * Additional data for Entity Node + */ +export type EntityNodeData = { /** * Name of the entity. */ @@ -68,11 +71,14 @@ export type EntityNode = DependencyGraphTypes.DependencyNode<{ * Optional click handler. */ onClick?: MouseEventHandler; -}>; +}; -export type GraphEdge = DependencyGraphTypes.GraphEdge; - -export type GraphNode = DependencyGraphTypes.GraphNode; +/** + * Node representing an entity. + * + * @public + */ +export type EntityNode = DependencyGraphTypes.DependencyNode; /** * Render direction of the graph. From d748dea84ace1e28847cd4a40174b777e03dacdc Mon Sep 17 00:00:00 2001 From: Tomasz Szuba Date: Mon, 4 Oct 2021 16:38:57 +0200 Subject: [PATCH 02/37] Something along the way does not support linking to enum values. Fix it. Signed-off-by: Tomasz Szuba --- packages/core-components/api-report.md | 5 ----- .../src/components/DependencyGraph/DependencyGraph.tsx | 10 +++++----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 1067cd3d02..235e11607f 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -305,24 +305,19 @@ export interface DependencyGraphProps align?: Alignment; defs?: SVGDefsElement | SVGDefsElement[]; // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver - // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver direction?: Direction; edgeMargin?: number; edgeRanks?: number; edges: DependencyEdge[]; edgeWeight?: number; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver - // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver labelOffset?: number; // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver - // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver labelPosition?: LabelPosition; nodeMargin?: number; nodes: DependencyNode[]; paddingX?: number; paddingY?: number; // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver - // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver ranker?: Ranker; rankMargin?: number; renderLabel?: RenderLabelFunction; diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index 7a9ef9a9c7..1c3a4fce96 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -56,7 +56,7 @@ export interface DependencyGraphProps * * @remarks * - * Default: {@link DependencyGraphTypes.Direction.TOP_BOTTOM} + * Default: `DependencyGraphTypes.Direction.TOP_BOTTOM` */ direction?: Direction; /** @@ -112,7 +112,7 @@ export interface DependencyGraphProps * * @remarks * - * Default: {@link DependencyGraphTypes.Ranker.NETWORK_SIMPLEX} + * Default: `DependencyGraphTypes.Ranker.NETWORK_SIMPLEX` */ ranker?: Ranker; /** @@ -120,7 +120,7 @@ export interface DependencyGraphProps * * @remarks * - * Default: {@link DependencyGraphTypes.LabelPosition.RIGHT} + * Default: `DependencyGraphTypes.LabelPosition.RIGHT` */ labelPosition?: LabelPosition; /** @@ -128,8 +128,8 @@ export interface DependencyGraphProps * * @remarks * - * Applies only when {@link DependencyGraphProps.labelPosition} is {@link DependencyGraphTypes.LabelPosition.LEFT} or - * {@link DependencyGraphTypes.LabelPosition.RIGHT} + * Applies only when {@link DependencyGraphProps.labelPosition} is `DependencyGraphTypes.LabelPosition.LEFT` or + * `DependencyGraphTypes.LabelPosition.RIGHT` */ labelOffset?: number; /** From 19681e1c99ee38f27aeb632c200c95e5770de935 Mon Sep 17 00:00:00 2001 From: Tomasz Szuba Date: Mon, 4 Oct 2021 22:41:42 +0200 Subject: [PATCH 03/37] Move @types/dagre to devDependencies in core-components Signed-off-by: Tomasz Szuba --- packages/core-components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 67e359da78..8053d4fe3c 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -37,7 +37,6 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/dagre": "^0.7.44", "@types/react": "*", "@types/react-sparklines": "^1.7.0", "@types/react-text-truncate": "^0.14.0", @@ -79,6 +78,7 @@ "@types/d3-selection": "^2.0.0", "@types/d3-shape": "^3.0.1", "@types/d3-zoom": "^2.0.0", + "@types/dagre": "^0.7.44", "@types/google-protobuf": "^3.7.2", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", From 0de80576e3d11725216d04a21c65ab44758250ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Frinnstr=C3=B6m?= Date: Tue, 5 Oct 2021 09:20:07 +0200 Subject: [PATCH 04/37] auth-backend: update aws-alb provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Frinnström --- .../src/providers/aws-alb/provider.test.ts | 265 +++++++++++++----- .../src/providers/aws-alb/provider.ts | 264 +++++++++++++---- plugins/auth-backend/src/providers/index.ts | 1 + plugins/auth-backend/src/providers/types.ts | 14 - 4 files changed, 399 insertions(+), 145 deletions(-) diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index ca4e521316..2f5ffa1833 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -17,8 +17,14 @@ import { getVoidLogger } from '@backstage/backend-common'; import express from 'express'; import { JWT } from 'jose'; -import { AwsAlbAuthProvider } from './provider'; -import { AuthResponse } from '../types'; +import { + ALB_ACCESSTOKEN_HEADER, + ALB_JWT_HEADER, + AwsAlbAuthProvider, +} from './provider'; +import { TokenIssuer } from '../../identity/types'; +import { CatalogIdentityClient } from '../../lib/catalog'; +import { makeProfileInfo } from '../../lib/passport'; const jwtMock = JWT as jest.Mocked; @@ -29,9 +35,21 @@ yOlxJ2VW88mLAQGJ7HPAvOdylxZsItMnzCuqNzZvie8m/NJsOjhDncVkrw== -----END PUBLIC KEY----- `; }; +const mockJwt = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWV9JRCIsImlzcyI6IklTU1VFUl9VUkwifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlVzZXIgTmFtZSIsImlhdCI6MTUxNjIzOTAyMn0.uMCSBGhij1xn5pnot8XgD-huQuTIBOFGs6kkW_p_X94'; +const mockAccessToken = 'ACCESS_TOKEN'; +const mockClaims = { + sub: '1234567890', + name: 'User Name', + family_name: 'Name', + given_name: 'User', + picture: 'PICTURE_URL', + email: 'user.name@email.test', + exp: 1632833763, + iss: 'ISSUER_URL', +}; jest.mock('jose'); - jest.mock('cross-fetch', () => ({ __esModule: true, default: async () => { @@ -43,53 +61,48 @@ jest.mock('cross-fetch', () => ({ }, })); -const identityResolutionCallbackMock = async (): Promise> => { - return { - backstageIdentity: { - id: 'foo', - idToken: '', - }, - profile: { - displayName: 'Foo Bar', - }, - providerInfo: {}, - }; -}; - -const identityResolutionCallbackRejectedMock = async (): Promise< - AuthResponse -> => { - throw new Error('failed'); -}; - beforeEach(() => { jest.clearAllMocks(); }); -describe('AwsALBAuthProvider', () => { - const catalogApi = { - addLocation: jest.fn(), - removeLocationById: jest.fn(), - getEntities: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), - getLocationById: jest.fn(), - removeEntityByUid: jest.fn(), - getEntityByName: jest.fn(), - refreshEntity: jest.fn(), - getEntityAncestors: jest.fn(), +describe('AwsAlbAuthProvider', () => { + const tokenIssuer: TokenIssuer = { + listPublicKeys: jest.fn(), + async issueToken(params) { + return `token-for-${params.claims.sub}`; + }, }; + const catalogIdentityClient: CatalogIdentityClient = { + findUser: jest.fn(), + } as unknown as CatalogIdentityClient; const mockRequest = { - header: jest.fn(() => { - return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo'; - }), - } as unknown as express.Request; - const mockRequestWithoutJwt = { - header: jest.fn(() => { + header: jest.fn(name => { + if (name === ALB_JWT_HEADER) { + return mockJwt; + } else if (name === ALB_ACCESSTOKEN_HEADER) { + return mockAccessToken; + } return undefined; }), } as unknown as express.Request; + const mockRequestWithoutJwt = { + header: jest.fn(name => { + if (name === ALB_ACCESSTOKEN_HEADER) { + return mockAccessToken; + } + return undefined; + }), + } as unknown as express.Request; + const mockRequestWithoutAccessToken = { + header: jest.fn(name => { + if (name === ALB_JWT_HEADER) { + return mockJwt; + } + return undefined; + }), + } as unknown as express.Request; + const mockResponse = { end: jest.fn(), header: () => jest.fn(), @@ -97,38 +110,78 @@ describe('AwsALBAuthProvider', () => { status: jest.fn(), } as unknown as express.Response; - describe('should transform to type OAuthResponse', () => { + describe('should transform to type AwsAlbResponse', () => { it('when JWT is valid and identity is resolved successfully', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foo', + const provider = new AwsAlbAuthProvider({ + region: 'eu-west-1', + issuer: 'ISSUER_URL', + logger: getVoidLogger(), + catalogIdentityClient, + tokenIssuer, + authHandler: async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }), + signInResolver: async () => { + return { id: 'user.name', token: 'TOKEN' }; + }, }); - jwtMock.verify.mockImplementationOnce(() => ({ - sub: 'foo', - })); + jwtMock.verify.mockReturnValueOnce(mockClaims); await provider.refresh(mockRequest, mockResponse); expect(mockResponse.json).toHaveBeenCalledWith({ backstageIdentity: { - id: 'foo', - idToken: '', + id: 'user.name', + token: 'TOKEN', }, profile: { - displayName: 'Foo Bar', + displayName: 'User Name', + email: 'user.name@email.test', + picture: 'PICTURE_URL', + }, + providerInfo: { + accessToken: mockAccessToken, + expiresInSeconds: mockClaims.exp, }, - providerInfo: {}, }); }); }); + describe('should fail when', () => { + it('Access token is missing', async () => { + const provider = new AwsAlbAuthProvider({ + region: 'eu-west-1', + issuer: 'ISSUER_URL', + logger: getVoidLogger(), + catalogIdentityClient, + tokenIssuer, + authHandler: async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }), + signInResolver: async () => { + return { id: 'user.name', token: 'TOKEN' }; + }, + }); + + await provider.refresh(mockRequestWithoutAccessToken, mockResponse); + + expect(mockResponse.status).toHaveBeenCalledWith(401); + }); + it('JWT is missing', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foo', + const provider = new AwsAlbAuthProvider({ + region: 'eu-west-1', + issuer: 'ISSUER_URL', + logger: getVoidLogger(), + catalogIdentityClient, + tokenIssuer, + authHandler: async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }), + signInResolver: async () => { + return { id: 'user.name', token: 'TOKEN' }; + }, }); await provider.refresh(mockRequestWithoutJwt, mockResponse); @@ -137,10 +190,18 @@ describe('AwsALBAuthProvider', () => { }); it('JWT is invalid', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foo', + const provider = new AwsAlbAuthProvider({ + region: 'eu-west-1', + issuer: 'ISSUER_URL', + logger: getVoidLogger(), + catalogIdentityClient, + tokenIssuer, + authHandler: async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }), + signInResolver: async () => { + return { id: 'user.name', token: 'TOKEN' }; + }, }); jwtMock.verify.mockImplementationOnce(() => { @@ -152,11 +213,19 @@ describe('AwsALBAuthProvider', () => { expect(mockResponse.status).toHaveBeenCalledWith(401); }); - it('issuer is invalid', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foobar', + it('issuer is missing', async () => { + const provider = new AwsAlbAuthProvider({ + region: 'eu-west-1', + issuer: 'ISSUER_URL', + logger: getVoidLogger(), + catalogIdentityClient, + tokenIssuer, + authHandler: async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }), + signInResolver: async () => { + return { id: 'user.name', token: 'TOKEN' }; + }, }); jwtMock.verify.mockReturnValueOnce({}); @@ -165,14 +234,68 @@ describe('AwsALBAuthProvider', () => { expect(mockResponse.status).toHaveBeenCalledWith(401); }); - it('identity resolution callback rejects', async () => { - const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackRejectedMock, - issuer: 'foo', + it('issuer is invalid', async () => { + const provider = new AwsAlbAuthProvider({ + region: 'eu-west-1', + issuer: 'ISSUER_URL', + logger: getVoidLogger(), + catalogIdentityClient, + tokenIssuer, + authHandler: async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }), + signInResolver: async () => { + return { id: 'user.name', token: 'TOKEN' }; + }, }); - jwtMock.verify.mockReturnValueOnce({}); + jwtMock.verify.mockReturnValueOnce({ + iss: 'INVALID_ISSUE_URL', + }); + + await provider.refresh(mockRequest, mockResponse); + expect(mockResponse.status).toHaveBeenCalledWith(401); + }); + + it('SignInResolver rejects', async () => { + const provider = new AwsAlbAuthProvider({ + region: 'eu-west-1', + issuer: 'ISSUER_URL', + logger: getVoidLogger(), + catalogIdentityClient, + tokenIssuer, + authHandler: async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }), + signInResolver: async () => { + throw new Error(); + }, + }); + + jwtMock.verify.mockReturnValueOnce(mockClaims); + + await provider.refresh(mockRequest, mockResponse); + + expect(mockResponse.status).toHaveBeenCalledWith(401); + expect(mockResponse.end).toHaveBeenCalledTimes(1); + }); + + it('AuthHandler rejects', async () => { + const provider = new AwsAlbAuthProvider({ + region: 'eu-west-1', + issuer: 'ISSUER_URL', + logger: getVoidLogger(), + catalogIdentityClient, + tokenIssuer, + authHandler: async () => { + throw new Error(); + }, + signInResolver: async () => { + return { id: 'user.name', token: 'TOKEN' }; + }, + }); + + jwtMock.verify.mockReturnValueOnce(mockClaims); await provider.refresh(mockRequest, mockResponse); diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index bd16ae1960..04b192d562 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -14,9 +14,11 @@ * limitations under the License. */ import { + AuthHandler, AuthProviderFactoryOptions, AuthProviderRouteHandlers, - ExperimentalIdentityResolver, + AuthResponse, + SignInResolver, } from '../types'; import express from 'express'; import fetch from 'cross-fetch'; @@ -25,66 +27,101 @@ import { KeyObject } from 'crypto'; import { Logger } from 'winston'; import NodeCache from 'node-cache'; import { JWT } from 'jose'; -import { CatalogApi } from '@backstage/catalog-client'; +import { TokenIssuer } from '../../identity/types'; +import { CatalogIdentityClient } from '../../lib/catalog'; +import { Profile as PassportProfile } from 'passport'; +import { makeProfileInfo } from '../../lib/passport'; +import { AuthenticationError } from '@backstage/errors'; -const ALB_JWT_HEADER = 'x-amzn-oidc-data'; -/** - * A callback function that receives a verified JWT and returns a UserEntity - * @param {payload} The verified JWT payload - */ -type AwsAlbAuthProviderOptions = { +export const ALB_JWT_HEADER = 'x-amzn-oidc-data'; +export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken'; + +type Options = { region: string; issuer?: string; - identityResolutionCallback: ExperimentalIdentityResolver; + logger: Logger; + authHandler: AuthHandler; + signInResolver: SignInResolver; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; }; -export const getJWTHeaders = (input: string) => { + +export const getJWTHeaders = (input: string): AwsAlbHeaders => { const encoded = input.split('.')[0]; return JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')); }; -export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { - private logger: Logger; - private readonly catalogClient: CatalogApi; - private options: AwsAlbAuthProviderOptions; - private readonly keyCache: NodeCache; +export type AwsAlbHeaders = { + alg: string; + kid: string; + signer: string; + iss: string; + client: string; + exp: number; +}; - constructor( - logger: Logger, - catalogClient: CatalogApi, - options: AwsAlbAuthProviderOptions, - ) { - this.logger = logger; - this.catalogClient = catalogClient; - this.options = options; +export type AwsAlbClaims = { + sub: string; + name: string; + family_name: string; + given_name: string; + picture: string; + email: string; + exp: number; + iss: string; +}; + +export type AwsAlbResult = { + fullProfile: PassportProfile; + expiresInSeconds?: number; + accessToken: string; +}; + +export type AwsAlbProviderInfo = { + /** + * An access token issued for the signed in user. + */ + accessToken: string; + /** + * Expiry of the access token in seconds. + */ + expiresInSeconds?: number; +}; + +export type AwsAlbResponse = AuthResponse; + +export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { + private readonly region: string; + private readonly issuer?: string; + private readonly tokenIssuer: TokenIssuer; + private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly logger: Logger; + private readonly keyCache: NodeCache; + private readonly authHandler: AuthHandler; + private readonly signInResolver: SignInResolver; + + constructor(options: Options) { + this.region = options.region; + this.issuer = options.issuer; + this.authHandler = options.authHandler; + this.signInResolver = options.signInResolver; + this.tokenIssuer = options.tokenIssuer; + this.catalogIdentityClient = options.catalogIdentityClient; + this.logger = options.logger; this.keyCache = new NodeCache({ stdTTL: 3600 }); } + frameHandler(): Promise { return Promise.resolve(undefined); } async refresh(req: express.Request, res: express.Response): Promise { - const jwt = req.header(ALB_JWT_HEADER); - if (jwt !== undefined) { - try { - const headers = getJWTHeaders(jwt); - const key = await this.getKey(headers.kid); - const payload = JWT.verify(jwt, key); - - if (this.options.issuer && headers.iss !== this.options.issuer) { - throw new Error('issuer mismatch on JWT'); - } - - const resolvedEntity = await this.options.identityResolutionCallback( - payload, - this.catalogClient, - ); - res.json(resolvedEntity); - } catch (e) { - this.logger.error('exception occurred during JWT processing', e); - res.status(401); - res.end(); - } - } else { + try { + const result = await this.getResult(req); + const response = await this.handleResult(result); + res.json(response); + } catch (e) { + this.logger.error('Exception occurred during AWS ALB token refresh', e); res.status(401); res.end(); } @@ -94,13 +131,85 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { return Promise.resolve(undefined); } + private async getResult(req: express.Request): Promise { + const jwt = req.header(ALB_JWT_HEADER); + const accessToken = req.header(ALB_ACCESSTOKEN_HEADER); + + if (jwt === undefined) { + throw new AuthenticationError( + `Missing ALB OIDC header: ${ALB_JWT_HEADER}`, + ); + } + + if (accessToken === undefined) { + throw new AuthenticationError( + `Missing ALB OIDC header: ${ALB_ACCESSTOKEN_HEADER}`, + ); + } + + try { + const headers = getJWTHeaders(jwt); + const key = await this.getKey(headers.kid); + const claims = JWT.verify(jwt, key) as AwsAlbClaims; + + if (this.issuer && claims.iss !== this.issuer) { + throw new AuthenticationError('Issuer mismatch on JWT token'); + } + + const fullProfile: PassportProfile = { + provider: 'unknown', + id: claims.sub, + displayName: claims.name, + username: claims.email.split('@')[0].toLowerCase(), + name: { + familyName: claims.family_name, + givenName: claims.given_name, + }, + emails: [{ value: claims.email.toLowerCase() }], + photos: [{ value: claims.picture }], + }; + + return { + fullProfile, + expiresInSeconds: claims.exp, + accessToken, + }; + } catch (e) { + throw new Error(`Exception occurred during JWT processing: ${e}`); + } + } + + private async handleResult(result: AwsAlbResult): Promise { + const { profile } = await this.authHandler(result); + const backstageIdentity = await this.signInResolver( + { + result, + profile, + }, + { + tokenIssuer: this.tokenIssuer, + catalogIdentityClient: this.catalogIdentityClient, + logger: this.logger, + }, + ); + + return { + providerInfo: { + accessToken: result.accessToken, + expiresInSeconds: result.expiresInSeconds, + }, + backstageIdentity, + profile, + }; + } + async getKey(keyId: string): Promise { const optionalCacheKey = this.keyCache.get(keyId); if (optionalCacheKey) { return crypto.createPublicKey(optionalCacheKey); } const keyText: string = await fetch( - `https://public-keys.auth.elb.${this.options.region}.amazonaws.com/${keyId}`, + `https://public-keys.auth.elb.${this.region}.amazonaws.com/${keyId}`, ).then(response => response.text()); const keyValue = crypto.createPublicKey(keyText); this.keyCache.set(keyId, keyValue.export({ format: 'pem', type: 'spki' })); @@ -108,26 +217,61 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { } } -export type AwsAlbProviderOptions = {}; +export type AwsAlbProviderOptions = { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; -export const createAwsAlbProvider = (_options?: AwsAlbProviderOptions) => { + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}; + +export const createAwsAlbProvider = (options?: AwsAlbProviderOptions) => { return ({ - logger, - catalogApi, config, - identityResolver, + tokenIssuer, + catalogApi, + logger, }: AuthProviderFactoryOptions) => { const region = config.getString('region'); const issuer = config.getOptionalString('iss'); - if (identityResolver !== undefined) { - return new AwsAlbAuthProvider(logger, catalogApi, { - region, - issuer, - identityResolutionCallback: identityResolver, - }); + + if (options?.signIn.resolver === undefined) { + throw new Error( + 'SignInResolver is required to use this authentication provider', + ); } - throw new Error( - 'Identity resolver is required to use this authentication provider', - ); + + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }); + + const signInResolver = options?.signIn.resolver; + + return new AwsAlbAuthProvider({ + region, + issuer, + signInResolver, + authHandler, + tokenIssuer, + catalogIdentityClient, + logger, + }); }; }; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 90466a8094..e63616ab91 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -21,6 +21,7 @@ export * from './microsoft'; export * from './oauth2'; export * from './okta'; export * from './bitbucket'; +export * from './aws-alb'; export { factories as defaultAuthProviderFactories } from './factories'; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 6f6df5d904..7f7b841c68 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -119,19 +119,6 @@ export interface AuthProviderRouteHandlers { logout?(req: express.Request, res: express.Response): Promise; } -/** - * EXPERIMENTAL - this will almost certainly break in a future release. - * - * Used to resolve an identity from auth information in some auth providers. - */ -export type ExperimentalIdentityResolver = ( - /** - * An object containing information specific to the auth provider. - */ - payload: object, - catalogApi: CatalogApi, -) => Promise>; - export type AuthProviderFactoryOptions = { providerId: string; globalConfig: AuthProviderConfig; @@ -140,7 +127,6 @@ export type AuthProviderFactoryOptions = { tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; catalogApi: CatalogApi; - identityResolver?: ExperimentalIdentityResolver; }; export type AuthProviderFactory = ( From 0cfeea8f8f350e406c9a3257defcd5f79ef642e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Frinnstr=C3=B6m?= Date: Wed, 6 Oct 2021 08:09:57 +0200 Subject: [PATCH 05/37] Add changeset and update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Frinnström --- .changeset/breezy-dolphins-lay.md | 8 ++ .../docs/tutorials/aws-alb-aad-oidc-auth.md | 76 +++++++++++++------ 2 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 .changeset/breezy-dolphins-lay.md diff --git a/.changeset/breezy-dolphins-lay.md b/.changeset/breezy-dolphins-lay.md new file mode 100644 index 0000000000..0197979531 --- /dev/null +++ b/.changeset/breezy-dolphins-lay.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +AWS-ALB: update provider to the latest changes described [here](https://backstage.io/docs/auth/identity-resolver). + +This removes the `ExperimentalIdentityResolver` type in favor of `SignInResolver` and `AuthHandler`. +The AWS ALB provider can now be configured in the same way as the Google provider in the example. diff --git a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md index b121bab25f..6b1b8cb4e7 100644 --- a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md +++ b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md @@ -100,17 +100,16 @@ const app = createApp({ ### Backend -When using ALB auth it is not possible to leverage the built-in auth config discovery mechanism implemented in the app created by default; bespoke logic needs to be implemented. +When using ALB auth you can configure it as described [here](https://backstage.io/docs/auth/identity-resolver). -- replace the content of `packages/backend/plugin/auth.ts` with the below +- replace the content of `packages/backend/plugin/auth.ts` with the below and tweak it according to your needs. ```ts import { createRouter, - AuthResponse, - AuthProviderFactoryOptions, - defaultAuthProviderFactories, + createAwsAlbProvider, } from '@backstage/plugin-auth-backend'; +import { Router } from 'express'; import { PluginEnvironment } from '../types'; export default async function createPlugin({ @@ -118,30 +117,59 @@ export default async function createPlugin({ database, config, discovery, -}: PluginEnvironment) { - const identityResolver = (payload: any): Promise> => { - return Promise.resolve({ - providerInfo: {}, - profile: { - email: payload.email, - displayName: payload.name, - picture: payload.picture, - }, - backstageIdentity: { - id: payload.email, - }, - }); - }; - const providerFactories = { - awsalb: (options: AuthProviderFactoryOptions) => - defaultAuthProviderFactories.awsalb({ ...options, identityResolver }), - }; +}: PluginEnvironment): Promise { return await createRouter({ logger, config, database, discovery, - providerFactories, + providerFactories: { + awsalb: createAwsAlbProvider({ + authHandler: async ({ fullProfile }) => { + let email: string | undefined = undefined; + if (fullProfile.emails && fullProfile.emails.length > 0) { + const [firstEmail] = fullProfile.emails; + email = firstEmail.value; + } + + let picture: string | undefined = undefined; + if (fullProfile.photos && fullProfile.photos.length > 0) { + const [firstPhoto] = fullProfile.photos; + picture = firstPhoto.value; + } + + const displayName: string | undefined = + fullProfile.displayName ?? fullProfile.username ?? fullProfile.id; + + return { + profile: { + email, + picture, + displayName, + }, + }; + }, + signIn: { + resolver: async ({ profile: { email } }, ctx) => { + const [id] = email?.split('@') ?? ''; + // Fetch from an external system that returns entity claims like: + // ['user:default/breanna.davison', ...] + const ent = [`user:default/${id}`]; + + // Resolve group membership from the Backstage catalog + const fullEnt = + await ctx.catalogIdentityClient.resolveCatalogMembership({ + entityRefs: [id].concat(ent), + logger: ctx.logger, + }); + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: id, ent: fullEnt }, + }); + return { id, token }; + }, + }, + }), + }, }); } ``` From f29ad9fe6d08e63acd8c335b8448fa7f6f22283c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Frinnstr=C3=B6m?= Date: Wed, 6 Oct 2021 08:13:19 +0200 Subject: [PATCH 06/37] Update api-report.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Frinnström --- plugins/auth-backend/api-report.md | 30 +++++++++++++++---- .../src/providers/aws-alb/provider.ts | 13 ++++---- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 0b9f0eb8c4..0bdd73f3e8 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; @@ -32,7 +34,6 @@ export type AuthProviderFactoryOptions = { tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; catalogApi: CatalogApi; - identityResolver?: ExperimentalIdentityResolver; }; // Warning: (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag @@ -74,6 +75,16 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentity; }; +// Warning: (ae-missing-release-tag) "AwsAlbProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AwsAlbProviderOptions = { + authHandler?: AuthHandler; + signIn: { + resolver: SignInResolver; + }; +}; + // Warning: (ae-missing-release-tag) "BackstageIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -135,6 +146,13 @@ export const bitbucketUserIdSignInResolver: SignInResolver // @public (undocumented) export const bitbucketUsernameSignInResolver: SignInResolver; +// Warning: (ae-missing-release-tag) "createAwsAlbProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const createAwsAlbProvider: ( + options?: AwsAlbProviderOptions | undefined, +) => AuthProviderFactory; + // Warning: (ae-missing-release-tag) "createBitbucketProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -526,9 +544,9 @@ export type WebMessageResponse = // // src/identity/types.d.ts:25:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts -// src/providers/bitbucket/provider.d.ts:61:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts -// src/providers/bitbucket/provider.d.ts:69:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:109:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:115:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:132:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative +// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts +// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts +// src/providers/aws-alb/provider.d.ts:85:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:99:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:121:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative ``` diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 04b192d562..ac5db5e69f 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -15,7 +15,7 @@ */ import { AuthHandler, - AuthProviderFactoryOptions, + AuthProviderFactory, AuthProviderRouteHandlers, AuthResponse, SignInResolver, @@ -235,13 +235,10 @@ export type AwsAlbProviderOptions = { }; }; -export const createAwsAlbProvider = (options?: AwsAlbProviderOptions) => { - return ({ - config, - tokenIssuer, - catalogApi, - logger, - }: AuthProviderFactoryOptions) => { +export const createAwsAlbProvider = ( + options?: AwsAlbProviderOptions, +): AuthProviderFactory => { + return ({ config, tokenIssuer, catalogApi, logger }) => { const region = config.getString('region'); const issuer = config.getOptionalString('iss'); From 25c0779443d586ab8f1285f83ca057445b9ba012 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Mon, 4 Oct 2021 11:35:08 +0100 Subject: [PATCH 07/37] docs(ADR): Added ADR for using Luxons toLocaleString and date presets Signed-off-by: Harry Hogg --- ...dr012-use-luxon-locale-and-date-presets.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md diff --git a/docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md b/docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md new file mode 100644 index 0000000000..a9a5ef5849 --- /dev/null +++ b/docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md @@ -0,0 +1,43 @@ +--- +id: adrs-adr012 +title: ADR000: Use Luxon.toLocaleString and date/time presets +description: Architecture Decision Record (ADR) for using Luxon's toLocaleString method and date/time presets for displaying dates and times +--- + +## Context + +User's locales will have their own style of reading dates. It's counter +intuitive to not have dates formatted in their familiar formats, it can cause +users to have to think harder about what the date is and could even lead to +interpreting dates incorrectly (e.g. 05/03/2021, this could be March 5th or May +3rd, depending on where the user is). At the moment, plugins are defining dates +and times using custom formats and the `toFormat` method, which leads to +inconsistent and unfamiliar formats. + +## Decision + +To keep the UI consistent and familiar to users, irrespective of their location, +we have decided that we use `toLocaleString` and Luxon's +[extensive list](https://github.com/moment/luxon/blob/master/docs/formatting.md#presets) +of Date and Time presets. + +Here is an example: + +```typescript +const date = new luxon.DateTime(); + +/* Avoid this: */ +date.toFormat('yyyy LLL dd'); // 2014 Aug 06 +date.toFormat('yyyy LLL dd hh:mm'); // 2014 Aug 06 12:01 + +/* Do this instead: */ +date.toLocaleString(luxon.DateTime.DATE_MED); // US: Oct 14, 1983 | FR: 14 oct. 1983 +date.toLocaleString(luxon.DateTime.DATETIME_MED); // US: Oct 14, 1983, 9:30 | FR: 14 oct. 1983 9:30 +``` + +## Consequences + +- We will need to audit the current places Date/Times are being displayed in the + UI and update them to follow this ADR. +- We will need to keep in mind for reviewing PRs going forward to follow this + ADR, or find/create a linting rule to automate this in the review process. From 9fb9256e50fd3e870c37e9c85a456177d5de3343 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 8 Oct 2021 13:32:48 +0200 Subject: [PATCH 08/37] Add missing catalog changeset Co-authored-by: Patrik Oldsberg Signed-off-by: Johan Haals --- .changeset/modern-clouds-guess.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/modern-clouds-guess.md diff --git a/.changeset/modern-clouds-guess.md b/.changeset/modern-clouds-guess.md new file mode 100644 index 0000000000..c316094f96 --- /dev/null +++ b/.changeset/modern-clouds-guess.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +This continues the deprecation of classes used by the legacy catalog engine. New deprecations can be viewed in this [PR](https://github.com/backstage/backstage/pull/7500) or in the API reference documentation. + +The `batchAddOrUpdateEntities` method of the `EntitiesCatalog` interface has been marked as optional and is being deprecated. It is still implemented and required to be implemented by the legacy catalog classes, but was never implemented in the new catalog. + +This change is only relevant if you are consuming the `EntitiesCatalog` interface directly, in which case you will get a type error that you need to resolve. It can otherwise be ignored. From 6e7623ccc94caf6e67eff5f58074e322a5d16eef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Oct 2021 04:10:27 +0000 Subject: [PATCH 09/37] build(deps-dev): bump @types/regression from 2.0.0 to 2.0.2 Bumps [@types/regression](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/regression) from 2.0.0 to 2.0.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/regression) --- updated-dependencies: - dependency-name: "@types/regression" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index cfe8430bbf..9ba4bc161a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7401,9 +7401,9 @@ "@types/node" "*" "@types/regression@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@types/regression/-/regression-2.0.0.tgz#0677ea78d7bdb37039c02ebbccf062042f756ae3" - integrity sha512-Ch2FD53M1HpFLL6zSTc/sfuyqQcIPy+/PV3xFT6QYtk9EOiMI29XOYmLNxBb1Y0lfMOR/NNa86J1gRc/1jGLyw== + version "2.0.2" + resolved "https://registry.npmjs.org/@types/regression/-/regression-2.0.2.tgz#a1ad747fbcc6726643a8eb2c42bb804bbf34ce02" + integrity sha512-i7KOGl6xdkfpq5+p2ooC+/XFIRUMkYymZ29SD8p+Ko9lesKGUsh6860ey3YM7Y+ZG7kEDGcjzyLO3sOhozqEeA== "@types/request@^2.47.1": version "2.48.5" From 686068ea015b0602ff1262e3d50195cd3256c0b4 Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Fri, 8 Oct 2021 17:30:56 +0200 Subject: [PATCH 10/37] sort and filter by `metadata.title` if present Signed-off-by: Alex Rybchenko --- .../src/components/EntityTable/columns.tsx | 11 ++++++---- .../src/components/CatalogTable/columns.tsx | 21 ++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx index 9a48b2d554..1f4f5ee0d0 100644 --- a/plugins/catalog-react/src/components/EntityTable/columns.tsx +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -20,6 +20,7 @@ import { RELATION_OWNED_BY, RELATION_PART_OF, } from '@backstage/catalog-model'; +import { OverflowTooltip, TableColumn } from '@backstage/core-components'; import React from 'react'; import { getEntityRelations } from '../../utils'; import { @@ -27,7 +28,6 @@ import { EntityRefLinks, formatEntityRefTitle, } from '../EntityRefLink'; -import { OverflowTooltip, TableColumn } from '@backstage/core-components'; export function createEntityRefColumn({ defaultKind, @@ -35,9 +35,12 @@ export function createEntityRefColumn({ defaultKind?: string; }): TableColumn { function formatContent(entity: T): string { - return formatEntityRefTitle(entity, { - defaultKind, - }); + return ( + entity.metadata?.title || + formatEntityRefTitle(entity, { + defaultKind, + }) + ); } return { diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index 89c1a23966..683ac33132 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -14,10 +14,15 @@ * limitations under the License. */ import React from 'react'; -import { EntityRefLink, EntityRefLinks } from '@backstage/plugin-catalog-react'; +import { + formatEntityRefTitle, + EntityRefLink, + EntityRefLinks, +} from '@backstage/plugin-catalog-react'; import { Chip } from '@material-ui/core'; import { EntityRow } from './types'; import { OverflowTooltip, TableColumn } from '@backstage/core-components'; +import { Entity } from '@backstage/catalog-model'; type NameColumnProps = { defaultKind?: string; @@ -26,10 +31,24 @@ type NameColumnProps = { export function createNameColumn( props?: NameColumnProps, ): TableColumn { + function formatContent(entity: Entity): string { + return ( + entity.metadata?.title || + formatEntityRefTitle(entity, { + defaultKind: props?.defaultKind, + }) + ); + } + return { title: 'Name', field: 'resolved.name', highlight: true, + customSort({ entity: entity1 }, { entity: entity2 }) { + // TODO: We could implement this more efficiently by comparing field by field. + // This has similar issues as above. + return formatContent(entity1).localeCompare(formatContent(entity2)); + }, render: ({ entity }) => ( Date: Fri, 8 Oct 2021 14:40:46 -0500 Subject: [PATCH 11/37] Handle undefined properties Signed-off-by: Andre Wanlin --- .../src/api/AzureDevOpsApi.test.ts | 96 +++++++++++++++++++ .../src/api/AzureDevOpsApi.ts | 12 ++- plugins/azure-devops-backend/src/api/types.ts | 2 +- 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts index 077badfba1..25022b4c02 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts @@ -94,4 +94,100 @@ describe('AzureDevOpsApi', () => { expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild); }); }); + + describe('repoBuildFromBuild with undefined status', () => { + it('should return BuildStatus of None for status', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: undefined, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.None, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + source: 'refs/heads/develop (f4f78b31)', + }; + + expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('repoBuildFromBuild with undefined result', () => { + it('should return BuildResult of None for result', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + source: 'refs/heads/develop (f4f78b31)', + }; + + expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('repoBuildFromBuild with undefined link', () => { + it('should return empty string for link', () => { + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: undefined, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: '', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: new Date('2020-09-12T06:10:23.9325232Z'), + source: 'refs/heads/develop (f4f78b31)', + }; + + expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild); + }); + }); }); diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 13d5b391f6..3da4dff11d 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -17,7 +17,11 @@ import { Logger } from 'winston'; import { WebApi } from 'azure-devops-node-api'; import { RepoBuild } from './types'; -import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { + Build, + BuildResult, + BuildStatus, +} from 'azure-devops-node-api/interfaces/BuildInterfaces'; export class AzureDevOpsApi { constructor( @@ -97,9 +101,9 @@ export function repoBuildFromBuild(build: Build) { title: [build.definition?.name, build.buildNumber] .filter(Boolean) .join(' - '), - link: build._links?.web.href, - status: build.status, - result: build.result, + link: build._links?.web.href ? build._links?.web.href : '', + status: build.status ? build.status : BuildStatus.None, + result: build.result ? build.result : BuildResult.None, queueTime: build.queueTime, source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`, }; diff --git a/plugins/azure-devops-backend/src/api/types.ts b/plugins/azure-devops-backend/src/api/types.ts index 35d4e38ae1..b3562f76c6 100644 --- a/plugins/azure-devops-backend/src/api/types.ts +++ b/plugins/azure-devops-backend/src/api/types.ts @@ -22,7 +22,7 @@ import { export type RepoBuild = { id?: number; title: string; - link: string; + link?: string; status?: BuildStatus; result?: BuildResult; queueTime?: Date; From d5714b0de9ccbbd2f6b3fc0e232adb5746edb827 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 8 Oct 2021 14:43:57 -0500 Subject: [PATCH 12/37] Updated API Report Signed-off-by: Andre Wanlin --- plugins/azure-devops-backend/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 4d66f769da..4ad2f833d7 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -47,7 +47,7 @@ export function createRouter(options: RouterOptions): Promise; export type RepoBuild = { id?: number; title: string; - link: string; + link?: string; status?: BuildStatus; result?: BuildResult; queueTime?: Date; From a23206049fa6ebbe58b621e55d53b272f6391d91 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 8 Oct 2021 14:47:33 -0500 Subject: [PATCH 13/37] Added changeset Signed-off-by: Andre Wanlin --- .changeset/plenty-bees-run.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/plenty-bees-run.md diff --git a/.changeset/plenty-bees-run.md b/.changeset/plenty-bees-run.md new file mode 100644 index 0000000000..8574bb7848 --- /dev/null +++ b/.changeset/plenty-bees-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-devops-backend': patch +--- + +Updates function for mapping RepoBuilds to handle undefined properties From 6ec56d5a5716b952d734d07d2deacc52d865a546 Mon Sep 17 00:00:00 2001 From: kim5566 Date: Sat, 9 Oct 2021 18:00:14 +1100 Subject: [PATCH 14/37] fix check null error Signed-off-by: kim5566 --- .changeset/hip-suns-fix.md | 5 +++++ packages/core-components/src/components/Avatar/utils.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/hip-suns-fix.md diff --git a/.changeset/hip-suns-fix.md b/.changeset/hip-suns-fix.md new file mode 100644 index 0000000000..4555c7b179 --- /dev/null +++ b/.changeset/hip-suns-fix.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +update the null check to use the optional chaining operator in case of non-null assertion operator is not working in function extractInitials(values: string) diff --git a/packages/core-components/src/components/Avatar/utils.ts b/packages/core-components/src/components/Avatar/utils.ts index 98ce01664a..d8310f18c0 100644 --- a/packages/core-components/src/components/Avatar/utils.ts +++ b/packages/core-components/src/components/Avatar/utils.ts @@ -28,5 +28,5 @@ export function stringToColor(str: string) { } export function extractInitials(value: string) { - return value.match(/\b\w/g)!.join('').substring(0, 2); + return value.match(/\b\w/g)?.join('').substring(0, 2); } From 490a905199241be9c90c32b1481572129fc454b1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 7 Oct 2021 11:08:31 +0200 Subject: [PATCH 15/37] docs: rename CLI section to Local Development + move in linking docs Signed-off-by: Patrik Oldsberg --- docs/deployment/docker.md | 2 +- docs/getting-started/create-an-app.md | 53 ------------------ .../commands.md => local-dev/cli-commands.md} | 4 +- .../index.md => local-dev/cli-overview.md} | 6 +- docs/local-dev/linking-local-packages.md | 56 +++++++++++++++++++ docs/plugins/create-a-plugin.md | 3 +- docs/plugins/github-apps.md | 2 +- microsite/sidebars.json | 9 ++- mkdocs.yml | 8 ++- 9 files changed, 78 insertions(+), 65 deletions(-) rename docs/{cli/commands.md => local-dev/cli-commands.md} (99%) rename docs/{cli/index.md => local-dev/cli-overview.md} (97%) create mode 100644 docs/local-dev/linking-local-packages.md diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index ba32706cc4..7230f5cf16 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -76,7 +76,7 @@ CMD ["node", "packages/backend", "--config", "app-config.yaml"] For more details on how the `backend:bundle` command and the `skeleton.tar.gz` file works, see the -[`backend:bundle` command docs](../cli/commands.md#backendbundle). +[`backend:bundle` command docs](../local-dev/cli-commands.md#backendbundle). The `Dockerfile` is located at `packages/backend/Dockerfile`, but needs to be executed with the root of the repo as the build context, in order to get access diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index ac78685a82..e5d15d5845 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -132,56 +132,3 @@ Now you're free to hack away on your own Backstage installation! As you get more experienced with the app, in future you can run just the frontend with `yarn start` in one window, and the backend with `yarn start-backend` in a different window. - -## Linking in local Backstage packages - -It can often be useful to try out changes to the packages in the main Backstage -repo within your own app. For example if you want to make modifications to -`@backstage/core-plugin-api` and try them out in your app. - -To link in external packages, add them to your `package.json` and `lerna.json` -workspace paths. These can be either relative or absolute paths with or without -globs. For example: - -```json -"packages": [ - "packages/*", - "plugins/*", - "../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api -], -``` - -Then reinstall packages to make yarn set up symlinks: - -```bash -yarn install -``` - -With this in place you can now modify the `@backstage/core-plugin-api` package -within the main repo, and have those changes be reflected and tested in your -app. Simply run your app using `yarn dev` (or `yarn start` for just frontend) as -normal. - -Note that for backend packages you need to make sure that linked packages are -not dependencies of any non-linked package. If you for example want to work on -`@backstage/backend-common`, you need to also link in other backend plugins and -packages that depend on `@backstage/backend-common`, or temporarily disable -those plugins in your backend. This is because the transformation of backend -module tree stops whenever a non-local package is encountered, and from that -point node will `require` packages directly for that entire module subtree. - -Type checking can also have issues when linking in external packages, since the -linked in packages will use the types in the external project and dependency -version mismatches between the two projects may cause errors. To fix any of -those errors you need to sync versions of the dependencies in the two projects. -A simple way to do this can be to copy over `yarn.lock` from the external -project and run `yarn install`, although this is quite intrusive and can cause -other issues in existing projects, so use this method with care. It can often be -best to simply ignore the type errors, as app serving will work just fine -anyway. - -Another issue with type checking is that the incremental type cache doesn't -invalidate correctly for the linked in packages, causing type checking to not -reflect changes made to types. You can work around this by either setting -`compilerOptions.incremental = false` in `tsconfig.json`, or by deleting the -types cache folder `dist-types` before running `yarn tsc`. diff --git a/docs/cli/commands.md b/docs/local-dev/cli-commands.md similarity index 99% rename from docs/cli/commands.md rename to docs/local-dev/cli-commands.md index 5727833562..0397ffa09c 100644 --- a/docs/cli/commands.md +++ b/docs/local-dev/cli-commands.md @@ -1,6 +1,6 @@ --- -id: commands -title: Commands +id: cli-commands +title: CLI Commands description: Descriptions of all commands available in the CLI. --- diff --git a/docs/cli/index.md b/docs/local-dev/cli-overview.md similarity index 97% rename from docs/cli/index.md rename to docs/local-dev/cli-overview.md index 1b2e01be62..de9e378632 100644 --- a/docs/cli/index.md +++ b/docs/local-dev/cli-overview.md @@ -1,6 +1,6 @@ --- -id: index -title: Overview +id: cli-overview +title: CLI Overview description: Overview of the Backstage CLI --- @@ -20,7 +20,7 @@ Under the hood the CLI uses [Webpack](https://webpack.js.org/) for bundling, linting. It also includes custom tooling for working within Backstage apps, for example for keeping the app up to date and verifying static configuration. -For a full list of CLI commands, see the [commands](./commands.md) page. +For a full list of CLI commands, see the [commands](./cli-commands.md) page. ## Introduction diff --git a/docs/local-dev/linking-local-packages.md b/docs/local-dev/linking-local-packages.md new file mode 100644 index 0000000000..40b722e302 --- /dev/null +++ b/docs/local-dev/linking-local-packages.md @@ -0,0 +1,56 @@ +--- +id: linking-local-packages +title: Linking in Local Packages +description: How to link in other local packages into your Backstage monorepo +--- + +It can often be useful to try out changes to the packages in the main Backstage +repo within your own app. For example if you want to make modifications to +`@backstage/core-plugin-api` and try them out in your app. + +To link in external packages, add them to your `package.json` and `lerna.json` +workspace paths. These can be either relative or absolute paths with or without +globs. For example: + +```json +"packages": [ + "packages/*", + "plugins/*", + "../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api +], +``` + +Then reinstall packages to make yarn set up symlinks: + +```bash +yarn install +``` + +With this in place you can now modify the `@backstage/core-plugin-api` package +within the main repo, and have those changes be reflected and tested in your +app. Simply run your app using `yarn dev` (or `yarn start` for just frontend) as +normal. + +Note that for backend packages you need to make sure that linked packages are +not dependencies of any non-linked package. If you for example want to work on +`@backstage/backend-common`, you need to also link in other backend plugins and +packages that depend on `@backstage/backend-common`, or temporarily disable +those plugins in your backend. This is because the transformation of backend +module tree stops whenever a non-local package is encountered, and from that +point node will `require` packages directly for that entire module subtree. + +Type checking can also have issues when linking in external packages, since the +linked in packages will use the types in the external project and dependency +version mismatches between the two projects may cause errors. To fix any of +those errors you need to sync versions of the dependencies in the two projects. +A simple way to do this can be to copy over `yarn.lock` from the external +project and run `yarn install`, although this is quite intrusive and can cause +other issues in existing projects, so use this method with care. It can often be +best to simply ignore the type errors, as app serving will work just fine +anyway. + +Another issue with type checking is that the incremental type cache doesn't +invalidate correctly for the linked in packages, causing type checking to not +reflect changes made to types. You can work around this by either setting +`compilerOptions.incremental = false` in `tsconfig.json`, or by deleting the +types cache folder `dist-types` before running `yarn tsc`. diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index c35f4b91fa..ef415d107a 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -10,7 +10,8 @@ A Backstage Plugin adds functionality to Backstage. To create a new plugin, make sure you've run `yarn install` and installed dependencies, then run the following on your command line (a shortcut to -invoking the [`backstage-cli create-plugin`](../cli/commands.md#create-plugin)) +invoking the +[`backstage-cli create-plugin`](../local-dev/cli-commands.md#create-plugin)) from the root of your project. ```bash diff --git a/docs/plugins/github-apps.md b/docs/plugins/github-apps.md index f9f7590e09..ef0b21d902 100644 --- a/docs/plugins/github-apps.md +++ b/docs/plugins/github-apps.md @@ -36,7 +36,7 @@ that we provide. This gives us a way to automate some of the work required to create a GitHub app. You can read more about the -[`backstage-cli create-github-app` method](../cli/commands.md#create-github-app). +[`backstage-cli create-github-app` method](../local-dev/cli-commands.md#create-github-app). Once you've gone through the CLI command, it should produce a YAML file in the root of the project which you can then use as an `include` in your diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 9cf3b204c2..bc9f827edc 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -29,7 +29,14 @@ "getting-started/contributors", "getting-started/project-structure" ], - "CLI": ["cli/index", "cli/commands"], + "Local Development": [ + { + "type": "subcategory", + "label": "CLI", + "ids": ["local-dev/cli-overview", "local-dev/cli-commands"] + }, + "local-dev/linking-local-packages" + ], "Core Features": [ { "type": "subcategory", diff --git a/mkdocs.yml b/mkdocs.yml index 366411c7c4..b668ffdb15 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,9 +27,11 @@ nav: - Key Concepts: 'getting-started/concepts.md' - Contributors: 'getting-started/contributors.md' - Project Structure: 'getting-started/project-structure.md' - - CLI: - - Overview: 'cli/index.md' - - Commands: 'cli/commands.md' + - Local Development: + - CLI: + - Overview: 'local-dev/cli-overview.md' + - Commands: 'local-dev/cli-commands.md' + - Linking in Local Packages: 'local-dev/linking-local-packages.md' - Core Features: - Software Catalog: - Overview: 'features/software-catalog/index.md' From af092b088409c45171b1408c147d1aed9345f27d Mon Sep 17 00:00:00 2001 From: PhakornKiong Date: Sat, 9 Oct 2021 22:08:58 +0800 Subject: [PATCH 16/37] fix sidebar from being pushed out of alignment Signed-off-by: PhakornKiong --- microsite/static/css/custom.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css index ac432372b1..139fa7b1d2 100644 --- a/microsite/static/css/custom.css +++ b/microsite/static/css/custom.css @@ -1217,3 +1217,7 @@ code { .medium-zoom-image { z-index: 10000; } + +h3.collapsible span.arrow { + margin-right: 4px; +} From 445d222a487d62629d3226addc3fcbcca90df055 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 5 Oct 2021 13:28:33 +0200 Subject: [PATCH 17/37] added scaffolder-common package with beta3 entity definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- plugins/scaffolder-common/.eslintrc.js | 3 + plugins/scaffolder-common/README.md | 3 + plugins/scaffolder-common/package.json | 44 +++++ .../src/Template.v1beta3.schema.json | 186 ++++++++++++++++++ .../src/TemplateEntityV1beta3.test.ts | 157 +++++++++++++++ .../src/TemplateEntityV1beta3.ts | 37 ++++ plugins/scaffolder-common/src/index.ts | 27 +++ 7 files changed, 457 insertions(+) create mode 100644 plugins/scaffolder-common/.eslintrc.js create mode 100644 plugins/scaffolder-common/README.md create mode 100644 plugins/scaffolder-common/package.json create mode 100644 plugins/scaffolder-common/src/Template.v1beta3.schema.json create mode 100644 plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts create mode 100644 plugins/scaffolder-common/src/TemplateEntityV1beta3.ts create mode 100644 plugins/scaffolder-common/src/index.ts diff --git a/plugins/scaffolder-common/.eslintrc.js b/plugins/scaffolder-common/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/scaffolder-common/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/scaffolder-common/README.md b/plugins/scaffolder-common/README.md new file mode 100644 index 0000000000..9217ceab86 --- /dev/null +++ b/plugins/scaffolder-common/README.md @@ -0,0 +1,3 @@ +# @backstage/plugin-scaffolder-common + +Common types and functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend. diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json new file mode 100644 index 0000000000..ad422fb149 --- /dev/null +++ b/plugins/scaffolder-common/package.json @@ -0,0 +1,44 @@ +{ + "name": "@backstage/plugin-scaffolder-common", + "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugin/scaffolder-common" + }, + "keywords": [ + "techdocs", + "scaffolder" + ], + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "bugs": { + "url": "https://github.com/backstage/backstage/issues" + }, + "dependencies": { + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10" + }, + "devDependencies": { + "@backstage/cli": "^0.7.13" + } +} diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json new file mode 100644 index 0000000000..afcc6dd011 --- /dev/null +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -0,0 +1,186 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "TemplateV1beta3", + "description": "A Template describes a scaffolding task for use with the Scaffolder. It describes the required parameters as well as a series of steps that will be taken to execute the scaffolding task.", + "examples": [ + { + "apiVersion": "templates.backstage.io/v1beta3", + "kind": "Template", + "metadata": { + "name": "react-ssr-template", + "title": "React SSR Template", + "description": "Next.js application skeleton for creating isomorphic web applications.", + "tags": ["recommended", "react"] + }, + "spec": { + "owner": "artist-relations-team", + "parameters": { + "required": ["name", "description", "repoUrl"], + "properties": { + "name": { + "title": "Name", + "type": "string", + "description": "Unique name of the component" + }, + "description": { + "title": "Description", + "type": "string", + "description": "Description of the component" + }, + "repoUrl": { + "title": "Pick a repository", + "type": "string", + "ui:field": "RepoUrlPicker" + } + } + }, + "steps": [ + { + "id": "fetch", + "name": "Fetch", + "action": "fetch:plain", + "parameters": { + "url": "./template" + } + }, + { + "id": "publish", + "name": "Publish to GitHub", + "action": "publish:github", + "parameters": { + "repoUrl": "${{ parameters.repoUrl }}" + }, + "if": "${{ parameters.repoUrl }}" + } + ], + "output": { + "catalogInfoUrl": "${{ steps.publish.output.catalogInfoUrl }}" + } + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["templates.backstage.io/v1beta3"] + }, + "kind": { + "enum": ["Template"] + }, + "spec": { + "type": "object", + "required": ["type", "steps"], + "properties": { + "type": { + "type": "string", + "description": "The type of component created by the template. The software catalog accepts any type value, but an organization should take great care to establish a proper taxonomy for these. Tools including Backstage itself may read this field and behave differently depending on its value. For example, a website type component may present tooling in the Backstage interface that is specific to just websites.", + "examples": ["service", "website", "library"], + "minLength": 1 + }, + "parameters": { + "oneOf": [ + { + "type": "object", + "description": "The JSONSchema describing the inputs for the template." + }, + { + "type": "array", + "description": "A list of separate forms to collect parameters.", + "items": { + "type": "object", + "description": "The JSONSchema describing the inputs for the template." + } + } + ] + }, + "steps": { + "type": "array", + "description": "A list of steps to execute.", + "items": { + "type": "object", + "description": "A description of the step to execute.", + "required": ["action"], + "properties": { + "id": { + "type": "string", + "description": "The ID of the step, which can be used to refer to its outputs." + }, + "name": { + "type": "string", + "description": "The name of the step, which will be displayed in the UI during the scaffolding process." + }, + "action": { + "type": "string", + "description": "The name of the action to execute." + }, + "input": { + "type": "object", + "description": "A templated object describing the inputs to the action." + }, + "if": { + "type": ["string", "boolean"], + "description": "A templated condition that skips the step when evaluated to false. If the condition is true or not defined, the step is executed. The condition is true, if the input is not `false`, `undefined`, `null`, `\"\"`, `0`, or `[]`." + } + } + } + }, + "output": { + "type": "object", + "description": "A templated object describing the outputs of the scaffolding task.", + "properties": { + "links": { + "type": "array", + "description": "A list of external hyperlinks, typically pointing to resources created or updated by the template", + "items": { + "type": "object", + "required": [], + "properties": { + "url": { + "type": "string", + "description": "A url in a standard uri format.", + "examples": ["https://github.com/my-org/my-new-repo"], + "minLength": 1 + }, + "entityRef": { + "type": "string", + "description": "An entity reference to an entity in the catalog.", + "examples": ["Component:default/my-app"], + "minLength": 1 + }, + "title": { + "type": "string", + "description": "A user friendly display name for the link.", + "examples": ["View new repo"], + "minLength": 1 + }, + "icon": { + "type": "string", + "description": "A key representing a visual icon to be displayed in the UI.", + "examples": ["dashboard"], + "minLength": 1 + } + } + } + } + }, + "additionalProperties": { + "type": "string" + } + }, + "owner": { + "type": "string", + "description": "The user (or group) owner of the template", + "minLength": 1 + } + } + } + } + } + ] +} diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts new file mode 100644 index 0000000000..dd3080333f --- /dev/null +++ b/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts @@ -0,0 +1,157 @@ +/* + * Copyright 2020 The Backstage 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. + */ + +import { entityKindSchemaValidator } from '@backstage/catalog-model'; +import type { TemplateEntityV1beta3 } from './TemplateEntityV1beta3'; +import schema from './Template.v1beta3.schema.json'; + +const validator = entityKindSchemaValidator(schema); + +describe('templateEntityV1beta3Validator', () => { + let entity: TemplateEntityV1beta3; + + beforeEach(() => { + entity = { + apiVersion: 'templates.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'test', + }, + spec: { + type: 'website', + parameters: { + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + }, + }, + steps: [ + { + id: 'fetch', + name: 'Fetch', + action: 'fetch:plan', + input: { + url: './template', + }, + if: '${{ parameters.owner }}', + }, + ], + output: { + fetchUrl: '${{ steps.fetch.output.targetUrl }}', + }, + owner: 'team-b@example.com', + }, + }; + }); + + it('happy path: accepts valid data', async () => { + expect(validator(entity)).toBe(entity); + }); + + it('ignores unknown apiVersion', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta0'; + expect(validator(entity)).toBe(false); + }); + + it('ignores unknown kind', async () => { + (entity as any).kind = 'Wizard'; + expect(validator(entity)).toBe(false); + }); + + it('rejects missing type', async () => { + delete (entity as any).spec.type; + expect(() => validator(entity)).toThrow(/type/); + }); + + it('accepts any other type', async () => { + (entity as any).spec.type = 'hallo'; + expect(validator(entity)).toBe(entity); + }); + + it('accepts missing parameters', async () => { + delete (entity as any).spec.parameters; + expect(validator(entity)).toBe(entity); + }); + + it('accepts missing outputs', async () => { + delete (entity as any).spec.outputs; + expect(validator(entity)).toBe(entity); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + expect(() => validator(entity)).toThrow(/type/); + }); + + it('rejects missing steps', async () => { + delete (entity as any).spec.steps; + expect(() => validator(entity)).toThrow(/steps/); + }); + + it('accepts step with missing id', async () => { + delete (entity as any).spec.steps[0].id; + expect(validator(entity)).toBe(entity); + }); + + it('accepts step with missing name', async () => { + delete (entity as any).spec.steps[0].name; + expect(validator(entity)).toBe(entity); + }); + + it('rejects step with missing action', async () => { + delete (entity as any).spec.steps[0].action; + expect(() => validator(entity)).toThrow(/action/); + }); + + it('accepts missing owner', async () => { + delete (entity as any).spec.owner; + expect(validator(entity)).toBe(entity); + }); + + it('rejects empty owner', async () => { + (entity as any).spec.owner = ''; + expect(() => validator(entity)).toThrow(/owner/); + }); + + it('rejects wrong type owner', async () => { + (entity as any).spec.owner = 5; + expect(() => validator(entity)).toThrow(/owner/); + }); + + it('accepts missing if', async () => { + delete (entity as any).spec.steps[0].if; + expect(validator(entity)).toBe(entity); + }); + + it('accepts boolean in if', async () => { + (entity as any).spec.steps[0].if = true; + expect(validator(entity)).toBe(entity); + }); + + it('accepts empty if', async () => { + (entity as any).spec.steps[0].if = ''; + expect(validator(entity)).toBe(entity); + }); + + it('rejects wrong type if', async () => { + (entity as any).spec.steps[0].if = 5; + expect(() => validator(entity)).toThrow(/if/); + }); +}); diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts new file mode 100644 index 0000000000..ebad23f94a --- /dev/null +++ b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2020 The Backstage 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. + */ + +import { JsonObject } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; + +/** @public */ +export interface TemplateEntityV1beta3 extends Entity { + apiVersion: 'templates.backstage.io/v1beta3'; + kind: 'Template'; + spec: { + type: string; + parameters?: JsonObject | JsonObject[]; + steps: Array<{ + id?: string; + name?: string; + action: string; + input?: JsonObject; + if?: string | boolean; + }>; + output?: { [name: string]: string }; + owner?: string; + }; +} diff --git a/plugins/scaffolder-common/src/index.ts b/plugins/scaffolder-common/src/index.ts new file mode 100644 index 0000000000..5cce1ceded --- /dev/null +++ b/plugins/scaffolder-common/src/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Backstage 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. + */ + +/** + * Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin + * + * @packageDocumentation + */ + +// The `{ default as ...}` re-export does not seem to work for json in tests +import templateEntityV1beta3Schema from './Template.v1beta3.schema.json'; + +export type { TemplateEntityV1beta3 } from './TemplateEntityV1beta3'; +export { templateEntityV1beta3Schema }; From 65742aa801f0dc62b1668c0eb05aaf2f8a2fca12 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 5 Oct 2021 17:37:10 +0200 Subject: [PATCH 18/37] scaffolder-backend: exported new TemplateEntityProcessor Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- plugins/scaffolder-backend/package.json | 2 + plugins/scaffolder-backend/src/index.ts | 1 + .../processor/TemplateEntityProcessor.test.ts | 80 +++++++++++++++ .../src/processor/TemplateEntityProcessor.ts | 97 +++++++++++++++++++ .../scaffolder-backend/src/processor/index.ts | 17 ++++ 5 files changed, 197 insertions(+) create mode 100644 plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.test.ts create mode 100644 plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.ts create mode 100644 plugins/scaffolder-backend/src/processor/index.ts diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index ffe8a1e1b8..4de879d41b 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -36,6 +36,8 @@ "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.2", "@backstage/integration": "^0.6.7", + "@backstage/plugin-catalog-backend": "^0.15.0", + "@backstage/plugin-scaffolder-common": "^0.1.0", "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.2", "@gitbeaker/core": "^30.2.0", "@gitbeaker/node": "^30.2.0", diff --git a/plugins/scaffolder-backend/src/index.ts b/plugins/scaffolder-backend/src/index.ts index a7ae821463..76cda63eef 100644 --- a/plugins/scaffolder-backend/src/index.ts +++ b/plugins/scaffolder-backend/src/index.ts @@ -23,3 +23,4 @@ export * from './scaffolder'; export * from './service/router'; export * from './lib/catalog'; +export * from './processor'; diff --git a/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.test.ts b/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.test.ts new file mode 100644 index 0000000000..6b9576695a --- /dev/null +++ b/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2020 The Backstage 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. + */ + +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityProcessor } from './TemplateEntityProcessor'; + +const mockLocation = { type: 'a', target: 'b' }; +const mockEntity: TemplateEntityV1beta3 = { + apiVersion: 'templates.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'n' }, + spec: { + parameters: {}, + steps: [], + type: 'service', + owner: 'o', + }, +}; + +describe('TemplateEntityProcessor', () => { + describe('validateEntityKind', () => { + it('validates the entity kind', async () => { + const processor = new TemplateEntityProcessor(); + + await expect(processor.validateEntityKind(mockEntity)).resolves.toBe( + true, + ); + await expect( + processor.validateEntityKind({ + ...mockEntity, + apiVersion: 'backstage.io/v1beta3', + }), + ).resolves.toBe(false); + await expect( + processor.validateEntityKind({ ...mockEntity, kind: 'Component' }), + ).resolves.toBe(false); + }); + }); + + describe('postProcessEntity', () => { + it('generates relations for component entities', async () => { + const processor = new TemplateEntityProcessor(); + + const emit = jest.fn(); + + await processor.postProcessEntity(mockEntity, mockLocation, emit); + + expect(emit).toBeCalledTimes(2); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Group', namespace: 'default', name: 'o' }, + type: 'ownerOf', + target: { kind: 'Template', namespace: 'default', name: 'n' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Template', namespace: 'default', name: 'n' }, + type: 'ownedBy', + target: { kind: 'Group', namespace: 'default', name: 'o' }, + }, + }); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.ts b/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.ts new file mode 100644 index 0000000000..dc11b6d86d --- /dev/null +++ b/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2020 The Backstage 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. + */ + +import { + Entity, + getEntityName, + LocationSpec, + parseEntityRef, + RELATION_OWNED_BY, + RELATION_OWNER_OF, + entityKindSchemaValidator, +} from '@backstage/catalog-model'; +import { + CatalogProcessor, + CatalogProcessorEmit, + results, +} from '@backstage/plugin-catalog-backend'; +import { + TemplateEntityV1beta3, + templateEntityV1beta3Schema, +} from '@backstage/plugin-scaffolder-common'; + +export class TemplateEntityProcessor implements CatalogProcessor { + private readonly validators = [ + entityKindSchemaValidator(templateEntityV1beta3Schema), + ]; + + async validateEntityKind(entity: Entity): Promise { + for (const validator of this.validators) { + if (validator(entity)) { + return true; + } + } + + return false; + } + + async postProcessEntity( + entity: Entity, + _location: LocationSpec, + emit: CatalogProcessorEmit, + ): Promise { + const selfRef = getEntityName(entity); + + if ( + entity.apiVersion === 'templates.backstage.io/v1beta3' && + entity.kind === 'Template' + ) { + const template = entity as TemplateEntityV1beta3; + + const target = template.spec.owner; + if (target) { + const targetRef = parseEntityRef(target, { + defaultKind: 'Group', + defaultNamespace: selfRef.namespace, + }); + emit( + results.relation({ + source: selfRef, + type: RELATION_OWNED_BY, + target: { + kind: targetRef.kind, + namespace: targetRef.namespace, + name: targetRef.name, + }, + }), + ); + emit( + results.relation({ + source: { + kind: targetRef.kind, + namespace: targetRef.namespace, + name: targetRef.name, + }, + type: RELATION_OWNER_OF, + target: selfRef, + }), + ); + } + } + + return entity; + } +} diff --git a/plugins/scaffolder-backend/src/processor/index.ts b/plugins/scaffolder-backend/src/processor/index.ts new file mode 100644 index 0000000000..621ac186a4 --- /dev/null +++ b/plugins/scaffolder-backend/src/processor/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage 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. + */ + +export { TemplateEntityProcessor } from './TemplateEntityProcessor'; From 987eb79f63f7b41389aacf79a4b0353578adc69a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 6 Oct 2021 11:23:08 +0200 Subject: [PATCH 19/37] scaffolder-common: use apiVersion group scaffolder.backstage.io Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- .../src/processor/TemplateEntityProcessor.test.ts | 2 +- .../src/processor/TemplateEntityProcessor.ts | 2 +- plugins/scaffolder-common/src/Template.v1beta3.schema.json | 4 ++-- plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts | 2 +- plugins/scaffolder-common/src/TemplateEntityV1beta3.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.test.ts b/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.test.ts index 6b9576695a..568231f673 100644 --- a/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.test.ts +++ b/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.test.ts @@ -19,7 +19,7 @@ import { TemplateEntityProcessor } from './TemplateEntityProcessor'; const mockLocation = { type: 'a', target: 'b' }; const mockEntity: TemplateEntityV1beta3 = { - apiVersion: 'templates.backstage.io/v1beta3', + apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { name: 'n' }, spec: { diff --git a/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.ts b/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.ts index dc11b6d86d..8bd741a4ce 100644 --- a/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.ts +++ b/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.ts @@ -56,7 +56,7 @@ export class TemplateEntityProcessor implements CatalogProcessor { const selfRef = getEntityName(entity); if ( - entity.apiVersion === 'templates.backstage.io/v1beta3' && + entity.apiVersion === 'scaffolder.backstage.io/v1beta3' && entity.kind === 'Template' ) { const template = entity as TemplateEntityV1beta3; diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json index afcc6dd011..6992e82f02 100644 --- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -4,7 +4,7 @@ "description": "A Template describes a scaffolding task for use with the Scaffolder. It describes the required parameters as well as a series of steps that will be taken to execute the scaffolding task.", "examples": [ { - "apiVersion": "templates.backstage.io/v1beta3", + "apiVersion": "scaffolder.backstage.io/v1beta3", "kind": "Template", "metadata": { "name": "react-ssr-template", @@ -68,7 +68,7 @@ "required": ["spec"], "properties": { "apiVersion": { - "enum": ["templates.backstage.io/v1beta3"] + "enum": ["scaffolder.backstage.io/v1beta3"] }, "kind": { "enum": ["Template"] diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts index dd3080333f..7863b7b476 100644 --- a/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts +++ b/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts @@ -25,7 +25,7 @@ describe('templateEntityV1beta3Validator', () => { beforeEach(() => { entity = { - apiVersion: 'templates.backstage.io/v1beta3', + apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { name: 'test', diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts index ebad23f94a..6471fe0ca6 100644 --- a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts +++ b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts @@ -19,7 +19,7 @@ import { Entity } from '@backstage/catalog-model'; /** @public */ export interface TemplateEntityV1beta3 extends Entity { - apiVersion: 'templates.backstage.io/v1beta3'; + apiVersion: 'scaffolder.backstage.io/v1beta3'; kind: 'Template'; spec: { type: string; From 17bca651a696cee0f57dd45abaecd87d690d2080 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 6 Oct 2021 11:32:07 +0200 Subject: [PATCH 20/37] scaffolder-backend: rename to ScaffolderEntitiesProcessor Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- ...cessor.test.ts => ScaffolderEntitiesProcessor.test.ts} | 8 ++++---- ...eEntityProcessor.ts => ScaffolderEntitiesProcessor.ts} | 2 +- plugins/scaffolder-backend/src/processor/index.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename plugins/scaffolder-backend/src/processor/{TemplateEntityProcessor.test.ts => ScaffolderEntitiesProcessor.test.ts} (90%) rename plugins/scaffolder-backend/src/processor/{TemplateEntityProcessor.ts => ScaffolderEntitiesProcessor.ts} (97%) diff --git a/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.test.ts b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.test.ts similarity index 90% rename from plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.test.ts rename to plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.test.ts index 568231f673..c5278edce3 100644 --- a/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.test.ts +++ b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.test.ts @@ -15,7 +15,7 @@ */ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { TemplateEntityProcessor } from './TemplateEntityProcessor'; +import { ScaffolderEntitiesProcessor } from './ScaffolderEntitiesProcessor'; const mockLocation = { type: 'a', target: 'b' }; const mockEntity: TemplateEntityV1beta3 = { @@ -30,10 +30,10 @@ const mockEntity: TemplateEntityV1beta3 = { }, }; -describe('TemplateEntityProcessor', () => { +describe('ScaffolderEntitiesProcessor', () => { describe('validateEntityKind', () => { it('validates the entity kind', async () => { - const processor = new TemplateEntityProcessor(); + const processor = new ScaffolderEntitiesProcessor(); await expect(processor.validateEntityKind(mockEntity)).resolves.toBe( true, @@ -52,7 +52,7 @@ describe('TemplateEntityProcessor', () => { describe('postProcessEntity', () => { it('generates relations for component entities', async () => { - const processor = new TemplateEntityProcessor(); + const processor = new ScaffolderEntitiesProcessor(); const emit = jest.fn(); diff --git a/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.ts b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts similarity index 97% rename from plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.ts rename to plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts index 8bd741a4ce..c69ba154f6 100644 --- a/plugins/scaffolder-backend/src/processor/TemplateEntityProcessor.ts +++ b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts @@ -33,7 +33,7 @@ import { templateEntityV1beta3Schema, } from '@backstage/plugin-scaffolder-common'; -export class TemplateEntityProcessor implements CatalogProcessor { +export class ScaffolderEntitiesProcessor implements CatalogProcessor { private readonly validators = [ entityKindSchemaValidator(templateEntityV1beta3Schema), ]; diff --git a/plugins/scaffolder-backend/src/processor/index.ts b/plugins/scaffolder-backend/src/processor/index.ts index 621ac186a4..518827668d 100644 --- a/plugins/scaffolder-backend/src/processor/index.ts +++ b/plugins/scaffolder-backend/src/processor/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { TemplateEntityProcessor } from './TemplateEntityProcessor'; +export { ScaffolderEntitiesProcessor } from './ScaffolderEntitiesProcessor'; From eaca0f53fb4fffb2b10b2eee4532a90f3fe2efee Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 6 Oct 2021 11:50:45 +0200 Subject: [PATCH 21/37] backend,create-app: install ScaffolderEntitiesProcessor Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- .changeset/funny-dolls-draw.md | 19 +++++++++++++++++++ packages/backend/src/plugins/catalog.ts | 2 ++ .../packages/backend/src/plugins/catalog.ts | 2 ++ 3 files changed, 23 insertions(+) create mode 100644 .changeset/funny-dolls-draw.md diff --git a/.changeset/funny-dolls-draw.md b/.changeset/funny-dolls-draw.md new file mode 100644 index 0000000000..f194b30750 --- /dev/null +++ b/.changeset/funny-dolls-draw.md @@ -0,0 +1,19 @@ +--- +'@backstage/create-app': patch +--- + +The scaffolder plugin has just released the beta 3 version of software templates, which replaces the handlebars templating syntax. As part of this change, the template entity schema is no longer included in the core catalog-model as with previous versions. The decoupling of the template entities version will allow us to more easily make updates in the future. + +In order to use the new beta 3 templates, the following changes are **required** for any existing installation, inside `packages/backend/src/plugins/catalog.ts`: + +```diff ++import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; + +... + + const builder = await CatalogBuilder.create(env); ++ builder.addProcessor(new ScaffolderEntitiesProcessor()); + const { processingEngine, router } = await builder.build(); +``` + +If you're interested in learning more about creating custom kinds, please check out the [extending the model](https://backstage.io/docs/features/software-catalog/extending-the-model) documentation. diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 6a903d0df8..7beeb4f35e 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -15,6 +15,7 @@ */ import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -22,6 +23,7 @@ export default async function createPlugin( env: PluginEnvironment, ): Promise { const builder = await CatalogBuilder.create(env); + builder.addProcessor(new ScaffolderEntitiesProcessor()); const { processingEngine, router } = await builder.build(); await processingEngine.start(); return router; diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts index d1ded511da..876cb6bccc 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts @@ -1,4 +1,5 @@ import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -6,6 +7,7 @@ export default async function createPlugin( env: PluginEnvironment, ): Promise { const builder = await CatalogBuilder.create(env); + builder.addProcessor(new ScaffolderEntitiesProcessor()); const { processingEngine, router } = await builder.build(); await processingEngine.start(); return router; From 1a6de93e200aba002d4fcbb019dcd73e4f1f8d44 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 6 Oct 2021 13:32:16 +0200 Subject: [PATCH 22/37] scaffolder-common,scaffolder-backend: udpate API reports Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- plugins/scaffolder-backend/api-report.md | 16 +++++++++++ plugins/scaffolder-common/api-report.md | 36 ++++++++++++++++++++++++ plugins/scaffolder-common/src/index.ts | 11 ++++++-- 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 plugins/scaffolder-common/api-report.md diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 4a8c103906..ef4defcc50 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -6,13 +6,17 @@ /// import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; +import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; import { ContainerRunner } from '@backstage/backend-common'; import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; import { createPullRequest } from 'octokit-plugin-create-pull-request'; +import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { JsonObject } from '@backstage/config'; import { JsonValue } from '@backstage/config'; +import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; import { Octokit } from '@octokit/rest'; import { PluginDatabaseManager } from '@backstage/backend-common'; @@ -244,6 +248,18 @@ export const runCommand: ({ logStream, }: RunCommandOptions) => Promise; +// @public (undocumented) +export class ScaffolderEntitiesProcessor implements CatalogProcessor { + // (undocumented) + postProcessEntity( + entity: Entity, + _location: LocationSpec, + emit: CatalogProcessorEmit, + ): Promise; + // (undocumented) + validateEntityKind(entity: Entity): Promise; +} + // Warning: (ae-missing-release-tag) "TemplateAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md new file mode 100644 index 0000000000..722e746967 --- /dev/null +++ b/plugins/scaffolder-common/api-report.md @@ -0,0 +1,36 @@ +## API Report File for "@backstage/plugin-scaffolder-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Entity } from '@backstage/catalog-model'; +import { JsonObject } from '@backstage/config'; +import { JSONSchema } from '@backstage/catalog-model'; + +// @public (undocumented) +export interface TemplateEntityV1beta3 extends Entity { + // (undocumented) + apiVersion: 'scaffolder.backstage.io/v1beta3'; + // (undocumented) + kind: 'Template'; + // (undocumented) + spec: { + type: string; + parameters?: JsonObject | JsonObject[]; + steps: Array<{ + id?: string; + name?: string; + action: string; + input?: JsonObject; + if?: string | boolean; + }>; + output?: { + [name: string]: string; + }; + owner?: string; + }; +} + +// @public (undocumented) +export const templateEntityV1beta3Schema: JSONSchema; +``` diff --git a/plugins/scaffolder-common/src/index.ts b/plugins/scaffolder-common/src/index.ts index 5cce1ceded..3b6fa71d7a 100644 --- a/plugins/scaffolder-common/src/index.ts +++ b/plugins/scaffolder-common/src/index.ts @@ -20,8 +20,13 @@ * @packageDocumentation */ -// The `{ default as ...}` re-export does not seem to work for json in tests -import templateEntityV1beta3Schema from './Template.v1beta3.schema.json'; +import { JSONSchema } from '@backstage/catalog-model'; +import v1beta3Schema from './Template.v1beta3.schema.json'; export type { TemplateEntityV1beta3 } from './TemplateEntityV1beta3'; -export { templateEntityV1beta3Schema }; + +/** @public */ +export const templateEntityV1beta3Schema: JSONSchema = v1beta3Schema as Omit< + JSONSchema, + 'examples' +>; From 7f2d184eb448e0780012af46e85a14adec060b4c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 6 Oct 2021 15:14:12 +0200 Subject: [PATCH 23/37] docs: add implementation section to extending the catalog model Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- .../software-catalog/extending-the-model.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md index ed106bb5dd..340daae7d3 100644 --- a/docs/features/software-catalog/extending-the-model.md +++ b/docs/features/software-catalog/extending-the-model.md @@ -429,3 +429,101 @@ from one environment to the other, do rollbacks, see their relative performance metrics, and similar. This coherency and collection of tooling in one place is where something like Backstage can offer the most value and effectiveness of use. Splitting your entities apart into small islands makes this harder. + +## Implementing custom model extensions + +This section walks you through the steps involved extending the catalog model +with a new Entity type. + +### Creating a custom entity definition + +The first step of introducing a custom entity is to define what shape and schema +it has. We do this both using a TypeScript type, along with a JSONSchema schema. + +Most of the time you will want to have at least the TypeScript type of your +extension available in both frontend and backend code, which means you likely +want to have an isomorphic package that houses these types. Within the Backstage +main repo the package naming pattern of `-common` is used for isomorphic +packages, and you may choose to adopt this pattern as well. + +There's at this point no existing templates for generating isomorphic plugins +using the `@backstage/cli`. Perhaps the simplest wat to get started right now is +to copy the contents of one of the existing packages in the main repository, +such as `plugins/scaffolder-common`, and rename the folder and file contents to +the desired name. This example uses _foobar_ as the plugin name so the plugin +will be named _foobar-common_. + +Once you have a common package in place you can start adding your own entity +definitions. For the exact details on how to do that we defer to getting +inspired by the existing +[scaffolder-common](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-common/src/index.ts) +package. But in short you will need to declare a TypeScript type and a +JSONSchema for the new entity kind. + +### Building a custom processor for the entity + +The next step is to create a custom processor for your new entity kind. This +will be used within the catalog to make sure that it's able to ingest and +validate entities of our new kind. Just like with the definition package, you +can find inspiration in for example the existing +[ScaffolderEntitiesProcessor](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts). +We also provide a high-level example of what a catalog process for a custom +entity might look like: + +```ts +import { entityKindSchemaValidator } from '@backstage/catalog-model'; + +export class FoobarEntitiesProcessor implements CatalogProcessor { + // You often end up wanting to support multiple versions of your kind as you + // iterate on the definition, so we keep each version inside this array. + private readonly validators = [ + // This is where we use the JSONSchema that we export from our isomorphic package + entityKindSchemaValidator(foobarEntityV1alpha1Schema), + ]; + + // validateEntityKind is responsible for signaling to the catalog processing engine + // that this entity is valid and should therefore be submitted for further processing. + async validateEntityKind(entity: Entity): Promise { + for (const validator of this.validators) { + if (validator(entity)) { + return true; + } + } + + return false; + } + + async postProcessEntity( + entity: Entity, + _location: LocationSpec, + emit: CatalogProcessorEmit, + ): Promise { + if ( + entity.apiVersion === 'example.com/v1alpha1' && + entity.kind === 'Foobar' + ) { + const foobarEntity = entity as FoobarEntityV1alpha1; + + // Here we can modify the entity or emit results related to the entity + // Typically you will want to emit any relations associated with the entity here + emit(results.relation({ ... })) + } + + return entity; + } +} +``` + +Once the processor is created it can be wired up to the catalog via the +`CatalogBuilder` in `packages/backend/src/plugins/catalog.ts`: + +```diff ++ import { FoobarEntitiesProcessor implements CatalogProcessor { + } from '@internal/plugin-foobar-backend'; + + // ... + + const builder = await CatalogBuilder.create(env); ++ builder.addProcessor(new FoobarEntitiesProcessor()); + const { processingEngine, router } = await builder.build(); +``` From 0f74ce87fb6bb70ba12ba5b4e75115c3dd7a3a5c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 6 Oct 2021 15:24:44 +0200 Subject: [PATCH 24/37] scaffolder: apply review fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johan Haals Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- .../src/processor/ScaffolderEntitiesProcessor.ts | 1 + plugins/scaffolder-common/.eslintrc.js | 2 +- plugins/scaffolder-common/package.json | 4 ++-- .../scaffolder-common/src/Template.v1beta3.schema.json | 10 +++++----- .../src/TemplateEntityV1beta3.test.ts | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts index c69ba154f6..4f3c13f342 100644 --- a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts +++ b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts @@ -33,6 +33,7 @@ import { templateEntityV1beta3Schema, } from '@backstage/plugin-scaffolder-common'; +/** @public */ export class ScaffolderEntitiesProcessor implements CatalogProcessor { private readonly validators = [ entityKindSchemaValidator(templateEntityV1beta3Schema), diff --git a/plugins/scaffolder-common/.eslintrc.js b/plugins/scaffolder-common/.eslintrc.js index 16a033dbc6..13573efa9c 100644 --- a/plugins/scaffolder-common/.eslintrc.js +++ b/plugins/scaffolder-common/.eslintrc.js @@ -1,3 +1,3 @@ module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], + extends: [require.resolve('@backstage/cli/config/eslint')], }; diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index ad422fb149..27aa2594b2 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -5,6 +5,7 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", + "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -14,10 +15,9 @@ "repository": { "type": "git", "url": "https://github.com/backstage/backstage", - "directory": "plugin/scaffolder-common" + "directory": "plugins/scaffolder-common" }, "keywords": [ - "techdocs", "scaffolder" ], "files": [ diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json index 6992e82f02..e45bfb788d 100644 --- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -83,6 +83,11 @@ "examples": ["service", "website", "library"], "minLength": 1 }, + "owner": { + "type": "string", + "description": "The user (or group) owner of the template", + "minLength": 1 + }, "parameters": { "oneOf": [ { @@ -172,11 +177,6 @@ "additionalProperties": { "type": "string" } - }, - "owner": { - "type": "string", - "description": "The user (or group) owner of the template", - "minLength": 1 } } } diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts index 7863b7b476..ab5900ab32 100644 --- a/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts +++ b/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts @@ -32,8 +32,9 @@ describe('templateEntityV1beta3Validator', () => { }, spec: { type: 'website', + owner: 'team-b', parameters: { - required: ['storePath', 'owner'], + required: ['owner'], properties: { owner: { type: 'string', @@ -56,7 +57,6 @@ describe('templateEntityV1beta3Validator', () => { output: { fetchUrl: '${{ steps.fetch.output.targetUrl }}', }, - owner: 'team-b@example.com', }, }; }); From f20547d7454654fa45c7adfbecc259a14344545b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 6 Oct 2021 20:08:28 +0200 Subject: [PATCH 25/37] scaffolder-common: fix module entrypoints Signed-off-by: Patrik Oldsberg --- plugins/scaffolder-common/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 27aa2594b2..d00918322b 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -8,7 +8,8 @@ "private": false, "publishConfig": { "access": "public", - "main": "dist/index.esm.js", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", From e23831dffb626a5bb07e8b4807b9979deead7c50 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 11 Oct 2021 11:12:51 +0200 Subject: [PATCH 26/37] scaffolder-{backend,common}: fix outdated dependency versions Signed-off-by: Patrik Oldsberg --- plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder-common/package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 4de879d41b..98fd053771 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -36,7 +36,7 @@ "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.2", "@backstage/integration": "^0.6.7", - "@backstage/plugin-catalog-backend": "^0.15.0", + "@backstage/plugin-catalog-backend": "^0.16.0", "@backstage/plugin-scaffolder-common": "^0.1.0", "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.2", "@gitbeaker/core": "^30.2.0", diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index d00918322b..7f79dab541 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -36,10 +36,10 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^0.9.3", + "@backstage/catalog-model": "^0.9.4", "@backstage/config": "^0.1.10" }, "devDependencies": { - "@backstage/cli": "^0.7.13" + "@backstage/cli": "^0.7.15" } } From b03b9f19e0da5330fde169584e345d6237b5888f Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Mon, 11 Oct 2021 11:45:46 +0200 Subject: [PATCH 27/37] added changeset Signed-off-by: Alex Rybchenko --- .changeset/wise-kids-add.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/wise-kids-add.md diff --git a/.changeset/wise-kids-add.md b/.changeset/wise-kids-add.md new file mode 100644 index 0000000000..cea2a1754e --- /dev/null +++ b/.changeset/wise-kids-add.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-react': patch +--- + +added sorting by `metadata.title` if present From d2f331ed87de8bdd28d0788e49ce6123d18f72b9 Mon Sep 17 00:00:00 2001 From: Alex Rybchenko Date: Mon, 11 Oct 2021 11:48:32 +0200 Subject: [PATCH 28/37] updated changeset Signed-off-by: Alex Rybchenko --- .changeset/wise-kids-add.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/wise-kids-add.md b/.changeset/wise-kids-add.md index cea2a1754e..c3fa8fdc11 100644 --- a/.changeset/wise-kids-add.md +++ b/.changeset/wise-kids-add.md @@ -3,4 +3,4 @@ '@backstage/plugin-catalog-react': patch --- -added sorting by `metadata.title` if present +added sorting in entity `Name` column by `metadata.title` if present From 6583c6ac40edd63bb101a082f63c5c7dfdca317a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20Zynger?= Date: Mon, 11 Oct 2021 12:35:44 +0200 Subject: [PATCH 29/37] Make prettier happy when generating plugins I've noticed that creating a plugin and immediately running `prettier check` causes failures due to the missing semicolon in this file. Signed-off-by: Julio Zynger --- .changeset/olive-mayflies-scream.md | 5 +++++ packages/cli/templates/default-plugin/src/setupTests.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/olive-mayflies-scream.md diff --git a/.changeset/olive-mayflies-scream.md b/.changeset/olive-mayflies-scream.md new file mode 100644 index 0000000000..7c96f00334 --- /dev/null +++ b/.changeset/olive-mayflies-scream.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add semicolon in template to make prettier happy diff --git a/packages/cli/templates/default-plugin/src/setupTests.ts b/packages/cli/templates/default-plugin/src/setupTests.ts index 292b0cc471..48c09b5346 100644 --- a/packages/cli/templates/default-plugin/src/setupTests.ts +++ b/packages/cli/templates/default-plugin/src/setupTests.ts @@ -1,2 +1,2 @@ import '@testing-library/jest-dom'; -import 'cross-fetch/polyfill' +import 'cross-fetch/polyfill'; From 81c2a1af86f13c8ae19c9a3bcfc06e599c9c1362 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 11 Oct 2021 12:48:21 +0200 Subject: [PATCH 30/37] Resolve a warning in `