tfplan2cai/converters/google/convert.go (345 lines of code) (raw):
// Copyright 2021 Google LLC
//
// 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.
package google
import (
errorssyslib "errors"
"fmt"
"runtime/debug"
"sort"
"strings"
"time"
"github.com/GoogleCloudPlatform/terraform-google-conversion/v6/caiasset"
"github.com/GoogleCloudPlatform/terraform-google-conversion/v6/tfplan2cai/ancestrymanager"
resources "github.com/GoogleCloudPlatform/terraform-google-conversion/v6/tfplan2cai/converters/google/resources"
"github.com/GoogleCloudPlatform/terraform-google-conversion/v6/tfplan2cai/tfdata"
"github.com/GoogleCloudPlatform/terraform-google-conversion/v6/tfplan2cai/tfplan"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport"
tfjson "github.com/hashicorp/terraform-json"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
provider "github.com/hashicorp/terraform-provider-google-beta/google-beta/provider"
"github.com/pkg/errors"
"go.uber.org/zap"
)
var ErrDuplicateAsset = errors.New("duplicate asset")
// Asset contains the resource data and metadata in the same format as
// Google CAI (Cloud Asset Inventory).
type Asset struct {
Name string `json:"name"`
Type string `json:"asset_type"`
Resource *caiasset.AssetResource `json:"resource,omitempty"`
IAMPolicy *caiasset.IAMPolicy `json:"iam_policy,omitempty"`
OrgPolicy []*caiasset.OrgPolicy `json:"org_policy,omitempty"`
V2OrgPolicies []*caiasset.V2OrgPolicies `json:"v2_org_policies,omitempty"`
// Store the converter's version of the asset to allow for merges which
// operate on this type. When matching json tags land in the conversions
// library, this could be nested to avoid the duplication of fields.
converterAsset resources.Asset
Ancestors []string `json:"ancestors"`
}
// NewConverter is a factory function for Converter.
func NewConverter(cfg *transport_tpg.Config, ancestryManager ancestrymanager.AncestryManager, offline bool, convertUnchanged bool, errorLogger *zap.Logger) *Converter {
return &Converter{
schema: provider.Provider(),
converters: resources.ResourceConverters(),
offline: offline,
cfg: cfg,
ancestryManager: ancestryManager,
assets: make(map[string]Asset),
convertUnchanged: convertUnchanged,
errorLogger: errorLogger,
}
}
// Converter knows how to convert terraform resources to their
// Google CAI (Cloud Asset Inventory) format (the Asset type).
type Converter struct {
schema *schema.Provider
// Map terraform resource kinds (i.e. "google_compute_instance")
// to a ResourceConverter that can convert them to CAI assets.
converters map[string][]resources.ResourceConverter
offline bool
cfg *transport_tpg.Config
// ancestryManager provides a manager to find the ancestry information for a project.
ancestryManager ancestrymanager.AncestryManager
// Map of converted assets (key = asset.Type + asset.Name)
assets map[string]Asset
// When set, Converter will convert ResourceChanges with no-op "actions".
convertUnchanged bool
// For logging error / status information that doesn't warrant an outright failure
errorLogger *zap.Logger
}
// AddResourceChange processes the resource changes in two stages:
// 1. Process deletions (fetching canonical resources from GCP as necessary)
// 2. Process creates, updates, and no-ops (fetching canonical resources from GCP as necessary)
// This will give us a deterministic end result even in cases where for example
// an IAM Binding and Member conflict with each other, but one is replacing the
// other.
func (c *Converter) AddResourceChanges(changes []*tfjson.ResourceChange) error {
var createOrUpdateOrNoops []*tfjson.ResourceChange
for _, rc := range changes {
// Silently skip non-google resources
if !strings.HasPrefix(rc.Type, "google_") {
continue
}
// Warn about google-beta resources
if rc.ProviderName == "registry.terraform.io/hashicorp/google-beta" {
c.errorLogger.Debug(fmt.Sprintf("%s: resource uses the google-beta provider and may not be convertible", rc.Address))
}
// Skip resources not found in the google GA provider's schema
if _, ok := c.schema.ResourcesMap[rc.Type]; !ok {
c.errorLogger.Debug(fmt.Sprintf("%s: resource type not found in google GA provider: %s.", rc.Address, rc.Type))
continue
}
// Skip unsupported resources
if _, ok := c.converters[rc.Type]; !ok {
c.errorLogger.Debug(fmt.Sprintf("%s: resource type cannot be converted for CAI-based policies: %s. For details, see https://cloud.google.com/docs/terraform/policy-validation/create-cai-constraints#supported_resources", rc.Address, rc.Type))
continue
}
if tfplan.IsCreate(rc) || tfplan.IsUpdate(rc) || tfplan.IsDeleteCreate(rc) || (c.convertUnchanged && tfplan.IsNoOp(rc)) {
createOrUpdateOrNoops = append(createOrUpdateOrNoops, rc)
} else if tfplan.IsDelete(rc) {
if err := c.addDelete(rc); err != nil {
return fmt.Errorf("%s: converting deleted TF resource to CAI: %w", rc.Address, err)
}
}
}
for _, rc := range createOrUpdateOrNoops {
if err := c.addCreateOrUpdateOrNoop(rc); err != nil {
if errorssyslib.Is(err, ErrDuplicateAsset) {
c.errorLogger.Warn(fmt.Sprintf("%s: converting TF resource to CAI: %v", rc.Address, err))
} else {
return fmt.Errorf("%s: converting TF resource to CAI: %w", rc.Address, err)
}
}
}
return nil
}
// For deletions, we only need to handle ResourceConverters that support
// both fetch and mergeDelete. Supporting just one doesn't
// make sense, and supporting neither means that the deletion
// can just happen without needing to be merged.
func (c *Converter) addDelete(rc *tfjson.ResourceChange) error {
resource := c.schema.ResourcesMap[rc.Type]
rd := tfdata.NewFakeResourceData(
rc.Type,
resource.Schema,
rc.Change.Before.(map[string]interface{}),
)
for _, converter := range c.converters[rd.Kind()] {
if converter.FetchFullResource == nil || converter.MergeDelete == nil {
continue
}
convertedItems, err := convertWrapper(converter, rd, c.cfg)
if err != nil {
if errors.Cause(err) == resources.ErrNoConversion {
continue
}
return err
}
for _, converted := range convertedItems {
key := converted.Type + converted.Name
var existingConverterAsset *resources.Asset
if existing, exists := c.assets[key]; exists {
existingConverterAsset = &existing.converterAsset
} else if !c.offline {
asset, err := converter.FetchFullResource(rd, c.cfg)
if errors.Cause(err) == resources.ErrEmptyIdentityField {
c.errorLogger.Debug(fmt.Sprintf("%s: Unable to fetch and merge remote %s asset due to unset or (known after apply) identity fields on the TF resource.", rc.Address, converted.Type))
existingConverterAsset = nil
} else if errors.Cause(err) == resources.ErrResourceInaccessible {
c.errorLogger.Warn(fmt.Sprintf("%s: Fetching %s for merge failed due to not existing or insufficient permission.", rc.Address, key))
existingConverterAsset = nil
} else if err != nil {
return fmt.Errorf("fetching remote asset %s: %w", key, err)
} else {
existingConverterAsset = &asset
}
}
if existingConverterAsset != nil {
converted = converter.MergeDelete(*existingConverterAsset, converted)
augmented, err := c.augmentAsset(rd, c.cfg, converted)
if err != nil {
return err
}
c.assets[key] = augmented
}
}
}
return nil
}
// For create/update/no-op, we need to handle both the case of no merging,
// and the case of merging. If merging, we expect both fetch and mergeCreateUpdate
// to be present.
func (c *Converter) addCreateOrUpdateOrNoop(rc *tfjson.ResourceChange) error {
resource := c.schema.ResourcesMap[rc.Type]
rd := tfdata.NewFakeResourceData(
rc.Type,
resource.Schema,
rc.Change.After.(map[string]interface{}),
)
for _, converter := range c.converters[rd.Kind()] {
convertedAssets, err := convertWrapper(converter, rd, c.cfg)
if err != nil {
if errors.Cause(err) == resources.ErrNoConversion {
continue
}
return err
}
for _, converted := range convertedAssets {
key := converted.Type + converted.Name
var existingConverterAsset *resources.Asset
if existing, exists := c.assets[key]; exists {
existingConverterAsset = &existing.converterAsset
} else if converter.FetchFullResource != nil && !c.offline {
asset, err := converter.FetchFullResource(rd, c.cfg)
if errors.Cause(err) == resources.ErrEmptyIdentityField {
c.errorLogger.Debug(fmt.Sprintf("%s: Unable to fetch and merge remote %s asset due to unset or (known after apply) identity fields on the TF resource.", rc.Address, converted.Type))
existingConverterAsset = nil
} else if errors.Cause(err) == resources.ErrResourceInaccessible {
c.errorLogger.Warn(fmt.Sprintf("%s: Fetching %s for merge failed due to not existing or insufficient permission.", rc.Address, key))
existingConverterAsset = nil
} else if err != nil {
return fmt.Errorf("fetching remote asset %s: %w", key, err)
} else {
existingConverterAsset = &asset
}
}
if existingConverterAsset != nil {
if converter.MergeCreateUpdate == nil {
// If a merge function does not exist ignore the asset and return
// a checkable error.
return fmt.Errorf("%w: type %s: name %s", ErrDuplicateAsset, converted.Type, converted.Name)
}
converted = converter.MergeCreateUpdate(*existingConverterAsset, converted)
}
augmented, err := c.augmentAsset(rd, c.cfg, converted)
if err != nil {
return err
}
c.assets[key] = augmented
}
}
return nil
}
type byName []caiasset.Asset
func (s byName) Len() int { return len(s) }
func (s byName) Less(i, j int) bool { return s[i].Name < s[j].Name }
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// Assets lists all converted assets previously added by calls to AddResource.
func (c *Converter) Assets() []caiasset.Asset {
list := make([]caiasset.Asset, 0, len(c.assets))
for _, a := range c.assets {
list = append(list, caiasset.Asset{
Name: a.Name,
Type: a.Type,
Resource: a.Resource,
IAMPolicy: a.IAMPolicy,
OrgPolicy: a.OrgPolicy,
V2OrgPolicies: a.V2OrgPolicies,
Ancestors: a.Ancestors,
})
}
sort.Sort(byName(list))
return list
}
// augmentAsset adds data to an asset that is not set by the conversion library.
func (c *Converter) augmentAsset(tfData tpgresource.TerraformResourceData, cfg *transport_tpg.Config, cai resources.Asset) (Asset, error) {
ancestors, parent, err := c.ancestryManager.Ancestors(cfg, tfData, &cai)
if err != nil {
return Asset{}, fmt.Errorf("getting resource ancestry or parent failed: %w", err)
}
var resource *caiasset.AssetResource
if cai.Resource != nil {
resource = &caiasset.AssetResource{
Version: cai.Resource.Version,
DiscoveryDocumentURI: cai.Resource.DiscoveryDocumentURI,
DiscoveryName: cai.Resource.DiscoveryName,
Parent: parent,
Data: cai.Resource.Data,
}
}
var policy *caiasset.IAMPolicy
if cai.IAMPolicy != nil {
policy = &caiasset.IAMPolicy{}
for _, b := range cai.IAMPolicy.Bindings {
policy.Bindings = append(policy.Bindings, caiasset.IAMBinding{
Role: b.Role,
Members: b.Members,
})
}
}
var orgPolicy []*caiasset.OrgPolicy
if cai.OrgPolicy != nil {
for _, o := range cai.OrgPolicy {
var listPolicy *caiasset.ListPolicy
var booleanPolicy *caiasset.BooleanPolicy
var restoreDefault *caiasset.RestoreDefault
if o.ListPolicy != nil {
listPolicy = &caiasset.ListPolicy{
AllowedValues: o.ListPolicy.AllowedValues,
AllValues: caiasset.ListPolicyAllValues(o.ListPolicy.AllValues),
DeniedValues: o.ListPolicy.DeniedValues,
SuggestedValue: o.ListPolicy.SuggestedValue,
InheritFromParent: o.ListPolicy.InheritFromParent,
}
}
if o.BooleanPolicy != nil {
booleanPolicy = &caiasset.BooleanPolicy{
Enforced: o.BooleanPolicy.Enforced,
}
}
if o.RestoreDefault != nil {
restoreDefault = &caiasset.RestoreDefault{}
}
//As time is not information in terraform resource data, time is fixed for testing purposes
fixedTime := time.Date(2021, time.April, 14, 15, 16, 17, 0, time.UTC)
orgPolicy = append(orgPolicy, &caiasset.OrgPolicy{
Constraint: o.Constraint,
ListPolicy: listPolicy,
BooleanPolicy: booleanPolicy,
RestoreDefault: restoreDefault,
UpdateTime: &caiasset.Timestamp{
Seconds: int64(fixedTime.Unix()),
Nanos: int64(fixedTime.UnixNano()),
},
})
}
}
var v2OrgPolicies []*caiasset.V2OrgPolicies
if cai.V2OrgPolicies != nil {
for _, o2 := range cai.V2OrgPolicies {
var spec *caiasset.PolicySpec
if o2.PolicySpec != nil {
var rules []*caiasset.PolicyRule
if o2.PolicySpec.PolicyRules != nil {
for _, rule := range o2.PolicySpec.PolicyRules {
var values *caiasset.StringValues
if rule.Values != nil {
values = &caiasset.StringValues{
AllowedValues: rule.Values.AllowedValues,
DeniedValues: rule.Values.DeniedValues,
}
}
var condition *caiasset.Expr
if rule.Condition != nil {
condition = &caiasset.Expr{
Expression: rule.Condition.Expression,
Title: rule.Condition.Title,
Description: rule.Condition.Description,
Location: rule.Condition.Location,
}
}
rules = append(rules, &caiasset.PolicyRule{
Values: values,
AllowAll: rule.AllowAll,
DenyAll: rule.DenyAll,
Enforce: rule.Enforce,
Condition: condition,
})
}
}
fixedTime := time.Date(2021, time.April, 14, 15, 16, 17, 0, time.UTC)
spec = &caiasset.PolicySpec{
Etag: o2.PolicySpec.Etag,
UpdateTime: &caiasset.Timestamp{
Seconds: int64(fixedTime.Unix()),
Nanos: int64(fixedTime.UnixNano()),
},
PolicyRules: rules,
InheritFromParent: o2.PolicySpec.InheritFromParent,
Reset: o2.PolicySpec.Reset,
}
}
v2OrgPolicies = append(v2OrgPolicies, &caiasset.V2OrgPolicies{
Name: o2.Name,
PolicySpec: spec,
})
}
}
return Asset{
Name: cai.Name,
Type: cai.Type,
Resource: resource,
IAMPolicy: policy,
OrgPolicy: orgPolicy,
V2OrgPolicies: v2OrgPolicies,
converterAsset: cai,
Ancestors: ancestors,
}, nil
}
func convertWrapper(conv resources.ResourceConverter, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (assets []resources.Asset, err error) {
defer func() {
if r := recover(); r != nil {
switch v := r.(type) {
case error:
err = v
default:
err = fmt.Errorf("unknown panic error: %v", v)
}
err = fmt.Errorf("%v\n Stack trace: %s", err, string(debug.Stack()))
}
}()
return conv.Convert(d, config)
}