2018-05-22 19:43:36 +00:00
|
|
|
// Copyright 2016-2018, Pulumi Corporation.
|
|
|
|
//
|
|
|
|
// 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.
|
2017-02-09 23:56:15 +00:00
|
|
|
|
2018-03-04 21:34:20 +00:00
|
|
|
// Package dotconv converts a resource graph into its DOT digraph equivalent. This is useful for integration with
|
Tidy up more lint
This change fixes a few things:
* Most importantly, we need to place a leading "." in the paths
to Gometalinter, otherwise some sub-linters just silently skip
the directory altogether. errcheck is one such linter, which
is a very important one!
* Use an explicit Gometalinter.json file to configure the various
settings. This flips on a few additional linters that aren't
on by default (line line length checking). Sadly, a few that
I'd like to enable take waaaay too much time, so in the future
we may consider a nightly job (this includes code similarity,
unused parameters, unused functions, and others that generally
require global analysis).
* Now that we're running more, however, linting takes a while!
The core Lumi project now takes 26 seconds to lint on my laptop.
That's not terrible, but it's long enough that we don't want to
do the silly "run them twice" thing our Makefiles were previously
doing. Instead, we shall deploy some $$($${PIPESTATUS[1]}-1))-fu
to rely on the fact that grep returns 1 on "zero lines".
* Finally, fix the many issues that this turned up.
I think(?) we are done, except, of course, for needing to drive
down some of the cyclomatic complexity issues (which I'm possibly
going to punt on; see pulumi/lumi#259 for more details).
2017-06-22 19:09:46 +00:00
|
|
|
// various visualization tools, like Graphviz. Please see http://www.graphviz.org/content/dot-language for a thorough
|
2017-02-09 23:56:15 +00:00
|
|
|
// specification of the DOT file format.
|
|
|
|
package dotconv
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"strconv"
|
2019-11-01 00:39:15 +00:00
|
|
|
"strings"
|
2017-02-09 23:56:15 +00:00
|
|
|
|
2021-03-17 13:20:05 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/graph"
|
2023-06-28 16:02:04 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
|
2021-03-17 13:20:05 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
2017-02-09 23:56:15 +00:00
|
|
|
)
|
|
|
|
|
2018-03-04 21:34:20 +00:00
|
|
|
// Print prints a resource graph.
|
2024-01-08 22:03:08 +00:00
|
|
|
func Print(g graph.Graph, w io.Writer, dotFragment string) error {
|
2017-02-09 23:56:15 +00:00
|
|
|
// Allocate a new writer. In general, we will ignore write errors throughout this function, for simplicity, opting
|
|
|
|
// instead to return the result of flushing the buffer at the end, which is generally latching.
|
|
|
|
b := bufio.NewWriter(w)
|
|
|
|
|
|
|
|
// Print the graph header.
|
2017-06-13 23:47:55 +00:00
|
|
|
if _, err := b.WriteString("strict digraph {\n"); err != nil {
|
2017-02-09 23:56:15 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-01-08 22:03:08 +00:00
|
|
|
// If the caller provided a fragment then insert it here.
|
|
|
|
if dotFragment != "" {
|
|
|
|
if _, err := b.WriteString(dotFragment); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure that the fragment is followed by newline, this reduces
|
|
|
|
// problems if the fragment doesn't end with a semicolon
|
|
|
|
if _, err := b.WriteString("\n"); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-09 23:56:15 +00:00
|
|
|
// Initialize the frontier with unvisited graph vertices.
|
2017-02-12 21:11:53 +00:00
|
|
|
queued := make(map[graph.Vertex]bool)
|
2023-06-28 16:02:04 +00:00
|
|
|
frontier := slice.Prealloc[graph.Vertex](len(g.Roots()))
|
2017-02-09 23:56:15 +00:00
|
|
|
for _, root := range g.Roots() {
|
2017-02-18 18:22:04 +00:00
|
|
|
to := root.To()
|
|
|
|
queued[to] = true
|
|
|
|
frontier = append(frontier, to)
|
2017-02-09 23:56:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// For now, we auto-generate IDs.
|
2017-09-22 02:18:21 +00:00
|
|
|
// TODO[pulumi/pulumi#76]: use the object URNs instead, once we have them.
|
2017-02-09 23:56:15 +00:00
|
|
|
c := 0
|
|
|
|
ids := make(map[graph.Vertex]string)
|
|
|
|
getID := func(v graph.Vertex) string {
|
|
|
|
if id, has := ids[v]; has {
|
|
|
|
return id
|
|
|
|
}
|
|
|
|
id := "Resource" + strconv.Itoa(c)
|
|
|
|
c++
|
|
|
|
ids[v] = id
|
|
|
|
return id
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now, until the frontier is empty, emit entries into the stream.
|
|
|
|
indent := " "
|
|
|
|
emitted := make(map[graph.Vertex]bool)
|
|
|
|
for len(frontier) > 0 {
|
|
|
|
// Dequeue the head of the frontier.
|
|
|
|
v := frontier[0]
|
|
|
|
frontier = frontier[1:]
|
2023-02-17 01:23:09 +00:00
|
|
|
contract.Assertf(!emitted[v], "vertex was emitted twice")
|
2017-02-09 23:56:15 +00:00
|
|
|
emitted[v] = true
|
|
|
|
|
|
|
|
// Get and lazily allocate the ID for this vertex.
|
|
|
|
id := getID(v)
|
|
|
|
|
|
|
|
// Print this vertex; first its "label" (type) and then its direct dependencies.
|
2017-06-06 01:11:51 +00:00
|
|
|
// IDEA: consider serializing properties on the node also.
|
2023-02-23 21:14:41 +00:00
|
|
|
if _, err := fmt.Fprintf(b, "%v%v", indent, id); err != nil {
|
2017-06-08 18:44:16 +00:00
|
|
|
return err
|
2017-06-08 17:21:17 +00:00
|
|
|
}
|
Implement updates
This change is a first whack at implementing updates.
Creation and deletion plans are pretty straightforward; we just take
a single graph, topologically sort it, and perform the operations in
the right order. For creation, this is in dependency order (things
that are depended upon must be created before dependents); for deletion,
this is in reverse-dependency order (things that depend on others must
be deleted before dependencies). These are just special cases of the more
general idea of performing DAG operations in dependency order.
Updates must work in terms of this more general notion. For example:
* It is an error to delete a resource while another refers to it; thus,
resources are deleted after deleting dependents, or after updating
dependent properties that reference the resource to new values.
* It is an error to depend on a create a resource before it is created;
thus, resources must be created before dependents are created, and/or
before updates to existing resource properties that would cause them
to refer to the new resource.
Of course, all of this is tangled up in a graph of dependencies. As a
result, we must create a DAG of the dependencies between creates, updates,
and deletes, and then topologically sort this DAG, in order to determine
the proper order of update operations.
To do this, we slightly generalize the existing graph infrastructure,
while also specializing two kinds of graphs; the existing one becomes a
heapstate.ObjectGraph, while this new one is resource.planGraph (internal).
2017-02-23 22:56:23 +00:00
|
|
|
if label := v.Label(); label != "" {
|
2023-02-23 21:14:41 +00:00
|
|
|
if _, err := fmt.Fprintf(b, " [label=\"%v\"]", label); err != nil {
|
2017-06-08 18:44:16 +00:00
|
|
|
return err
|
2017-06-08 17:21:17 +00:00
|
|
|
}
|
|
|
|
}
|
2017-06-13 23:47:55 +00:00
|
|
|
if _, err := b.WriteString(";\n"); err != nil {
|
2017-06-08 18:44:16 +00:00
|
|
|
return err
|
2017-02-09 23:56:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Now print out all dependencies as "ID -> {A ... Z}".
|
|
|
|
outs := v.Outs()
|
|
|
|
if len(outs) > 0 {
|
2018-02-23 01:31:45 +00:00
|
|
|
base := fmt.Sprintf("%v%v", indent, id)
|
2017-02-09 23:56:15 +00:00
|
|
|
// Print the ID of each dependency and, for those we haven't seen, add them to the frontier.
|
2018-02-23 01:31:45 +00:00
|
|
|
for _, out := range outs {
|
2017-02-13 22:26:46 +00:00
|
|
|
to := out.To()
|
2023-02-23 21:14:41 +00:00
|
|
|
if _, err := fmt.Fprintf(b, "%s -> %s", base, getID(to)); err != nil {
|
2018-02-23 01:31:45 +00:00
|
|
|
return err
|
|
|
|
}
|
2017-02-13 22:26:46 +00:00
|
|
|
|
2019-11-01 00:39:15 +00:00
|
|
|
var attrs []string
|
2018-02-23 01:31:45 +00:00
|
|
|
if out.Color() != "" {
|
2019-11-01 00:39:15 +00:00
|
|
|
attrs = append(attrs, fmt.Sprintf("color = \"%s\"", out.Color()))
|
|
|
|
}
|
|
|
|
if out.Label() != "" {
|
|
|
|
attrs = append(attrs, fmt.Sprintf("label = \"%s\"", out.Label()))
|
|
|
|
}
|
|
|
|
if len(attrs) > 0 {
|
2023-02-23 21:14:41 +00:00
|
|
|
if _, err := fmt.Fprintf(b, " [%s]", strings.Join(attrs, ", ")); err != nil {
|
2017-06-08 18:44:16 +00:00
|
|
|
return err
|
2017-06-08 17:21:17 +00:00
|
|
|
}
|
|
|
|
}
|
2018-02-23 01:31:45 +00:00
|
|
|
|
|
|
|
if _, err := b.WriteString(";\n"); err != nil {
|
2017-06-08 18:44:16 +00:00
|
|
|
return err
|
2017-02-09 23:56:15 +00:00
|
|
|
}
|
2018-02-23 01:31:45 +00:00
|
|
|
|
2017-02-13 22:26:46 +00:00
|
|
|
if _, q := queued[to]; !q {
|
|
|
|
queued[to] = true
|
|
|
|
frontier = append(frontier, to)
|
2017-02-09 23:56:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Finish the graph.
|
2017-06-13 23:47:55 +00:00
|
|
|
if _, err := b.WriteString("}\n"); err != nil {
|
2017-06-08 18:44:16 +00:00
|
|
|
return err
|
2017-06-08 17:21:17 +00:00
|
|
|
}
|
2017-02-09 23:56:15 +00:00
|
|
|
return b.Flush()
|
|
|
|
}
|