internal/policygen/load.go (151 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 policygen
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"cloud.google.com/go/storage"
"github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/hcl"
"github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/jsonschema"
"github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/terraform"
"github.com/ghodss/yaml"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/hashicorp/terraform/states"
"github.com/zclconf/go-cty/cty"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
)
// config is the struct representing the Policy Generator configuration.
type config struct {
// Optional constraint on the binary version required for this config.
// Syntax: https://www.terraform.io/docs/configuration/version-constraints.html
Version string `hcl:"version,optional" json:"version,omitempty"`
TemplateDir string `hcl:"template_dir" json:"template_dir"`
// HCL decoder can't unmarshal into map[string]interface{},
// so make it unmarshal to a cty.Value and manually convert to map.
// TODO(https://github.com/hashicorp/hcl/issues/291): Remove the need for ForsetiPoliciesCty and GCPOrgPoliciesCty.
ForsetiPoliciesCty *cty.Value `hcl:"forseti_policies,optional" json:"-"`
ForsetiPolicies map[string]interface{} `json:"forseti_policies"`
}
func loadConfig(path string) (*config, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config %q: %v", path, err)
}
// Convert yaml to json so hcl decoder can parse it.
if filepath.Ext(path) == ".yaml" {
b, err = yaml.YAMLToJSON(b)
if err != nil {
return nil, err
}
// hclsimple.Decode doesn't actually use the path for anything other
// than its extension, so just pass in any file name ending with json so
// the library knows to treat these bytes as json and not yaml.
path = "file.json"
}
c := new(config)
if err := hclsimple.Decode(path, b, nil, c); err != nil {
return nil, err
}
if err := c.init(); err != nil {
return nil, err
}
return c, nil
}
func (c *config) init() error {
var err error
if c.ForsetiPoliciesCty != nil {
c.ForsetiPolicies, err = hcl.CtyValueToMap(c.ForsetiPoliciesCty)
if err != nil {
return fmt.Errorf("failed to convert %v to map: %v", c.ForsetiPoliciesCty, err)
}
}
sj, err := hcl.ToJSON(Schema)
if err != nil {
return fmt.Errorf("convert schema to JSON: %v", err)
}
cj, err := json.Marshal(c)
if err != nil {
return err
}
return jsonschema.ValidateJSONBytes(sj, cj)
}
// loadResources loads Terraform state resources from the given path.
// - If the path is a single local file, it loads resouces from it, regardless of the file name.
// - If the path is a local directory, it walks the directory recursively and loads resources from each .tfstate file.
// - If the path is a Cloud Storage bucket (indicated by 'gs://' prefix), it walks the bucket recursively and loads resources from each .tfstate file.
// It only reads the bucket name from the path and ignores the file/dir, if specified. All .tfstate file from the bucket will be read.
func loadResources(ctx context.Context, path string) ([]*states.Resource, error) {
if strings.HasPrefix(path, "gs://") {
return loadResourcesFromCloudStorageBucket(ctx, path)
}
fi, err := os.Stat(path)
if os.IsNotExist(err) {
return nil, err
}
// If the input is a file, also process it even if the extension is not .tfstate.
if !fi.IsDir() {
return loadResourcesFromSingleLocalFile(path)
}
return loadResourcesFromLocalDir(path)
}
func loadResourcesFromLocalDir(path string) ([]*states.Resource, error) {
var allResources []*states.Resource
fn := func(path string, _ os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("walk path %q: %v", path, err)
}
if filepath.Ext(path) != terraform.StateFileExtension {
return nil
}
resources, err := loadResourcesFromSingleLocalFile(path)
if err != nil {
return err
}
allResources = append(allResources, resources...)
return nil
}
if err := filepath.Walk(path, fn); err != nil {
return nil, err
}
return allResources, nil
}
func loadResourcesFromSingleLocalFile(path string) ([]*states.Resource, error) {
resources, err := terraform.ResourcesFromStateFile(path)
if err != nil {
return nil, fmt.Errorf("read resources from Terraform state file %q: %v", path, err)
}
return resources, nil
}
func loadResourcesFromCloudStorageBucket(ctx context.Context, path string) ([]*states.Resource, error) {
// Trim the 'gs://' prefix and split the path into the bucket name and cloud storage file path.
bucketName := strings.SplitN(strings.TrimPrefix(path, "gs://"), "/", 2)[0]
log.Printf("Reading state files from Cloud Storage bucket %q", bucketName)
client, err := storage.NewClient(ctx, option.WithScopes(storage.ScopeReadOnly))
if err != nil {
return nil, fmt.Errorf("start cloud storage client: %v", err)
}
bucket := client.Bucket(bucketName)
// Stat the bucket, check existence and caller permission.
if _, err := bucket.Attrs(ctx); err != nil {
return nil, fmt.Errorf("read bucket: %v", err)
}
var names []string
it := bucket.Objects(ctx, &storage.Query{})
for {
attrs, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
name := attrs.Name
if filepath.Ext(name) != terraform.StateFileExtension {
continue
}
names = append(names, name)
}
var allResources []*states.Resource
for _, name := range names {
log.Printf("reading remote file: gs://%s/%s", bucketName, name)
obj := bucket.Object(name)
r, err := obj.NewReader(ctx)
if err != nil {
return nil, err
}
defer r.Close()
resources, err := terraform.ResourcesFromState(r)
if err != nil {
return nil, err
}
allResources = append(allResources, resources...)
}
return allResources, nil
}