From 010353e60eb67f9d61bae0708233609f1c695f6a Mon Sep 17 00:00:00 2001 From: Thorarinn Sigurdsson Date: Mon, 3 Dec 2018 13:08:24 +0100 Subject: [PATCH] feat: added get graph command This command returns a serialized representation of the project's DependencyGraph. --- docs/reference/commands.md | 9 +++ garden-service/package-lock.json | 5 ++ garden-service/package.json | 1 + garden-service/src/commands/get/get-graph.ts | 40 +++++++++++ garden-service/src/commands/get/get.ts | 2 + garden-service/src/dependency-graph.ts | 66 +++++++++++++++++-- .../test/src/commands/get/get-graph.ts | 25 +++++++ 7 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 garden-service/src/commands/get/get-graph.ts create mode 100644 garden-service/test/src/commands/get/get-graph.ts diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 43220faf3c..ed2d392431 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -287,6 +287,15 @@ Examples: | -------- | ----- | ---- | ----------- | | `--interactive` | | boolean | Set to false to skip interactive mode and just output the command result +### garden get graph + +Outputs the dependency relationships specified in this project's garden.yml files. + + +##### Usage + + garden get graph + ### garden get config Outputs the fully resolved configuration for this project and environment. diff --git a/garden-service/package-lock.json b/garden-service/package-lock.json index f628c0ae5c..11314477f3 100644 --- a/garden-service/package-lock.json +++ b/garden-service/package-lock.json @@ -10619,6 +10619,11 @@ "hoek": "5.x.x" } }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", diff --git a/garden-service/package.json b/garden-service/package.json index 0168083baf..2fc6ee197d 100644 --- a/garden-service/package.json +++ b/garden-service/package.json @@ -75,6 +75,7 @@ "sywac": "^1.2.1", "tar": "^4.4.6", "terminal-link": "^1.1.0", + "toposort": "^2.0.2", "ts-stream": "^1.0.1", "typescript-memoize": "^1.0.0-alpha.3", "uniqid": "^5.0.3", diff --git a/garden-service/src/commands/get/get-graph.ts b/garden-service/src/commands/get/get-graph.ts new file mode 100644 index 0000000000..7870843ee2 --- /dev/null +++ b/garden-service/src/commands/get/get-graph.ts @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import * as yaml from "js-yaml" +import { RenderedEdge, RenderedNode } from "../../dependency-graph" +import { highlightYaml } from "../../util/util" +import { + Command, + CommandResult, + CommandParams, +} from "../base" + +interface GraphOutput { + nodes: RenderedNode[], + relationships: RenderedEdge[], +} + +export class GetGraphCommand extends Command { + name = "graph" + help = "Outputs the dependency relationships specified in this project's garden.yml files." + + async action({ garden, log }: CommandParams): Promise> { + const dependencyGraph = await garden.getDependencyGraph() + const renderedGraph = dependencyGraph.render() + const output: GraphOutput = { nodes: renderedGraph.nodes, relationships: renderedGraph.relationships } + + const yamlGraph = yaml.safeDump(renderedGraph, { noRefs: true, skipInvalid: true }) + + log.info(highlightYaml(yamlGraph)) + + return { result: output } + + } + +} diff --git a/garden-service/src/commands/get/get.ts b/garden-service/src/commands/get/get.ts index 1fff25ad32..88e96bf856 100644 --- a/garden-service/src/commands/get/get.ts +++ b/garden-service/src/commands/get/get.ts @@ -7,6 +7,7 @@ */ import { Command } from "../base" +import { GetGraphCommand } from "./get-graph" import { GetConfigCommand } from "./get-config" import { GetSecretCommand } from "./get-secret" import { GetStatusCommand } from "./get-status" @@ -16,6 +17,7 @@ export class GetCommand extends Command { help = "Retrieve and output data and objects, e.g. secrets, status info etc." subCommands = [ + GetGraphCommand, GetConfigCommand, GetSecretCommand, GetStatusCommand, diff --git a/garden-service/src/dependency-graph.ts b/garden-service/src/dependency-graph.ts index c4e6087969..3b92f03174 100644 --- a/garden-service/src/dependency-graph.ts +++ b/garden-service/src/dependency-graph.ts @@ -7,6 +7,7 @@ */ import * as Bluebird from "bluebird" +const toposort = require("toposort") import { flatten, fromPairs, pick, uniq } from "lodash" import { Garden } from "./garden" import { BuildDependencyConfig } from "./config/module" @@ -37,6 +38,16 @@ type DependencyRelationNames = { export type DependencyRelationFilterFn = (DependencyGraphNode) => boolean +// Output types for rendering/logging + +export type RenderedGraph = { nodes: RenderedNode[], relationships: RenderedEdge[] } + +export type RenderedEdge = { dependant: RenderedNode, dependency: RenderedNode } + +export type RenderedNode = { type: RenderedNodeType, name: string } + +export type RenderedNodeType = "build" | "deploy" | "runTask" | "test" | "push" | "publish" + /** * A graph data structure that facilitates querying (recursive or non-recursive) of the project's dependency and * dependant relationships. @@ -312,20 +323,54 @@ export class DependencyGraph { } } - // For testing/debugging. - renderGraph() { + render(): RenderedGraph { const nodes = Object.values(this.index) - const edges: string[][] = [] - for (const node of nodes) { - for (const dep of node.dependencies) { - edges.push([nodeKey(node.type, node.name), nodeKey(dep.type, dep.name)]) + let edges: { dependant: DependencyGraphNode, dependency: DependencyGraphNode }[] = [] + let simpleEdges: string[][] = [] + for (const dependant of nodes) { + for (const dependency of dependant.dependencies) { + edges.push({ dependant, dependency }) + simpleEdges.push([ + nodeKey(dependant.type, dependant.name), + nodeKey(dependency.type, dependency.name), + ]) } } - return edges + + const sortedNodeKeys = toposort(simpleEdges) + + const edgeSortIndex = (e) => { + return sortedNodeKeys.findIndex(k => k === nodeKey(e.dependency.type, e.dependency.name)) + } + edges = edges.sort((e1, e2) => edgeSortIndex(e2) - edgeSortIndex(e1)) + const renderedEdges = edges.map(e => ({ + dependant: e.dependant.render(), + dependency: e.dependency.render(), + })) + + const nodeSortIndex = (n) => { + return sortedNodeKeys.findIndex(k => k === nodeKey(n.type, n.name)) + } + const renderedNodes = nodes.sort((n1, n2) => nodeSortIndex(n2) - nodeSortIndex(n1)) + .map(n => n.render()) + + return { + relationships: renderedEdges, + nodes: renderedNodes, + } } } +const renderedNodeTypeMap = { + build: "build", + service: "deploy", + task: "runTask", + test: "test", + push: "push", + publish: "publish", +} + export class DependencyGraphNode { type: DependencyGraphNodeType @@ -342,6 +387,13 @@ export class DependencyGraphNode { this.dependants = [] } + render(): RenderedNode { + return { + type: renderedNodeTypeMap[this.type], + name: this.name, + } + } + // Idempotent. addDependency(node: DependencyGraphNode) { const key = nodeKey(node.type, node.name) diff --git a/garden-service/test/src/commands/get/get-graph.ts b/garden-service/test/src/commands/get/get-graph.ts new file mode 100644 index 0000000000..e79a98f72b --- /dev/null +++ b/garden-service/test/src/commands/get/get-graph.ts @@ -0,0 +1,25 @@ +import { expect } from "chai" +import { dataDir, makeTestGarden } from "../../../helpers" +import { GetGraphCommand } from "../../../../src/commands/get/get-graph" +import { resolve } from "path" + +describe("GetGraphCommand", () => { + const pluginName = "test-plugin" + const provider = pluginName + + // TODO: Switch to a stable topological sorting algorithm that's more amenable to testing. + it("should get the project's serialized dependency graph", async () => { + const garden = await makeTestGarden(resolve(dataDir, "test-project-dependants")) + const log = garden.log + const command = new GetGraphCommand() + + const res = await command.action({ + garden, + log, + args: { provider }, + opts: {}, + }) + + expect(Object.keys(res.result!).sort()).to.eql(["nodes", "relationships"]) + }) +})