2017-11-28 21:54:36 +01:00
|
|
|
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
|
|
|
|
2017-11-20 07:28:49 +01:00
|
|
|
package operations
|
|
|
|
|
|
|
|
import (
|
2017-11-20 22:31:20 +01:00
|
|
|
"sort"
|
2017-11-20 07:28:49 +01:00
|
|
|
|
2017-11-28 21:54:36 +01:00
|
|
|
"github.com/hashicorp/go-multierror"
|
|
|
|
|
2017-11-20 07:28:49 +01:00
|
|
|
"github.com/pulumi/pulumi/pkg/resource"
|
|
|
|
"github.com/pulumi/pulumi/pkg/tokens"
|
|
|
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Resource is a tree representation of a resource/component hierarchy
|
|
|
|
type Resource struct {
|
2017-11-28 16:43:07 +01:00
|
|
|
NS tokens.QName
|
|
|
|
Alloc tokens.PackageName
|
|
|
|
State *resource.State
|
|
|
|
Parent *Resource
|
|
|
|
Children map[resource.URN]*Resource
|
2017-11-20 07:28:49 +01:00
|
|
|
}
|
|
|
|
|
2017-11-28 16:43:07 +01:00
|
|
|
// NewResourceMap constructs a map of resources with parent/child relations, indexed by URN.
|
|
|
|
func NewResourceMap(source []*resource.State) map[resource.URN]*Resource {
|
|
|
|
_, resources := makeResourceTreeMap(source)
|
|
|
|
return resources
|
2017-11-20 07:28:49 +01:00
|
|
|
}
|
|
|
|
|
2017-11-28 16:43:07 +01:00
|
|
|
// NewResourceTree constructs a tree representation of a resource/component hierarchy
|
|
|
|
func NewResourceTree(source []*resource.State) *Resource {
|
|
|
|
root, _ := makeResourceTreeMap(source)
|
|
|
|
return root
|
|
|
|
}
|
|
|
|
|
|
|
|
// makeResourceTreeMap is a helper used by the two above functions to construct a resource hierarchy.
|
|
|
|
func makeResourceTreeMap(source []*resource.State) (*Resource, map[resource.URN]*Resource) {
|
|
|
|
resources := make(map[resource.URN]*Resource)
|
|
|
|
|
2017-11-20 07:28:49 +01:00
|
|
|
var ns tokens.QName
|
|
|
|
var alloc tokens.PackageName
|
|
|
|
|
2017-11-29 03:03:18 +01:00
|
|
|
// First create a list of resource nodes, without parent/child relations hooked up.
|
2017-11-20 07:28:49 +01:00
|
|
|
for _, state := range source {
|
|
|
|
ns = state.URN.Namespace()
|
|
|
|
alloc = state.URN.Alloc()
|
2018-02-12 17:47:09 +01:00
|
|
|
if !state.Delete {
|
|
|
|
// Only include resources which are not marked as pending-deletion.
|
|
|
|
contract.Assertf(resources[state.URN] == nil, "Unexpected duplicate resource %s", state.URN)
|
|
|
|
resources[state.URN] = &Resource{
|
|
|
|
NS: ns,
|
|
|
|
Alloc: alloc,
|
|
|
|
State: state,
|
|
|
|
Children: make(map[resource.URN]*Resource),
|
|
|
|
}
|
2017-11-20 07:28:49 +01:00
|
|
|
}
|
2017-11-26 21:29:13 +01:00
|
|
|
}
|
|
|
|
|
2017-11-29 03:03:18 +01:00
|
|
|
// Next, walk the list of resources, and wire up parents and children. We do this in a second pass so
|
|
|
|
// that the creation of the tree isn't order dependent.
|
2017-11-29 17:16:38 +01:00
|
|
|
for _, child := range resources {
|
|
|
|
if parurn := child.State.Parent; parurn != "" {
|
|
|
|
parent, ok := resources[parurn]
|
2017-11-26 21:29:13 +01:00
|
|
|
contract.Assertf(ok, "Expected to find parent node '%v' in checkpoint tree nodes", parurn)
|
2017-11-29 17:16:38 +01:00
|
|
|
child.Parent = parent
|
|
|
|
parent.Children[child.State.URN] = child
|
2017-11-20 07:28:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a single root node which is the parent of all unparented nodes
|
|
|
|
root := &Resource{
|
2017-11-28 16:43:07 +01:00
|
|
|
NS: ns,
|
|
|
|
Alloc: alloc,
|
|
|
|
State: nil,
|
|
|
|
Parent: nil,
|
|
|
|
Children: make(map[resource.URN]*Resource),
|
2017-11-20 07:28:49 +01:00
|
|
|
}
|
2017-11-28 16:43:07 +01:00
|
|
|
for _, node := range resources {
|
|
|
|
if node.Parent == nil {
|
|
|
|
root.Children[node.State.URN] = node
|
|
|
|
node.Parent = root
|
2017-11-20 07:28:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-28 16:43:07 +01:00
|
|
|
// Return the root node and map of children.
|
|
|
|
return root, resources
|
2017-11-20 07:28:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetChild find a child with the given type and name or returns `nil`.
|
|
|
|
func (r *Resource) GetChild(typ string, name string) *Resource {
|
2017-12-04 23:50:55 +01:00
|
|
|
for childURN, childResource := range r.Children {
|
|
|
|
if childURN.Namespace() == r.NS &&
|
|
|
|
childURN.Alloc() == r.Alloc &&
|
|
|
|
childURN.Type() == tokens.Type(typ) &&
|
|
|
|
childURN.Name() == tokens.QName(name) {
|
|
|
|
return childResource
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2017-11-20 07:28:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// OperationsProvider gets an OperationsProvider for this resource.
|
|
|
|
func (r *Resource) OperationsProvider(config map[tokens.ModuleMember]string) Provider {
|
|
|
|
return &resourceOperations{
|
|
|
|
resource: r,
|
|
|
|
config: config,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResourceOperations is an OperationsProvider for Resources
|
|
|
|
type resourceOperations struct {
|
|
|
|
resource *Resource
|
|
|
|
config map[tokens.ModuleMember]string
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ Provider = (*resourceOperations)(nil)
|
|
|
|
|
|
|
|
// GetLogs gets logs for a Resource
|
|
|
|
func (ops *resourceOperations) GetLogs(query LogQuery) (*[]LogEntry, error) {
|
2017-11-23 05:58:46 +01:00
|
|
|
// Only get logs for this resource if it matches the resource filter query
|
2017-11-23 06:33:36 +01:00
|
|
|
if ops.matchesResourceFilter(query.ResourceFilter) {
|
|
|
|
// Set query to be a new query with `ResourceFilter` nil so that we don't filter out logs from any children of
|
|
|
|
// this resource since this resource did match the resource filter.
|
2017-11-23 06:09:13 +01:00
|
|
|
query = LogQuery{
|
2017-11-23 06:33:36 +01:00
|
|
|
StartTime: query.StartTime,
|
|
|
|
EndTime: query.EndTime,
|
|
|
|
ResourceFilter: nil,
|
2017-11-23 06:09:13 +01:00
|
|
|
}
|
2017-11-23 05:58:46 +01:00
|
|
|
// Try to get an operations provider for this resource, it may be `nil`
|
|
|
|
opsProvider, err := ops.getOperationsProvider()
|
2017-11-20 07:28:49 +01:00
|
|
|
if err != nil {
|
2017-11-23 05:58:46 +01:00
|
|
|
return nil, err
|
2017-11-20 07:28:49 +01:00
|
|
|
}
|
2017-11-23 05:58:46 +01:00
|
|
|
if opsProvider != nil {
|
|
|
|
// If this resource has an operations provider - use it and don't recur into children. It is the
|
|
|
|
// responsibility of it's GetLogs implementation to aggregate all logs from children, either by passing them
|
|
|
|
// through or by filtering specific content out.
|
|
|
|
logsResult, err := opsProvider.GetLogs(query)
|
|
|
|
if err != nil {
|
|
|
|
return logsResult, err
|
|
|
|
}
|
|
|
|
if logsResult != nil {
|
|
|
|
return logsResult, nil
|
|
|
|
}
|
2017-11-20 07:28:49 +01:00
|
|
|
}
|
|
|
|
}
|
2017-11-23 05:58:46 +01:00
|
|
|
// If this resource did not choose to provide it's own logs, recur into children and collect + aggregate their logs.
|
2017-11-20 07:28:49 +01:00
|
|
|
var logs []LogEntry
|
2017-11-23 06:33:36 +01:00
|
|
|
// Kick off GetLogs on all children in parallel, writing results to shared channels
|
|
|
|
ch := make(chan *[]LogEntry)
|
|
|
|
errch := make(chan error)
|
2017-11-28 16:43:07 +01:00
|
|
|
for _, child := range ops.resource.Children {
|
2017-11-20 07:28:49 +01:00
|
|
|
childOps := &resourceOperations{
|
|
|
|
resource: child,
|
|
|
|
config: ops.config,
|
|
|
|
}
|
2017-11-23 06:33:36 +01:00
|
|
|
go func() {
|
|
|
|
childLogs, err := childOps.GetLogs(query)
|
|
|
|
ch <- childLogs
|
|
|
|
errch <- err
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
// Handle results from GetLogs calls as they complete
|
2017-11-28 21:54:36 +01:00
|
|
|
var err error
|
2017-11-28 16:43:07 +01:00
|
|
|
for range ops.resource.Children {
|
2017-11-23 06:33:36 +01:00
|
|
|
childLogs := <-ch
|
2017-12-02 22:00:49 +01:00
|
|
|
childErr := <-errch
|
|
|
|
if childErr != nil {
|
|
|
|
err = multierror.Append(err, childErr)
|
|
|
|
}
|
2017-11-20 07:28:49 +01:00
|
|
|
if childLogs != nil {
|
|
|
|
logs = append(logs, *childLogs...)
|
|
|
|
}
|
|
|
|
}
|
2017-11-28 21:54:36 +01:00
|
|
|
if err != nil {
|
|
|
|
return &logs, err
|
|
|
|
}
|
2017-11-20 22:31:20 +01:00
|
|
|
// Sort
|
|
|
|
sort.SliceStable(logs, func(i, j int) bool { return logs[i].Timestamp < logs[j].Timestamp })
|
|
|
|
// Remove duplicates
|
|
|
|
var retLogs []LogEntry
|
2017-11-20 23:55:09 +01:00
|
|
|
var lastLogTimestamp int64
|
|
|
|
var lastLogs []LogEntry
|
2017-11-20 22:31:20 +01:00
|
|
|
for _, log := range logs {
|
2017-11-20 23:55:09 +01:00
|
|
|
shouldContinue := false
|
|
|
|
if log.Timestamp == lastLogTimestamp {
|
|
|
|
for _, lastLog := range lastLogs {
|
|
|
|
if log.Message == lastLog.Message {
|
|
|
|
shouldContinue = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
lastLogs = nil
|
|
|
|
}
|
|
|
|
if shouldContinue {
|
2017-11-20 22:31:20 +01:00
|
|
|
continue
|
|
|
|
}
|
2017-11-20 23:55:09 +01:00
|
|
|
lastLogs = append(lastLogs, log)
|
|
|
|
lastLogTimestamp = log.Timestamp
|
2017-11-20 22:31:20 +01:00
|
|
|
retLogs = append(retLogs, log)
|
|
|
|
}
|
|
|
|
return &retLogs, nil
|
2017-11-20 07:28:49 +01:00
|
|
|
}
|
|
|
|
|
2017-11-23 05:58:46 +01:00
|
|
|
// matchesResourceFilter determines whether this resource matches the provided resource filter.
|
|
|
|
func (ops *resourceOperations) matchesResourceFilter(filter *ResourceFilter) bool {
|
|
|
|
if filter == nil {
|
|
|
|
// No filter, all resources match it.
|
|
|
|
return true
|
|
|
|
}
|
2017-11-28 16:43:07 +01:00
|
|
|
if ops.resource == nil || ops.resource.State == nil {
|
2017-11-23 05:58:46 +01:00
|
|
|
return false
|
|
|
|
}
|
2017-11-28 16:43:07 +01:00
|
|
|
urn := ops.resource.State.URN
|
2017-11-23 05:58:46 +01:00
|
|
|
if resource.URN(*filter) == urn {
|
|
|
|
// The filter matched the full URN
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if string(*filter) == string(urn.Type())+"::"+string(urn.Name()) {
|
|
|
|
// The filter matched the '<type>::<name>' part of the URN
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if tokens.QName(*filter) == urn.Name() {
|
|
|
|
// The filter matched the '<name>' part of the URN
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2017-11-20 22:31:20 +01:00
|
|
|
func (ops *resourceOperations) getOperationsProvider() (Provider, error) {
|
2017-11-28 16:43:07 +01:00
|
|
|
if ops.resource == nil || ops.resource.State == nil {
|
2017-11-20 22:31:20 +01:00
|
|
|
return nil, nil
|
|
|
|
}
|
2017-11-28 16:43:07 +01:00
|
|
|
switch ops.resource.State.Type.Package() {
|
2017-11-20 22:31:20 +01:00
|
|
|
case "cloud":
|
|
|
|
return CloudOperationsProvider(ops.config, ops.resource)
|
|
|
|
case "aws":
|
|
|
|
return AWSOperationsProvider(ops.config, ops.resource)
|
|
|
|
default:
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
}
|