Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use plan file is already exists #70

Merged
merged 8 commits into from
Jun 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.3.7
terraform_version: 1.4.6
terraform_wrapper: false
- run: terraform version -json

Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,15 @@ The report is customizable (text or JSON, per hour, month...), cf [Configuration
</p>
</details>

### Existing terraform plan file

In case you want to read an existing terraform file, you need to pass it as argument. It can either be a raw tfplan or a json plan.
This is useful when some variables or credentials are required to run `terraform plan`. In that case `carbonifer plan` won't try to run `terraform plan` for you, and won't expect to have any credentials or variable set (via env var...)

```bash
carbonifer plan /path/to/my/project.tfplan
```

## Methodology

This tool will:
Expand Down Expand Up @@ -299,9 +308,12 @@ See the [Scope](doc/scope.md) document for more details.

## Usage

`carbonifer [path of terraform files]`
`carbonifer plan [target]`

The targeted terraform folder is provided as the only argument. By default, it uses the current folder.
- `target` can be
- a terraform project folder
- a terraform plan file (json or raw)
- default: the current folder

### Prerequisites

Expand Down
40 changes: 27 additions & 13 deletions cmd/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ package cmd
import (
"bufio"
"os"
"path"
"strings"
"path/filepath"

log "github.com/sirupsen/logrus"

Expand All @@ -22,9 +21,21 @@ var test_planCmdHasRun = false

// planCmd represents the plan command
var planCmd = &cobra.Command{
Use: "plan",
Short: "Estimate CO2 from your infrastructure code",
Args: cobra.MaximumNArgs(1),
Use: "plan",
Long: `Estimate CO2 from your infrastructure code.

The 'plan' command optionally takes a single argument:

directory :
- default: current directory
- directory: a terraform project directory
- file: a terraform plan file (raw or json)
Example usages:
carbonifer plan
carbonifer plan /path/to/terraform/project
carbonifer plan /path/to/terraform/plan.json
carbonifer plan /path/to/terraform/plan.tfplan`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
test_planCmdHasRun = true
log.Debug("Running command 'plan'")
Expand All @@ -33,20 +44,23 @@ var planCmd = &cobra.Command{
if err != nil {
log.Fatal(err)
}

input := workdir
if len(args) != 0 {
terraformProject := args[0]
if strings.HasPrefix(terraformProject, "/") {
workdir = terraformProject
} else {
workdir = path.Join(workdir, terraformProject)
input = args[0]
if !filepath.IsAbs(input) {
input = filepath.Join(workdir, input)
}
}

viper.Set("workdir", workdir)
log.Debugf("Workdir : %v", workdir)
// Generate or Read Terraform plan
tfPlan, err := terraform.CarboniferPlan(input)
if err != nil {
log.Fatal(err)
}

// Read resources from terraform plan
resources, err := terraform.GetResources()
resources, err := terraform.GetResources(tfPlan)
if err != nil {
log.Fatal(err)
}
Expand Down
1 change: 1 addition & 0 deletions doc/scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

Currently, Carbonifer can read only Terraform files. It has been tested with the following versions:

- 1.4.6
- 1.3.7
- 1.3.6

Expand Down
107 changes: 107 additions & 0 deletions internal/terraform/resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package terraform

import (
"encoding/json"
"os"
"strings"

"github.com/carboniferio/carbonifer/internal/resources"
"github.com/carboniferio/carbonifer/internal/terraform/aws"
"github.com/carboniferio/carbonifer/internal/terraform/gcp"
"github.com/carboniferio/carbonifer/internal/terraform/tfrefs"
tfjson "github.com/hashicorp/terraform-json"

log "github.com/sirupsen/logrus"
)

func GetResources(tfPlan *tfjson.Plan) (map[string]resources.Resource, error) {

log.Debugf("Reading resources from Terraform plan: %d resources", len(tfPlan.PlannedValues.RootModule.Resources))
resourcesMap := make(map[string]resources.Resource)
terraformRefs := tfrefs.References{
ResourceConfigs: map[string]*tfjson.ConfigResource{},
ResourceReferences: map[string]*tfjson.StateResource{},
DataResources: map[string]resources.DataResource{},
ProviderConfigs: map[string]string{},
}
for _, priorRes := range tfPlan.PlannedValues.RootModule.Resources {
log.Debugf("Reading prior state resources %v", priorRes.Address)
if priorRes.Mode == "data" {
if strings.HasPrefix(priorRes.Type, "google") {
dataResource := gcp.GetDataResource(*priorRes)
terraformRefs.DataResources[dataResource.GetKey()] = dataResource
}
}
}

// Find template first
for _, res := range tfPlan.PlannedValues.RootModule.Resources {
log.Debugf("Reading resource %v", res.Address)
if strings.HasPrefix(res.Type, "google") && (strings.HasSuffix(res.Type, "_template") ||
strings.HasSuffix(res.Type, "_autoscaler")) {
if res.Mode == "managed" {
terraformRefs.ResourceReferences[res.Address] = res
}
}
}

// Index configurations in order to find relationships
for _, resConfig := range tfPlan.Config.RootModule.Resources {
log.Debugf("Reading resource config %v", resConfig.Address)
if strings.HasPrefix(resConfig.Type, "google") {
if resConfig.Mode == "managed" {
terraformRefs.ResourceConfigs[resConfig.Address] = resConfig
}
}
}

// Get default values
for provider, resConfig := range tfPlan.Config.ProviderConfigs {
if provider == "aws" {
log.Debugf("Reading provider config %v", resConfig.Name)
// TODO #58 Improve way we get default regions (env var, profile...)
var region interface{}
regionExpr := resConfig.Expressions["region"]
if regionExpr != nil {
region = regionExpr.ConstantValue
} else {
if os.Getenv("AWS_REGION") != "" {
region = os.Getenv("AWS_REGION")
}
}
if region != nil {
terraformRefs.ProviderConfigs["region"] = region.(string)
}
}
}

// Get All resources
for _, res := range tfPlan.PlannedValues.RootModule.Resources {
log.Debugf("Reading resource %v", res.Address)

if res.Mode == "managed" {
var resource resources.Resource
prefix := strings.Split(res.Type, "_")[0]
if prefix == "google" {
resource = gcp.GetResource(*res, &terraformRefs)
} else if prefix == "aws" {
resource = aws.GetResource(*res, &terraformRefs)
} else {
log.Warnf("Skipping resource %s. Provider not supported : %s", res.Type, prefix)
}
if resource != nil {
resourcesMap[resource.GetAddress()] = resource
if log.IsLevelEnabled(log.DebugLevel) {
computeJsonStr := "<RESOURCE TYPE CURRENTLY NOT SUPPORTED>"
if resource.IsSupported() {
computeJson, _ := json.Marshal(resource)
computeJsonStr = string(computeJson)
}
log.Debugf(" Compute resource : %v", string(computeJsonStr))
}
}
}

}
return resourcesMap, nil
}
Loading