diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9f4f31deaca..b6c631afb4e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ -/.github/workflows/*.yml @infracost/security +/.github/workflows/*.yml /tools/release/*.go @infracost/security diff --git a/.github/workflows/test-arm-parser.yml b/.github/workflows/test-arm-parser.yml new file mode 100644 index 00000000000..1a1d04e0457 --- /dev/null +++ b/.github/workflows/test-arm-parser.yml @@ -0,0 +1,24 @@ +name: ARM Template Parser Tests +on: [push, workflow_dispatch] + + +env: + PARSER_TEST_PATH: ./internal/providers/arm/parser_test.go + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go 1.22 + uses: actions/setup-go@v4 + with: + go-version: 1.22 + id: go + + + - name: Run Parse Resource Data Test + run: go test ./internal/providers/arm -run ./internal/providers/arm/parser_test.go + diff --git a/.github/workflows/test-arm-template-provider.yml b/.github/workflows/test-arm-template-provider.yml new file mode 100644 index 00000000000..321fe652609 --- /dev/null +++ b/.github/workflows/test-arm-template-provider.yml @@ -0,0 +1,17 @@ +name: ARM Template Provider Tests +on: [push, workflow_dispatch] +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go 1.22 + uses: actions/setup-go@v4 + with: + go-version: 1.22 + id: go + + - name: Run ARM Template Tests + run: go test ./internal/providers/arm -run ./internal/providers/arm/template_provider_test.go \ No newline at end of file diff --git a/cmd/infracost/breakdown_test.go b/cmd/infracost/breakdown_test.go index b7b6b0f994c..6f6aca8ba9e 100644 --- a/cmd/infracost/breakdown_test.go +++ b/cmd/infracost/breakdown_test.go @@ -286,6 +286,22 @@ func TestBreakdownMultiProjectSkipPathsRootLevel(t *testing.T) { ) } +func TestBreakdownArmTemplateConfigFile(t *testing.T) { + testName := testutil.CalcGoldenFileTestdataDirName() + dir := path.Join("./testdata", testName) + GoldenFileCommandTest( + t, + testutil.CalcGoldenFileTestdataDirName(), + []string{ + "breakdown", + "--config-file", path.Join(dir, "infracost.yml"), + }, + &GoldenFileOptions{ + IsJSON: true, + }, + ) +} + func TestBreakdownTerraformDirectoryWithDefaultVarFiles(t *testing.T) { testName := testutil.CalcGoldenFileTestdataDirName() dir := path.Join("./testdata", testName) diff --git a/cmd/infracost/testdata/breakdown_arm_template_config_file/breakdown_arm_template_config_file.golden b/cmd/infracost/testdata/breakdown_arm_template_config_file/breakdown_arm_template_config_file.golden new file mode 100644 index 00000000000..3e28acc7e78 --- /dev/null +++ b/cmd/infracost/testdata/breakdown_arm_template_config_file/breakdown_arm_template_config_file.golden @@ -0,0 +1,24 @@ +Project: standard_disk + + Name Monthly Qty Unit Monthly Cost + + Microsoft.Compute/disks/standard + ├─ Storage (S40, LRS) 1 months $85.60 + └─ Disk operations Monthly cost depends on usage: $0.0005 per 10k operations + + OVERALL TOTAL $85.60 + +*Usage costs can be estimated by updating Infracost Cloud settings, see docs for other options. + +────────────────────────────────── +1 cloud resource was detected: +∙ 1 was estimated + +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━┓ +┃ Project ┃ Baseline cost ┃ Usage cost* ┃ Total cost ┃ +┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━╋━━━━━━━━━━━━┫ +┃ standard_disk ┃ $86 ┃ - ┃ $86 ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━┻━━━━━━━━━━━━┛ + +Err: + diff --git a/cmd/infracost/testdata/breakdown_arm_template_config_file/infracost.yml b/cmd/infracost/testdata/breakdown_arm_template_config_file/infracost.yml new file mode 100644 index 00000000000..374a9add0d9 --- /dev/null +++ b/cmd/infracost/testdata/breakdown_arm_template_config_file/infracost.yml @@ -0,0 +1,4 @@ +version: 0.1 +projects: + - path: ./testdata/breakdown_arm_template_config_file/standard_disk.json + name : standard_disk \ No newline at end of file diff --git a/cmd/infracost/testdata/breakdown_arm_template_config_file/standard_disk.json b/cmd/infracost/testdata/breakdown_arm_template_config_file/standard_disk.json new file mode 100644 index 00000000000..16733f9d05b --- /dev/null +++ b/cmd/infracost/testdata/breakdown_arm_template_config_file/standard_disk.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "standard", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "Standard_LRS" + } + } + ] + } \ No newline at end of file diff --git a/examples/arm/managed_disk/config.yml b/examples/arm/managed_disk/config.yml new file mode 100644 index 00000000000..c52ec2f7aaa --- /dev/null +++ b/examples/arm/managed_disk/config.yml @@ -0,0 +1,4 @@ +version: 0.1 +projects: + - path: ./examples/arm/managed_disk/standard_disk.json + name : standard_disk \ No newline at end of file diff --git a/examples/arm/managed_disk/standard_disk.json b/examples/arm/managed_disk/standard_disk.json new file mode 100644 index 00000000000..16733f9d05b --- /dev/null +++ b/examples/arm/managed_disk/standard_disk.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "standard", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "Standard_LRS" + } + } + ] + } \ No newline at end of file diff --git a/go.mod b/go.mod index 3a381c105f0..40a75fc60af 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/tidwall/gjson v1.17.0 github.com/zclconf/go-cty v1.14.1 - golang.org/x/crypto v0.21.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/mod v0.14.0 gopkg.in/go-playground/assert.v1 v1.2.1 gopkg.in/yaml.v2 v2.4.0 @@ -48,7 +48,7 @@ require ( require ( github.com/aws/aws-sdk-go-v2/service/eks v1.27.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20210625153042-09f34846faab - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.21.0 // indirect ) require ( @@ -88,7 +88,7 @@ require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect github.com/gorilla/websocket v1.4.2 // indirect - golang.org/x/sync v0.5.0 + golang.org/x/sync v0.7.0 ) require ( @@ -108,22 +108,22 @@ require ( github.com/hashicorp/terraform-svchost v0.1.1 github.com/maruel/panicparse/v2 v2.3.1 github.com/mitchellh/hashstructure/v2 v2.0.2 - github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/profile v1.2.1 github.com/rs/zerolog v1.31.0 github.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2 - github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a + github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 github.com/soongo/path-to-regexp v1.6.4 + github.com/spacelift-io/spacectl v1.2.0 github.com/turbot/terraform-components v0.0.0-20231213122222-1f3526cab7a7 github.com/withfig/autocomplete-tools/packages/cobra v1.2.0 github.com/xanzy/go-gitlab v0.86.0 - golang.org/x/oauth2 v0.8.0 + golang.org/x/oauth2 v0.21.0 k8s.io/apimachinery v0.29.2 ) require ( - cloud.google.com/go/compute v1.19.1 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/iam v0.13.0 // indirect dario.cat/mergo v1.0.0 // indirect filippo.io/age v1.0.0 // indirect @@ -153,7 +153,7 @@ require ( github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/continuity v0.3.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/creack/pty v1.1.11 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect @@ -161,7 +161,7 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.3.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-github/v35 v35.3.0 // indirect @@ -199,7 +199,7 @@ require ( github.com/owenrumney/go-sarif v1.1.1 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect - github.com/rivo/uniseg v0.4.4 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect @@ -211,16 +211,16 @@ require ( github.com/terraform-linters/tflint v0.46.1 // indirect github.com/terraform-linters/tflint-plugin-sdk v0.16.1 // indirect github.com/terraform-linters/tflint-ruleset-terraform v0.3.0 // indirect - github.com/urfave/cli/v2 v2.25.5 // indirect + github.com/urfave/cli/v2 v2.27.2 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect go.mozilla.org/gopgagent v0.0.0-20170926210634-4d7ea76ff71a // indirect go.mozilla.org/sops/v3 v3.7.3 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/term v0.18.0 // indirect + golang.org/x/term v0.21.0 // indirect golang.org/x/time v0.3.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.4 // indirect @@ -261,7 +261,7 @@ require ( github.com/yashtewari/glob-intersection v0.1.0 // indirect github.com/zclconf/go-cty-yaml v1.0.3 go.opencensus.io v0.24.0 // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/net v0.24.0 // indirect google.golang.org/api v0.114.0 google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect @@ -277,3 +277,5 @@ replace github.com/spf13/cobra => github.com/spf13/cobra v1.4.0 replace github.com/gruntwork-io/terragrunt => github.com/infracost/terragrunt v0.47.1-0.20240501143558-4c01e72103df replace github.com/heimdalr/dag => github.com/aliscott/dag v1.3.2-0.20231115114512-4ce18c825f94 + +replace github.com/shurcooL/graphql => github.com/spacelift-io/graphql v1.2.0 diff --git a/go.sum b/go.sum index 7c22efc9a09..99f969f9bd5 100644 --- a/go.sum +++ b/go.sum @@ -70,10 +70,8 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= -cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= @@ -450,8 +448,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -520,6 +518,8 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8= github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= +github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf h1:NrF81UtW8gG2LBGkXFQFqlfNnvMt9WdB46sfdJY4oqc= +github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= @@ -580,8 +580,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= -github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= @@ -1023,8 +1023,8 @@ github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27H github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= -github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/open-policy-agent/opa v0.46.1 h1:iG998SLK0rzalex7Hyekeq17b9WtUexM0AuyHrQ7fCc= github.com/open-policy-agent/opa v0.46.1/go.mod h1:DY9ZkCyz+DKoWI5gDuLw5rGC2RSb37QUeEf+9VjsWkI= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -1053,8 +1053,9 @@ github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9F github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= -github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23 h1:dofHuld+js7eKSemxqTVIo8yRlpRw+H1SdpzZxWruBc= github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -1093,8 +1094,8 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -1118,8 +1119,6 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2 h1:82EIpiGB79OIPgSGa63Oj4Ipf+YAX1c6A9qjmEYoRXc= github.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= -github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU= -github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -1139,6 +1138,10 @@ github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d h1:afLbh+ltiygT github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d/go.mod h1:SULmZY7YNBsvNiQbrb/BEDdEJ84TGnfyUQxaHt8t8rY= github.com/sourcegraph/jsonrpc2 v0.2.0 h1:KjN/dC4fP6aN9030MZCJs9WQbTOjWHhrtKVpzzSrr/U= github.com/sourcegraph/jsonrpc2 v0.2.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= +github.com/spacelift-io/graphql v1.2.0 h1:oUS1fyO4cqMGOcydu26BVbZkTf/pdDnLWVal6lYv49Q= +github.com/spacelift-io/graphql v1.2.0/go.mod h1:Q/JMkvFWF8uXZQkDn8i9Nc3aXucbgTFeuFZ6WQMt0Wg= +github.com/spacelift-io/spacectl v1.2.0 h1:ZUBBh5XtAVzHoGdU9N9/4Ew+mkPiXZnZugk64YQBdZw= +github.com/spacelift-io/spacectl v1.2.0/go.mod h1:3WKuZ4MhEEb3X+4bjkgl2/VmRfEFw1jI4yQtN/U1eoU= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= @@ -1198,8 +1201,8 @@ github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.25.5 h1:d0NIAyhh5shGscroL7ek/Ya9QYQE0KNabJgiUinIQkc= -github.com/urfave/cli/v2 v2.25.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= +github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= @@ -1222,8 +1225,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg= github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1290,8 +1293,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1397,8 +1400,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1424,8 +1427,8 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1441,8 +1444,8 @@ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1535,8 +1538,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1545,8 +1548,8 @@ golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/config/config.go b/internal/config/config.go index 8a3e1159ff3..8cf03a19d15 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -96,6 +96,14 @@ type Project struct { // TerraformCloudToken sets the Team API Token or User API Token so infracost can use it to access the plan. // Only applicable for terraform cloud/enterprise users. TerraformCloudToken string `yaml:"terraform_cloud_token,omitempty" envconfig:"TERRAFORM_CLOUD_TOKEN"` + // SpaceliftAPIKeyEndpoint is the endpoint that the spacelift API client will communicate with. + SpaceliftAPIKeyEndpoint string `yaml:"spacelift_api_key_endpoint,omitempty" envconfig:"SPACELIFT_API_KEY_ENDPOINT"` + // SpaceliftAPIKeyID is the spacelift API key ID. This is used in combination + // with the API key secret to generate a JWT token. + SpaceliftAPIKeyID string `yaml:"spacelift_api_key_id,omitempty" envconfig:"SPACELIFT_API_KEY_ID"` + // SpaceliftAPIKeySecret is the spacelift API key secret.This is used in combination + // with the API key id to generate a JWT token. + SpaceliftAPIKeySecret string `yaml:"spacelift_api_key_secret,omitempty" envconfig:"SPACELIFT_API_KEY_SECRET"` // TerragruntFlags set additional flags that should be passed to terragrunt. TerragruntFlags string `yaml:"terragrunt_flags,omitempty" envconfig:"TERRAGRUNT_FLAGS"` // UsageFile is the full path to usage file that specifies values for usage-based resources diff --git a/internal/hcl/parser.go b/internal/hcl/parser.go index 7e112e8393b..6cf78896530 100644 --- a/internal/hcl/parser.go +++ b/internal/hcl/parser.go @@ -247,16 +247,41 @@ func OptionWithRawCtyInput(input cty.Value) (op Option) { } } -// OptionWithRemoteVarLoader accepts Terraform Cloud/Enterprise host and token +// OptionWithTFCRemoteVarLoader accepts Terraform Cloud/Enterprise host and token // values to load remote execution variables. -func OptionWithRemoteVarLoader(host, token, localWorkspace string, loaderOpts ...RemoteVariablesLoaderOption) Option { +func OptionWithTFCRemoteVarLoader(host, token, localWorkspace string, loaderOpts ...TFCRemoteVariablesLoaderOption) Option { return func(p *Parser) { if host == "" || token == "" { return } client := extclient.NewAuthedAPIClient(host, token) - p.remoteVariablesLoader = NewRemoteVariablesLoader(client, localWorkspace, p.logger, loaderOpts...) + p.remoteVariableLoaders = append(p.remoteVariableLoaders, NewTFCRemoteVariablesLoader(client, localWorkspace, p.logger, loaderOpts...)) + } +} + +// OptionWithSpaceliftRemoteVarLoader attempts to build a SpaceLift remote +// variable loader and set this on the parser. If the required environment +// variables are not set, the loader is skipped. +func OptionWithSpaceliftRemoteVarLoader(ctx *config.ProjectContext) Option { + return func(p *Parser) { + if ctx.ProjectConfig.SpaceliftAPIKeyEndpoint == "" || ctx.ProjectConfig.SpaceliftAPIKeyID == "" || ctx.ProjectConfig.SpaceliftAPIKeySecret == "" { + p.logger.Trace().Msg("Required Spacelift API key environment variables not set, skipping Spacelift remote variable loader.") + return + } + + loader, err := NewSpaceliftRemoteVariableLoader( + ctx.RunContext.VCSMetadata, + ctx.ProjectConfig.SpaceliftAPIKeyEndpoint, + ctx.ProjectConfig.SpaceliftAPIKeyID, + ctx.ProjectConfig.SpaceliftAPIKeySecret, + ) + if err != nil { + p.logger.Debug().Err(err).Msg("could not create Spacelift remote variable loader") + return + } + + p.remoteVariableLoaders = append(p.remoteVariableLoaders, loader) } } @@ -310,7 +335,7 @@ type Parser struct { moduleLoader *modules.ModuleLoader hclParser *modules.SharedHCLParser blockBuilder BlockBuilder - remoteVariablesLoader *RemoteVariablesLoader + remoteVariableLoaders []RemoteVariableLoader logger zerolog.Logger isGraph bool hasChanges bool @@ -626,15 +651,20 @@ func (p *Parser) loadVars(blocks Blocks, filenames []string) (map[string]cty.Val combinedVars = make(map[string]cty.Value) } - if p.remoteVariablesLoader != nil { - remoteVars, err := p.remoteVariablesLoader.Load(blocks) - if err != nil { - p.logger.Debug().Msgf("could not load vars from Terraform Cloud: %s", err) - return combinedVars, err - } + if p.remoteVariableLoaders != nil { + for _, loader := range p.remoteVariableLoaders { + remoteVars, err := loader.Load(RemoteVarLoaderOptions{ + Blocks: blocks, + EnvName: p.EnvName(), + }) + if err != nil { + p.logger.Debug().Msgf("could not load vars from Terraform Cloud: %s", err) + return combinedVars, err + } - for k, v := range remoteVars { - combinedVars[k] = v + for k, v := range remoteVars { + combinedVars[k] = v + } } } diff --git a/internal/hcl/remote_variables_loader.go b/internal/hcl/remote_variables_loader.go index 6006bec13c2..80aab8d13a4 100644 --- a/internal/hcl/remote_variables_loader.go +++ b/internal/hcl/remote_variables_loader.go @@ -1,28 +1,43 @@ package hcl import ( + "context" "encoding/json" "fmt" + "net/http" "os" "sort" + "sync" "github.com/pkg/errors" "github.com/rs/zerolog" + "github.com/shurcooL/graphql" + "github.com/spacelift-io/spacectl/client" + spaceliftSession "github.com/spacelift-io/spacectl/client/session" + "github.com/spacelift-io/spacectl/client/structs" "github.com/zclconf/go-cty/cty" "github.com/infracost/infracost/internal/extclient" + "github.com/infracost/infracost/internal/logging" + "github.com/infracost/infracost/internal/vcs" ) -// RemoteVariablesLoader handles loading remote variables from Terraform Cloud. -type RemoteVariablesLoader struct { +// RemoteVariableLoader is an interface for loading remote variables from a remote service. +type RemoteVariableLoader interface { + // Load fetches remote variables from a remote service. + Load(options RemoteVarLoaderOptions) (map[string]cty.Value, error) +} + +// TFCRemoteVariablesLoader handles loading remote variables from Terraform Cloud. +type TFCRemoteVariablesLoader struct { client *extclient.AuthedAPIClient localWorkspace string remoteConfig *TFCRemoteConfig logger zerolog.Logger } -// RemoteVariablesLoaderOption defines a function that can set properties on an RemoteVariablesLoader. -type RemoteVariablesLoaderOption func(r *RemoteVariablesLoader) +// TFCRemoteVariablesLoaderOption defines a function that can set properties on an TFCRemoteVariablesLoader. +type TFCRemoteVariablesLoaderOption func(r *TFCRemoteVariablesLoader) type tfcWorkspaceResponse struct { Data struct { @@ -75,21 +90,21 @@ type tfcVarResponse struct { } // RemoteVariablesLoaderWithRemoteConfig sets a user defined configuration for -// the RemoteVariablesLoader. This is normally done to override the configuration +// the TFCRemoteVariablesLoader. This is normally done to override the configuration // detected from the HCL blocks. -func RemoteVariablesLoaderWithRemoteConfig(config TFCRemoteConfig) RemoteVariablesLoaderOption { - return func(r *RemoteVariablesLoader) { +func RemoteVariablesLoaderWithRemoteConfig(config TFCRemoteConfig) TFCRemoteVariablesLoaderOption { + return func(r *TFCRemoteVariablesLoader) { r.remoteConfig = &config } } -// NewRemoteVariablesLoader constructs a new loader for fetching remote variables. -func NewRemoteVariablesLoader(client *extclient.AuthedAPIClient, localWorkspace string, logger zerolog.Logger, opts ...RemoteVariablesLoaderOption) *RemoteVariablesLoader { +// NewTFCRemoteVariablesLoader constructs a new loader for fetching remote variables. +func NewTFCRemoteVariablesLoader(client *extclient.AuthedAPIClient, localWorkspace string, logger zerolog.Logger, opts ...TFCRemoteVariablesLoaderOption) *TFCRemoteVariablesLoader { if localWorkspace == "" { localWorkspace = os.Getenv("TF_WORKSPACE") } - r := &RemoteVariablesLoader{ + r := &TFCRemoteVariablesLoader{ client: client, localWorkspace: localWorkspace, logger: logger, @@ -102,9 +117,16 @@ func NewRemoteVariablesLoader(client *extclient.AuthedAPIClient, localWorkspace return r } +type RemoteVarLoaderOptions struct { + Blocks Blocks + EnvName string +} + // Load fetches remote variables if terraform block contains organization and // workspace name. -func (r *RemoteVariablesLoader) Load(blocks Blocks) (map[string]cty.Value, error) { +func (r *TFCRemoteVariablesLoader) Load(options RemoteVarLoaderOptions) (map[string]cty.Value, error) { + blocks := options.Blocks + r.logger.Debug().Msg("Downloading Terraform remote variables") vars := map[string]cty.Value{} @@ -240,7 +262,7 @@ func (c TFCRemoteConfig) valid() bool { return c.Organization != "" && c.Workspace != "" } -func (r *RemoteVariablesLoader) getCloudOrganizationWorkspace(blocks Blocks) TFCRemoteConfig { +func (r *TFCRemoteVariablesLoader) getCloudOrganizationWorkspace(blocks Blocks) TFCRemoteConfig { var conf TFCRemoteConfig for _, block := range blocks.OfType("terraform") { @@ -258,7 +280,7 @@ func (r *RemoteVariablesLoader) getCloudOrganizationWorkspace(blocks Blocks) TFC return conf } -func (r *RemoteVariablesLoader) getBackendOrganizationWorkspace(blocks Blocks) (TFCRemoteConfig, error) { +func (r *TFCRemoteVariablesLoader) getBackendOrganizationWorkspace(blocks Blocks) (TFCRemoteConfig, error) { var conf TFCRemoteConfig for _, block := range blocks.OfType("terraform") { @@ -315,3 +337,142 @@ func getVarValue(variable tfcVar) cty.Value { return cty.StringVal(variable.Value) } + +// SpaceliftRemoteVariableLoader orchestrates communicating with the Spacelift API to fetch remote variables. +type SpaceliftRemoteVariableLoader struct { + Client client.Client + Metadata vcs.Metadata + + cache *sync.Map +} + +// NewSpaceliftRemoteVariableLoader creates a new SpaceliftRemoteVariableLoader, this function +// expects that the required environment variables are set. +func NewSpaceliftRemoteVariableLoader(metadata vcs.Metadata, apiKeyEndpoint, apiKeyId, apiKeySecret string) (*SpaceliftRemoteVariableLoader, error) { + httpClient := http.DefaultClient + + session, err := spaceliftSession.FromAPIKey(context.Background(), httpClient)(apiKeyEndpoint, apiKeyId, apiKeySecret) + if err != nil { + return nil, fmt.Errorf("could not create Spacelift session: %w", err) + } + + return &SpaceliftRemoteVariableLoader{ + Client: client.New(httpClient, session), + Metadata: metadata, + cache: &sync.Map{}, + }, nil +} + +// Load fetches remote variables from Spacelift by querying the stacks for the +// provided environment name and remote name. +func (s *SpaceliftRemoteVariableLoader) Load(options RemoteVarLoaderOptions) (map[string]cty.Value, error) { + if options.EnvName == "" { + logging.Logger.Trace().Msg("no environment name provided, skipping Spacelift remote variable loading") + return nil, nil + } + + // get the stack which matches the remote name and the environment name + // in future we should get all stacks for the remote name and then + // dynamically create projects out of the stacks returned. + stacks, err := s.getStacks(context.Background(), &getStackOptions{ + count: 1, + repositoryName: s.Metadata.Remote.Name, + name: options.EnvName, + }) + if err != nil { + return nil, fmt.Errorf("could not get stacks: %w", err) + } + + var stackEnvs []stackConfig + for _, s := range stacks { + if s.Name == options.EnvName { + stackEnvs = s.Config + break + } + } + + if len(stackEnvs) == 0 { + logging.Logger.Trace().Msg("no stack environments found, skipping Spacelift remote variable loading") + return nil, nil + } + + vars := map[string]cty.Value{} + for _, env := range stackEnvs { + vars[env.ID] = cty.StringVal(env.Value) + } + + return vars, nil +} + +type stack struct { + ID string `graphql:"id" json:"id,omitempty"` + Name string `graphql:"name" json:"name,omitempty"` + Config []stackConfig `graphql:"config" json:"config,omitempty"` +} + +type stackConfig struct { + ID string `graphql:"id" json:"id,omitempty"` + Value string `graphql:"value" json:"value,omitempty"` +} + +type getStackOptions struct { + count int + + repositoryName string + name string +} + +func (s *SpaceliftRemoteVariableLoader) getStacks(ctx context.Context, p *getStackOptions) ([]stack, error) { + if v, ok := s.cache.Load(p); ok { + return v.([]stack), nil + } + + var query struct { + SearchStacksOutput struct { + Edges []struct { + Node stack `graphql:"node"` + } `graphql:"edges"` + PageInfo structs.PageInfo `graphql:"pageInfo"` + } `graphql:"searchStacks(input: $input)"` + } + var conditions []structs.QueryPredicate + if p.repositoryName != "" { + conditions = append(conditions, structs.QueryPredicate{ + Field: graphql.String("repository"), + Constraint: structs.QueryFieldConstraint{ + StringMatches: &[]graphql.String{graphql.String(p.repositoryName)}, + }, + }) + } + + if p.name != "" { + conditions = append(conditions, structs.QueryPredicate{ + Field: graphql.String("name"), + Constraint: structs.QueryFieldConstraint{ + StringMatches: &[]graphql.String{graphql.String(p.name)}, + }, + }) + + } + + variables := map[string]interface{}{"input": structs.SearchInput{ + First: graphql.NewInt(graphql.Int(p.count)), + Predicates: &conditions, + }} + + if err := s.Client.Query( + ctx, + &query, + variables, + ); err != nil { + return nil, errors.Wrap(err, "failed search for stacks") + } + + result := make([]stack, 0) + for _, q := range query.SearchStacksOutput.Edges { + result = append(result, q.Node) + } + + s.cache.Store(p, result) + return result, nil +} diff --git a/internal/hcl/remote_variables_loader_test.go b/internal/hcl/remote_variables_loader_test.go new file mode 100644 index 00000000000..cb4894952f2 --- /dev/null +++ b/internal/hcl/remote_variables_loader_test.go @@ -0,0 +1,137 @@ +package hcl + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/spacelift-io/spacectl/client" + "github.com/spacelift-io/spacectl/client/session" + "github.com/stretchr/testify/assert" + "github.com/zclconf/go-cty/cty" + + "github.com/infracost/infracost/internal/vcs" +) + +func TestSpaceliftRemoteVariableLoader_Load(t *testing.T) { + type fields struct { + Metadata vcs.Metadata + } + type args struct { + options RemoteVarLoaderOptions + } + tests := []struct { + name string + fields fields + args args + want map[string]cty.Value + wantErr assert.ErrorAssertionFunc + testServerFunc func(t *testing.T) http.HandlerFunc + }{ + { + name: "return variables from spacelift context", + fields: fields{ + Metadata: vcs.Metadata{ + Remote: vcs.Remote{ + Name: "test/test", + }, + }, + }, + args: args{ + options: RemoteVarLoaderOptions{ + EnvName: "dev", + }, + }, + want: map[string]cty.Value{ + "test": cty.StringVal("1"), + "test2": cty.StringVal("foo"), + }, + wantErr: assert.NoError, + testServerFunc: func(t *testing.T) http.HandlerFunc { + // test that the /graqphl endpoint has been called and the correct query has been sent + return func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + s, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.JSONEq(t, `{"query":"query($input:SearchInput!){searchStacks(input: $input){edges{node{id,name,config{id,value}}},pageInfo{endCursor,hasNextPage,hasPreviousPage}}}","variables":{"input":{"first":1,"after":null,"fullTextSearch":null,"predicates":[{"field":"repository","constraint":{"booleanEquals":null,"enumEquals":null,"stringMatches":["test/test"]}},{"field":"name","constraint":{"booleanEquals":null,"enumEquals":null,"stringMatches":["dev"]}}],"orderBy":null}}}`, string(s)) + assert.Equal(t, "Bearer test", r.Header.Get("Authorization")) + + _, err = w.Write([]byte(`{"data":{"searchStacks":{"edges":[{"node":{"id":"dev","name":"dev","config":[{"id":"test","value":"1"},{"id":"test2","value":"foo"}]}}],"pageInfo":{"endCursor":"MDFKNEtQMzVETjFYRDcxNTY1UkJCMzBITUQ=","hasNextPage":true,"hasPreviousPage":false}}}}`)) + assert.NoError(t, err) + } + }, + }, + { + name: "returns empty map if no variables found", + fields: fields{ + Metadata: vcs.Metadata{ + Remote: vcs.Remote{ + Name: "test/test", + }, + }, + }, + args: args{ + options: RemoteVarLoaderOptions{ + EnvName: "dev", + }, + }, + want: nil, + wantErr: assert.NoError, + testServerFunc: func(t *testing.T) http.HandlerFunc { + // test that the /graqphl endpoint has been called and the correct query has been sent + return func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + s, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.JSONEq(t, `{"query":"query($input:SearchInput!){searchStacks(input: $input){edges{node{id,name,config{id,value}}},pageInfo{endCursor,hasNextPage,hasPreviousPage}}}","variables":{"input":{"first":1,"after":null,"fullTextSearch":null,"predicates":[{"field":"repository","constraint":{"booleanEquals":null,"enumEquals":null,"stringMatches":["test/test"]}},{"field":"name","constraint":{"booleanEquals":null,"enumEquals":null,"stringMatches":["dev"]}}],"orderBy":null}}}`, string(s)) + assert.Equal(t, "Bearer test", r.Header.Get("Authorization")) + + _, err = w.Write([]byte(`{"data":{"searchStacks":{"edges":[{"node":{"id":"dev","name":"dev","config":[]}}],"pageInfo":{"endCursor":"MDFKNEtQMzVETjFYRDcxNTY1UkJCMzBITUQ=","hasNextPage":true,"hasPreviousPage":false}}}}`)) + assert.NoError(t, err) + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(tt.testServerFunc(t)) + defer ts.Close() + + s := &SpaceliftRemoteVariableLoader{ + Client: client.New(http.DefaultClient, &stubSession{ + token: "test", + endpoint: ts.URL, + }), + Metadata: tt.fields.Metadata, + cache: &sync.Map{}, + } + + got, err := s.Load(tt.args.options) + if !tt.wantErr(t, err, fmt.Sprintf("Load(%v)", tt.args.options)) { + return + } + assert.Equalf(t, tt.want, got, "Load(%v)", tt.args.options) + }) + } +} + +type stubSession struct { + token string + endpoint string +} + +func (s stubSession) BearerToken(ctx context.Context) (string, error) { + return s.token, nil +} + +func (s stubSession) Endpoint() string { + return s.endpoint +} + +func (s stubSession) Type() session.CredentialsType { + return session.CredentialsTypeAPIToken +} diff --git a/internal/output/output.go b/internal/output/output.go index 0878a3f7e8c..8e5ba067e25 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -840,7 +840,8 @@ func hasSupportedProvider(rType string) bool { return strings.HasPrefix(rType, "aws_") || // tf strings.HasPrefix(rType, "google_") || // tf strings.HasPrefix(rType, "azurerm_") || // tf - strings.HasPrefix(rType, "AWS::") // cf + strings.HasPrefix(rType, "AWS::") || // cf + strings.HasPrefix(rType, "Microsoft.") // arm } func BuildSummary(resources []*schema.Resource, opts SummaryOptions) (*Summary, error) { diff --git a/internal/providers/arm/armtest/armtest.go b/internal/providers/arm/armtest/armtest.go new file mode 100644 index 00000000000..7dc46c3e7e3 --- /dev/null +++ b/internal/providers/arm/armtest/armtest.go @@ -0,0 +1,256 @@ +package armtest + +import ( + "bytes" + "context" + "os" + "path" + "path/filepath" + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/infracost/infracost/internal/output" + "github.com/infracost/infracost/internal/usage" + + "github.com/stretchr/testify/assert" + + "github.com/infracost/infracost/internal/config" + "github.com/infracost/infracost/internal/prices" + "github.com/infracost/infracost/internal/schema" + "github.com/infracost/infracost/internal/testutil" + + "github.com/infracost/infracost/internal/providers/arm" +) + +type ARMProject struct { + Files []File +} + +type File struct { + Path string + Contents string +} + +func ResourceTests(t *testing.T, contents string, usage schema.UsageMap, checks []testutil.ResourceCheck) { + project := ARMProject{ + Files: []File{ + { + Path: "main.json", + Contents: contents, + }, + }, + } + + ResourceTestsForARMProject(t, project, usage, checks) +} + +func ResourceTestsForARMProject(t *testing.T, armProject ARMProject, usage schema.UsageMap, checks []testutil.ResourceCheck, ctxOptions ...func(ctx *config.RunContext)) { + t.Run("ARM", func(t *testing.T) { + resourceTestsForARMProject(t, armProject, usage, checks, ctxOptions...) + }) +} + +func resourceTestsForARMProject(t *testing.T, armProject ARMProject, usage schema.UsageMap, checks []testutil.ResourceCheck, ctxOptions ...func(ctx *config.RunContext)) { + t.Helper() + + runCtx, err := config.NewRunContextFromEnv(context.Background()) + assert.NoError(t, err) + + for _, ctxOption := range ctxOptions { + ctxOption(runCtx) + } + + projects := loadResources(t, armProject, runCtx, usage) + + projects, err = RunCostCalculations(runCtx, projects) + assert.NoError(t, err) + assert.Len(t, projects, 1) + + testutil.TestResources(t, projects[0].Resources, checks) +} + +type GoldenFileOptions = struct { + Currency string + CaptureLogs bool + IgnoreCLI bool + LogLevel *string +} + +func DefaultGoldenFileOptions() *GoldenFileOptions { + return &GoldenFileOptions{ + Currency: "USD", + CaptureLogs: false, + } +} + +func GoldenFileResourceTests(t *testing.T, testName string) { + GoldenFileResourceTestsWithOpts(t, testName, DefaultGoldenFileOptions()) +} + +func GoldenFileResourceTestsWithOpts(t *testing.T, testName string, options *GoldenFileOptions, ctxOptions ...func(ctx *config.RunContext)) { + t.Run("ARM", func(t *testing.T) { + goldenFileResourceTestWithOpts(t, testName, options, ctxOptions...) + }) +} + +func goldenFileResourceTestWithOpts(t *testing.T, testName string, options *GoldenFileOptions, ctxOptions ...func(ctx *config.RunContext)) { + t.Helper() + + runCtx, err := config.NewRunContextFromEnv(context.Background()) + assert.NoError(t, err) + + for _, ctxOption := range ctxOptions { + ctxOption(runCtx) + } + + level := "warn" + if options.LogLevel != nil { + level = *options.LogLevel + } + + logBuf := testutil.ConfigureTestToCaptureLogs(t, runCtx, level) + + if options != nil && options.Currency != "" { + runCtx.Config.Currency = options.Currency + } + + require.NoError(t, err) + + // Load the arm projects + armProjectData, err := os.ReadFile(filepath.Join("testdata", testName, testName+".json")) + require.NoError(t, err) + armProject := ARMProject{ + Files: []File{ + { + Path: "main.json", + Contents: string(armProjectData), + }, + }, + } + + // Load the usage data, if any. + var usageData schema.UsageMap + usageFilePath := filepath.Join("testdata", testName, testName+".usage.yml") + if _, err := os.Stat(usageFilePath); err == nil || !os.IsNotExist(err) { + // usage file exists, load the data + usageFile, err := usage.LoadUsageFile(usageFilePath) + require.NoError(t, err) + usageData = usageFile.ToUsageDataMap() + } + + projects := loadResources(t, armProject, runCtx, usageData) + + // Generate the output + projects, err = RunCostCalculations(runCtx, projects) + require.NoError(t, err) + + r, err := output.ToOutputFormat(runCtx.Config, projects) + if err != nil { + require.NoError(t, err) + } + r.Currency = runCtx.Config.Currency + + opts := output.Options{ + ShowSkipped: true, + NoColor: true, + Fields: runCtx.Config.Fields, + } + + actual, err := output.ToTable(r, opts) + require.NoError(t, err) + + // strip the first line of output since it contains the temporary project path + endOfFirstLine := bytes.Index(actual, []byte("\n")) + if endOfFirstLine > 0 { + actual = actual[endOfFirstLine+1:] + } + + if logBuf != nil && logBuf.Len() > 0 { + actual = append(actual, "\nLogs:\n"...) + + // need to sort the logs so they can be compared consistently + logLines := strings.Split(logBuf.String(), "\n") + sort.Strings(logLines) + actual = append(actual, strings.Join(logLines, "\n")...) + } + + goldenFilePath := filepath.Join("testdata", testName, testName+".golden") + testutil.AssertGoldenFile(t, goldenFilePath, actual) +} + +func loadResources(t *testing.T, armProject ARMProject, runCtx *config.RunContext, usageData schema.UsageMap) []*schema.Project { + t.Helper() + + armdir := createARMProject(t, armProject) + runCtx.Config.RootPath = armdir + + provider := arm.NewTemplateProvider(config.NewProjectContext(runCtx, &config.Project{ + Path: path.Join(armdir, "main.json"), + }, nil), false, path.Join(armdir, "main.json")) + + projects, err := provider.LoadResources(usageData) + require.NoError(t, err) + + for _, project := range projects { + project.Name = strings.ReplaceAll(project.Name, armdir, t.Name()) + project.Name = strings.ReplaceAll(project.Name, "/arm", "") + project.Name = strings.ReplaceAll(project.Name, "/main.json", "") + project.BuildResources(schema.UsageMap{}) + } + + return projects +} + +func RunCostCalculations(runCtx *config.RunContext, projects []*schema.Project) ([]*schema.Project, error) { + pf := prices.NewPriceFetcher(runCtx) + for _, project := range projects { + err := pf.PopulatePrices(project) + if err != nil { + return projects, err + } + + schema.CalculateCosts(project) + } + + return projects, nil +} + +func CreateARMProject(tmpDir string, armProject ARMProject) (string, error) { + return writeToTmpDir(tmpDir, armProject) +} + +func createARMProject(t *testing.T, armProject ARMProject) string { + t.Helper() + tmpDir := t.TempDir() + + armdir, err := CreateARMProject(tmpDir, armProject) + require.NoError(t, err) + + return armdir +} + +func writeToTmpDir(tmpDir string, armProject ARMProject) (string, error) { + var err error + + for _, armFile := range armProject.Files { + fullPath := filepath.Join(tmpDir, armFile.Path) + dir := filepath.Dir(fullPath) + + if _, err := os.Stat(dir); os.IsNotExist(err) { + err := os.MkdirAll(dir, os.ModePerm) + if err != nil { + return tmpDir, err + } + } + + err = os.WriteFile(fullPath, []byte(armFile.Contents), 0600) + if err != nil { + return tmpDir, err + } + } + + return tmpDir, err +} diff --git a/internal/providers/arm/azure/azure.go b/internal/providers/arm/azure/azure.go new file mode 100644 index 00000000000..7628afbf802 --- /dev/null +++ b/internal/providers/arm/azure/azure.go @@ -0,0 +1,19 @@ +package azure + +import ( + "github.com/infracost/infracost/internal/schema" +) + +var DefaultProviderRegion = "eastus" + +func GetDefaultRefIDFunc(d *schema.ResourceData) []string { + return []string{d.Get("id").String()} +} + +func DefaultCloudResourceIDFunc(d *schema.ResourceData) []string { + return []string{} +} + +func GetSpecialContext(d *schema.ResourceData) map[string]interface{} { + return map[string]interface{}{} +} diff --git a/internal/providers/arm/azure/linux_virtual_machine.go b/internal/providers/arm/azure/linux_virtual_machine.go new file mode 100644 index 00000000000..c6361194476 --- /dev/null +++ b/internal/providers/arm/azure/linux_virtual_machine.go @@ -0,0 +1,35 @@ +package azure + +import ( + "github.com/infracost/infracost/internal/resources/azure" + "github.com/infracost/infracost/internal/schema" +) + +func getLinuxVirtualMachineRegistryItem() *schema.RegistryItem { + return &schema.RegistryItem{ + Name: "Microsoft.Compute/virtualMachines/Linux", + CoreRFunc: NewAzureLinuxVirtualMachine, + Notes: []string{ + "Non-standard images such as RHEL are not supported.", + "Low priority, Spot and Reserved instances are not supported.", + }, + } +} +func NewAzureLinuxVirtualMachine(d *schema.ResourceData) schema.CoreResource { + r := &azure.LinuxVirtualMachine{ + Address: d.Address, + Region: d.Region, + Size: d.Get("properties.hardwareProfile.vmSize").String(), + UltraSSDEnabled: d.Get("properties.additionalCapabilities.ultraSSDEnabled").Bool(), + } + + if len(d.Get("properties.storageProfile.osDisk").Array()) > 0 { + storageData := d.Get("properties.storageProfile.osDisk").Array()[0] + r.OSDiskData = &azure.ManagedDiskData{ + DiskType: storageData.Get("managedDisk.storageAccountType").String(), + DiskSizeGB: storageData.Get("diskSizeGB").Int(), + } + } + + return r +} diff --git a/internal/providers/arm/azure/linux_virtual_machine_test.go b/internal/providers/arm/azure/linux_virtual_machine_test.go new file mode 100644 index 00000000000..9d74ba8a65c --- /dev/null +++ b/internal/providers/arm/azure/linux_virtual_machine_test.go @@ -0,0 +1,16 @@ +package azure_test + +import ( + "testing" + + "github.com/infracost/infracost/internal/providers/arm/armtest" +) + +func TestAzureRMLinuxVirtualMachineGoldenFile(t *testing.T) { + t.Parallel() + if testing.Short() { + t.Skip("skipping test in short mode") + } + + armtest.GoldenFileResourceTests(t, "linux_virtual_machine_test") +} diff --git a/internal/providers/arm/azure/managed_disk.go b/internal/providers/arm/azure/managed_disk.go new file mode 100644 index 00000000000..3cb4ef5ab9c --- /dev/null +++ b/internal/providers/arm/azure/managed_disk.go @@ -0,0 +1,28 @@ +package azure + +import ( + "github.com/infracost/infracost/internal/resources/azure" + "github.com/infracost/infracost/internal/schema" +) + +func getManagedDiskRegistryItem() *schema.RegistryItem { + return &schema.RegistryItem{ + Name: "Microsoft.Compute/disks", + CoreRFunc: NewManagedDisk, + } +} + +func NewManagedDisk(d *schema.ResourceData) schema.CoreResource { + r := &azure.ManagedDisk{ + Address: d.Address, + Region: d.Region, + ManagedDiskData: azure.ManagedDiskData{ + DiskType: d.Get("sku.name").String(), + DiskSizeGB: d.Get("properties.diskSizeGB").Int(), + DiskIOPSReadWrite: d.Get("properties.diskIOPSReadWrite").Int(), + DiskMBPSReadWrite: d.Get("properties.diskMBpsReadWrite").Int(), + }, + } + + return r +} diff --git a/internal/providers/arm/azure/managed_disk_test.go b/internal/providers/arm/azure/managed_disk_test.go new file mode 100644 index 00000000000..2de3df4d186 --- /dev/null +++ b/internal/providers/arm/azure/managed_disk_test.go @@ -0,0 +1,16 @@ +package azure_test + +import ( + "testing" + + "github.com/infracost/infracost/internal/providers/arm/armtest" +) + +func TestAzureRMManagedDiskGoldenFile(t *testing.T) { + t.Parallel() + if testing.Short() { + t.Skip("skipping test in short mode") + } + + armtest.GoldenFileResourceTests(t, "managed_disk_test") +} diff --git a/internal/providers/arm/azure/registry.go b/internal/providers/arm/azure/registry.go new file mode 100644 index 00000000000..a641784bed0 --- /dev/null +++ b/internal/providers/arm/azure/registry.go @@ -0,0 +1,670 @@ +package azure + +import "github.com/infracost/infracost/internal/schema" + +// ResourceRegistry grouped alphabetically +var ResourceRegistry []*schema.RegistryItem = []*schema.RegistryItem{ + // getActiveDirectoryDomainServiceRegistryItem(), + // getActiveDirectoryDomainServiceReplicaSetRegistryItem(), + // getAPIManagementRegistryItem(), + // getApplicationGatewayRegistryItem(), + // getAppServiceEnvironmentRegistryItem(), + // GetAzureRMAppIntegrationServiceEnvironmentRegistryItem(), + // getFunctionAppRegistryItem(), + // GetAzureRMAppNATGatewayRegistryItem(), + // getAppServiceCertificateBindingRegistryItem(), + // getAppServiceCertificateOrderRegistryItem(), + // getAppServiceCustomHostnameBindingRegistryItem(), + // getAppServicePlanRegistryItem(), + // getApplicationInsightsWebTestRegistryItem(), + // getApplicationInsightsRegistryItem(), + // getAutomationAccountRegistryItem(), + // getAutomationDSCConfigurationRegistryItem(), + // getAutomationDSCNodeConfigurationRegistryItem(), + // getAutomationJobScheduleRegistryItem(), + // getBastionHostRegistryItem(), + // GetAzureRMCDNEndpointRegistryItem(), + // getContainerRegistryRegistryItem(), + // getCosmosDBAccountRegistryItem(), + // GetAzureRMCosmosdbCassandraKeyspaceRegistryItem(), + // GetAzureRMCosmosdbCassandraTableRegistryItem(), + // GetAzureRMCosmosdbGremlinDatabaseRegistryItem(), + // GetAzureRMCosmosdbGremlinGraphRegistryItem(), + // GetAzureRMCosmosdbMongoCollectionRegistryItem(), + // GetAzureRMCosmosdbMongoDatabaseRegistryItem(), + // GetAzureRMCosmosdbSQLContainerRegistryItem(), + // GetAzureRMCosmosdbSQLDatabaseRegistryItem(), + // GetAzureRMCosmosdbTableRegistryItem(), + // getDatabricksWorkspaceRegistryItem(), + // getDNSARecordRegistryItem(), + // getDNSAAAARecordRegistryItem(), + // getDNSCAARecordRegistryItem(), + // getDNSCNameRecordRegistryItem(), + // getDNSMXRecordRegistryItem(), + // getDNSNSRecordRegistryItem(), + // getDNSPtrRecordRegistryItem(), + // getDNSSrvRecordRegistryItem(), + // getDNSTxtRecordRegistryItem(), + // getDNSPrivateZoneRegistryItem(), + // getDNSZoneRegistryItem(), + // GetAzureRMEventHubsNamespaceRegistryItem(), + // getExpressRouteConnectionRegistryItem(), + // getExpressRouteGatewayRegistryItem(), + // GetAzureRMFirewallRegistryItem(), + // getAzureRMFirewallPolicyRegistryItem(), + // getAzureRMFirewallPolicyRuleCollectionGroupRegistryItem(), + // getFrontdoorFirewallPolicyRegistryItem(), + // getFrontdoorRegistryItem(), + // GetAzureRMHDInsightHadoopClusterRegistryItem(), + // GetAzureRMHDInsightHBaseClusterRegistryItem(), + // GetAzureRMHDInsightInteractiveQueryClusterRegistryItem(), + // GetAzureRMHDInsightKafkaClusterRegistryItem(), + // GetAzureRMHDInsightSparkClusterRegistryItem(), + // GetAzureRMKeyVaultCertificateRegistryItem(), + // GetAzureRMKeyVaultKeyRegistryItem(), + // GetAzureRMKeyVaultManagedHSMRegistryItem(), + // getKubernetesClusterRegistryItem(), + // getKubernetesClusterNodePoolRegistryItem(), + // getLoadBalancerRegistryItem(), + // GetAzureRMLoadBalancerRuleRegistryItem(), + // GetAzureRMLoadBalancerOutboundRuleRegistryItem(), + // getLinuxFunctionAppRegistryItem(), + getLinuxVirtualMachineRegistryItem(), + // getLinuxVirtualMachineScaleSetRegistryItem(), + // getLogAnalyticsWorkspaceRegistryItem(), + getManagedDiskRegistryItem(), + // GetAzureRMMariaDBServerRegistryItem(), + // getMSSQLDatabaseRegistryItem(), + // GetAzureRMMySQLServerRegistryItem(), + // GetAzureRMNotificationHubNamespaceRegistryItem(), + // getPointToSiteVpnGatewayRegistryItem(), + // getPostgreSQLFlexibleServerRegistryItem(), + // GetAzureRMPostgreSQLServerRegistryItem(), + // getPrivateDNSARecordRegistryItem(), + // getPrivateDNSAAAARecordRegistryItem(), + // getPrivateDNSCNameRecordRegistryItem(), + // getPrivateDNSMXRecordRegistryItem(), + // getPrivateDNSPTRRecordRegistryItem(), + // getPrivateDNSSRVRecordRegistryItem(), + // getPrivateDNSTXTRecordRegistryItem(), + // GetAzureRMPrivateEndpointRegistryItem(), + // GetAzureRMPublicIPRegistryItem(), + // GetAzureRMPublicIPPrefixRegistryItem(), + // GetAzureRMSearchServiceRegistryItem(), + // GetAzureRMRedisCacheRegistryItem(), + // getAzureRMMSSQLManagedInstanceRegistryItem(), + // getStorageAccountRegistryItem(), + // getSQLDatabaseRegistryItem(), + // getSQLManagedInstanceRegistryItem(), + // GetAzureRMSynapseSparkPoolRegistryItem(), + // GetAzureRMSynapseSQLPoolRegistryItem(), + // GetAzureRMSynapseWorkspacRegistryItem(), + // getVirtualHubRegistryItem(), + // getVirtualMachineScaleSetRegistryItem(), + // getVirtualMachineRegistryItem(), + // GetAzureRMVirtualNetworkGatewayConnectionRegistryItem(), + // GetAzureRMVirtualNetworkGatewayRegistryItem(), + // getWindowsVirtualMachineRegistryItem(), + // getWindowsVirtualMachineScaleSetRegistryItem(), + // getVPNGatewayRegistryItem(), + // getVPNGatewayConnectionRegistryItem(), + // getDataFactoryRegistryItem(), + // getDataFactoryIntegrationRuntimeAzureRegistryItem(), + // getDataFactoryIntegrationRuntimeAzureSSISRegistryItem(), + // getDataFactoryIntegrationRuntimeManagedRegistryItem(), + // getDataFactoryIntegrationRuntimeSelfHostedRegistryItem(), + // getLogAnalyticsSolutionRegistryItem(), + // getMySQLFlexibleServerRegistryItem(), + // getServicePlanRegistryItem(), + // getSentinelDataConnectorAwsCloudTrailRegistryItem(), + // getSentinelDataConnectorAzureActiveDirectoryRegistryItem(), + // getSentinelDataConnectorAzureAdvancedThreatProtectionRegistryItem(), + // getSentinelDataConnectorAzureSecurityCenterRegistryItem(), + // getSentinelDataConnectorMicrosoftCloudAppSecurityRegistryItem(), + // getSentinelDataConnectorMicrosoftDefenderAdvancedThreatProtectionRegistryItem(), + // getSentinelDataConnectorOffice365RegistryItem(), + // getSentinelDataConnectorThreatIntelligenceRegistryItem(), + // getIoTHubRegistryItem(), + // getIoTHubDPSRegistryItem(), + // getVirtualNetworkPeeringRegistryItem(), + // geWindowsFunctionAppRegistryItem(), + // getPowerBIEmbeddedRegistryItem(), + // getMSSQLElasticPoolRegistryItem(), + // getSQLElasticPoolRegistryItem(), + // getMonitorActionGroupRegistryItem(), + // getMonitorDataCollectionRuleRegistryItem(), + // getMonitorDiagnosticSettingRegistryItem(), + // getMonitorMetricAlertRegistryItem(), + // getMonitorScheduledQueryRulesAlertRegistryItem(), + // getMonitorScheduledQueryRulesAlertV2RegistryItem(), + // getApplicationInsightsStandardWebTestRegistryItem(), + // getRecoveryServicesVaultRegistryItem(), + // getBackupProtectedVmRegistryItem(), + // getStorageManagementPolicyRegistryItem(), + // getStorageQueueRegistryItem(), + // getStorageShareRegistryItem(), + // getLogicAppIntegrationAccountRegistryItem(), + // getSignalRServiceRegistryItem(), + // getTrafficManagerProfileRegistryItem(), + // getTrafficManagerAzureEndpointRegistryItem(), + // getTrafficManagerExternalEndpointRegistryItem(), + // getTrafficManagerNestedEndpointRegistryItem(), + // getEventgridSystemTopicRegistryItem(), + // getEventgridTopicRegistryItem(), + // getSecurityCenterSubscriptionPricingRegistryItem(), + // getNetworkWatcherFlowLogRegistryItem(), + // getNetworkWatcherRegistryItem(), + // getNetworkConnectionMonitorRegistryItem(), + // getServiceBusNamespaceRegistryItem(), + // getLogicAppStandardRegistryItem(), + // getImageRegistryItem(), + // getSnapshotRegistryItem(), + // getPrivateDnsResolverInboundEndpointRegistryItem(), + // getPrivateDnsResolverOutboundEndpointRegistryItem(), + // getPrivateDnsResolverDnsForwardingRulesetRegistryItem(), + // getMachineLearningComputeInstanceRegistryItem(), + // getMachineLearningComputeClusterRegistryItem(), + // getNetworkDdosProtectionPlanRegistryItem(), + // getAppConfigurationRegistryItem(), + // getFederatedIdentityCredentialRegistryItem(), + // getCognitiveAccountRegistryItem(), + // getCognitiveDeploymentRegistryItem(), +} + +// FreeResources grouped alphabetically +var FreeResources = []string{ + // Azure App Configuration + "azurerm_app_configuration_feature", + "azurerm_app_configuration_key", + // Azure AI Services + "azurerm_cognitive_account_customer_managed_key", + // Azure Api Management + "azurerm_api_management_api", + "azurerm_api_management_api_diagnostic", + "azurerm_api_management_api_operation", + "azurerm_api_management_api_operation_policy", + "azurerm_api_management_api_operation_tag", + "azurerm_api_management_api_policy", + "azurerm_api_management_api_schema", + "azurerm_api_management_api_version_set", + "azurerm_api_management_authorization_server", + "azurerm_api_management_backend", + "azurerm_api_management_certificate", + "azurerm_api_management_custom_domain", + "azurerm_api_management_diagnostic", + "azurerm_api_management_email_template", + "azurerm_api_management_group", + "azurerm_api_management_group_user", + "azurerm_api_management_identity_provider_aad", + "azurerm_api_management_identity_provider_aadb2c", + "azurerm_api_management_identity_provider_facebook", + "azurerm_api_management_identity_provider_google", + "azurerm_api_management_identity_provider_microsoft", + "azurerm_api_management_identity_provider_twitter", + "azurerm_api_management_logger", + "azurerm_api_management_named_value", + "azurerm_api_management_openid_connect_provider", + "azurerm_api_management_policy", + "azurerm_api_management_product", + "azurerm_api_management_product_api", + "azurerm_api_management_product_group", + "azurerm_api_management_product_policy", + "azurerm_api_management_property", + "azurerm_api_management_subscription", + "azurerm_api_management_user", + + // Azure Application Gateway + "azurerm_web_application_firewall_policy", + + // Azure App Service + "azurerm_app_service", + "azurerm_app_service_active_slot", + "azurerm_app_service_certificate", + "azurerm_app_service_managed_certificate", + "azurerm_app_service_slot", + "azurerm_app_service_slot_virtual_network_swift_connection", + "azurerm_app_service_source_control_token", + "azurerm_app_service_virtual_network_swift_connection", + + // Azure Attestation + "azurerm_attestation_provider", + + // Azure Automation + "azurerm_automation_certificate", + "azurerm_automation_connection", + "azurerm_automation_connection_certificate", + "azurerm_automation_connection_type", + "azurerm_automation_connection_classic_certificate", + "azurerm_automation_connection_service_principal", + "azurerm_automation_credential", + "azurerm_automation_hybrid_runbook_worker", + "azurerm_automation_hybrid_runbook_worker_group", + "azurerm_automation_module", + "azurerm_automation_runbook", + "azurerm_automation_schedule", + "azurerm_automation_software_update_configuration", + "azurerm_automation_source_control", + "azurerm_automation_variable_bool", + "azurerm_automation_variable_datetime", + "azurerm_automation_variable_int", + "azurerm_automation_variable_string", + "azurerm_automation_webhook", + + // Azure Backup & Recovery Services Vault + "azurerm_backup_policy_vm", + "azurerm_backup_policy_file_share", + "azurerm_site_recovery_network_mapping", + "azurerm_site_recovery_replication_policy", + + // Azure Base + "azurerm_resource_group", + "azurerm_resource_provider_registration", + "azurerm_subscription", + "azurerm_role_assignment", + "azurerm_role_definition", + "azurerm_user_assigned_identity", + + // Azure Blueprints + "azurerm_blueprint_assignment", + + // Azure CDN + "azurerm_cdn_frontdoor_custom_domain_association", + "azurerm_cdn_profile", + + // Azure Consumption + "azurerm_consumption_budget_management_group", + "azurerm_consumption_budget_resource_group", + "azurerm_consumption_budget_subscription", + + // Azure CosmosDB + "azurerm_cosmosdb_notebook_workspace", + "azurerm_cosmosdb_sql_role_assignment", + "azurerm_cosmosdb_sql_role_definition", + "azurerm_cosmosdb_sql_stored_procedure", + "azurerm_cosmosdb_sql_trigger", + "azurerm_cosmosdb_sql_user_defined_function", + + // Azure Cost Management + "azurerm_cost_anomaly_alert", + "azurerm_cost_management_scheduled_action", + "azurerm_resource_group_cost_management_export", + "azurerm_resource_group_cost_management_view", + "azurerm_subscription_cost_management_export", + "azurerm_subscription_cost_management_view", + + // Azure DNS + "azurerm_private_dns_zone_virtual_network_link", + "azurerm_private_dns_resolver", + + // Azure Dev Test + "azurerm_dev_test_global_vm_shutdown_schedule", + "azurerm_dev_test_policy", + "azurerm_dev_test_schedule", + "azurerm_dev_test_lab", + + // Azure Data Factory + "azurerm_data_factory_custom_dataset", + "azurerm_data_factory_data_flow", + "azurerm_data_factory_dataset_azure_blob", + "azurerm_data_factory_dataset_binary", + "azurerm_data_factory_dataset_cosmosdb_sqlapi", + "azurerm_data_factory_dataset_delimited_text", + "azurerm_data_factory_dataset_http", + "azurerm_data_factory_dataset_json", + "azurerm_data_factory_dataset_mysql", + "azurerm_data_factory_dataset_parquet", + "azurerm_data_factory_dataset_postgresql", + "azurerm_data_factory_dataset_snowflake", + "azurerm_data_factory_dataset_sql_server_table", + "azurerm_data_factory_linked_custom_service", + "azurerm_data_factory_linked_service_azure_blob_storage", + "azurerm_data_factory_linked_service_azure_databricks", + "azurerm_data_factory_linked_service_azure_file_storage", + "azurerm_data_factory_linked_service_azure_function", + "azurerm_data_factory_linked_service_azure_search", + "azurerm_data_factory_linked_service_azure_sql_database", + "azurerm_data_factory_linked_service_azure_table_storage", + "azurerm_data_factory_linked_service_cosmosdb", + "azurerm_data_factory_linked_service_cosmosdb_mongoapi", + "azurerm_data_factory_linked_service_data_lake_storage_gen2", + "azurerm_data_factory_linked_service_key_vault", + "azurerm_data_factory_linked_service_kusto", + "azurerm_data_factory_linked_service_mysql", + "azurerm_data_factory_linked_service_odata", + "azurerm_data_factory_linked_service_odbc", + "azurerm_data_factory_linked_service_postgresql", + "azurerm_data_factory_linked_service_sftp", + "azurerm_data_factory_linked_service_snowflake", + "azurerm_data_factory_linked_service_sql_server", + "azurerm_data_factory_linked_service_synapse", + "azurerm_data_factory_linked_service_web", + "azurerm_data_factory_managed_private_endpoint", + "azurerm_data_factory_pipeline", + "azurerm_data_factory_trigger_blob_event", + "azurerm_data_factory_trigger_custom_event", + "azurerm_data_factory_trigger_schedule", + "azurerm_data_factory_tumbling_window", + + // Azure Database + "azurerm_mariadb_configuration", + "azurerm_mariadb_database", + "azurerm_mariadb_firewall_rule", + "azurerm_mariadb_virtual_network_rule", + + "azurerm_mysql_active_directory_administrator", + "azurerm_mysql_configuration", + "azurerm_mysql_database", + "azurerm_mysql_firewall_rule", + "azurerm_mysql_flexible_database", + "azurerm_mysql_flexible_server_configuration", + "azurerm_mysql_flexible_server_firewall_rule", + "azurerm_mysql_server_key", + "azurerm_mysql_virtual_network_rule", + + "azurerm_postgresql_active_directory_administrator", + "azurerm_postgresql_configuration", + "azurerm_postgresql_database", + "azurerm_postgresql_firewall_rule", + "azurerm_postgresql_flexible_server_active_directory_administrator", + "azurerm_postgresql_flexible_server_configuration", + "azurerm_postgresql_flexible_server_database", + "azurerm_postgresql_flexible_server_firewall_rule", + "azurerm_postgresql_server_key", + "azurerm_postgresql_virtual_network_rule", + + // Azure Datalake Gen 2 + "azurerm_storage_data_lake_gen2_filesystem", + + // Azure Event Grid + "azurerm_eventgrid_domain", + "azurerm_eventgrid_event_subscription", + "azurerm_eventgrid_system_topic_event_subscription", + + // Azure Event Hub + "azurerm_eventhub", + "azurerm_eventhub_authorization_rule", + "azurerm_eventhub_cluster", + "azurerm_eventhub_consumer_group", + "azurerm_eventhub_namespace_authorization_rule", + "azurerm_eventhub_namespace_customer_managed_key", + "azurerm_eventhub_namespace_disaster_recovery_config", + + // Azure Firewall + "azurerm_firewall_application_rule_collection", + "azurerm_firewall_nat_rule_collection", + "azurerm_firewall_network_rule_collection", + + // Azure Front Door + "azurerm_frontdoor_custom_https_configuration", + "azurerm_frontdoor_rules_engine", + + // Azure Key Vault + "azurerm_key_vault", + "azurerm_key_vault_access_policy", + "azurerm_key_vault_certificate_data", + "azurerm_key_vault_certificate_issuer", + "azurerm_key_vault_secret", + + // Azure IoT + "azurerm_iothub_certificate", + "azurerm_iothub_consumer_group", + "azurerm_iothub_dps_certificate", + "azurerm_iothub_dps_shared_access_policy", + "azurerm_iothub_endpoint_eventhub", + "azurerm_iothub_enrichment", + "azurerm_iothub_route", + "azurerm_iothub_shared_access_policy", + + // Azure Lighthouse (Delegated Resource Management) + "azurerm_lighthouse_definition", + "azurerm_lighthouse_assignment", + + // Azure Load Balancer + "azurerm_lb_backend_address_pool", + "azurerm_lb_backend_address_pool_address", + "azurerm_lb_nat_pool", + "azurerm_lb_nat_rule", + "azurerm_lb_probe", + + // Azure Logic App + "azurerm_logic_app_action_custom", + "azurerm_logic_app_action_http", + "azurerm_logic_app_integration_account_agreement", + "azurerm_logic_app_integration_account_assembly", + "azurerm_logic_app_integration_account_batch_configuration", + "azurerm_logic_app_integration_account_certificate", + "azurerm_logic_app_integration_account_map", + "azurerm_logic_app_integration_account_partner", + "azurerm_logic_app_integration_account_schema", + "azurerm_logic_app_integration_account_session", + "azurerm_logic_app_trigger_custom", + "azurerm_logic_app_trigger_http_request", + "azurerm_logic_app_trigger_recurrence", + "azurerm_logic_app_workflow", + + // Azure Machine Learning + "azurerm_machine_learning_workspace", + + // Azure Management + "azurerm_management_group", + "azurerm_management_group_subscription_association", + "azurerm_management_lock", + + // Azure Managed Applications + "azurerm_managed_application", + "azurerm_managed_application_definition", + + // Azure Monitor + "azurerm_monitor_aad_diagnostic_setting", + "azurerm_monitor_action_rule_action_group", + "azurerm_monitor_action_rule_suppression", + "azurerm_monitor_activity_log_alert", + "azurerm_monitor_alert_processing_rule_action_group", + "azurerm_monitor_alert_processing_rule_suppression", + "azurerm_monitor_autoscale_setting", + "azurerm_monitor_data_collection", + "azurerm_monitor_data_collection_rule_association", + "azurerm_monitor_log_profile", + "azurerm_monitor_private_link_scope", + "azurerm_monitor_private_link_scoped_service", + "azurerm_monitor_scheduled_query_rules_log", + "azurerm_monitor_smart_detector_alert_rule", + + // Azure Monitor - Application Insights + "azurerm_application_insights_analytics_item", + "azurerm_application_insights_api_key", + "azurerm_application_insights_smart_detection_rule", + "azurerm_application_insights_workbook", + "azurerm_application_insights_workbook_template", + + // Azure Monitor - Log Analytics + "azurerm_log_analytics_cluster_customer_managed_key", + "azurerm_log_analytics_data_export_rule", + "azurerm_log_analytics_datasource_windows_event", + "azurerm_log_analytics_datasource_windows_performance_counter", + "azurerm_log_analytics_linked_service", + "azurerm_log_analytics_linked_storage_account", + "azurerm_log_analytics_query_pack", + "azurerm_log_analytics_query_pack_query", + "azurerm_log_analytics_saved_search", + "azurerm_log_analytics_storage_insights", + + // Azure Networking + "azurerm_application_security_group", + "azurerm_ip_group", + "azurerm_local_network_gateway", + "azurerm_nat_gateway_public_ip_association", + "azurerm_nat_gateway_public_ip_prefix_association", + "azurerm_network_interface", + "azurerm_network_interface_application_gateway_backend_address_pool_association", + "azurerm_network_interface_application_security_group_association", + "azurerm_network_interface_backend_address_pool_association", + "azurerm_network_interface_nat_rule_association", + "azurerm_network_interface_security_group_association", + "azurerm_network_security_group", + "azurerm_network_security_rule", + "azurerm_private_link_service", + "azurerm_route", + "azurerm_route_filter", + "azurerm_route_map", + "azurerm_route_table", + "azurerm_storage_account_local_user", + "azurerm_storage_account_network_rules", + "azurerm_subnet", + "azurerm_subnet_nat_gateway_association", + "azurerm_subnet_network_security_group_association", + "azurerm_subnet_route_table_association", + "azurerm_subnet_service_endpoint_storage_policy", + "azurerm_virtual_network", + "azurerm_virtual_network_dns_servers", + + // Azure Notification Hub + "azurerm_notification_hub", + + // Azure Policy + "azurerm_policy_assignment", + "azurerm_policy_definition", + "azurerm_policy_remediation", + "azurerm_policy_set_definition", + "azurerm_subscription_policy_assignment", + "azurerm_subscription_policy_exemption", + "azurerm_subscription_policy_remediation", + "azurerm_resource_group_policy_assignment", + "azurerm_resource_group_policy_exemption", + "azurerm_resource_group_policy_remediation", + "azurerm_management_group_policy_exemption", + "azurerm_management_group_policy_assignment", + "azurerm_management_group_policy_remediation", + + // Azure Portal + "azurerm_dashboard", + "azurerm_portal_dashboard", + + // Azure Redis + "azurerm_redis_firewall_rule", + "azurerm_redis_linked_server", + + // Azure Registry + "azurerm_container_registry_scope_map", + "azurerm_container_registry_token", + "azurerm_container_registry_webhook", + + // Azure Sentinel + "azurerm_sentinel_alert_rule_machine_learning_behavior_analytics", + "azurerm_sentinel_alert_rule_fusion", + "azurerm_sentinel_alert_rule_ms_security_incident", + "azurerm_sentinel_alert_rule_scheduled", + + // Azure Service Bus + "azurerm_servicebus_namespace_authorization_rule", + "azurerm_servicebus_namespace_disaster_recovery_config", + "azurerm_servicebus_namespace_network_rule_set", + "azurerm_servicebus_queue", + "azurerm_servicebus_queue_authorization_rule", + "azurerm_servicebus_subscription", + "azurerm_servicebus_subscription_rule", + "azurerm_servicebus_topic", + "azurerm_servicebus_topic_authorization_rule", + "azurerm_relay_hybrid_connection_authorization_rule", + "azurerm_relay_namespace_authorization_rule", + + // Azure Shared Image Gallery + "azurerm_shared_image", + "azurerm_shared_image_gallery", + + // Azure SignalR + "azurerm_signalr_service_network_acl", + "azurerm_signalr_shared_private_link", + + // Azure Site Recovery + "azurerm_site_recovery_protection_container_mapping", + + // Azure SQL + "azurerm_sql_failover_group", + "azurerm_sql_firewall_rule", + "azurerm_sql_server", + "azurerm_sql_virtual_network_rule", + + "azurerm_mssql_database_extended_auditing_policy", + "azurerm_mssql_database_vulnerability_assessment_rule_baseline", + "azurerm_mssql_failover_group", + "azurerm_mssql_firewall_rule", + "azurerm_mssql_job_agent", + "azurerm_mssql_job_credential", + "azurerm_mssql_managed_instance_active_directory_administrator", + "azurerm_mssql_managed_instance_security_alert_policy", + "azurerm_mssql_managed_instance_transparent_data_encryption", + "azurerm_mssql_managed_instance_vulnerability_assessment", + "azurerm_mssql_outbound_firewall_rule", + "azurerm_mssql_server", + "azurerm_mssql_server_dns_alias", + "azurerm_mssql_server_extended_auditing_policy", + "azurerm_mssql_server_microsoft_support_auditing_policy", + "azurerm_mssql_server_security_alert_policy", + "azurerm_mssql_server_transparent_data_encryption", + "azurerm_mssql_server_vulnerability_assessment", + "azurerm_mssql_virtual_network_rule", + + // Azure Storage + "azurerm_storage_account_customer_managed_key", + "azurerm_storage_account_local_user", + "azurerm_storage_account_network_rules", + "azurerm_storage_blob", + "azurerm_storage_blob_inventory_policy", + "azurerm_storage_container", + "azurerm_storage_data_lake_gen2_path", + "azurerm_storage_object_replication", + "azurerm_storage_share_directory", + "azurerm_storage_share_file", + "azurerm_storage_sync_cloud_endpoint", + "azurerm_storage_sync_group", + "azurerm_storage_table_entity", + + // Azure Virtual Desktop + "azurerm_virtual_desktop_application", + "azurerm_virtual_desktop_application_group", + "azurerm_virtual_desktop_workspace", + "azurerm_virtual_desktop_workspace_application_group_association", + "azurerm_virtual_desktop_host_pool", + "azurerm_virtual_desktop_host_pool_registration_info", + + // Azure Service Plan + "azurerm_windows_web_app", + "azurerm_linux_web_app", + + // Azure Synapse Analytics + "azurerm_synapse_firewall_rule", + "azurerm_synapse_private_link_hub", + + // Azure Virtual Hub + "azurerm_virtual_hub_route_table", + "azurerm_virtual_hub_route_table_route", + + // Azure Virtual Machines + "azurerm_virtual_machine_data_disk_attachment", + "azurerm_virtual_machine_extension", + "azurerm_virtual_machine_scale_set_extension", + "azurerm_availability_set", + "azurerm_proximity_placement_group", + "azurerm_ssh_public_key", + "azurerm_marketplace_agreement", + + // Azure WAN + "azurerm_virtual_hub_connection", + "azurerm_virtual_wan", + "azurerm_vpn_server_configuration", + + // Microsoft Defender for Cloud + "azurerm_security_center_automation", + "azurerm_security_center_server_vulnerability_assessment", + "azurerm_security_center_assessment", + "azurerm_security_center_assessment_policy", + "azurerm_security_center_auto_provisioning", + "azurerm_security_center_automation", + "azurerm_security_center_contact", + "azurerm_security_center_server_vulnerability_assessment_virtual_machine", + "azurerm_security_center_setting", + "azurerm_security_center_workspace", +} + +var UsageOnlyResources = []string{} diff --git a/internal/providers/arm/azure/testdata/linux_virtual_machine_test/linux_virtual_machine_test.golden b/internal/providers/arm/azure/testdata/linux_virtual_machine_test/linux_virtual_machine_test.golden new file mode 100644 index 00000000000..34bcb4442c9 --- /dev/null +++ b/internal/providers/arm/azure/testdata/linux_virtual_machine_test/linux_virtual_machine_test.golden @@ -0,0 +1,63 @@ + + Name Monthly Qty Unit Monthly Cost + + Microsoft.Compute/virtualMachines/Linux/standard_a2_v2_custom_disk + ├─ Instance usage (Linux, pay as you go, Standard_A2_v2) 730 hours $65.92 + └─ os_disk + ├─ Storage (E30, LRS) 1 months $76.80 + └─ Disk operations 2 10k operations $0.00 * + + Microsoft.Compute/virtualMachines/Linux/standard_f2_lowercase + ├─ Instance usage (Linux, pay as you go, standard_f2) 730 hours $72.27 + └─ os_disk + └─ Storage (P4, LRS) 1 months $5.28 + + Microsoft.Compute/virtualMachines/Linux/standard_f2_premium_disk + ├─ Instance usage (Linux, pay as you go, Standard_F2) 730 hours $72.27 + └─ os_disk + └─ Storage (P4, LRS) 1 months $5.28 + + Microsoft.Compute/virtualMachines/Linux/standard_a2_ultra_enabled + ├─ Instance usage (Linux, pay as you go, Standard_A2_v2) 730 hours $65.92 + ├─ Ultra disk reservation (if unattached) Monthly cost depends on usage: $4.38 per vCPU + └─ os_disk + ├─ Storage (E4, LRS) 1 months $2.40 + └─ Disk operations Monthly cost depends on usage: $0.002 per 10k operations + + Microsoft.Compute/virtualMachines/Linux/basic_a2 + ├─ Instance usage (Linux, pay as you go, Basic_A2) 730 hours $57.67 + └─ os_disk + ├─ Storage (S4, LRS) 1 months $1.54 + └─ Disk operations Monthly cost depends on usage: $0.0005 per 10k operations + + Microsoft.Compute/virtualMachines/Linux/basic_b1 + ├─ Instance usage (Linux, pay as you go, Standard_B1s) 730 hours $7.59 + └─ os_disk + ├─ Storage (S4, LRS) 1 months $1.54 + └─ Disk operations Monthly cost depends on usage: $0.0005 per 10k operations + + Microsoft.Compute/virtualMachines/Linux/basic_b1_lowercase + ├─ Instance usage (Linux, pay as you go, standard_b1s) 730 hours $7.59 + └─ os_disk + ├─ Storage (S4, LRS) 1 months $1.54 + └─ Disk operations Monthly cost depends on usage: $0.0005 per 10k operations + + Microsoft.Compute/virtualMachines/Linux/basic_b1_withMonthlyHours + ├─ Instance usage (Linux, pay as you go, Standard_B1s) 100 hours $1.04 + └─ os_disk + ├─ Storage (S4, LRS) 1 months $1.54 + └─ Disk operations Monthly cost depends on usage: $0.0005 per 10k operations + + OVERALL TOTAL $446.18 + +*Usage costs can be estimated by updating Infracost Cloud settings, see docs for other options. + +────────────────────────────────── +8 cloud resources were detected: +∙ 8 were estimated + +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━┓ +┃ Project ┃ Baseline cost ┃ Usage cost* ┃ Total cost ┃ +┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━╋━━━━━━━━━━━━┫ +┃ TestAzureRMLinuxVirtualMachineGoldenFile/ARM ┃ $446 ┃ $0.00 ┃ $446 ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━┻━━━━━━━━━━━━┛ \ No newline at end of file diff --git a/internal/providers/arm/azure/testdata/linux_virtual_machine_test/linux_virtual_machine_test.json b/internal/providers/arm/azure/testdata/linux_virtual_machine_test/linux_virtual_machine_test.json new file mode 100644 index 00000000000..b6617da8975 --- /dev/null +++ b/internal/providers/arm/azure/testdata/linux_virtual_machine_test/linux_virtual_machine_test.json @@ -0,0 +1,321 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "example", + "version": "0.27.1.19265", + "templateHash": "4270386830956032562" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "basic_b1", + "location": "eastus", + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "caching": "ReadWrite" + } + } + }, + "osProfile": { + "computerName": "basic_b1", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testrg/providers/Microsoft.Network/networkInterfaces/fakenic')]" + } + ] + } + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "basic_b1_lowercase", + "location": "eastus", + "properties": { + "hardwareProfile": { + "vmSize": "standard_b1s" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "caching": "ReadWrite" + } + } + }, + "osProfile": { + "computerName": "basic_b1", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testrg/providers/Microsoft.Network/networkInterfaces/fakenic')]" + } + ] + } + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "basic_a2", + "location": "eastus", + "properties": { + "hardwareProfile": { + "vmSize": "Basic_A2" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "caching": "ReadWrite" + } + } + }, + "osProfile": { + "computerName": "basic_a2", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testrg/providers/Microsoft.Network/networkInterfaces/fakenic')]" + } + ] + } + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "standard_f2_premium_disk", + "location": "eastus", + "properties": { + "hardwareProfile": { + "vmSize": "Standard_F2" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Premium_LRS", + "caching": "ReadWrite" + } + } + }, + "osProfile": { + "computerName": "standard_f2", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testrg/providers/Microsoft.Network/networkInterfaces/fakenic')]" + } + ] + } + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "standard_f2_lowercase", + "location": "eastus", + "properties": { + "hardwareProfile": { + "vmSize": "standard_f2" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Premium_LRS", + "caching": "ReadWrite" + } + } + }, + "osProfile": { + "computerName": "standard_f2", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testrg/providers/Microsoft.Network/networkInterfaces/fakenic')]" + } + ] + } + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "standard_a2_v2_custom_disk", + "location": "eastus", + "properties": { + "hardwareProfile": { + "vmSize": "Standard_A2_v2" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "StandardSSD_LRS", + "caching": "ReadWrite" + }, + "diskSizeGB": 1000 + } + }, + "osProfile": { + "computerName": "standard_a2_v2_custom_disk", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testrg/providers/Microsoft.Network/networkInterfaces/fakenic')]" + } + ] + } + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "standard_a2_ultra_enabled", + "location": "eastus", + "properties": { + "additionalCapabilities": { + "ultraSSDEnabled": true + }, + "hardwareProfile": { + "vmSize": "Standard_A2_v2" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "StandardSSD_LRS", + "caching": "ReadWrite" + } + } + }, + "osProfile": { + "computerName": "standard_a2_ultra_enabled", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testrg/providers/Microsoft.Network/networkInterfaces/fakenic')]" + } + ] + } + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "basic_b1_withMonthlyHours", + "location": "eastus", + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "caching": "ReadWrite" + } + } + }, + "osProfile": { + "computerName": "basic_b1", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testrg/providers/Microsoft.Network/networkInterfaces/fakenic')]" + } + ] + } + } + } + ] + } \ No newline at end of file diff --git a/internal/providers/arm/azure/testdata/linux_virtual_machine_test/linux_virtual_machine_test.usage.yml b/internal/providers/arm/azure/testdata/linux_virtual_machine_test/linux_virtual_machine_test.usage.yml new file mode 100644 index 00000000000..19c935f1a65 --- /dev/null +++ b/internal/providers/arm/azure/testdata/linux_virtual_machine_test/linux_virtual_machine_test.usage.yml @@ -0,0 +1,7 @@ +version: 0.1 +resource_usage: + Microsoft.Compute/virtualMachines/Linux/standard_a2_v2_custom_disk: + os_disk: + monthly_disk_operations: 20000 + Microsoft.Compute/virtualMachines/Linux/basic_b1_withMonthlyHours: + monthly_hrs: 100 diff --git a/internal/providers/arm/azure/testdata/managed_disk_test/managed_disk_test.golden b/internal/providers/arm/azure/testdata/managed_disk_test/managed_disk_test.golden new file mode 100644 index 00000000000..fad5ee57abe --- /dev/null +++ b/internal/providers/arm/azure/testdata/managed_disk_test/managed_disk_test.golden @@ -0,0 +1,32 @@ + + Name Monthly Qty Unit Monthly Cost + + Microsoft.Compute/disks/ultra + ├─ Storage (ultra, 2048 GiB) 2,048 GiB $245.19 + ├─ Provisioned IOPS 4,000 IOPS $198.56 + └─ Throughput 20 MB/s $6.99 + + Microsoft.Compute/disks/custom_size_ssd + ├─ Storage (E30, LRS) 1 months $76.80 + └─ Disk operations 2 10k operations $0.00 * + + Microsoft.Compute/disks/premium + └─ Storage (P4, LRS) 1 months $5.28 + + Microsoft.Compute/disks/standard + ├─ Storage (S4, LRS) 1 months $1.54 + └─ Disk operations Monthly cost depends on usage: $0.0005 per 10k operations + + OVERALL TOTAL $534.36 + +*Usage costs can be estimated by updating Infracost Cloud settings, see docs for other options. + +────────────────────────────────── +4 cloud resources were detected: +∙ 4 were estimated + +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━┓ +┃ Project ┃ Baseline cost ┃ Usage cost* ┃ Total cost ┃ +┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━╋━━━━━━━━━━━━┫ +┃ TestAzureRMManagedDiskGoldenFile/ARM ┃ $534 ┃ $0.00 ┃ $534 ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━┻━━━━━━━━━━━━┛ \ No newline at end of file diff --git a/internal/providers/arm/azure/testdata/managed_disk_test/managed_disk_test.json b/internal/providers/arm/azure/testdata/managed_disk_test/managed_disk_test.json new file mode 100644 index 00000000000..f13608b7300 --- /dev/null +++ b/internal/providers/arm/azure/testdata/managed_disk_test/managed_disk_test.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "example", + "version": "0.26.54.24096", + "templateHash": "7729680081436334184" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "standard", + "location": "eastus", + "properties": { + "creationData": { + "createOption": "Empty" + } + }, + "sku": { + "name": "Standard_LRS" + } + }, + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "premium", + "location": "eastus", + "properties": { + "creationData": { + "createOption": "Empty" + } + }, + "sku": { + "name": "Premium_LRS" + } + }, + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "custom_size_ssd", + "location": "eastus", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 1000 + }, + "sku": { + "name": "StandardSSD_LRS" + } + }, + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "ultra", + "location": "eastus", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "UltraSSD_LRS" + } + } + ] + } \ No newline at end of file diff --git a/internal/providers/arm/azure/testdata/managed_disk_test/managed_disk_test.usage.yml b/internal/providers/arm/azure/testdata/managed_disk_test/managed_disk_test.usage.yml new file mode 100644 index 00000000000..d047d27389f --- /dev/null +++ b/internal/providers/arm/azure/testdata/managed_disk_test/managed_disk_test.usage.yml @@ -0,0 +1,4 @@ +version: 0.1 +resource_usage: + Microsoft.Compute/disks/custom_size_ssd: + monthly_disk_operations: 20000 diff --git a/internal/providers/arm/parser.go b/internal/providers/arm/parser.go new file mode 100644 index 00000000000..07ccc564aa3 --- /dev/null +++ b/internal/providers/arm/parser.go @@ -0,0 +1,149 @@ +package arm + +import ( + "strings" + + "github.com/infracost/infracost/internal/config" + "github.com/infracost/infracost/internal/providers/arm/azure" + "github.com/infracost/infracost/internal/schema" + "github.com/tidwall/gjson" +) + +type Parser struct { + ctx *config.ProjectContext + includePastResources bool +} + +func NewParser(ctx *config.ProjectContext, includePastResources bool) *Parser { + return &Parser{ctx: ctx, includePastResources: includePastResources} +} + +type parsedResource struct { + PartialResource *schema.PartialResource + ResourceData *schema.ResourceData +} + +func (p *Parser) ParseJSON(data gjson.Result, usage schema.UsageMap) ([]*parsedResource, error) { + parsedResources := []*parsedResource{} + + resourceData, _ := p.parseResourceData(&data) + + p.populateUsageData(resourceData, usage) + + for _, d := range resourceData { + + parsedResource := p.createParsedResource(d, d.UsageData) + parsedResources = append(parsedResources, &parsedResource) + + } + return parsedResources, nil +} + +func (p *Parser) createParsedResource(d *schema.ResourceData, u *schema.UsageData) parsedResource { + for cKey, cValue := range azure.GetSpecialContext(d) { + p.ctx.ContextValues.SetValue(cKey, cValue) + } + + if registryItem, ok := (*ResourceRegistryMap)[d.Type]; ok { + if registryItem.NoPrice { + resource := &schema.Resource{ + Name: d.Address, + IsSkipped: true, + NoPrice: true, + SkipMessage: "Free resource.", + Metadata: d.Metadata, + } + return parsedResource{ + PartialResource: schema.NewPartialResource(d, resource, nil, registryItem.CloudResourceIDFunc(d)), + ResourceData: d, + } + + } + + // Use the CoreRFunc to generate a CoreResource if possible. This is + // the new/preferred way to create provider-agnostic resources that + // support advanced features such as Infracost Cloud usage estimates + // and actual costs. + if registryItem.CoreRFunc != nil { + coreRes := registryItem.CoreRFunc(d) + if coreRes != nil { + return parsedResource{ + PartialResource: schema.NewPartialResource(d, nil, coreRes, registryItem.CloudResourceIDFunc(d)), + ResourceData: d, + } + } + } else { + res := registryItem.RFunc(d, u) + if res != nil { + if u != nil { + res.EstimationSummary = u.CalcEstimationSummary() + } + + return parsedResource{ + PartialResource: schema.NewPartialResource(d, res, nil, registryItem.CloudResourceIDFunc(d)), + ResourceData: d, + } + } + } + } + + return parsedResource{ + PartialResource: schema.NewPartialResource( + d, + &schema.Resource{ + Name: d.Address, + IsSkipped: true, + SkipMessage: "This resource is not currently supported", + Metadata: d.Metadata, + }, + nil, + []string{}, + ), + ResourceData: d, + } +} + +func (p *Parser) parseResourceData(data *gjson.Result) (map[string]*schema.ResourceData, error) { + resources := make(map[string]*schema.ResourceData) + for _, res := range data.Array() { + t := res.Get("type").String() + if t == "Microsoft.Compute/virtualMachines" { + // There is no official naming for Linux or Windows virtual machines, so we need to modify the resource name to obtain the resource from the registry + GetOSResourceType(&t, &res) + } + // New address will consist of the official Microsoft resource type, with an extra '/' at the end to separate the resource type from the name + newAddress := strings.Clone(t) + "/" + res.Get("name").Str + resData := schema.NewResourceData(strings.Clone(t), "azurerm", newAddress, nil, res) + resData.Region = res.Get("location").String() + resources[strings.Clone(resData.Address)] = resData + } + return resources, nil +} + +/* +** +There doesn't seem to be an official Microsoft resource name that indicates whether the VM is Linux or Windows +So, we have to check the OS type from the properties and modified the register and mappping accordingly +In a previous version of the Infracost code, the azure_virtual_machine function used to check the os type and create a linux/windows virtual machine accordingly +** +*/ +func GetOSResourceType(resourceType *string, data *gjson.Result) { + name := "Microsoft.Compute/virtualMachines" + os := "Linux" + if data.Get("storage_image_reference.0.offer").Type != gjson.Null { + if strings.ToLower((data.Get("storage_image_reference.0.offer")).String()) == "windowsserver" { + os = "Windows" + } + } + if strings.ToLower((data.Get("storage_image_reference.0.offer")).String()) == "windows" { + os = "Windows" + } + *resourceType = name + "/" + os + +} + +func (p *Parser) populateUsageData(resData map[string]*schema.ResourceData, usage schema.UsageMap) { + for _, d := range resData { + d.UsageData = usage.Get(d.Address) + } +} diff --git a/internal/providers/arm/parser_test.go b/internal/providers/arm/parser_test.go new file mode 100644 index 00000000000..42d278efa96 --- /dev/null +++ b/internal/providers/arm/parser_test.go @@ -0,0 +1,186 @@ +package arm + +import ( + "reflect" + "testing" + + "github.com/infracost/infracost/internal/resources/azure" + "github.com/infracost/infracost/internal/schema" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" +) + +var testData = `{ + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "ultra", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "UltraSSD_LRS" + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "basic_b1", + "location": "francecentral", + "properties": { + "hardwareProfile": { + "vmSize": "standard_b1s" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Standard_LRS" + } + } + }, + "osProfile": { + "computerName": "standard_b1s", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + } + } + ] + } + ` + +func TestParseResourceData(t *testing.T) { + + expected := []schema.ResourceData{ + { + Type: "Microsoft.Compute/disks", + ProviderName: "azurerm", + Address: "Microsoft.Compute/disks/ultra", + }, + { + Type: "Microsoft.Compute/virtualMachines/Linux", + ProviderName: "azurerm", + Address: "Microsoft.Compute/virtualMachines/Linux/basic_b1", + }, + } + parser := Parser{} + data := gjson.Parse(testData).Get("resources") + resources, _ := parser.parseResourceData(&data) + for i := range expected { + assert.Equal(t, resources[expected[i].Address].Type, expected[i].Type) + assert.Equal(t, resources[expected[i].Address].ProviderName, expected[i].ProviderName) + assert.Equal(t, resources[expected[i].Address].Address, expected[i].Address) + } + +} + +func TestCreateParsedResoureceData(t *testing.T) { + + expected := []schema.CoreResource{ + &azure.ManagedDisk{ + Address: "Microsoft.Compute/disks", + Region: "francecentral", + ManagedDiskData: azure.ManagedDiskData{ + DiskType: "UltraSSD_LRS", + DiskSizeGB: 2000, + DiskIOPSReadWrite: 4000, + DiskMBPSReadWrite: 20, + }, + }, + // &azure.LinuxVirtualMachine{ + // Address: "Microsoft.Compute/virtualMachines", + // Region: "francecentral", + // Size: "standard_b1s", + // UltraSSDEnabled: false, + // OSDiskData: &azure.ManagedDiskData{ + // DiskType: "Standard_LRS", + // }, + // }, + } + resourceArray := gjson.Parse(testData).Get("resources").Array() + data := []map[string]*schema.ResourceData{ + { + "Microsoft.Compute/disks": &schema.ResourceData{ + Type: "Microsoft.Compute/disks", + ProviderName: "azurerm", + Region: "francecentral", + Address: "Microsoft.Compute/disks", + RawValues: resourceArray[0], + UsageData: &schema.UsageData{}, + }, + }, + // { + // "Microsoft.Compute/virtualMachines": &schema.ResourceData{ + // Type: "AZURE_Virtual_Machine_Linux", + // ProviderName: "azurerm", + // Address: "Microsoft.Compute/virtualMachines", + // RawValues: resourceArray[1], + // UsageData: &schema.UsageData{}, + // }, + // }, + } + parser := Parser{} + + for i := range data { + parsedResources := []*parsedResource{} + for _, d := range data[i] { + parsedData := parser.createParsedResource(d, d.UsageData) + parsedResources = append(parsedResources, &parsedData) + } + equal := reflect.DeepEqual(parsedResources[0].PartialResource.CoreResource, expected[i]) + assert.True(t, equal) + } + +} + +func TestParseJSON(t *testing.T) { + parser := Parser{} + data := gjson.Parse(testData).Get("resources") + expected := map[string]schema.CoreResource{ + "Microsoft.Compute/disks/ultra": &azure.ManagedDisk{ + Address: "Microsoft.Compute/disks/ultra", + Region: "francecentral", + ManagedDiskData: azure.ManagedDiskData{ + DiskType: "UltraSSD_LRS", + DiskSizeGB: 2000, + DiskIOPSReadWrite: 4000, + DiskMBPSReadWrite: 20, + }, + }, + // &azure.LinuxVirtualMachine{ + // Address: "Microsoft.Compute/virtualMachines", + // Region: "francecentral", + // Size: "standard_b1s", + // UltraSSDEnabled: false, + // OSDiskData: &azure.ManagedDiskData{ + // DiskType: "Standard_LRS", + // }, + // }, + } + parsedResources, err := parser.ParseJSON(data, schema.UsageMap{}) + if err != nil { + assert.Fail(t, "Error occurred while parsing JSON") + } + for i := range parsedResources { + res := parsedResources[i].PartialResource + if ex, ok := expected[res.Address]; ok { + + equal := reflect.DeepEqual(res.CoreResource, ex) + assert.True(t, equal) + } + } + +} diff --git a/internal/providers/arm/registry.go b/internal/providers/arm/registry.go new file mode 100644 index 00000000000..35b9b42ff9f --- /dev/null +++ b/internal/providers/arm/registry.go @@ -0,0 +1,53 @@ +package arm + +import ( + "github.com/infracost/infracost/internal/providers/arm/azure" + "github.com/infracost/infracost/internal/schema" +) + +type RegistryItemMap map[string]*schema.RegistryItem + +var ( + ResourceRegistryMap = buildResourceRegistryMap() +) + +func buildResourceRegistryMap() *RegistryItemMap { + resourceRegistryMap := make(RegistryItemMap) + + for _, registryItem := range azure.ResourceRegistry { + if registryItem.CloudResourceIDFunc == nil { + registryItem.CloudResourceIDFunc = azure.DefaultCloudResourceIDFunc + } + resourceRegistryMap[registryItem.Name] = registryItem + resourceRegistryMap[registryItem.Name].DefaultRefIDFunc = azure.GetDefaultRefIDFunc + } + for _, registryItem := range createFreeResources(azure.FreeResources, azure.GetDefaultRefIDFunc, azure.DefaultCloudResourceIDFunc) { + resourceRegistryMap[registryItem.Name] = registryItem + } + + return &resourceRegistryMap +} + +// GetRegion returns the region lookup function for the given resource data type if it exists. +func (r *RegistryItemMap) GetRegion(resourceDataType string) schema.RegionLookupFunc { + item, ok := (*r)[resourceDataType] + if ok { + return item.GetRegion + } + + return nil +} + +func createFreeResources(l []string, defaultRefsFunc schema.ReferenceIDFunc, resourceIdFunc schema.CloudResourceIDFunc) []*schema.RegistryItem { + freeResources := make([]*schema.RegistryItem, 0) + for _, resourceName := range l { + freeResources = append(freeResources, &schema.RegistryItem{ + Name: resourceName, + NoPrice: true, + Notes: []string{"Free resource."}, + DefaultRefIDFunc: defaultRefsFunc, + CloudResourceIDFunc: resourceIdFunc, + }) + } + return freeResources +} diff --git a/internal/providers/arm/template_provider.go b/internal/providers/arm/template_provider.go new file mode 100644 index 00000000000..e68ce43a0cd --- /dev/null +++ b/internal/providers/arm/template_provider.go @@ -0,0 +1,204 @@ +package arm + +import ( + "encoding/json" + "log" + "os" + "regexp" + "strings" + + "github.com/infracost/infracost/internal/config" + "github.com/infracost/infracost/internal/logging" + "github.com/infracost/infracost/internal/schema" + "github.com/tidwall/gjson" +) + +type TemplateProvider struct { + ctx *config.ProjectContext + Path string + includePastResources bool + content Content +} + +type Content struct { + FileContents map[string]FileContent + MergedBytes []byte +} + +type FileContent struct { + Schema string `json:"$schema"` + Parameters map[string]interface{} `json:"parameters"` + Variables map[string]interface{} `json:"variables"` + ContentVersion string `json:"contentVersion"` + Resources []interface{} `json:"resources"` +} + +func NewTemplateProvider(ctx *config.ProjectContext, includePastResources bool, path string) *TemplateProvider { + return &TemplateProvider{ + ctx: ctx, + Path: path, + includePastResources: includePastResources, + content: Content{FileContents: map[string]FileContent{}}, + } +} + +func (p *TemplateProvider) Type() string { + return "arm" +} +func (p *TemplateProvider) Context() *config.ProjectContext { return p.ctx } + +func (p *TemplateProvider) DisplayType() string { + return "Azure Resource Manager" +} + +func (p *TemplateProvider) AddMetadata(metadata *schema.ProjectMetadata) { + // no op +} + +func (p *TemplateProvider) ProjectName() string { + return config.CleanProjectName(p.ctx.ProjectConfig.Path) +} + +func (p *TemplateProvider) RelativePath() string { + return p.ctx.ProjectConfig.Path +} + +func (p *TemplateProvider) VarFiles() []string { + return nil +} + +func (p *TemplateProvider) LoadResources(usage schema.UsageMap) ([]*schema.Project, error) { + + logging.Logger.Debug().Msg("Extracting only cost-related params from arm template") + + rootPath := p.ctx.ProjectConfig.Path + if rootPath == "" { + log.Fatal("Root path is not provided") + } + + projects := make([]*schema.Project, 0) + + // Merge all the resources from the files in the directory + p.MergeFileResources(p.Path) + + p.content.MergeBytes() + + project, _ := p.loadProject(p.Path, usage) + projects = append(projects, project) + + return projects, nil + +} + +func (p *TemplateProvider) loadProject(filePath string, usage schema.UsageMap) (*schema.Project, error) { + + metadata := schema.DetectProjectMetadata(filePath) + metadata.Type = p.Type() + p.AddMetadata(metadata) + name := p.ctx.ProjectConfig.Name + if name == "" { + name = metadata.GenerateProjectName(p.ctx.RunContext.VCSMetadata.Remote, p.ctx.RunContext.IsCloudEnabled()) + } + + project := schema.NewProject(name, metadata) + p.parseFiles(project, usage) + p.content.MergedBytes = nil + return project, nil +} + +func (p *TemplateProvider) parseFiles(project *schema.Project, usage schema.UsageMap) { + parser := NewParser(p.ctx, p.includePastResources) + content := gjson.ParseBytes(p.content.MergedBytes) + resources, err := parser.ParseJSON(content, usage) + if err != nil { + log.Fatal(err, "Error parsing ARM template JSON") + } + + for _, res := range resources { + project.PartialResources = append(project.PartialResources, res.PartialResource) + } + +} + +func (p *TemplateProvider) LoadFileContent(filePath string) { + + data, err := os.ReadFile(filePath) + if err != nil { + log.Fatalf("Failed to read file: %v", err) + } + + // Store the file content in the content struct + var content FileContent + if err = json.Unmarshal(data, &content); err != nil { + log.Fatalf("Failed to unmarshal JSON: %v", err) + } + // If it is not an ARM template, return + if !IsARMTemplate(content) { + return + } + p.content.FileContents[filePath] = content + +} + +func (p *TemplateProvider) MergeFileResources(dirPath string) { + + // If the path is a file, load the file resources + if strings.HasSuffix(dirPath, ".json") { + p.LoadFileContent(dirPath) + return + + } + // If the path is a directory, load all the file resources in the directory that have a .json extension + fileInfos, _ := os.ReadDir(dirPath) + for _, info := range fileInfos { + + if info.IsDir() { + continue + } + + name := info.Name() + filePath := dirPath + "/" + name + + if !strings.HasSuffix(name, ".json") { + continue + } + p.LoadFileContent(filePath) + + } + +} + +func (c *Content) MergeBytes() { + var resources []interface{} + for _, content := range c.FileContents { + resources = append(resources, content.Resources...) + } + + mergedBytes, err := json.Marshal(resources) + if err != nil { + log.Fatalf("Failed to marshal JSON: %v", err) + } + + c.MergedBytes = mergedBytes +} + +func IsARMTemplate(content FileContent) bool { + /* + The schema property is the location of the JavaScript Object Notation (JSON) schema file that describes the version of the template language. + Since it is a required property in an ARM Template, then it will be used to detect whether the file is an ARM Template or not. + + For more information, see: https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/syntax + */ + if content.Schema == "" { + return false + } + + schemaPattern := "^https://schema\\.management\\.azure\\.com/schemas/\\d{4}-\\d{2}-\\d{2}/(tenant|managementGroup|subscription)?deploymentTemplate\\.json#$" + matched, err := regexp.Match(schemaPattern, []byte(content.Schema)) + if err != nil { + return false + } + + // Another way to check if the file is an ARM template is to check if the contentVersion and resources properties are present, since they are required in an ARM template + return matched && content.ContentVersion != "" && content.Resources != nil +} diff --git a/internal/providers/arm/template_provider_test.go b/internal/providers/arm/template_provider_test.go new file mode 100644 index 00000000000..a5f51f65008 --- /dev/null +++ b/internal/providers/arm/template_provider_test.go @@ -0,0 +1,129 @@ +package arm + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/infracost/infracost/internal/config" + "github.com/infracost/infracost/internal/resources/azure" + "github.com/infracost/infracost/internal/schema" + "github.com/sirupsen/logrus" + "gopkg.in/go-playground/assert.v1" +) + +var filePath string = "testdata" + +func TestARMTemplateDetection(t *testing.T) { + expected := map[string]bool{ + "template_valid": true, + "template_invalid_1": false, + } + + for fileName, expectedValue := range expected { + filePath := filepath.Join(filePath, fileName+".json") + data, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("Failed to read file: %v", err) + } + + var content FileContent + if err = json.Unmarshal(data, &content); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + assert.Equal(t, IsARMTemplate(content), expectedValue) + } +} + +func TestDetectInvalidTemplates(t *testing.T) { + expected := 4 + actual := 0 + // Get all files in testdata directory + fileInfos, _ := os.ReadDir(filePath) + for _, info := range fileInfos { + file := info.Name() + data, err := os.ReadFile(filePath + "/" + file) + if err != nil { + t.Fatalf("Failed to read file: %v", err) + } + + var content FileContent + if err = json.Unmarshal(data, &content); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + if !IsARMTemplate(content) { + actual++ + } + } + assert.Equal(t, actual, expected) +} + +func TestLoadFileContent(t *testing.T) { + + provider := TemplateProvider{ + content: Content{FileContents: map[string]FileContent{}}, + } + + fileInfos, _ := os.ReadDir(filePath) + for _, info := range fileInfos { + if info.IsDir() { + continue + } + name := info.Name() + filePath := filepath.Join(filePath, name) + provider.LoadFileContent(filePath) + } + assert.Equal(t, len(provider.content.FileContents), 4) + +} + +func TestParseFiles(t *testing.T) { + + data := `{ + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "ultra", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "UltraSSD_LRS" + } + }` + provider := NewTemplateProvider(&config.ProjectContext{ProjectConfig: &config.Project{Path: ""}}, false, "") + project := schema.NewProject("azurerm", &schema.ProjectMetadata{}) + provider.content.MergedBytes = []byte(data) + provider.parseFiles(project, schema.UsageMap{}) + + expected := &azure.ManagedDisk{ + Address: "Microsoft.Compute/disks/ultra", + Region: "francecentral", + ManagedDiskData: azure.ManagedDiskData{ + DiskType: "UltraSSD_LRS", + DiskSizeGB: 2000, + DiskIOPSReadWrite: 4000, + DiskMBPSReadWrite: 20, + }, + } + + assert.Equal(t, project.PartialResources[0].CoreResource, expected) + +} + +func TestLoadResources(t *testing.T) { + ctx := config.NewProjectContext(config.EmptyRunContext(), &config.Project{Path: filePath}, logrus.Fields{}) + provider := NewTemplateProvider(ctx, false, filePath) + projects, err := provider.LoadResources(schema.UsageMap{}) + if err != nil { + t.Fatalf("Failed to load resources: %v", err) + } + assert.Equal(t, len(projects), 1) + assert.Equal(t, len(projects[0].PartialResources), 3) +} diff --git a/internal/providers/arm/testdata/simple_linux_vm.json b/internal/providers/arm/testdata/simple_linux_vm.json new file mode 100644 index 00000000000..be726fbe5c7 --- /dev/null +++ b/internal/providers/arm/testdata/simple_linux_vm.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "basic_b1", + "location": "francecentral", + "properties": { + "hardwareProfile": { + "vmSize": "standard_b1s" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Standard_LRS" + } + } + }, + "osProfile": { + "computerName": "standard_b1s", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + } + } + } + ] + } \ No newline at end of file diff --git a/internal/providers/arm/testdata/standard_disk.json b/internal/providers/arm/testdata/standard_disk.json new file mode 100644 index 00000000000..16733f9d05b --- /dev/null +++ b/internal/providers/arm/testdata/standard_disk.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "standard", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "Standard_LRS" + } + } + ] + } \ No newline at end of file diff --git a/internal/providers/arm/testdata/template_invalid_1.json b/internal/providers/arm/testdata/template_invalid_1.json new file mode 100644 index 00000000000..1e9fe236ce3 --- /dev/null +++ b/internal/providers/arm/testdata/template_invalid_1.json @@ -0,0 +1,5 @@ +{ + "contentVersion": "1.0.0.0", + "resources": [] + +} \ No newline at end of file diff --git a/internal/providers/arm/testdata/template_invalid_2.json b/internal/providers/arm/testdata/template_invalid_2.json new file mode 100644 index 00000000000..6d5d4b8396c --- /dev/null +++ b/internal/providers/arm/testdata/template_invalid_2.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/TestdeploymentTemplate.json#", + + "contentVersion": "1.0.0.0", + "resources": [] + +} \ No newline at end of file diff --git a/internal/providers/arm/testdata/template_invalid_3.json b/internal/providers/arm/testdata/template_invalid_3.json new file mode 100644 index 00000000000..0e0071ce7b7 --- /dev/null +++ b/internal/providers/arm/testdata/template_invalid_3.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/subscriptionDeploymentTemplate.json#", + + "resources": [] + +} \ No newline at end of file diff --git a/internal/providers/arm/testdata/template_invalid_4.json b/internal/providers/arm/testdata/template_invalid_4.json new file mode 100644 index 00000000000..4e3cc89dd2b --- /dev/null +++ b/internal/providers/arm/testdata/template_invalid_4.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0" + + +} \ No newline at end of file diff --git a/internal/providers/arm/testdata/template_valid.json b/internal/providers/arm/testdata/template_valid.json new file mode 100644 index 00000000000..4666c213efc --- /dev/null +++ b/internal/providers/arm/testdata/template_valid.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [] + } \ No newline at end of file diff --git a/internal/providers/arm/testdata/ultra_disk.json b/internal/providers/arm/testdata/ultra_disk.json new file mode 100644 index 00000000000..01e03a8a681 --- /dev/null +++ b/internal/providers/arm/testdata/ultra_disk.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "ultra", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "UltraSSD_LRS" + } + } + ] + } \ No newline at end of file diff --git a/internal/providers/detect.go b/internal/providers/detect.go index 413c93f07bc..30cd57aed8c 100644 --- a/internal/providers/detect.go +++ b/internal/providers/detect.go @@ -4,6 +4,7 @@ import ( "archive/zip" "encoding/json" "fmt" + "log" "os" "path/filepath" "sync" @@ -13,6 +14,7 @@ import ( "github.com/infracost/infracost/internal/config" "github.com/infracost/infracost/internal/hcl" "github.com/infracost/infracost/internal/logging" + "github.com/infracost/infracost/internal/providers/arm" "github.com/infracost/infracost/internal/providers/cloudformation" "github.com/infracost/infracost/internal/providers/terraform" "github.com/infracost/infracost/internal/schema" @@ -51,6 +53,8 @@ func Detect(ctx *config.RunContext, project *config.Project, includePastResource return &DetectionOutput{Providers: []schema.Provider{terraform.NewStateJSONProvider(projectContext, includePastResources)}, RootModules: 1}, nil case ProjectTypeCloudFormation: return &DetectionOutput{Providers: []schema.Provider{cloudformation.NewTemplateProvider(projectContext, includePastResources)}, RootModules: 1}, nil + case ProjectTypeARMTemplate: + return &DetectionOutput{Providers: []schema.Provider{arm.NewTemplateProvider(projectContext, includePastResources, project.Path)}}, nil } pathOverrides := make([]hcl.PathOverrideConfig, len(ctx.Config.Autodetect.PathOverrides)) @@ -102,7 +106,7 @@ func Detect(ctx *config.RunContext, project *config.Project, includePastResource } else { detectedProjectContext.ContextValues.SetValue("project_type", "terraform_dir") if ctx.Config.ConfigFilePath == "" && len(project.TerraformVarFiles) == 0 { - autoProviders = append(autoProviders, autodetectedRootToProviders(pl, detectedProjectContext, rootPath)...) + autoProviders = append(autoProviders, autodetectedRootToProviders(detectedProjectContext, rootPath)...) } else { autoProviders = append(autoProviders, configFileRootToProvider(rootPath, nil, detectedProjectContext, pl)) } @@ -144,7 +148,7 @@ func configFileRootToProvider(rootPath hcl.RootPath, options []hcl.Option, proje // autodetectedRootToProviders returns a list of providers for the given root // path. These providers are generated by autodetected environments defined in // the root module. These are defined by var file naming conventions. -func autodetectedRootToProviders(pl *hcl.ProjectLocator, projectContext *config.ProjectContext, rootPath hcl.RootPath, options ...hcl.Option) []schema.Provider { +func autodetectedRootToProviders(projectContext *config.ProjectContext, rootPath hcl.RootPath, options ...hcl.Option) []schema.Provider { var providers []schema.Provider autoVarFiles := rootPath.AutoFiles() autoVarFiles = append(autoVarFiles, rootPath.GlobalFiles()...) @@ -201,10 +205,12 @@ var ( ProjectTypeTerragruntCLI ProjectType = "terragrunt_cli" ProjectTypeTerraformStateJSON ProjectType = "terraform_state_json" ProjectTypeCloudFormation ProjectType = "cloudformation" + ProjectTypeARMTemplate ProjectType = "arm_template" ProjectTypeAutodetect ProjectType = "autodetect" ) func DetectProjectType(path string, forceCLI bool) ProjectType { + if isCloudFormationTemplate(path) { return ProjectTypeCloudFormation } @@ -220,6 +226,9 @@ func DetectProjectType(path string, forceCLI bool) ProjectType { if isTerraformPlan(path) { return ProjectTypeTerraformPlanBinary } + if IsARMTemplate(path) { + return ProjectTypeARMTemplate + } if forceCLI { if isTerragruntNestedDir(path, 5) { @@ -232,6 +241,21 @@ func DetectProjectType(path string, forceCLI bool) ProjectType { return ProjectTypeAutodetect } +func IsARMTemplate(path string) bool { + data, err := os.ReadFile(path) + if err != nil { + return false + } + + // Store the file content in the content struct + var content arm.FileContent + if err = json.Unmarshal(data, &content); err != nil { + log.Fatalf("Failed to unmarshal JSON: %v", err) + } + // If it is not an ARM template, return + return arm.IsARMTemplate(content) +} + func isTerraformPlanJSON(path string) bool { b, err := os.ReadFile(path) if err != nil { diff --git a/internal/providers/terraform/hcl_provider.go b/internal/providers/terraform/hcl_provider.go index 3f5e156eb04..2de501a07c1 100644 --- a/internal/providers/terraform/hcl_provider.go +++ b/internal/providers/terraform/hcl_provider.go @@ -101,7 +101,7 @@ func NewHCLProvider(ctx *config.ProjectContext, rootPath hcl.RootPath, config *H return nil, fmt.Errorf("could not parse vars from plan flags %w", err) } - options := []hcl.Option{hcl.OptionWithTFEnvVars(ctx.ProjectConfig.Env)} + options := []hcl.Option{hcl.OptionWithTFEnvVars(ctx.ProjectConfig.Env), hcl.OptionWithSpaceliftRemoteVarLoader(ctx)} if len(v.vars) > 0 { withPlanFlagVars := hcl.OptionWithPlanFlagVars(v.vars) @@ -127,7 +127,7 @@ func NewHCLProvider(ctx *config.ProjectContext, rootPath hcl.RootPath, config *H }) localWorkspace := ctx.ProjectConfig.TerraformWorkspace if err == nil { - var loaderOpts []hcl.RemoteVariablesLoaderOption + var loaderOpts []hcl.TFCRemoteVariablesLoaderOption if ctx.ProjectConfig.TerraformCloudWorkspace != "" && ctx.ProjectConfig.TerraformCloudOrg != "" { loaderOpts = append(loaderOpts, hcl.RemoteVariablesLoaderWithRemoteConfig(hcl.TFCRemoteConfig{ Organization: ctx.ProjectConfig.TerraformCloudOrg, @@ -136,7 +136,7 @@ func NewHCLProvider(ctx *config.ProjectContext, rootPath hcl.RootPath, config *H })) } - options = append(options, hcl.OptionWithRemoteVarLoader( + options = append(options, hcl.OptionWithTFCRemoteVarLoader( credsSource.BaseCredentialSet.Host, credsSource.BaseCredentialSet.Token, localWorkspace, diff --git a/schema/config.schema.json b/schema/config.schema.json index 023af96b09f..d5ac5133b54 100644 --- a/schema/config.schema.json +++ b/schema/config.schema.json @@ -95,6 +95,15 @@ "terraform_cloud_token": { "type": "string" }, + "spacelift_api_key_endpoint": { + "type": "string" + }, + "spacelift_api_key_id": { + "type": "string" + }, + "spacelift_api_key_secret": { + "type": "string" + }, "terragrunt_flags": { "type": "string" },