pulumi/pkg/operations/resources.go

280 lines
8.4 KiB
Go
Raw Normal View History

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-11-28 20:54:36 +00:00
2017-11-20 06:28:49 +00:00
package operations
import (
"sort"
"strings"
2017-11-20 06:28:49 +00:00
2017-11-28 20:54:36 +00:00
"github.com/hashicorp/go-multierror"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
2017-11-20 06:28:49 +00:00
)
// Resource is a tree representation of a resource/component hierarchy
type Resource struct {
Stack tokens.QName
Project tokens.PackageName
State *resource.State
Parent *Resource
Provider *Resource
Children map[resource.URN]*Resource
2017-11-20 06:28:49 +00: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 06:28:49 +00: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)
var stack tokens.QName
var proj tokens.PackageName
2017-11-20 06:28:49 +00:00
// First create a list of resource nodes, without parent/child relations hooked up.
2017-11-20 06:28:49 +00:00
for _, state := range source {
stack = state.URN.Stack()
proj = state.URN.Project()
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{
Stack: stack,
Project: proj,
State: state,
Children: make(map[resource.URN]*Resource),
}
2017-11-20 06:28:49 +00:00
}
}
// Next, walk the list of resources, and wire up parents/providers to children. We do this in a second
// pass so that the creation of the tree isn't order dependent.
for _, child := range resources {
if parurn := child.State.Parent; parurn != "" {
parent, ok := resources[parurn]
contract.Assertf(ok, "Expected to find parent node '%v' in checkpoint tree nodes", parurn)
child.Parent = parent
parent.Children[child.State.URN] = child
2017-11-20 06:28:49 +00:00
}
if providerref := child.State.Provider; providerref != "" {
ref, err := providers.ParseReference(providerref)
contract.AssertNoErrorf(err, "Expected to find provider node '%v' in checkpoint tree nodes", providerref)
provider, ok := resources[ref.URN()]
contract.Assertf(ok, "Expected to find provider node '%v' in checkpoint tree nodes", ref.URN())
child.Provider = provider
}
2017-11-20 06:28:49 +00:00
}
// Create a single root node which is the parent of all unparented nodes
root := &Resource{
Stack: stack,
Project: proj,
State: nil,
Parent: nil,
Children: make(map[resource.URN]*Resource),
2017-11-20 06:28:49 +00:00
}
for _, node := range resources {
if node.Parent == nil {
root.Children[node.State.URN] = node
node.Parent = root
2017-11-20 06:28:49 +00:00
}
}
// Return the root node and map of children.
return root, resources
2017-11-20 06:28:49 +00:00
}
// GetChild find a child with the given type and name or returns `nil`.
func (r *Resource) GetChild(typ string, name string) (*Resource, bool) {
for childURN, childResource := range r.Children {
if childURN.Stack() == r.Stack &&
childURN.Project() == r.Project &&
childURN.Type() == tokens.Type(typ) &&
Allow anything in resource names (#14107) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Fixes https://github.com/pulumi/pulumi/issues/13968. Fixes https://github.com/pulumi/pulumi/issues/8949. This requires changing the parsing of URN's slightly, it is _very_ likely that providers will need to update to handle URNs like this correctly. This changes resource names to be `string` not `QName`. We never validated this before and it turns out that users have put all manner of text for resource names so we just updating the system to correctly reflect that. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-11-20 08:59:00 +00:00
childURN.Name() == name {
return childResource, true
}
}
return nil, false
2017-11-20 06:28:49 +00:00
}
// OperationsProvider gets an OperationsProvider for this resource.
func (r *Resource) OperationsProvider(config map[config.Key]string) Provider {
2017-11-20 06:28:49 +00:00
return &resourceOperations{
resource: r,
config: config,
}
}
// ResourceOperations is an OperationsProvider for Resources
type resourceOperations struct {
resource *Resource
config map[config.Key]string
2017-11-20 06:28:49 +00:00
}
var _ Provider = (*resourceOperations)(nil)
// GetLogs gets logs for a Resource
func (ops *resourceOperations) GetLogs(query LogQuery) (*[]LogEntry, error) {
if ops.resource == nil {
return nil, nil
}
2017-11-23 04:58:46 +00:00
// Only get logs for this resource if it matches the resource filter query
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.
query = LogQuery{
StartTime: query.StartTime,
EndTime: query.EndTime,
ResourceFilter: nil,
}
2017-11-23 04:58:46 +00:00
// Try to get an operations provider for this resource, it may be `nil`
opsProvider, err := ops.getOperationsProvider()
2017-11-20 06:28:49 +00:00
if err != nil {
2017-11-23 04:58:46 +00:00
return nil, err
2017-11-20 06:28:49 +00:00
}
2017-11-23 04:58:46 +00: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 06:28:49 +00:00
}
}
2017-11-23 04:58:46 +00:00
// If this resource did not choose to provide it's own logs, recur into children and collect + aggregate their logs.
2017-11-20 06:28:49 +00:00
var logs []LogEntry
// Kick off GetLogs on all children in parallel, writing results to shared channels
ch := make(chan *[]LogEntry)
errch := make(chan error)
for _, child := range ops.resource.Children {
2017-11-20 06:28:49 +00:00
childOps := &resourceOperations{
resource: child,
config: ops.config,
}
go func() {
childLogs, err := childOps.GetLogs(query)
ch <- childLogs
errch <- err
}()
}
// Handle results from GetLogs calls as they complete
2017-11-28 20:54:36 +00:00
var err error
for range ops.resource.Children {
childLogs := <-ch
childErr := <-errch
if childErr != nil {
err = multierror.Append(err, childErr)
}
2017-11-20 06:28:49 +00:00
if childLogs != nil {
logs = append(logs, *childLogs...)
}
}
2017-11-28 20:54:36 +00:00
if err != nil {
return &logs, err
}
// Sort
sort.SliceStable(logs, func(i, j int) bool { return logs[i].Timestamp < logs[j].Timestamp })
// Remove duplicates
retLogs := slice.Prealloc[LogEntry](len(logs))
var lastLogTimestamp int64
lastLogs := slice.Prealloc[LogEntry](len(logs))
for _, log := range logs {
shouldContinue := false
if log.Timestamp == lastLogTimestamp {
for _, lastLog := range lastLogs {
if log.Message == lastLog.Message {
shouldContinue = true
break
}
}
} else {
lastLogs = nil
}
if shouldContinue {
continue
}
lastLogs = append(lastLogs, log)
lastLogTimestamp = log.Timestamp
retLogs = append(retLogs, log)
}
return &retLogs, nil
2017-11-20 06:28:49 +00:00
}
2017-11-23 04:58:46 +00: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
}
if ops.resource == nil || ops.resource.State == nil {
2017-11-23 04:58:46 +00:00
return false
}
urn := ops.resource.State.URN
2017-11-23 04:58:46 +00:00
if resource.URN(*filter) == urn {
// The filter matched the full URN
return true
}
Allow anything in resource names (#14107) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Fixes https://github.com/pulumi/pulumi/issues/13968. Fixes https://github.com/pulumi/pulumi/issues/8949. This requires changing the parsing of URN's slightly, it is _very_ likely that providers will need to update to handle URNs like this correctly. This changes resource names to be `string` not `QName`. We never validated this before and it turns out that users have put all manner of text for resource names so we just updating the system to correctly reflect that. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-11-20 08:59:00 +00:00
if string(*filter) == string(urn.Type())+"::"+urn.Name() {
2017-11-23 04:58:46 +00:00
// The filter matched the '<type>::<name>' part of the URN
return true
}
Allow anything in resource names (#14107) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Fixes https://github.com/pulumi/pulumi/issues/13968. Fixes https://github.com/pulumi/pulumi/issues/8949. This requires changing the parsing of URN's slightly, it is _very_ likely that providers will need to update to handle URNs like this correctly. This changes resource names to be `string` not `QName`. We never validated this before and it turns out that users have put all manner of text for resource names so we just updating the system to correctly reflect that. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-11-20 08:59:00 +00:00
if string(*filter) == urn.Name() {
2017-11-23 04:58:46 +00:00
// The filter matched the '<name>' part of the URN
return true
}
return false
}
func (ops *resourceOperations) getOperationsProvider() (Provider, error) {
if ops.resource == nil || ops.resource.State == nil {
return nil, nil
}
tokenSeparators := strings.Count(ops.resource.State.Type.String(), ":")
if tokenSeparators != 2 {
return nil, nil
}
switch ops.resource.State.Type.Package() {
case "cloud":
return CloudOperationsProvider(ops.config, ops.resource)
case "aws":
return AWSOperationsProvider(ops.config, ops.resource)
case "gcp":
return GCPOperationsProvider(ops.config, ops.resource)
default:
return nil, nil
}
}