├── common
├── config
│ ├── testdata
│ │ ├── source_dir
│ │ │ └── dummy.txt
│ │ └── config_files
│ │ │ ├── invalid_yaml.yaml
│ │ │ ├── missing_llm.yaml
│ │ │ ├── missing_target.yaml
│ │ │ ├── missing_llm_api_key.yaml
│ │ │ ├── missing_llm_provider.yaml
│ │ │ ├── missing_llm_model.yaml
│ │ │ ├── valid.yaml
│ │ │ ├── target_dir_equals_root.yaml
│ │ │ ├── invalid_llm_model.yaml
│ │ │ ├── invalid_llm_provider.yaml
│ │ │ ├── invalid_platform.yaml
│ │ │ ├── invalid_source_dir.yaml
│ │ │ ├── missing_target_platform.yaml
│ │ │ └── target_dir_outside_root.yaml
│ ├── config_test.go
│ ├── errors.go
│ ├── config.go
│ ├── validate_test.go
│ └── validate.go
├── lock_file_manager
│ ├── testdata
│ │ └── lock_files
│ │ │ ├── empty.toml
│ │ │ ├── missing_target_code.toml
│ │ │ ├── missing_variable_name.toml
│ │ │ ├── missing_target_file_checksum.toml
│ │ │ ├── missing_resource_type.toml
│ │ │ ├── missing_target_file_path.toml
│ │ │ ├── missing_resource_logical_name.toml
│ │ │ ├── missing_resource_source_file_path.toml
│ │ │ ├── missing_variable_source_file_path.toml
│ │ │ ├── missing_version.toml
│ │ │ ├── invalid_semver.toml
│ │ │ ├── missing_variable_type.toml
│ │ │ ├── missing_resource_source_file_line.toml
│ │ │ ├── missing_variable_source_file_line.toml
│ │ │ ├── valid.toml
│ │ │ └── invalid_variable_type.toml
│ ├── errors.go
│ ├── lock_file_manager.go
│ ├── type_mapper.go
│ ├── validate.go
│ └── validate_test.go
├── constants
│ └── constants.go
├── utils
│ ├── file_utils
│ │ ├── get_relative_file_paths.go
│ │ ├── get_file_paths.go
│ │ ├── get_subdirectory_paths.go
│ │ └── write_file_if_changed.go
│ └── object_utils
│ │ ├── objects_to_parsed_objects.go
│ │ └── get_object_maps.go
├── driver
│ ├── validate.go
│ ├── run.go
│ └── frontend.go
├── metrics
│ └── metrics.go
├── logger
│ └── logger.go
├── symbol_table
│ ├── symbol_table_test.go
│ └── symbol_table.go
├── errors
│ └── errors.go
├── change_set
│ ├── testdata
│ │ ├── new_objects.json
│ │ └── previous_objects.json
│ └── change_set_test.go
└── types
│ └── types.go
├── go.work
├── .gitignore
├── docs
├── images
│ ├── salami-icon.png
│ ├── compiler-diagram.png
│ └── salami-example.png
└── architecture.md
├── examples
├── simple_s3_bucket
│ ├── .gitignore
│ ├── salami
│ │ └── bucket.sami
│ ├── terraform
│ │ ├── bucket.tf
│ │ └── .terraform.lock.hcl
│ ├── salami.yaml
│ ├── Makefile
│ ├── salami-lock.toml
│ └── README.md
└── public_and_private_ecs_services
│ ├── .gitignore
│ ├── salami
│ ├── cloudwatch.sami
│ ├── ecr_vpc_endpoint_sg.sami
│ ├── user_assumed_role.sami
│ ├── variables.sami
│ ├── cloudtrail.sami
│ ├── alb.sami
│ ├── vpc.sami
│ ├── task_definitions.sami
│ ├── vpc_endpoints.sami
│ ├── ecs.sami
│ └── ecr.sami
│ ├── terraform
│ ├── cloudwatch.tf
│ ├── ecr_vpc_endpoint_sg.tf
│ ├── variables.tf
│ ├── user_assumed_role.tf
│ ├── cloudtrail.tf
│ ├── .terraform.lock.hcl
│ ├── alb.tf
│ ├── vpc.tf
│ ├── ecs.tf
│ ├── vpc_endpoints.tf
│ └── task_definitions.tf
│ ├── salami.yaml
│ ├── Makefile
│ └── README.md
├── .github
├── scripts
│ └── verify-compile
│ │ ├── s3_bucket.tf
│ │ ├── s3_bucket.sami
│ │ ├── salami.yaml
│ │ ├── run_bash.sh
│ │ └── run_powershell.ps1
├── workflows
│ ├── run_tests.yml
│ ├── test_macos_install.yml
│ ├── test_windows_install.yml
│ ├── test_linux_install.yml
│ └── create_gh_release.yml
└── templates
│ └── salami_tap_template.rb
├── backend
├── prompts
│ └── terraform
│ │ └── openai_gpt4
│ │ └── templates
│ │ ├── create
│ │ ├── variable.tmpl
│ │ └── resource.tmpl
│ │ ├── fix_validation_issue
│ │ ├── variable.tmpl
│ │ └── resource.tmpl
│ │ ├── update
│ │ ├── variable.tmpl
│ │ └── resource.tmpl
│ │ ├── system.tmpl
│ │ └── templates.go
├── target_file_manager
│ ├── errors.go
│ ├── testdata
│ │ ├── target_file_2.tf
│ │ └── target_file_1.tf
│ ├── write_target_files.go
│ ├── target_file_manager.go
│ └── target_file_manager_test.go
├── target
│ ├── terraform
│ │ ├── verify_peer_dependencies.go
│ │ ├── get_files_from_objects.go
│ │ └── generate_code.go
│ └── target.go
├── llm
│ └── llm.go
└── types
│ └── types.go
├── cli
├── go.mod
├── go.sum
└── main.go
├── Makefile
├── frontend
├── types
│ └── types.go
├── lexer
│ ├── testdata
│ │ └── source.sami
│ ├── lexer.go
│ └── process_line.go
├── parser
│ ├── parse_constructor.go
│ ├── parse_natural_language.go
│ └── parser.go
└── semantic_analyzer
│ └── semantic_analyzer.go
├── go.mod
└── go.sum
/common/config/testdata/source_dir/dummy.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/empty.toml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/go.work:
--------------------------------------------------------------------------------
1 | go 1.21.2
2 |
3 | use (
4 | .
5 | ./cli
6 | )
7 |
--------------------------------------------------------------------------------
/common/config/testdata/config_files/invalid_yaml.yaml:
--------------------------------------------------------------------------------
1 | "not valid yaml file"
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | main
3 | cpu.pprof
4 | .DS_Store
5 | coverage.out
6 | /salami
--------------------------------------------------------------------------------
/docs/images/salami-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/petrgazarov/salami/HEAD/docs/images/salami-icon.png
--------------------------------------------------------------------------------
/examples/simple_s3_bucket/.gitignore:
--------------------------------------------------------------------------------
1 | .terraform/
2 | *.tfstate
3 | *.tfstate.backup
4 | *.tfstate.lock.info
--------------------------------------------------------------------------------
/docs/images/compiler-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/petrgazarov/salami/HEAD/docs/images/compiler-diagram.png
--------------------------------------------------------------------------------
/docs/images/salami-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/petrgazarov/salami/HEAD/docs/images/salami-example.png
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/.gitignore:
--------------------------------------------------------------------------------
1 | .terraform/
2 | *.tfstate
3 | *.tfstate.backup
4 | *.tfstate.lock.info
--------------------------------------------------------------------------------
/.github/scripts/verify-compile/s3_bucket.tf:
--------------------------------------------------------------------------------
1 | resource "aws_s3_bucket" "salami_github_actions_test_bucket" {
2 | bucket = "my-bucket-1jsdfu4bmne2346djfnf"
3 | }
--------------------------------------------------------------------------------
/common/config/testdata/config_files/missing_llm.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | source_dir: testdata/source_dir
5 | target_dir: terraform
6 |
--------------------------------------------------------------------------------
/examples/simple_s3_bucket/salami/bucket.sami:
--------------------------------------------------------------------------------
1 | @resource(aws.s3.Bucket, TestBucket)
2 | Name: {test_bucket_name}
3 |
4 | @variable(test_bucket_name, string, test-bucket-br31m11)
--------------------------------------------------------------------------------
/.github/scripts/verify-compile/s3_bucket.sami:
--------------------------------------------------------------------------------
1 | @resource(aws.s3.bucket, salami_github_actions_test_bucket)
2 | Name: my-bucket-1jsdfu4bmne2346djfnf
3 | (Do not include any other properties in the configuration)
--------------------------------------------------------------------------------
/common/config/testdata/config_files/missing_target.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | llm:
3 | provider: openai
4 | model: gpt4
5 | api_key: 1234567890
6 | source_dir: testdata/source_dir
7 | target_dir: terraform
8 |
--------------------------------------------------------------------------------
/backend/prompts/terraform/openai_gpt4/templates/create/variable.tmpl:
--------------------------------------------------------------------------------
1 | Variable Name: {{.Name}}
2 | Type: {{.Type}}
3 | {{with .Default -}}
4 | Default: {{.}}
5 | {{end}}
6 | {{with .NaturalLanguage -}}
7 | {{.}}
8 | {{- end -}}
--------------------------------------------------------------------------------
/examples/simple_s3_bucket/terraform/bucket.tf:
--------------------------------------------------------------------------------
1 | resource "aws_s3_bucket" "TestBucket" {
2 | bucket = var.test_bucket_name
3 | }
4 |
5 | variable "test_bucket_name" {
6 | type = string
7 | default = "test-bucket-br31m11"
8 | }
--------------------------------------------------------------------------------
/common/config/testdata/config_files/missing_llm_api_key.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | provider: openai
6 | model: gpt4
7 | source_dir: testdata/source_dir
8 | target_dir: terraform
9 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/salami/cloudwatch.sami:
--------------------------------------------------------------------------------
1 | @resource(aws.cloudwatch.LogGroup, ServerLogGroup)
2 | Name: server-log-group
3 |
4 | @resource(aws.cloudwatch.LogGroup, PythonExecLogGroup)
5 | Name: python-exec-log-group
--------------------------------------------------------------------------------
/common/config/testdata/config_files/missing_llm_provider.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | model: gpt4
6 | api_key: 1234567890
7 | source_dir: testdata/source_dir
8 | target_dir: terraform
9 |
--------------------------------------------------------------------------------
/backend/target_file_manager/errors.go:
--------------------------------------------------------------------------------
1 | package target_file_manager
2 |
3 | type TargetFileError struct {
4 | Message string
5 | }
6 |
7 | func (e *TargetFileError) Error() string {
8 | return "target file error: " + e.Message
9 | }
10 |
--------------------------------------------------------------------------------
/common/config/testdata/config_files/missing_llm_model.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | provider: openai
6 | api_key: 1234567890
7 | source_dir: testdata/source_dir
8 | target_dir: terraform
9 |
--------------------------------------------------------------------------------
/examples/simple_s3_bucket/salami.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | provider: openai
6 | model: gpt4
7 | api_key: ${OPENAI_API_KEY}
8 | source_dir: salami
9 | target_dir: terraform
10 |
--------------------------------------------------------------------------------
/.github/scripts/verify-compile/salami.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | provider: openai
6 | model: gpt4
7 | api_key: ${OPENAI_API_KEY}
8 | source_dir: salami
9 | target_dir: terraform
10 |
--------------------------------------------------------------------------------
/backend/target_file_manager/testdata/target_file_2.tf:
--------------------------------------------------------------------------------
1 | provider "aws" {
2 | region = "us-east-2"
3 | }
4 |
5 | variable "server_container_name" {
6 | description = "Server container name"
7 | type = string
8 | default = "server-container"
9 | }
--------------------------------------------------------------------------------
/common/config/testdata/config_files/valid.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | provider: openai
6 | model: gpt4
7 | api_key: 1234567890
8 | source_dir: testdata/source_dir
9 | target_dir: terraform
10 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/terraform/cloudwatch.tf:
--------------------------------------------------------------------------------
1 | resource "aws_cloudwatch_log_group" "ServerLogGroup" {
2 | name = "server-log-group"
3 | }
4 |
5 | resource "aws_cloudwatch_log_group" "PythonExecLogGroup" {
6 | name = "python-exec-log-group"
7 | }
--------------------------------------------------------------------------------
/common/config/testdata/config_files/target_dir_equals_root.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | provider: openai
6 | model: gpt4
7 | api_key: 1234567890
8 | source_dir: testdata/source_dir
9 | target_dir: .
10 |
--------------------------------------------------------------------------------
/common/config/testdata/config_files/invalid_llm_model.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | provider: openai
6 | model: gpt-agi
7 | api_key: 1234567890
8 | source_dir: testdata/source_dir
9 | target_dir: terraform
10 |
--------------------------------------------------------------------------------
/common/config/testdata/config_files/invalid_llm_provider.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | provider: us-gov
6 | model: gpt4
7 | api_key: 1234567890
8 | source_dir: testdata/source_dir
9 | target_dir: terraform
10 |
--------------------------------------------------------------------------------
/common/config/testdata/config_files/invalid_platform.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: invalid_platform
4 | llm:
5 | provider: openai
6 | model: gpt4
7 | api_key: 1234567890
8 | source_dir: testdata/source_dir
9 | target_dir: terraform
10 |
--------------------------------------------------------------------------------
/common/config/testdata/config_files/invalid_source_dir.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | provider: openai
6 | model: gpt4
7 | api_key: 1234567890
8 | source_dir: testdata/non_existent_dir
9 | target_dir: terraform
10 |
--------------------------------------------------------------------------------
/common/config/testdata/config_files/missing_target_platform.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | some_key: some_value
4 | llm:
5 | provider: openai
6 | model: gpt4
7 | api_key: 1234567890
8 | source_dir: testdata/source_dir
9 | target_dir: terraform
10 |
--------------------------------------------------------------------------------
/common/config/testdata/config_files/target_dir_outside_root.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | provider: openai
6 | model: gpt4
7 | api_key: 1234567890
8 | source_dir: testdata/source_dir
9 | target_dir: /tmp/terraform
10 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/salami.yaml:
--------------------------------------------------------------------------------
1 | compiler:
2 | target:
3 | platform: terraform
4 | llm:
5 | provider: openai
6 | model: gpt4
7 | api_key: ${OPENAI_API_KEY}
8 | max_concurrent: 3
9 | source_dir: salami
10 | target_dir: terraform
11 |
--------------------------------------------------------------------------------
/backend/prompts/terraform/openai_gpt4/templates/fix_validation_issue/variable.tmpl:
--------------------------------------------------------------------------------
1 | I'm getting the following {{ if eq (len .ErrorMessages) 1 }}error{{else}}errors{{end}} when I run `terraform validate`:
2 | {{range .ErrorMessages}}
3 | {{.}}
4 | {{end}}
5 | Please respond with the corrected code snippet.
--------------------------------------------------------------------------------
/backend/prompts/terraform/openai_gpt4/templates/update/variable.tmpl:
--------------------------------------------------------------------------------
1 | Here is the updated description. Please provide the updated Terraform code.
2 |
3 | Variable Name: {{.Name}}
4 | Type: {{.Type}}
5 | {{with .Default -}}
6 | Default: {{.}}
7 | {{end}}
8 | {{with .NaturalLanguage -}}
9 | {{.}}
10 | {{- end -}}
--------------------------------------------------------------------------------
/cli/go.mod:
--------------------------------------------------------------------------------
1 | module salami-cli
2 |
3 | go 1.21.2
4 |
5 | require github.com/urfave/cli/v2 v2.25.7
6 |
7 | require (
8 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
9 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
10 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
11 | )
12 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install:
2 | go mod download
3 |
4 | test: # Resolves all test files ending in _test.go and runs them
5 | find . -name "*_test.go" -print0 | xargs -0 -n1 dirname | sort -u | xargs -L1 go test
6 |
7 | test_coverage:
8 | go test ./... -coverprofile=coverage.out
9 |
10 | build:
11 | go build -o salami cli/main.go
--------------------------------------------------------------------------------
/common/constants/constants.go:
--------------------------------------------------------------------------------
1 | package constants
2 |
3 | const SalamiVersion = "0.0.3"
4 |
5 | const SalamiFileExtension = ".sami"
6 |
7 | const TerraformFileExtension = ".tf"
8 |
9 | const MaxFixValidationErrorRetries = 2
10 |
11 | const DefaultMaxConcurrentLlmExecutions = 3
12 |
13 | const LlmTimeoutDurationSeconds = 90
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/salami/ecr_vpc_endpoint_sg.sami:
--------------------------------------------------------------------------------
1 | @resource(aws.ec2.SecurityGroup, EcrVpcEndpointSG)
2 | In $MainVpc
3 | Name: ecr-vpc-endpoint-sg
4 | Description: Security Group for ECR VPC Endpoint
5 | Egress: []
6 | Ingress: Allow TCP traffic on port 443 from security groups $ServerEcsSecurityGroup and $PythonExecEcsSecurityGroup
--------------------------------------------------------------------------------
/examples/simple_s3_bucket/Makefile:
--------------------------------------------------------------------------------
1 | run:
2 | ../../salami compile
3 |
4 | init:
5 | cd iac && terraform init
6 |
7 | plan:
8 | cd iac && AWS_PROFILE=salami terraform plan
9 |
10 | apply:
11 | cd iac && AWS_PROFILE=salami terraform apply
12 |
13 | destroy:
14 | cd iac && AWS_PROFILE=salami terraform destroy
15 |
16 | validate:
17 | cd iac && terraform validate
--------------------------------------------------------------------------------
/backend/target/terraform/verify_peer_dependencies.go:
--------------------------------------------------------------------------------
1 | package terraform
2 |
3 | import (
4 | "os/exec"
5 | "salami/common/errors"
6 | )
7 |
8 | func (t *Terraform) VerifyPeerDependencies() error {
9 | if _, err := exec.LookPath("terraform"); err != nil {
10 | return &errors.ConfigError{Message: "'terraform' could not be found in your PATH"}
11 | }
12 | return nil
13 | }
14 |
--------------------------------------------------------------------------------
/backend/target/target.go:
--------------------------------------------------------------------------------
1 | package target
2 |
3 | import (
4 | "salami/backend/target/terraform"
5 | backendTypes "salami/backend/types"
6 | commonTypes "salami/common/types"
7 | )
8 |
9 | func ResolveTarget(targetConfig commonTypes.TargetConfig) backendTypes.Target {
10 | var target backendTypes.Target
11 | if targetConfig.Platform == commonTypes.TerraformPlatform {
12 | target = terraform.NewTarget()
13 | }
14 | return target
15 | }
16 |
--------------------------------------------------------------------------------
/common/utils/file_utils/get_relative_file_paths.go:
--------------------------------------------------------------------------------
1 | package file_utils
2 |
3 | import "path/filepath"
4 |
5 | func GetRelativeFilePaths(baseDir string, paths []string) ([]string, error) {
6 | relativePaths := make([]string, len(paths))
7 | for i, path := range paths {
8 | relativePath, err := filepath.Rel(baseDir, path)
9 | if err != nil {
10 | return nil, err
11 | }
12 | relativePaths[i] = relativePath
13 | }
14 | return relativePaths, nil
15 | }
16 |
--------------------------------------------------------------------------------
/backend/llm/llm.go:
--------------------------------------------------------------------------------
1 | package llm
2 |
3 | import (
4 | openaiGpt4 "salami/backend/llm/openai/gpt4"
5 | backendTypes "salami/backend/types"
6 | commonTypes "salami/common/types"
7 | )
8 |
9 | func ResolveLlm(llmConfig commonTypes.LlmConfig) backendTypes.Llm {
10 | var llm backendTypes.Llm
11 | if llmConfig.Provider == commonTypes.LlmOpenaiProvider && llmConfig.Model == commonTypes.LlmGpt4Model {
12 | llm = openaiGpt4.NewLlm(llmConfig)
13 | }
14 | return llm
15 | }
16 |
--------------------------------------------------------------------------------
/.github/workflows/run_tests.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - "**"
7 | workflow_dispatch:
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v3
16 |
17 | - name: Set up Go
18 | uses: actions/setup-go@v4
19 | with:
20 | go-version: "1.21.2"
21 |
22 | - name: Run tests
23 | run: make test
24 |
--------------------------------------------------------------------------------
/backend/prompts/terraform/openai_gpt4/templates/create/resource.tmpl:
--------------------------------------------------------------------------------
1 | Resource type: {{.ResourceType}}
2 | Logical name: {{.LogicalName}}
3 | {{with .NaturalLanguage -}}
4 | {{.}}
5 | {{end}}
6 | {{- with .ReferencedResources}}
7 | The following resources are referenced:
8 | {{range .}}
9 | {{- .LogicalName}}: {{.ResourceType}}
10 | {{end}}{{end -}}
11 | {{- with .ReferencedVariables}}
12 | The following variables are referenced:
13 | {{range .}}{{.Name}}: {{.Type}}
14 | {{end -}}{{end -}}
--------------------------------------------------------------------------------
/backend/prompts/terraform/openai_gpt4/templates/system.tmpl:
--------------------------------------------------------------------------------
1 | You are an AI assistant helping the user generate Terraform code based on natural language descriptions.
2 | Keep in mind the following:
3 |
4 | 1) The generated code must be valid and cannot have any placeholders.
5 | 2) Always use best coding practices and write clean, readable code.
6 | 3) Your function response should only contain the generated code, nothing else.
7 | 4) Assume all referenced variables are already defined elsewhere in the program.
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/salami/user_assumed_role.sami:
--------------------------------------------------------------------------------
1 | @resource(aws.s3.Bucket, AssumedRolesBucket)
2 | Bucket: assumed-roles-hg12a
3 | Versioning enabled
4 |
5 | @resource(aws.s3.BucketPublicAccessBlock, AssetsPublicAccessBlock)
6 | For $AssumedRolesBucket
7 | Block public ACLs: True
8 | Block public policy: False
9 | Ignore public ACLs: True
10 | Restrict public buckets: False
11 |
12 | @resource(aws.s3.BucketPolicy, AssumedRolesBucketPolicy)
13 | For $AssumedRolesBucket
14 | Policy allows all principals to GET all objects in the bucket
--------------------------------------------------------------------------------
/common/utils/file_utils/get_file_paths.go:
--------------------------------------------------------------------------------
1 | package file_utils
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | )
7 |
8 | type FilterFunc func(string) bool
9 |
10 | func GetFilePaths(directory string, filter FilterFunc) ([]string, error) {
11 | var files []string
12 |
13 | err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
14 | if err != nil {
15 | return err
16 | }
17 | if !info.IsDir() && filter(path) {
18 | files = append(files, path)
19 | }
20 | return nil
21 | })
22 |
23 | return files, err
24 | }
25 |
--------------------------------------------------------------------------------
/backend/prompts/terraform/openai_gpt4/templates/update/resource.tmpl:
--------------------------------------------------------------------------------
1 | Here is the updated description. Please provide the updated Terraform code.
2 |
3 | Resource type: {{.ResourceType}}
4 | Logical name: {{.LogicalName}}
5 | {{with .NaturalLanguage -}}
6 | {{.}}
7 | {{end}}
8 | {{- with .ReferencedResources}}
9 | The following resources are referenced:
10 | {{range .}}
11 | {{- .LogicalName}}: {{.ResourceType}}
12 | {{end}}{{end -}}
13 | {{- with .ReferencedVariables}}
14 | The following variables are referenced:
15 | {{range .}}{{.Name}}: {{.Type}}
16 | {{end -}}{{end -}}
--------------------------------------------------------------------------------
/backend/prompts/terraform/openai_gpt4/templates/fix_validation_issue/resource.tmpl:
--------------------------------------------------------------------------------
1 | I'm getting the following {{ if eq (len .ErrorMessages) 1 }}error{{else}}errors{{end}} when I run `terraform validate`:
2 | {{range .ErrorMessages}}
3 | {{.}}
4 | {{end}}
5 | {{- with .ReferencedObjects}}
6 | Here is the code I'm using for the referenced resources/variables:
7 |
8 | ```terraform
9 | {{range .}}
10 | {{- .TargetCode}}
11 |
12 | {{end -}}
13 | ```
14 | {{- end}}
15 | Please respond with the corrected code snippet for the {{.LogicalName}} resource only. No need to include the entire file.
--------------------------------------------------------------------------------
/common/utils/file_utils/get_subdirectory_paths.go:
--------------------------------------------------------------------------------
1 | package file_utils
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | )
7 |
8 | func GetSubdirectoryPaths(directory string, filter FilterFunc) ([]string, error) {
9 | var subdirectories []string
10 |
11 | err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
12 | if err != nil {
13 | return err
14 | }
15 | if info.IsDir() && path != directory && filter(path) {
16 | subdirectories = append(subdirectories, path)
17 | }
18 | return nil
19 | })
20 |
21 | return subdirectories, err
22 | }
23 |
--------------------------------------------------------------------------------
/common/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config_test
2 |
3 | import (
4 | "salami/common/config"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestConfigGetters(t *testing.T) {
11 | t.Run("GetSourceDir", func(t *testing.T) {
12 | setConfigFile(t, "valid.yaml")
13 | config.LoadConfig()
14 | require.Equal(t, "testdata/source_dir", config.GetSourceDir())
15 | })
16 |
17 | t.Run("GetTargetDir", func(t *testing.T) {
18 | setConfigFile(t, "valid.yaml")
19 | config.LoadConfig()
20 | require.Equal(t, "terraform", config.GetTargetDir())
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/salami/variables.sami:
--------------------------------------------------------------------------------
1 | @variable(local_dns_namespace_name, string, local)
2 |
3 | @variable(aws_account_id, string)
4 |
5 | @variable(aws_region, string, us-west-2)
6 |
7 | @variable(server_container_name, string, server-container)
8 | Description: Server container name
9 |
10 | @variable(python_exec_container_name, string, python-exec-container)
11 |
12 | @variable(container_port, number, 8000)
13 |
14 | @variable(python_exec_local_service_name, string, python-exec)
15 |
16 | @variable(openai_api_key, string)
17 |
18 | @variable(assumed_role_secret_token, string)
--------------------------------------------------------------------------------
/common/utils/object_utils/objects_to_parsed_objects.go:
--------------------------------------------------------------------------------
1 | package object_utils
2 |
3 | import "salami/common/types"
4 |
5 | func ObjectsToParsedObjects(
6 | objects []*types.Object,
7 | ) ([]*types.ParsedResource, []*types.ParsedVariable) {
8 | resources := make([]*types.ParsedResource, 0)
9 | variables := make([]*types.ParsedVariable, 0)
10 | for _, object := range objects {
11 | if object.IsResource() {
12 | resources = append(resources, object.ParsedResource)
13 | } else if object.IsVariable() {
14 | variables = append(variables, object.ParsedVariable)
15 | }
16 | }
17 |
18 | return resources, variables
19 | }
20 |
--------------------------------------------------------------------------------
/common/utils/object_utils/get_object_maps.go:
--------------------------------------------------------------------------------
1 | package object_utils
2 |
3 | import "salami/common/types"
4 |
5 | func GetObjectMaps(
6 | objects []*types.Object,
7 | ) (map[types.LogicalName]*types.Object, map[string]*types.Object) {
8 | resourcesMap := make(map[types.LogicalName]*types.Object)
9 | variablesMap := make(map[string]*types.Object)
10 |
11 | for _, object := range objects {
12 | if object.IsResource() {
13 | resourcesMap[object.ParsedResource.LogicalName] = object
14 | } else if object.IsVariable() {
15 | variablesMap[object.ParsedVariable.Name] = object
16 | }
17 | }
18 |
19 | return resourcesMap, variablesMap
20 | }
21 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/terraform/ecr_vpc_endpoint_sg.tf:
--------------------------------------------------------------------------------
1 | resource "aws_security_group" "EcrVpcEndpointSG" {
2 | name = "ecr-vpc-endpoint-sg"
3 | description = "Security Group for ECR VPC Endpoint"
4 | vpc_id = aws_vpc.MainVpc.id
5 |
6 | egress {
7 | from_port = 0
8 | to_port = 0
9 | protocol = "-1"
10 | cidr_blocks = ["0.0.0.0/0"]
11 | }
12 |
13 | ingress {
14 | from_port = 443
15 | to_port = 443
16 | protocol = "tcp"
17 | security_groups = [aws_security_group.ServerEcsSecurityGroup.id, aws_security_group.PythonExecEcsSecurityGroup.id]
18 | }
19 | }
--------------------------------------------------------------------------------
/common/utils/file_utils/write_file_if_changed.go:
--------------------------------------------------------------------------------
1 | package file_utils
2 |
3 | import (
4 | "io"
5 | "os"
6 | )
7 |
8 | func WriteFileIfChanged(fullRelativeFilePath string, newContent string) error {
9 | file, err := os.OpenFile(fullRelativeFilePath, os.O_RDWR|os.O_CREATE, 0644)
10 | if err != nil {
11 | return err
12 | }
13 | defer file.Close()
14 |
15 | oldContent, err := io.ReadAll(file)
16 | if err != nil {
17 | return err
18 | }
19 |
20 | if string(oldContent) != newContent {
21 | file.Truncate(0)
22 | file.Seek(0, 0)
23 | _, err = file.WriteString(newContent)
24 | if err != nil {
25 | return err
26 | }
27 | }
28 |
29 | return nil
30 | }
31 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/salami/cloudtrail.sami:
--------------------------------------------------------------------------------
1 | @resource(aws.s3.Bucket, SalamiCloudtrailLogsBucket)
2 | Bucket name: salami-cloudtrail-logs
3 |
4 | @resource(aws.s3.BucketPolicy, SalamiCloudtrailLogsBucketPolicy)
5 | For $SalamiCloudtrailLogsBucket
6 | Policy with two statements:
7 | 1. Allow the "cloudtrail.amazonaws.com" service to put objects into the bucket, with the condition that the bucket owner has full control.
8 | 2. Allow the same service to get the bucket's ACL.
9 |
10 | @resource(aws.cloudtrail.Trail, Cloudtrail)
11 | For $SalamiCloudtrailLogsBucket
12 | Name: salami-cloudtrail
13 | Global service events: True
14 | Multi-region: True
15 | Logging enabled: True
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/Makefile:
--------------------------------------------------------------------------------
1 | run:
2 | ../../salami compile
3 |
4 | init:
5 | cd iac && terraform init
6 |
7 | plan:
8 | export TF_VAR_openai_api_key=$(OPENAI_API_KEY); \
9 | export TF_VAR_assumed_role_secret_token=$(shell uuidgen); \
10 | cd iac && AWS_PROFILE=salami terraform plan
11 |
12 | apply:
13 | export TF_VAR_openai_api_key=$(OPENAI_API_KEY); \
14 | export TF_VAR_assumed_role_secret_token=$(shell uuidgen); \
15 | cd iac && AWS_PROFILE=salami terraform apply
16 |
17 | destroy:
18 | export TF_VAR_openai_api_key=$(OPENAI_API_KEY); \
19 | export TF_VAR_assumed_role_secret_token=$(shell uuidgen); \
20 | cd iac && AWS_PROFILE=salami terraform destroy
21 |
22 | validate:
23 | cd iac && terraform validate
--------------------------------------------------------------------------------
/frontend/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type TokenType int
4 |
5 | const (
6 | ConstructorName TokenType = iota
7 | ConstructorArg
8 | NaturalLanguage
9 | Newline
10 | EOF
11 | Error
12 | )
13 |
14 | var TokenTypeNames = map[TokenType]string{
15 | ConstructorName: "ConstructorName",
16 | ConstructorArg: "ConstructorArg",
17 | NaturalLanguage: "NaturalLanguage",
18 | Newline: "Newline",
19 | EOF: "EOF",
20 | Error: "Error",
21 | }
22 |
23 | func (t TokenType) String() string {
24 | return TokenTypeNames[t]
25 | }
26 |
27 | type Token struct {
28 | Type TokenType
29 | Value string
30 | Line int
31 | Column int
32 | }
33 |
34 | type ParsedObject interface {
35 | AddNaturalLanguage(string)
36 | }
37 |
--------------------------------------------------------------------------------
/cli/go.sum:
--------------------------------------------------------------------------------
1 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
2 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
3 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
5 | github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
6 | github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
7 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
8 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
9 |
--------------------------------------------------------------------------------
/backend/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "salami/common/change_set"
5 | "salami/common/symbol_table"
6 | "salami/common/types"
7 | )
8 |
9 | type Target interface {
10 | VerifyPeerDependencies() error
11 | GenerateCode(*symbol_table.SymbolTable, *change_set.ChangeSetRepository, Llm) []error
12 | GetFilesFromObjects([]*types.Object) []*types.TargetFile
13 | ValidateCode([]*types.Object, *symbol_table.SymbolTable, *change_set.ChangeSetRepository, Llm, int) error
14 | }
15 |
16 | type Llm interface {
17 | GetSlug() string
18 | GetMaxConcurrentExecutions() int
19 | CreateCompletion(messages []interface{}) (string, error)
20 | }
21 |
22 | type CodeValidationResult struct {
23 | ValidatedObject *types.Object
24 | ErrorMessages []string
25 | ReferencedObjects []*types.Object
26 | }
27 |
--------------------------------------------------------------------------------
/docs/architecture.md:
--------------------------------------------------------------------------------
1 | # Architecture
2 |
3 | ## Compiler
4 |
5 | Salami compiler performs the following steps:
6 |
7 | 1. **Lexing** - converts the input text into a stream of tokens.
8 | 2. **Parsing** - converts the stream of tokens into parsed objects.
9 | 3. **Semantic analysis** - performs semantic analysis on the parsed objects.
10 | 4. **Change set generation** - generates a change set between the previous state and the current state.
11 | 5. **Code generation** - generates the target code using LLM.
12 | 6. **Code validation** - validates the generated code, sending errors back to LLM and receiving fixed code.
13 | 7. **Target file generation** - generates the target files.
14 | 8. **Lock file generation** - generates the lock file for the current state.
15 |
16 | A simplified diagram of the compiler:
17 |
18 |
--------------------------------------------------------------------------------
/common/driver/validate.go:
--------------------------------------------------------------------------------
1 | package driver
2 |
3 | import (
4 | "salami/backend/target"
5 | "salami/backend/target_file_manager"
6 | "salami/common/config"
7 | "salami/common/lock_file_manager"
8 | )
9 |
10 | func runValidations() []error {
11 | if err := config.ValidateConfig(); err != nil {
12 | return []error{err}
13 | }
14 |
15 | if err := lock_file_manager.ValidateLockFile(); err != nil {
16 | return []error{err}
17 | }
18 |
19 | targetFileMetas := lock_file_manager.GetTargetFileMetas()
20 | targetDir := config.GetTargetDir()
21 | if errors := target_file_manager.VerifyChecksums(targetFileMetas, targetDir); len(errors) > 0 {
22 | return errors
23 | }
24 |
25 | target := target.ResolveTarget(config.GetTargetConfig())
26 | if err := target.VerifyPeerDependencies(); err != nil {
27 | return []error{err}
28 | }
29 |
30 | return nil
31 | }
32 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module salami
2 |
3 | go 1.21.0
4 |
5 | require (
6 | github.com/BurntSushi/toml v1.3.2
7 | github.com/go-playground/validator/v10 v10.15.4
8 | github.com/sashabaranov/go-openai v1.15.3
9 | github.com/stretchr/testify v1.8.4
10 | go.uber.org/zap v1.26.0
11 | golang.org/x/sync v0.3.0
12 | gopkg.in/yaml.v3 v3.0.1
13 | )
14 |
15 | require (
16 | github.com/davecgh/go-spew v1.1.1 // indirect
17 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect
18 | github.com/go-playground/locales v0.14.1 // indirect
19 | github.com/go-playground/universal-translator v0.18.1 // indirect
20 | github.com/leodido/go-urn v1.2.4 // indirect
21 | github.com/pmezard/go-difflib v1.0.0 // indirect
22 | go.uber.org/multierr v1.10.0 // indirect
23 | golang.org/x/crypto v0.7.0 // indirect
24 | golang.org/x/net v0.8.0 // indirect
25 | golang.org/x/sys v0.6.0 // indirect
26 | golang.org/x/text v0.8.0 // indirect
27 | )
28 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/terraform/variables.tf:
--------------------------------------------------------------------------------
1 | variable "local_dns_namespace_name" {
2 | type = string
3 | default = "local"
4 | }
5 |
6 | variable "aws_account_id" {
7 | type = string
8 | }
9 |
10 | variable "aws_region" {
11 | type = string
12 | default = "us-west-2"
13 | }
14 |
15 | variable "server_container_name" {
16 | description = "Server container name"
17 | type = string
18 | default = "server-container"
19 | }
20 |
21 | variable "python_exec_container_name" {
22 | type = string
23 | default = "python-exec-container"
24 | }
25 |
26 | variable "container_port" {
27 | type = number
28 | default = 8000
29 | }
30 |
31 | variable "python_exec_local_service_name" {
32 | type = string
33 | default = "python-exec"
34 | }
35 |
36 | variable "openai_api_key" {
37 | type = string
38 | }
39 |
40 | variable "assumed_role_secret_token" {
41 | type = string
42 | }
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/terraform/user_assumed_role.tf:
--------------------------------------------------------------------------------
1 | resource "aws_s3_bucket" "AssumedRolesBucket" {
2 | bucket = "assumed-roles-hg12a"
3 | versioning {
4 | enabled = true
5 | }
6 | }
7 |
8 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
9 | bucket = aws_s3_bucket.AssumedRolesBucket.id
10 |
11 | block_public_acls = true
12 | block_public_policy = false
13 | ignore_public_acls = true
14 | restrict_public_buckets = false
15 | }
16 |
17 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
18 | bucket = aws_s3_bucket.AssumedRolesBucket.id
19 |
20 | policy = < 0 {
16 | return errors
17 | }
18 |
19 | symbolTable, errors := runFrontend()
20 | if len(errors) > 0 {
21 | return errors
22 | }
23 |
24 | newTargetFileMetas, newObjects, errors := runBackend(symbolTable)
25 | if len(errors) > 0 {
26 | return errors
27 | }
28 |
29 | if err := lock_file_manager.UpdateLockFile(newTargetFileMetas, newObjects); err != nil {
30 | return []error{err}
31 | }
32 | logCompletion()
33 |
34 | return nil
35 | }
36 |
37 | func logCompletion() {
38 | message := fmt.Sprintf(
39 | "✨ Done in %s. Changed objects: %s, added objects: %s, removed objects: %s, processed files: %s",
40 | metrics.GetDuration(),
41 | strconv.Itoa(metrics.GetMetrics().ObjectsChanged),
42 | strconv.Itoa(metrics.GetMetrics().ObjectsAdded),
43 | strconv.Itoa(metrics.GetMetrics().ObjectsRemoved),
44 | strconv.Itoa(metrics.GetMetrics().SourceFilesProcessed),
45 | )
46 | logger.Log(message)
47 | }
48 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/README.md:
--------------------------------------------------------------------------------
1 | # Example: public and private ECS services
2 |
3 | Creates a VPC with public and private subnets, 2 ECS Fargate services, a load balancer and a few other resources.
4 |
5 | ## Running the example
6 |
7 | ### Prerequisites
8 |
9 | To run this example, you need:
10 |
11 | - `terraform` installed
12 | - `salami` installed (follow installation instructions in the [README](../../README.md))
13 | - AWS credentials (optional, to deploy the infrastructure)
14 |
15 | ### Steps
16 |
17 | 1. Clone this repository
18 | 2. `cd` into the `examples/public_and_private_ecs_services` directory
19 | 3. Run `salami compile` to run the compiler
20 | 4. Optionally, `cd` into the `examples/public_and_private_ecs_services/terraform` directory and run the `terraform init` and `terraform apply` commands to deploy to AWS.
21 |
22 | ### FYI
23 |
24 | 1. `salami compile` command examines `salami-lock.toml` and the source `.sami` files to determine the changeset. To force a complete regeneration, delete the `salami-lock.toml` file and rerun the compiler.
25 |
26 | 2. If timeout error is raised, try setting `compiler.llm.max_concurrent` config to a lower number. This slows down the compilation process, but reduces the likelihood of timeouts from OpenAI.
--------------------------------------------------------------------------------
/common/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "os"
5 |
6 | "go.uber.org/zap"
7 | )
8 |
9 | type salamiLogger struct {
10 | verbose bool
11 | instance *zap.SugaredLogger
12 | }
13 |
14 | var logger *salamiLogger
15 |
16 | func InitializeLogger(verbose bool) {
17 | zapConfig := zap.NewDevelopmentConfig()
18 | zapConfig.EncoderConfig.EncodeCaller = nil
19 | zapConfig.EncoderConfig.LevelKey = ""
20 |
21 | zapLogger, err := zapConfig.Build()
22 | if err != nil {
23 | panic(err)
24 | }
25 |
26 | logger = &salamiLogger{
27 | verbose: verbose,
28 | instance: zapLogger.Sugar(),
29 | }
30 | }
31 |
32 | // Log logs the message always
33 | func Log(message string) {
34 | if logger == nil {
35 | return
36 | }
37 |
38 | defer logger.instance.Sync()
39 | logger.instance.Info(message)
40 | }
41 |
42 | // Verbose logs the message if the verbose flag is set to true
43 | func Verbose(message string) {
44 | if logger == nil {
45 | return
46 | }
47 | if !logger.verbose {
48 | return
49 | }
50 |
51 | defer logger.instance.Sync()
52 | logger.instance.Info(message)
53 | }
54 |
55 | // Debug logs the message if the DEBUG environment variable is set to true
56 | func Debug(message string) {
57 | if logger == nil {
58 | return
59 | }
60 | if os.Getenv("DEBUG") != "true" {
61 | return
62 | }
63 |
64 | defer logger.instance.Sync()
65 | logger.instance.Debug(message)
66 | }
67 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/terraform/cloudtrail.tf:
--------------------------------------------------------------------------------
1 | resource "aws_s3_bucket" "SalamiCloudtrailLogsBucket" {
2 | bucket = "salami-cloudtrail-logs"
3 | }
4 |
5 | resource "aws_s3_bucket_policy" "SalamiCloudtrailLogsBucketPolicy" {
6 | bucket = aws_s3_bucket.SalamiCloudtrailLogsBucket.id
7 |
8 | policy = < 0 {
43 | return errors
44 | }
45 |
46 | return nil
47 | }
48 |
49 | func writeTargetFile(targetFile *types.TargetFile, targetDir string) error {
50 | fullRelativeFilePath := filepath.Join(targetDir, targetFile.FilePath)
51 | dir := filepath.Dir(fullRelativeFilePath)
52 | if _, err := os.Stat(dir); os.IsNotExist(err) {
53 | if err := os.MkdirAll(dir, 0755); err != nil {
54 | return err
55 | }
56 | }
57 |
58 | if err := file_utils.WriteFileIfChanged(fullRelativeFilePath, targetFile.Content); err != nil {
59 | return err
60 | }
61 |
62 | return nil
63 | }
64 |
--------------------------------------------------------------------------------
/.github/workflows/test_macos_install.yml:
--------------------------------------------------------------------------------
1 | name: Test Mac OS install
2 |
3 | on:
4 | workflow_run:
5 | workflows: ["Release packages"]
6 | types:
7 | - completed
8 | workflow_dispatch:
9 |
10 | jobs:
11 | test_homebrew_install:
12 | name: Test Homebrew install
13 |
14 | runs-on: macos-latest
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v3
19 | with:
20 | path: salami-repo
21 |
22 | - name: Tap Salami
23 | run: brew tap petrgazarov/salami
24 |
25 | - name: Install Salami
26 | run: brew install salami
27 |
28 | - name: Install Terraform
29 | run: brew install terraform
30 |
31 | - name: Get latest release tag
32 | id: latest-tag
33 | run: |
34 | set -euo pipefail
35 |
36 | LATEST_TAG=$(curl --silent "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r .tag_name)
37 | echo "tag=${LATEST_TAG}" >> $GITHUB_OUTPUT
38 |
39 | - name: Verify version
40 | run: |
41 | set -euo pipefail
42 |
43 | version=$(salami version) || exit 1
44 | if [[ $version != "Salami version ${{ steps.latest-tag.outputs.tag }}" ]]; then
45 | echo "Version mismatch. Expected: Salami ${{ steps.latest-tag.outputs.tag }}, Got: $version"
46 | exit 1
47 | fi
48 |
49 | - name: Verify compile
50 | env:
51 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
52 | run: salami-repo/.github/scripts/verify-compile/run_bash.sh salami-repo
53 |
54 | - name: Uninstall Salami
55 | run: brew uninstall salami
56 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/terraform/alb.tf:
--------------------------------------------------------------------------------
1 | resource "aws_security_group" "AlbSecurityGroup" {
2 | name = "alb-security-group"
3 | description = "Security group for ALB"
4 | vpc_id = aws_vpc.MainVpc.id
5 |
6 | egress {
7 | from_port = 0
8 | to_port = 0
9 | protocol = "-1"
10 | cidr_blocks = ["0.0.0.0/0"]
11 | }
12 |
13 | ingress {
14 | from_port = 80
15 | to_port = 80
16 | protocol = "tcp"
17 | cidr_blocks = ["0.0.0.0/0"]
18 | }
19 | }
20 |
21 | resource "aws_lb" "ServerAlb" {
22 | name = "server-alb"
23 | internal = false
24 | load_balancer_type = "application"
25 | security_groups = [aws_security_group.AlbSecurityGroup.id]
26 | subnets = [aws_subnet.PublicSubnetA.id, aws_subnet.PublicSubnetB.id]
27 |
28 | enable_deletion_protection = true
29 |
30 | idle_timeout = 3600
31 |
32 | tags = {
33 | Name = "server-alb"
34 | }
35 | }
36 |
37 | resource "aws_lb_target_group" "ServerTargetGroup" {
38 | name = "server-target-group"
39 | port = 80
40 | protocol = "HTTP"
41 | vpc_id = aws_vpc.MainVpc.id
42 | target_type = "ip"
43 |
44 | health_check {
45 | interval = 30
46 | path = "/"
47 | protocol = "HTTP"
48 | }
49 |
50 | stickiness {
51 | type = "lb_cookie"
52 | cookie_duration = 86400
53 | enabled = true
54 | }
55 | }
56 |
57 | resource "aws_lb_listener" "ServerListener" {
58 | load_balancer_arn = aws_lb.ServerAlb.arn
59 | port = 80
60 | protocol = "HTTP"
61 |
62 | default_action {
63 | type = "forward"
64 | target_group_arn = aws_lb_target_group.ServerTargetGroup.arn
65 | }
66 | }
--------------------------------------------------------------------------------
/backend/prompts/terraform/openai_gpt4/templates/templates.go:
--------------------------------------------------------------------------------
1 | package templates
2 |
3 | import (
4 | "bytes"
5 | "embed"
6 | "io/fs"
7 | "strings"
8 | "text/template"
9 | )
10 |
11 | const SystemTemplatePath = "system.tmpl"
12 | const UpdateResourceTemplatePath = "update/resource.tmpl"
13 | const UpdateVariableTemplatePath = "update/variable.tmpl"
14 | const CreateResourceTemplatePath = "create/resource.tmpl"
15 | const CreateVariableTemplatePath = "create/variable.tmpl"
16 | const FixResourceValidationIssueTemplatePath = "fix_validation_issue/resource.tmpl"
17 | const FixVariableValidationIssueTemplatePath = "fix_validation_issue/variable.tmpl"
18 |
19 | func PopulateTemplate(templatePath string, dataStruct interface{}) (string, error) {
20 | templateString, err := ReadTemplateFile(templatePath)
21 | if err != nil {
22 | return "", err
23 | }
24 |
25 | result, err := replaceTemplateVariables(templateString, dataStruct)
26 | if err != nil {
27 | return "", err
28 | }
29 |
30 | return strings.TrimSpace(result), nil
31 | }
32 |
33 | func replaceTemplateVariables(templateString string, dataStruct interface{}) (string, error) {
34 | tmpl, err := template.New("template").Parse(templateString)
35 | if err != nil {
36 | return "", err
37 | }
38 |
39 | var buf bytes.Buffer
40 | err = tmpl.Execute(&buf, dataStruct)
41 | if err != nil {
42 | return "", err
43 | }
44 |
45 | return buf.String(), nil
46 | }
47 |
48 | //go:embed create update fix_validation_issue system.tmpl
49 | var templatesFS embed.FS
50 |
51 | func ReadTemplateFile(filePath string) (string, error) {
52 | data, err := fs.ReadFile(templatesFS, filePath)
53 | if err != nil {
54 | return "", err
55 | }
56 |
57 | return string(data), nil
58 | }
59 |
--------------------------------------------------------------------------------
/.github/workflows/test_windows_install.yml:
--------------------------------------------------------------------------------
1 | name: Test Windows install
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | test_chocolatey_install:
8 | name: Test Chocolatey install
9 |
10 | runs-on: windows-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v3
15 | with:
16 | path: salami-repo
17 |
18 | - name: Install Chocolatey
19 | run: |
20 | Set-ExecutionPolicy Bypass -Scope Process -Force
21 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
22 | iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
23 |
24 | - name: Install Salami
25 | run: choco install salami
26 |
27 | - name: Get latest release tag
28 | id: latest-tag
29 | run: |
30 | $LATEST_TAG = Invoke-RestMethod -Uri "https://api.github.com/repos/${{ github.repository }}/releases/latest" | Select-Object -ExpandProperty tag_name
31 | echo "tag=$LATEST_TAG" | Out-File -FilePath $env:GITHUB_ENV -Append
32 |
33 | - name: Verify version
34 | run: |
35 | $version = salami version
36 | if ($version -ne "Salami version ${{ steps.latest-tag.outputs.tag }}") {
37 | Write-Output "Version mismatch. Expected: Salami v${{ steps.latest-tag.outputs.tag }}, Got: $version"
38 | exit 1
39 | }
40 |
41 | - name: Verify compile
42 | env:
43 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
44 | run: .\salami-repo\.github\scripts\verify-compile\run_powershell.ps1 salami-repo
45 |
46 | - name: Uninstall Salami
47 | run: choco uninstall salami -y
48 |
--------------------------------------------------------------------------------
/common/symbol_table/symbol_table_test.go:
--------------------------------------------------------------------------------
1 | package symbol_table_test
2 |
3 | import (
4 | "salami/common/symbol_table"
5 | "salami/common/types"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestSymbolTable(t *testing.T) {
12 | t.Run("NewSymbolTable", func(t *testing.T) {
13 | t.Run("should return error if logical name is not unique", func(t *testing.T) {
14 | resources := []*types.ParsedResource{
15 | {
16 | ResourceType: types.ResourceType("my-resource-type"),
17 | LogicalName: types.LogicalName("my-resource"),
18 | SourceFilePath: "path/to/my-resource.sami",
19 | },
20 | {
21 | ResourceType: types.ResourceType("my-resource-type"),
22 | LogicalName: types.LogicalName("my-resource"),
23 | SourceFilePath: "path/to/my-resource.sami",
24 | },
25 | }
26 | variables := []*types.ParsedVariable{}
27 | _, err := symbol_table.NewSymbolTable(resources, variables)
28 | require.EqualError(t, err, "\npath/to/my-resource.sami\n semantic error: my-resource logical name is not unique")
29 | })
30 | t.Run("should return error if variable name is not unique", func(t *testing.T) {
31 | resources := []*types.ParsedResource{}
32 | variables := []*types.ParsedVariable{
33 | {
34 | Name: "my-variable",
35 | Type: types.VariableType("string"),
36 | SourceFilePath: "path/to/my-variable.sami",
37 | },
38 | {
39 | Name: "my-variable",
40 | Type: types.VariableType("string"),
41 | SourceFilePath: "path/to/my-variable.sami",
42 | },
43 | }
44 | _, err := symbol_table.NewSymbolTable(resources, variables)
45 | require.EqualError(t, err, "\npath/to/my-variable.sami\n semantic error: my-variable variable name is not unique")
46 | })
47 | })
48 | }
49 |
--------------------------------------------------------------------------------
/.github/templates/salami_tap_template.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file was generated by https://github.com/salami/salami/blob/master/.github/workflows/release_packages.yml
4 | class Salami < Formula
5 | desc "Salami - Infrastructure as Natural Language"
6 | homepage "https://github.com/petrgazarov/salami"
7 | version "${SALAMI_VERSION}"
8 | license "Apache-2.0"
9 |
10 | on_macos do
11 | if Hardware::CPU.intel?
12 | url "${SALAMI_DARWIN_X64_URL}"
13 | sha256 "${SALAMI_DARWIN_X64_SHA256}"
14 | end
15 |
16 | if Hardware::CPU.arm?
17 | url "${SALAMI_DARWIN_ARM64_URL}"
18 | sha256 "${SALAMI_DARWIN_ARM64_SHA256}"
19 | end
20 |
21 | def install
22 | if Hardware::CPU.intel?
23 | filename = File.basename("${SALAMI_DARWIN_X64_URL}")
24 | bin.install filename => "salami"
25 | end
26 |
27 | if Hardware::CPU.arm?
28 | filename = File.basename("${SALAMI_DARWIN_ARM64_URL}")
29 | bin.install filename => "salami"
30 | end
31 | end
32 | end
33 |
34 | on_linux do
35 | if Hardware::CPU.arm? && Hardware::CPU.is_64_bit?
36 | url "${SALAMI_LINUX_ARM64_URL}"
37 | sha256 "${SALAMI_LINUX_ARM64_SHA256}"
38 | end
39 |
40 | if Hardware::CPU.intel?
41 | url "${SALAMI_LINUX_X64_URL}"
42 | sha256 "${SALAMI_LINUX_X64_SHA256}"
43 | end
44 |
45 | def install
46 | if Hardware::CPU.arm? && Hardware::CPU.is_64_bit?
47 | filename = File.basename("${SALAMI_LINUX_ARM64_URL}")
48 | bin.install filename => "salami"
49 | end
50 |
51 | if Hardware::CPU.intel?
52 | filename = File.basename("${SALAMI_LINUX_X64_URL}")
53 | bin.install filename => "salami"
54 | end
55 | end
56 | end
57 |
58 | test do
59 | system "#{bin}/salami version"
60 | end
61 | end
--------------------------------------------------------------------------------
/common/symbol_table/symbol_table.go:
--------------------------------------------------------------------------------
1 | package symbol_table
2 |
3 | import (
4 | "fmt"
5 | "salami/common/errors"
6 | "salami/common/types"
7 | )
8 |
9 | type SymbolTable struct {
10 | ResourceTable map[types.LogicalName]*types.ParsedResource
11 | VariableTable map[string]*types.ParsedVariable
12 | }
13 |
14 | func NewSymbolTable(resources []*types.ParsedResource, variables []*types.ParsedVariable) (*SymbolTable, error) {
15 | st := &SymbolTable{
16 | ResourceTable: make(map[types.LogicalName]*types.ParsedResource),
17 | VariableTable: make(map[string]*types.ParsedVariable),
18 | }
19 | for _, r := range resources {
20 | if _, exists := st.ResourceTable[r.LogicalName]; exists {
21 | return nil, &errors.SemanticError{
22 | SourceFilePath: r.SourceFilePath,
23 | Message: fmt.Sprintf("%s logical name is not unique", r.LogicalName),
24 | }
25 | }
26 | st.insertResource(r)
27 | }
28 | for _, v := range variables {
29 | if _, exists := st.VariableTable[v.Name]; exists {
30 | return nil, &errors.SemanticError{
31 | SourceFilePath: v.SourceFilePath,
32 | Message: fmt.Sprintf("%s variable name is not unique", v.Name),
33 | }
34 | }
35 | st.insertVariable(v)
36 | }
37 | return st, nil
38 | }
39 |
40 | func (st *SymbolTable) insertResource(r *types.ParsedResource) {
41 | st.ResourceTable[r.LogicalName] = r
42 | }
43 |
44 | func (st *SymbolTable) insertVariable(v *types.ParsedVariable) {
45 | st.VariableTable[v.Name] = v
46 | }
47 |
48 | func (st *SymbolTable) LookupResource(identifier types.LogicalName) (*types.ParsedResource, bool) {
49 | res, exists := st.ResourceTable[identifier]
50 | return res, exists
51 | }
52 |
53 | func (st *SymbolTable) LookupVariable(identifier string) (*types.ParsedVariable, bool) {
54 | vari, exists := st.VariableTable[identifier]
55 | return vari, exists
56 | }
57 |
--------------------------------------------------------------------------------
/.github/workflows/test_linux_install.yml:
--------------------------------------------------------------------------------
1 | name: Test Linux install
2 |
3 | on:
4 | workflow_run:
5 | workflows: ["Release packages"]
6 | types:
7 | - completed
8 | workflow_dispatch:
9 |
10 | jobs:
11 | test_homebrew_install:
12 | name: Test Homebrew install
13 |
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v3
19 | with:
20 | path: salami-repo
21 |
22 | - name: Install Homebrew
23 | run: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
24 |
25 | - name: Add Homebrew to PATH
26 | run: echo "/home/linuxbrew/.linuxbrew/bin" >> $GITHUB_PATH
27 |
28 | - name: Tap Salami
29 | run: brew tap petrgazarov/salami
30 |
31 | - name: Install Salami
32 | run: brew install salami
33 |
34 | - name: Get latest release tag
35 | id: latest-tag
36 | run: |
37 | set -euo pipefail
38 |
39 | LATEST_TAG=$(curl --silent "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r .tag_name)
40 | echo "tag=${LATEST_TAG}" >> $GITHUB_OUTPUT
41 |
42 | - name: Verify version
43 | run: |
44 | set -euo pipefail
45 |
46 | version=$(salami version) || exit 1
47 | if [[ $version != "Salami version ${{ steps.latest-tag.outputs.tag }}" ]]; then
48 | echo "Version mismatch. Expected: Salami ${{ steps.latest-tag.outputs.tag }}, Got: $version"
49 | exit 1
50 | fi
51 |
52 | - name: Verify compile
53 | env:
54 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
55 | run: salami-repo/.github/scripts/verify-compile/run_bash.sh salami-repo
56 |
57 | - name: Uninstall Salami
58 | run: brew uninstall salami
59 |
--------------------------------------------------------------------------------
/common/lock_file_manager/errors.go:
--------------------------------------------------------------------------------
1 | package lock_file_manager
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | )
7 |
8 | var fieldNameMap = map[string]string{
9 | "LockFile.Version": "lock file version",
10 | "LockFile.TargetFileMetas[*].FilePath": "target file path",
11 | "LockFile.TargetFileMetas[*].Checksum": "target file checksum",
12 | "LockFile.Objects[*].ParsedResource": "parsed resource",
13 | "LockFile.Objects[*].ParsedResource.LogicalName": "parsed resource logical name",
14 | "LockFile.Objects[*].ParsedResource.ResourceType": "parsed resource type",
15 | "LockFile.Objects[*].ParsedResource.SourceFileLine": "parsed resource source file line",
16 | "LockFile.Objects[*].ParsedResource.SourceFilePath": "parsed resource source file path",
17 | "LockFile.Objects[*].ParsedVariable": "parsed variable",
18 | "LockFile.Objects[*].ParsedVariable.Name": "parsed variable name",
19 | "LockFile.Objects[*].ParsedVariable.SourceFileLine": "parsed variable source file line",
20 | "LockFile.Objects[*].ParsedVariable.SourceFilePath": "parsed variable source file path",
21 | "LockFile.Objects[*].ParsedVariable.VariableType": "parsed variable type",
22 | "LockFile.Objects[*].TargetCode": "object target code",
23 | }
24 |
25 | type LockFileError struct {
26 | Message string
27 | }
28 |
29 | func (e *LockFileError) Error() string {
30 | return fmt.Sprintf("lock file error: %s", e.Message)
31 | }
32 |
33 | func getMissingFieldError(field string) error {
34 | re := regexp.MustCompile(`\[\d+\]`)
35 | field = re.ReplaceAllStringFunc(field, func(s string) string {
36 | return "[*]"
37 | })
38 |
39 | fieldName := fieldNameMap[field]
40 | if fieldName == "" {
41 | fieldName = field
42 | }
43 | return &LockFileError{Message: fmt.Sprintf("missing or invalid %s", fieldName)}
44 | }
45 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/salami/vpc.sami:
--------------------------------------------------------------------------------
1 | @resource(aws.ec2.Vpc, MainVpc)
2 | Name: main-vpc
3 | Cidr block: 10.0.0.0/16
4 | Enable DNS support: True
5 | Enable DNS hostnames: True
6 |
7 | @resource(aws.ec2.Subnet, PrivateSubnetA)
8 | In $MainVpc
9 | Name private-subnet-a
10 | Cidr block: 10.0.1.0/24
11 | Availability zone: {aws_region}a
12 | Map public IP on launch: False
13 |
14 | @resource(aws.ec2.Subnet, PrivateSubnetB)
15 | In $MainVpc
16 | Name: private-subnet-b
17 | Cidr block: 10.0.2.0/24
18 | Availability zone: {aws_region}c
19 | Map public IP on launch: False
20 |
21 | @resource(aws.ec2.RouteTable, PrivateRouteTable)
22 | In $MainVpc
23 | Name: private-route-table
24 |
25 | @resource(aws.ec2.RouteTableAssociation, PrivateSubnetARouteTableAssociation)
26 | Associated with $PrivateSubnetA and $PrivateRouteTable
27 |
28 | @resource(aws.ec2.RouteTableAssociation, PrivateSubnetBRouteTableAssociation)
29 | Associated with $PrivateSubnetB and $PrivateRouteTable
30 |
31 | @resource(aws.ec2.Subnet, PublicSubnetA)
32 | In $MainVpc
33 | Name: public-subnet-a
34 | Cidr block: 10.0.3.0/24
35 | Availability zone: {aws_region}a
36 | Map public IP on launch: True
37 |
38 | @resource(aws.ec2.Subnet, PublicSubnetB)
39 | In $MainVpc
40 | Name: public-subnet-b
41 | Cidr block: 10.0.4.0/24
42 | Availability zone: {aws_region}c
43 | Map public IP on launch: True
44 |
45 | @resource(aws.ec2.InternetGateway, InternetGateway)
46 | In $MainVpc
47 | Name: internet-gateway
48 |
49 | @resource(aws.ec2.RouteTable, PublicRouteTable)
50 | In $MainVpc
51 | Name: public-route-table
52 | Routes: A route with a CIDR block of "0.0.0.0/0" and $InternetGateway internet gateway
53 |
54 | @resource(aws.ec2.RouteTableAssociation, PublicSubnetARouteTableAssociation)
55 | Associated with $PublicSubnetA and $PublicRouteTable
56 |
57 | @resource(aws.ec2.RouteTableAssociation, PublicSubnetBRouteTableAssociation)
58 | Associated with $PublicSubnetB and $PublicRouteTable
--------------------------------------------------------------------------------
/common/errors/errors.go:
--------------------------------------------------------------------------------
1 | package errors
2 |
3 | import (
4 | "fmt"
5 | "salami/frontend/types"
6 | )
7 |
8 | type ConfigError struct {
9 | Message string
10 | }
11 |
12 | func (e *ConfigError) Error() string {
13 | return fmt.Sprintf("config error: %s", e.Message)
14 | }
15 |
16 | type SemanticError struct {
17 | SourceFilePath string
18 | Message string
19 | }
20 |
21 | func (e *SemanticError) Error() string {
22 | return fmt.Sprintf(
23 | "\n%s\n semantic error: %s",
24 | e.SourceFilePath,
25 | e.Message,
26 | )
27 | }
28 |
29 | type LexerError struct {
30 | FilePath string
31 | Line int
32 | Column int
33 | Message string
34 | }
35 |
36 | func (e *LexerError) Error() string {
37 | return fmt.Sprintf(
38 | "\n%s\n lexical error on line %d, column %d: %s",
39 | e.FilePath,
40 | e.Line,
41 | e.Column,
42 | e.Message,
43 | )
44 | }
45 |
46 | type ParseError struct {
47 | FilePath string
48 | Token *types.Token
49 | Message string
50 | }
51 |
52 | func (e *ParseError) Error() string {
53 | message := e.Message
54 | if message == "" {
55 | message = fmt.Sprintf(
56 | "unexpected token %s of type %s",
57 | e.Token.Value,
58 | e.Token.Type,
59 | )
60 | }
61 |
62 | return fmt.Sprintf(
63 | "\n%s\n parsing error on line %d, column %d: %s",
64 | e.FilePath,
65 | e.Token.Line,
66 | e.Token.Column,
67 | message,
68 | )
69 | }
70 |
71 | type MissingEOFTokenError struct {
72 | FilePath string
73 | }
74 |
75 | func (e *MissingEOFTokenError) Error() string {
76 | return fmt.Sprintf("\n%s\n parsing error: EOF token missing", e.FilePath)
77 | }
78 |
79 | type LlmError struct {
80 | Message string
81 | }
82 |
83 | func (e *LlmError) Error() string {
84 | return fmt.Sprintf("llm error: %s", e.Message)
85 | }
86 |
87 | type TargetError struct {
88 | Message string
89 | }
90 |
91 | func (e *TargetError) Error() string {
92 | return fmt.Sprintf("target error: %s", e.Message)
93 | }
--------------------------------------------------------------------------------
/cli/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "salami/common/constants"
8 | "salami/common/driver"
9 | "strings"
10 |
11 | "github.com/urfave/cli/v2"
12 | )
13 |
14 | type SalamiMultiError struct {
15 | errors []error
16 | }
17 |
18 | func (m *SalamiMultiError) Error() string {
19 | msgs := []string{}
20 | for _, err := range m.errors {
21 | msgs = append(msgs, err.Error())
22 | }
23 | return strings.Join(msgs, ", ")
24 | }
25 |
26 | func (m *SalamiMultiError) Errors() []error {
27 | return m.errors
28 | }
29 |
30 | func main() {
31 | app := &cli.App{
32 | Name: "salami",
33 | HelpName: "Salami",
34 | Version: constants.SalamiVersion,
35 | Usage: "a declarative DSL for cloud infrastructure based on natural language descriptions",
36 | UsageText: "salami [global options] [command] [command options]",
37 | HideVersion: true,
38 | Suggest: true,
39 | Flags: []cli.Flag{
40 | &cli.BoolFlag{
41 | Name: "verbose",
42 | Aliases: []string{"v"},
43 | Usage: "Enable verbose mode",
44 | },
45 | },
46 | Commands: []*cli.Command{
47 | {
48 | Name: "compile",
49 | Usage: "Runs the compilation end-to-end",
50 | UsageText: "salami [global options] compile [command options]",
51 | Action: func(cCtx *cli.Context) error {
52 | verbose := cCtx.Bool("verbose")
53 | errors := driver.Run(verbose)
54 |
55 | if len(errors) == 1 {
56 | return errors[0]
57 | } else if len(errors) > 1 {
58 | return &SalamiMultiError{errors: errors}
59 | }
60 |
61 | return nil
62 | },
63 | },
64 | {
65 | Name: "version",
66 | Usage: "Prints the version",
67 | Action: func(cCtx *cli.Context) error {
68 | fmt.Println("Salami version " + constants.SalamiVersion)
69 | return nil
70 | },
71 | },
72 | },
73 | Authors: []*cli.Author{{
74 | Name: "Petr Gazarov",
75 | Email: "petrgazarov@gmail.com",
76 | }},
77 | }
78 |
79 | if err := app.Run(os.Args); err != nil {
80 | log.Fatal(err)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/common/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "log"
5 | "os"
6 | "regexp"
7 | "salami/common/constants"
8 | "salami/common/types"
9 |
10 | "gopkg.in/yaml.v3"
11 | )
12 |
13 | var configFilePath = "salami.yaml"
14 | var loadedConfig *ConfigType
15 |
16 | func SetConfigFilePath(path string) {
17 | configFilePath = path
18 | loadedConfig = nil
19 | }
20 |
21 | func LoadConfig() error {
22 | yamlFile, err := os.ReadFile(configFilePath)
23 | if err != nil {
24 | return err
25 | }
26 | yamlFile = []byte(substituteEnvVars(string(yamlFile)))
27 | if err = yaml.Unmarshal(yamlFile, &loadedConfig); err != nil {
28 | return &ConfigError{Message: "could not parse config file. Ensure it is valid yaml format"}
29 | }
30 | return nil
31 | }
32 |
33 | func GetSourceDir() string {
34 | return getConfig().Compiler.SourceDir
35 | }
36 |
37 | func GetTargetDir() string {
38 | return getConfig().Compiler.TargetDir
39 | }
40 |
41 | func GetTargetConfig() types.TargetConfig {
42 | compilerTargetConfig := getConfig().Compiler.Target
43 | return types.TargetConfig{
44 | Platform: compilerTargetConfig.Platform,
45 | }
46 | }
47 |
48 | func GetLlmConfig() types.LlmConfig {
49 | compilerLlmConfig := getConfig().Compiler.Llm
50 |
51 | maxConcurrentExecutions := compilerLlmConfig.MaxConcurrentExecutions
52 | if maxConcurrentExecutions == 0 {
53 | maxConcurrentExecutions = constants.DefaultMaxConcurrentLlmExecutions
54 | }
55 |
56 | return types.LlmConfig{
57 | Provider: compilerLlmConfig.Provider,
58 | Model: compilerLlmConfig.Model,
59 | ApiKey: compilerLlmConfig.ApiKey,
60 | MaxConcurrentExecutions: maxConcurrentExecutions,
61 | }
62 | }
63 |
64 | func getConfig() *ConfigType {
65 | if loadedConfig == nil {
66 | log.Fatal("Config not loaded")
67 | }
68 | return loadedConfig
69 | }
70 |
71 | func substituteEnvVars(input string) string {
72 | re := regexp.MustCompile(`\$\{(.+?)\}`)
73 | return re.ReplaceAllStringFunc(input, func(s string) string {
74 | varName := s[2 : len(s)-1]
75 | return os.Getenv(varName)
76 | })
77 | }
78 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/salami/task_definitions.sami:
--------------------------------------------------------------------------------
1 | @resource(aws.iam.Role, ServerTaskRole)
2 | name: server-task-role
3 | Assume role policy allows the "ecs-tasks.amazonaws.com" service to assume this role
4 |
5 | @resource(aws.iam.Policy, ServerAssumeRolePolicy)
6 | Name: server-assume-role-policy
7 | Description: A policy that allows the ECS task to assume a role in users' accounts
8 | Policy allows the "sts:AssumeRole" action on resources matching the pattern "arn:aws:iam::*:role/salami-assumed-role-v0.1-*"
9 |
10 | @resource(aws.iam.RolePolicyAttachment, ServerTaskRolePolicyAttachment)
11 | Attaches $ServerAssumeRolePolicy to $ServerTaskRole
12 |
13 | @resource(aws.ecs.TaskDefinition, ServerTaskDefinition)
14 | Family: server
15 | Cpu: 256, Memory: 512
16 | Network mode: awsvpc
17 | Task role: $ServerTaskRole
18 | Requires FARGATE compatibility
19 | Execution role: $ServerEcsExecutionRole
20 | Container definition:
21 | Name: {server_container_name}
22 | Image: $ServerRepository url with the "latest" tag
23 | Memory: 512
24 | Cpu: 256
25 | Essential: True
26 | Port mappings:
27 | Container port is {container_port}. Protocol is TCP. No host port is set.
28 | Environment:
29 | OPENAI_API_KEY: {openai_api_key}
30 | ASSUMED_ROLE_SECRET_TOKEN: {assumed_role_secret_token}
31 | PYTHON_EXEC_URL: {python_exec_local_service_name}.{local_dns_namespace_name}:{container_port}.
32 | Log configuration: awslogs log driver, $ServerLogGroup log group name, and {aws_region} AWS region. The stream prefix is set to "ecs".
33 |
34 | @resource(aws.ecs.TaskDefinition, PythonExecTaskDefinition)
35 | Family: python-exec
36 | Cpu: 256, Memory: 512
37 | Network mode: awsvpc
38 | Requires FARGATE compatibility
39 | Execution role: $PythonExecEcsExecutionRole
40 | Container definition:
41 | Name: {python_exec_container_name}
42 | Image: $PythonExecRepository url with the "latest" tag
43 | Memory: 512
44 | Cpu: 256
45 | Essential: True
46 | Port mappings: Container port is {container_port}. Protocol is TCP. No host port is set.
47 | Log configuration: awslogs log driver, $PythonExecLogGroup log group name, and {aws_region} AWS region. The stream prefix is set to ecs.
--------------------------------------------------------------------------------
/backend/target_file_manager/target_file_manager.go:
--------------------------------------------------------------------------------
1 | package target_file_manager
2 |
3 | import (
4 | "crypto/md5"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "sync"
9 |
10 | "salami/common/types"
11 | )
12 |
13 | func VerifyChecksums(targetFileMetas []types.TargetFileMeta, targetDir string) []error {
14 | var wg sync.WaitGroup
15 | errChan := make(chan error, len(targetFileMetas))
16 |
17 | for _, meta := range targetFileMetas {
18 | meta := meta
19 | wg.Add(1)
20 | go func() {
21 | defer wg.Done()
22 | fullRelativePath := filepath.Join(targetDir, meta.FilePath)
23 | data, err := os.ReadFile(fullRelativePath)
24 | if err != nil {
25 | errChan <- err
26 | return
27 | }
28 |
29 | md5Checksum := fmt.Sprintf("%x", md5.Sum(data))
30 | if md5Checksum != meta.Checksum {
31 | errChan <- &TargetFileError{
32 | Message: fmt.Sprintf("checksum mismatch for file %s", meta.FilePath),
33 | }
34 | }
35 | }()
36 | }
37 |
38 | go func() {
39 | wg.Wait()
40 | close(errChan)
41 | }()
42 |
43 | var errs []error
44 | for err := range errChan {
45 | errs = append(errs, err)
46 | }
47 |
48 | return errs
49 | }
50 |
51 | func GenerateTargetFileMetas(targetFiles []*types.TargetFile) []types.TargetFileMeta {
52 | var wg sync.WaitGroup
53 | targetFileMetas := make([]types.TargetFileMeta, len(targetFiles))
54 |
55 | for i, targetFile := range targetFiles {
56 | i, targetFile := i, targetFile
57 | wg.Add(1)
58 | go func() {
59 | defer wg.Done()
60 | data := []byte(targetFile.Content)
61 |
62 | md5Checksum := fmt.Sprintf("%x", md5.Sum(data))
63 | targetFileMetas[i] = types.TargetFileMeta{
64 | FilePath: targetFile.FilePath,
65 | Checksum: md5Checksum,
66 | }
67 | }()
68 | }
69 |
70 | wg.Wait()
71 |
72 | return targetFileMetas
73 | }
74 |
75 | func DeleteTargetFiles(filePaths []string, targetDir string) []error {
76 | var errs []error
77 |
78 | for _, filePath := range filePaths {
79 | fullRelativePath := filepath.Join(targetDir, filePath)
80 | err := os.Remove(fullRelativePath)
81 | if err != nil {
82 | if !os.IsNotExist(err) {
83 | errs = append(errs, err)
84 | }
85 | }
86 | }
87 |
88 | return errs
89 | }
90 |
--------------------------------------------------------------------------------
/frontend/lexer/lexer.go:
--------------------------------------------------------------------------------
1 | package lexer
2 |
3 | import (
4 | "salami/frontend/types"
5 | "strings"
6 | "unicode/utf8"
7 | )
8 |
9 | type Lexer struct {
10 | filePath string
11 | source string
12 | pos int
13 | line int
14 | column int
15 | tokens []*types.Token
16 | }
17 |
18 | func (l *Lexer) Run() ([]*types.Token, error) {
19 | for {
20 | switch {
21 | case l.current() == 0:
22 | l.tokens = append(l.tokens, l.newToken(types.EOF, "", l.line, l.column, false))
23 | return l.tokens, nil
24 |
25 | case l.current() == '\n':
26 | l.tokens = append(l.tokens, l.newToken(types.Newline, "", l.line, l.column, false))
27 | l.advance()
28 |
29 | case l.current() == '@':
30 | constructorTokens, err := l.processConstructorLine()
31 | if err != nil {
32 | return nil, err
33 | }
34 | l.tokens = append(l.tokens, constructorTokens...)
35 |
36 | default:
37 | lineToken := l.processLine()
38 | l.tokens = append(l.tokens, lineToken)
39 | }
40 | }
41 | }
42 |
43 | func (l *Lexer) newToken(
44 | tokenType types.TokenType,
45 | value string,
46 | line int,
47 | column int,
48 | trimWhitespace bool,
49 | ) *types.Token {
50 | normalizedValue := value
51 | if trimWhitespace {
52 | normalizedValue = strings.TrimSpace(value)
53 | leadingSpaces := len(value) - len(strings.TrimLeft(value, " "))
54 | column += leadingSpaces
55 | }
56 | return &types.Token{Type: tokenType, Value: normalizedValue, Line: line, Column: column}
57 | }
58 |
59 | func (l *Lexer) current() rune {
60 | if l.pos >= len(l.source) {
61 | return 0
62 | }
63 | r, _ := utf8.DecodeRuneInString(l.source[l.pos:])
64 | return r
65 | }
66 |
67 | func (l *Lexer) advance() {
68 | if l.pos >= len(l.source) {
69 | return
70 | }
71 | if l.current() == '\n' {
72 | l.line++
73 | l.column = 1
74 | } else {
75 | l.column++
76 | }
77 | _, width := utf8.DecodeRuneInString(l.source[l.pos:])
78 | l.pos += width
79 | }
80 |
81 | func (l *Lexer) skipWhitespace() {
82 | for l.current() == ' ' {
83 | l.advance()
84 | }
85 | }
86 |
87 | func NewLexer(filePath string, source string) *Lexer {
88 | return &Lexer{
89 | filePath: filePath,
90 | source: source,
91 | pos: 0,
92 | line: 1,
93 | column: 1,
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/salami/vpc_endpoints.sami:
--------------------------------------------------------------------------------
1 | @resource(aws.ec2.VpcEndpoint, EcrDkrVpcEndpoint)
2 | In $MainVpc, $PrivateSubnetA and $PrivateSubnetB
3 | VPC endpoint type: Interface
4 | Service name: com.amazonaws.{aws_region}.ecr.dkr
5 | Private DNS enabled: True
6 | Security group: $EcrVpcEndpointSG
7 | Policy allows two AWS principals - $ServerEcsExecutionRole and $PythonExecEcsExecutionRole - to perform three actions on all ECR repositories:
8 | 1. "ecr:BatchCheckLayerAvailability"
9 | 2. "ecr:GetDownloadUrlForLayer"
10 | 3. "ecr:BatchGetImage"
11 |
12 | @resource(aws.ec2.VpcEndpoint, EcrApiVpcEndpoint)
13 | In $MainVpc, $PrivateSubnetA and $PrivateSubnetB
14 | VPC endpoint type: Interface
15 | Service name: com.amazonaws.{aws_region}.ecr.api
16 | Private DNS enabled: True
17 | Security group: $EcrVpcEndpointSG
18 | Policy that allows two AWS principals - $ServerEcsExecutionRole and $PythonExecEcsExecutionRole - to perform four actions on all ECR resources:
19 | 1. "ecr:GetAuthorizationToken"
20 | 2. "ecr:BatchCheckLayerAvailability"
21 | 3. "ecr:GetDownloadUrlForLayer"
22 | 4. "ecr:BatchGetImage"
23 |
24 | @resource(aws.ec2.VpcEndpoint, S3VpcEndpoint)
25 | In $MainVpc, associated with $PrivateRouteTable
26 | VPC endpoint type: Gateway
27 | Service name: com.amazonaws.{aws_region}.s3
28 | Policy allows all principals to perform all actions on resources defined by the ARN pattern "arn:aws:s3:::prod-{aws_region}-starport-layer-bucket/*"
29 |
30 | @resource(aws.ec2.SecurityGroup, CloudWatchLogsVpcEndpointSG)
31 | In $MainVpc
32 | name: cloudwatch-logs-vpc-endpoint-sg
33 | description: Security Group for CloudWatch Logs VPC Endpoint
34 | egress: []
35 | ingress: Allow all TCP traffic on port 443 from any IP address (0.0.0.0/0)
36 |
37 | @resource(aws.ec2.VpcEndpoint, CloudWatchLogsVpcEndpoint)
38 | In $MainVpc, $PrivateSubnetA and $PrivateSubnetB
39 | VPC endpoint type: Interface
40 | Service name: com.amazonaws.{aws_region}.logs
41 | Private DNS enabled: True
42 | Security group: $CloudWatchLogsVpcEndpointSG
43 | Policy allows the root user of the AWS account to perform four actions on all CloudWatch Logs resources:
44 | 1. "logs:CreateLogGroup"
45 | 2. "logs:CreateLogStream"
46 | 3. "logs:PutLogEvents"
47 | 4. "logs:DescribeLogStreams"
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/salami/ecs.sami:
--------------------------------------------------------------------------------
1 | @resource(aws.ecs.Cluster, EcsCluster)
2 | Name: cluster
3 |
4 | @resource(aws.servicediscovery.PrivateDnsNamespace, EcsPrivateDnsNamespace)
5 | Vpc: $MainVpc
6 | Description: Private namespace for ECS cluster
7 | Name: {local_dns_namespace_name}
8 |
9 | @resource(aws.ecs.Service, ServerEcsService)
10 | In $EcsCluster, has $ServerTaskDefinition
11 | Resource type: aws.ecs.Service
12 | Logical name: ServerEcsService
13 | Name: server
14 | Desired count: 1
15 | Launch type: FARGATE
16 | ---
17 | Network configuration:
18 | Assigned public IP
19 | Subnets: $PublicSubnetA and $PublicSubnetB
20 | Security group: $ServerEcsSecurityGroup
21 | Load balancers:
22 | Target group: $ServerTargetGroup
23 | Container name: {server_container_name}
24 | Port: {container_port}
25 | Deployment:
26 | ECS type deployment controller
27 | Deployment circuit breaker: enabled with rollback
28 | Wait for steady state: True
29 |
30 | @resource(aws.servicediscovery.Service, PythonExecEcsServiceDiscovery)
31 | Using $EcsPrivateDnsNamespace
32 | Name: {python_exec_local_service_name}
33 | Dns config: Record of type A with ttl set to 10
34 |
35 | @resource(aws.ec2.SecurityGroup, ServerEcsSecurityGroup)
36 | In $MainVpc
37 | Name: server-ecs-security-group
38 | Description: Security group for Server ECS service
39 | Egress: all traffic allowed
40 | Ingress: Allow access on "tcp" protocol and {container_port} port, and limited to $AlbSecurityGroup security group
41 |
42 | @resource(aws.ecs.Service, PythonExecEcsService)
43 | In $EcsCluster, using $PythonExecTaskDefinition
44 | Name: python-exec
45 | Desired_count: 1
46 | Launch type: FARGATE
47 | Network configuration:
48 | - Do not assign public IP
49 | - The subnets are $PrivateSubnetA and $PrivateSubnetB. Security group is $PythonExecEcsSecurityGroup
50 | Has ECS type deployment controller
51 | Enabled deployment circuit breaker with rollback
52 | Wait for steady state: True
53 |
54 | @resource(aws.ec2.SecurityGroup, PythonExecEcsSecurityGroup)
55 | In $MainVpc
56 | Name: python-exec-ecs-security-group
57 | Description: Security group for python exec ECS service
58 | Egress: allow all tcp traffic on port 443
59 | Ingress: allow access on "tcp" protocol, {container_port} port and limited to $ServerEcsSecurityGroup security group.
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/salami/ecr.sami:
--------------------------------------------------------------------------------
1 | @resource(aws.ecr.Repository, ServerRepository)
2 | Name: server
3 | Has mutable image tags
4 |
5 | @resource(aws.ecr.LifecyclePolicy, ServerRepoLifecyclePolicy)
6 | For $ServerRepository
7 | Policy retains only the last 10 untagged images in the repository. Images beyond this count will expire.
8 |
9 | @resource(aws.iam.Role, ServerEcsExecutionRole)
10 | Name: server-ecs-execution-role
11 | Assume role policy allows the "ecs-tasks.amazonaws.com" service to assume the role
12 |
13 | @resource(aws.ecr.RepositoryPolicy, ServerRepositoryPolicy)
14 | For $ServerRepository
15 | Policy allows $ServerEcsExecutionRole to perform three actions:
16 | 1. "ecr:GetDownloadUrlForLayer"
17 | 2. "ecr:BatchGetImage"
18 | 3. "ecr:BatchCheckLayerAvailability"
19 |
20 | @resource(aws.iam.RolePolicyAttachment, ServerRepositoryPolicyAttachment1)
21 | Attached to $ServerRepositoryPolicy
22 | Policy: arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
23 |
24 | @resource(aws.iam.RolePolicyAttachment, ServerRepositoryPolicyAttachment2)
25 | Attached to $ServerRepositoryPolicy
26 | Policy: arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
27 |
28 | @resource(aws.ecr.Repository, PythonExecRepository)
29 | Name: python-exec
30 | Has mutable image tags
31 |
32 | @resource(aws.ecr.LifecyclePolicy, PythonExecRepoLifecyclePolicy)
33 | For $PythonExecRepository
34 | Policy retains only the last 10 untagged images in the repository. Images beyond this count will expire.
35 |
36 | @resource(aws.iam.Role, PythonExecEcsExecutionRole)
37 | Name: python-exec-ecs-execution-role
38 | Assume role policy allows the "ecs-tasks.amazonaws.com" service to assume the role
39 |
40 | @resource(aws.ecr.RepositoryPolicy, PythonExecRepositoryPolicy)
41 | For $PythonExecRepository
42 | Policy allows $PythonExecEcsExecutionRole to perform three actions:
43 | 1. "ecr:GetDownloadUrlForLayer"
44 | 2. "ecr:BatchGetImage"
45 | 3. "ecr:BatchCheckLayerAvailability"
46 |
47 | @resource(aws.iam.RolePolicyAttachment, PythonExecEcsExecutionRolePolicyAttachment1)
48 | Attached to $PythonExecEcsExecutionRole
49 | Policy: arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
50 |
51 | @resource(aws.iam.RolePolicyAttachment, PythonExecEcsExecutionRolePolicyAttachment2)
52 | Attached to $PythonExecEcsExecutionRole
53 | Policy: arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
--------------------------------------------------------------------------------
/backend/target_file_manager/target_file_manager_test.go:
--------------------------------------------------------------------------------
1 | package target_file_manager_test
2 |
3 | import (
4 | "salami/backend/target_file_manager"
5 | "salami/common/types"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestVerifyChecksums(t *testing.T) {
12 | testCases := []struct {
13 | name string
14 | filesAndChecksums []types.TargetFileMeta
15 | wantErr bool
16 | expectedErrorMessage string
17 | }{
18 | {
19 | "When checksums match",
20 | []types.TargetFileMeta{
21 | {FilePath: "target_file_1.tf", Checksum: "415d4b5a48f2a887fffc285382fc4db1"},
22 | {FilePath: "target_file_2.tf", Checksum: "6700c5970a3183c2ecdc06900f7b30d4"}},
23 | false,
24 | "",
25 | },
26 | {
27 | "When one of the checksums doesn't match",
28 | []types.TargetFileMeta{
29 | {FilePath: "target_file_1.tf", Checksum: "415d4b5a48f2a887fffc285382fc4db1"},
30 | {FilePath: "target_file_2.tf", Checksum: "invalid_checksum"}},
31 | true,
32 | "target file error: checksum mismatch for file target_file_2.tf",
33 | },
34 | }
35 | for _, tc := range testCases {
36 | t.Run(tc.name, func(t *testing.T) {
37 | err := target_file_manager.VerifyChecksums(tc.filesAndChecksums, "testdata")
38 | require.Equal(t, err != nil, tc.wantErr, "unexpected error status: got error = %v, wantErr %v", err, tc.wantErr)
39 | if err != nil {
40 | require.Equal(
41 | t,
42 | err[0].Error(),
43 | tc.expectedErrorMessage,
44 | "unexpected error message: got = %v, want = %v",
45 | err[0].Error(),
46 | tc.expectedErrorMessage,
47 | )
48 | }
49 | })
50 | }
51 | }
52 |
53 | func TestGenerateTargetFileMetas(t *testing.T) {
54 | targetFiles := []*types.TargetFile{
55 | {FilePath: "testdata/target_file_1.tf", Content: "some content123"},
56 | {FilePath: "testdata/target_file_2.tf", Content: "another content456"},
57 | }
58 | expectedFileMetas := []types.TargetFileMeta{
59 | {FilePath: "testdata/target_file_1.tf", Checksum: "7fb90ebc6a8f51aedc1568b9f709ddf0"},
60 | {FilePath: "testdata/target_file_2.tf", Checksum: "d673f200b33c4c5f92bd7d1a1ca3b27f"},
61 | }
62 |
63 | t.Run("should compute checksums and return TargetFileMetas", func(t *testing.T) {
64 | fileMetas := target_file_manager.GenerateTargetFileMetas(targetFiles)
65 | require.Equal(t, expectedFileMetas, fileMetas, "unexpected file metas: got = %v, want = %v", fileMetas, expectedFileMetas)
66 | })
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/lexer/process_line.go:
--------------------------------------------------------------------------------
1 | package lexer
2 |
3 | import (
4 | "salami/common/errors"
5 | "salami/frontend/types"
6 | "unicode"
7 | )
8 |
9 | func (l *Lexer) processConstructorLine() ([]*types.Token, error) {
10 | constructorNameToken := l.getConstructorNameToken()
11 | constructorArgTokens := []*types.Token{}
12 | var err error
13 | if l.current() == '(' {
14 | l.advance()
15 | constructorArgTokens, err = l.getConstructorArgTokens()
16 | if err != nil {
17 | return nil, err
18 | }
19 | }
20 | l.skipWhitespace()
21 | if l.current() != '\n' && l.current() != 0 {
22 | return nil, &errors.LexerError{
23 | FilePath: l.filePath,
24 | Line: l.line,
25 | Column: l.column,
26 | Message: "constructor line must end in a newline or EOF",
27 | }
28 | }
29 |
30 | return append(
31 | []*types.Token{constructorNameToken},
32 | constructorArgTokens...,
33 | ), nil
34 | }
35 |
36 | func (l *Lexer) getConstructorNameToken() *types.Token {
37 | startPosition := l.pos
38 | startLine := l.line
39 | startColumn := l.column
40 | l.advance()
41 | for unicode.IsLetter(l.current()) {
42 | l.advance()
43 | }
44 | value := l.source[startPosition:l.pos]
45 | return l.newToken(types.ConstructorName, value, startLine, startColumn, false)
46 | }
47 |
48 | func (l *Lexer) getConstructorArgTokens() ([]*types.Token, error) {
49 | var tokens []*types.Token
50 | l.skipWhitespace()
51 |
52 | for {
53 | startPosition := l.pos
54 | startLine := l.line
55 | startColumn := l.column
56 | for l.current() != ',' && l.current() != ')' && l.current() != '\n' && l.current() != 0 {
57 | l.advance()
58 | }
59 | if l.current() != ',' && l.current() != ')' {
60 | return nil, &errors.LexerError{
61 | FilePath: l.filePath,
62 | Line: l.line,
63 | Column: l.column,
64 | Message: "constructor arguments must be followed by a comma or a closing parenthesis",
65 | }
66 | }
67 | value := l.source[startPosition:l.pos]
68 | tokens = append(tokens, l.newToken(types.ConstructorArg, value, startLine, startColumn, true))
69 |
70 | if l.current() == ')' {
71 | l.advance()
72 | break
73 | } else {
74 | l.advance()
75 | l.skipWhitespace()
76 | }
77 | }
78 | return tokens, nil
79 | }
80 |
81 | func (l *Lexer) processLine() *types.Token {
82 | startPosition := l.pos
83 | startColumn := l.column
84 | for l.current() != '\n' && l.current() != 0 {
85 | l.advance()
86 | }
87 | lineText := l.source[startPosition:l.pos]
88 | return l.newToken(types.NaturalLanguage, lineText, l.line, startColumn, false)
89 | }
90 |
--------------------------------------------------------------------------------
/common/change_set/testdata/new_objects.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ParsedResource": {
4 | "ResourceType": "aws.s3.Bucket",
5 | "LogicalName": "AssumedRolesBucket",
6 | "NaturalLanguage": "Bucket: assumed-roles\nVersioning enabled: True",
7 | "Uses": [],
8 | "Exports": {
9 | "name": "assumed-roles-bucket-name"
10 | },
11 | "ReferencedVariables": [],
12 | "SourceFilePath": "path/to/source_file",
13 | "SourceFileLine": 5
14 | },
15 | "TargetCode": "resource \"aws_s3_bucket\" \"AssumedRolesBucket\" {\n bucket = \"assumed-roles\"\n versioning {\n enabled = true\n }\n}"
16 | },
17 | {
18 | "ParsedResource": {
19 | "ResourceType": "aws.s3.BucketPolicy",
20 | "LogicalName": "AssumedRolesBucketPolicy",
21 | "NaturalLanguage": "Policy: A JSON policy that denies everyone access",
22 | "Uses": ["AssumedRolesBucket"],
23 | "Exports": {},
24 | "ReferencedVariables": [],
25 | "SourceFilePath": "path/to/source_file",
26 | "SourceFileLine": 0
27 | },
28 | "TargetCode": "resource \"aws_s3_bucket_policy\" \"AssumedRolesBucketPolicy\" {\n bucket = aws_s3_bucket.AssumedRolesBucket.id\n\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [\n {\n Action = \"s3:GetObject\"\n Effect = \"Allow\"\n Resource = \"${aws_s3_bucket.AssumedRolesBucket.arn}/*\"\n Principal = \"*\"\n }\n ]\n })\n}"
29 | },
30 | {
31 | "ParsedResource": {
32 | "ResourceType": "aws.logs.LogGroup",
33 | "LogicalName": "LogsGroup",
34 | "NaturalLanguage": "Log group: /ecs/my-app\nRetention: 30 days",
35 | "Uses": [],
36 | "Exports": {},
37 | "ReferencedVariables": [],
38 | "SourceFilePath": "path/to/source_file",
39 | "SourceFileLine": 1
40 | }
41 | },
42 | {
43 | "ParsedVariable": {
44 | "Name": "server_container_name",
45 | "Description": "Server container name",
46 | "Type": "string",
47 | "Default": "server-container",
48 | "SourceFilePath": "path/to/source_file",
49 | "SourceFileLine": 0
50 | },
51 | "TargetCode": "variable \"server_container_name\" {\n description = \"Server container name\"\n type = string\n default = \"server-container\"\n}"
52 | },
53 | {
54 | "ParsedVariable": {
55 | "Name": "container port",
56 | "Description": "Container port",
57 | "Type": "string",
58 | "Default": "8080",
59 | "SourceFilePath": "path/to/source_file",
60 | "SourceFileLine": 15
61 | }
62 | }
63 | ]
--------------------------------------------------------------------------------
/backend/target/terraform/generate_code.go:
--------------------------------------------------------------------------------
1 | package terraform
2 |
3 | import (
4 | "fmt"
5 | "salami/backend/prompts/terraform/openai_gpt4"
6 | backendTypes "salami/backend/types"
7 | "salami/common/change_set"
8 | "salami/common/logger"
9 | "salami/common/symbol_table"
10 | commonTypes "salami/common/types"
11 |
12 | "golang.org/x/sync/errgroup"
13 | )
14 |
15 | type Terraform struct{}
16 |
17 | func NewTarget() backendTypes.Target {
18 | return &Terraform{}
19 | }
20 |
21 | func (t *Terraform) GenerateCode(
22 | symbolTable *symbol_table.SymbolTable,
23 | changeSetRepository *change_set.ChangeSetRepository,
24 | llm backendTypes.Llm,
25 | ) []error {
26 | var g errgroup.Group
27 | semaphoreChannel := make(chan struct{}, llm.GetMaxConcurrentExecutions())
28 |
29 | for _, diff := range changeSetRepository.Diffs {
30 | if !(diff.IsUpdate() || diff.IsAdd()) {
31 | continue
32 | }
33 | diff := diff
34 | g.Go(func() error {
35 | semaphoreChannel <- struct{}{}
36 | defer func() { <-semaphoreChannel }()
37 |
38 | logDiffProgress(diff)
39 |
40 | messages, err := getGenerateCodeLlmMessages(symbolTable, diff, llm)
41 | if err != nil {
42 | return err
43 | }
44 | completion, err := llm.CreateCompletion(messages)
45 | if err != nil {
46 | return err
47 | }
48 | diff.NewObject.SetTargetCode(completion)
49 | return nil
50 | })
51 | }
52 |
53 | if err := g.Wait(); err != nil {
54 | return []error{err}
55 | }
56 |
57 | return nil
58 | }
59 |
60 | func getGenerateCodeLlmMessages(
61 | symbolTable *symbol_table.SymbolTable,
62 | diff *commonTypes.ChangeSetDiff,
63 | llm backendTypes.Llm,
64 | ) ([]interface{}, error) {
65 | var messages []interface{}
66 |
67 | switch llm.GetSlug() {
68 | case commonTypes.LlmOpenaiGpt4:
69 | llmMessages, err := openai_gpt4.GetGenerateCodeMessages(symbolTable, diff)
70 | if err != nil {
71 | return nil, err
72 | }
73 |
74 | for _, v := range llmMessages {
75 | messages = append(messages, v)
76 | }
77 | }
78 | return messages, nil
79 | }
80 |
81 | func logDiffProgress(diff *commonTypes.ChangeSetDiff) {
82 | var objectType string
83 | var objectId string
84 |
85 | if diff.NewObject.IsResource() {
86 | objectType = "resource"
87 | objectId = string(diff.NewObject.ParsedResource.LogicalName)
88 | } else if diff.NewObject.IsVariable() {
89 | objectType = "variable"
90 | objectId = diff.NewObject.ParsedVariable.Name
91 | }
92 |
93 | message := fmt.Sprintf("🖋 Generating code for %s '%s' (diff type: %s)...", objectType, objectId, diff.DiffType)
94 | logger.Verbose(message)
95 | }
96 |
--------------------------------------------------------------------------------
/common/change_set/testdata/previous_objects.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ParsedResource": {
4 | "ResourceType": "aws.s3.Bucket",
5 | "LogicalName": "AssumedRolesBucket",
6 | "NaturalLanguage": "Bucket: assumed-roles\nVersioning enabled: True",
7 | "Uses": [],
8 | "Exports": {
9 | "name": "assumed-roles-bucket-name"
10 | },
11 | "ReferencedVariables": [],
12 | "SourceFilePath": "path/to/source_file",
13 | "SourceFileLine": 0
14 | },
15 | "TargetCode": "resource \"aws_s3_bucket\" \"AssumedRolesBucket\" {\n bucket = \"assumed-roles\"\n versioning {\n enabled = true\n }\n}"
16 | },
17 | {
18 | "ParsedResource": {
19 | "ResourceType": "aws.s3.BucketPublicAccessBlock",
20 | "LogicalName": "AssetsPublicAccessBlock",
21 | "NaturalLanguage": "Block public ACLs: True\nBlock public policy: False\nIgnore public ACLs: True\nRestrict public buckets: False",
22 | "Uses": ["AssumedRolesBucket"],
23 | "Exports": {},
24 | "ReferencedVariables": [],
25 | "SourceFilePath": "path/to/source_file",
26 | "SourceFileLine": 0
27 | },
28 | "TargetCode": "resource \"aws_s3_bucket_public_access_block\" \"AssetsPublicAccessBlock\" {\n bucket = aws_s3_bucket.AssumedRolesBucket.id\n\n block_public_acls = true\n block_public_policy = false\n ignore_public_acls = true\n restrict_public_buckets = false\n}"
29 | },
30 | {
31 | "ParsedResource": {
32 | "ResourceType": "aws.s3.BucketPolicy",
33 | "LogicalName": "AssumedRolesBucketPolicy",
34 | "NaturalLanguage": "Policy: A JSON policy that allows all principals to perform the \"s3:GetObject\" action on all objects in the specified S3 bucket.",
35 | "Uses": ["AssumedRolesBucket"],
36 | "Exports": {},
37 | "ReferencedVariables": [],
38 | "SourceFilePath": "path/to/source_file",
39 | "SourceFileLine": 0
40 | },
41 | "TargetCode": "resource \"aws_s3_bucket_policy\" \"AssumedRolesBucketPolicy\" {\n bucket = aws_s3_bucket.AssumedRolesBucket.id\n\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [\n {\n Action = \"s3:GetObject\"\n Effect = \"Allow\"\n Resource = \"${aws_s3_bucket.AssumedRolesBucket.arn}/*\"\n Principal = \"*\"\n }\n ]\n })\n}"
42 | },
43 | {
44 | "ParsedVariable": {
45 | "Name": "server_container_name",
46 | "Description": "Server container name",
47 | "Type": "string",
48 | "Default": "server-container",
49 | "SourceFilePath": "path/to/source_file",
50 | "SourceFileLine": 0
51 | },
52 | "TargetCode": "variable \"server_container_name\" {\n description = \"Server container name\"\n type = string\n default = \"server-container\"\n}"
53 | }
54 | ]
--------------------------------------------------------------------------------
/common/lock_file_manager/lock_file_manager.go:
--------------------------------------------------------------------------------
1 | package lock_file_manager
2 |
3 | import (
4 | "bytes"
5 | "log"
6 | "salami/common/constants"
7 | "salami/common/types"
8 | "salami/common/utils/file_utils"
9 |
10 | "github.com/BurntSushi/toml"
11 | )
12 |
13 | var lockFilePath = "salami-lock.toml"
14 | var loadedLockFile *LockFile
15 |
16 | func SetLockFilePath(path string) {
17 | lockFilePath = path
18 | loadedLockFile = nil
19 | }
20 |
21 | func GetTargetFileMetas() []types.TargetFileMeta {
22 | targetFileMetas := getLockFile().TargetFileMetas
23 | result := make([]types.TargetFileMeta, len(targetFileMetas))
24 | for i := range targetFileMetas {
25 | result[i] = types.TargetFileMeta{
26 | FilePath: targetFileMetas[i].FilePath,
27 | Checksum: targetFileMetas[i].Checksum,
28 | }
29 | }
30 | return result
31 | }
32 |
33 | func GetObjects() []*types.Object {
34 | lockFileObjects := getLockFile().Objects
35 | result := make([]*types.Object, len(lockFileObjects))
36 | for i := range lockFileObjects {
37 | currentObject := lockFileObjects[i]
38 | var parsedResource *types.ParsedResource
39 | var parsedVariable *types.ParsedVariable
40 | if currentObject.IsResource() {
41 | parsedResource = lockFileToCommonResource(currentObject)
42 | } else if currentObject.IsVariable() {
43 | parsedVariable = lockFileToCommonVariable(currentObject)
44 | }
45 |
46 | result[i] = &types.Object{
47 | ParsedResource: parsedResource,
48 | ParsedVariable: parsedVariable,
49 | TargetCode: currentObject.TargetCode,
50 | }
51 | }
52 | return result
53 | }
54 |
55 | func UpdateLockFile(targetFileMetas []types.TargetFileMeta, objects []*types.Object) error {
56 | lockFile := getLockFile()
57 | lockFile.Version = constants.SalamiVersion
58 | lockFile.TargetFileMetas = make([]TargetFileMeta, len(targetFileMetas))
59 | lockFile.Objects = make([]Object, len(objects))
60 | for i := range targetFileMetas {
61 | lockFile.TargetFileMetas[i] = TargetFileMeta{
62 | FilePath: targetFileMetas[i].FilePath,
63 | Checksum: targetFileMetas[i].Checksum,
64 | }
65 | }
66 | for i := range objects {
67 | currentObject := objects[i]
68 |
69 | lockFile.Objects[i] = Object{
70 | ParsedResource: commonToLockFileResource(currentObject.ParsedResource),
71 | ParsedVariable: commonToLockFileVariable(currentObject.ParsedVariable),
72 | TargetCode: currentObject.TargetCode,
73 | }
74 | }
75 |
76 | buf := new(bytes.Buffer)
77 | encoder := toml.NewEncoder(buf)
78 | if err := encoder.Encode(lockFile); err != nil {
79 | return err
80 | }
81 | tomlString := buf.String()
82 |
83 | if err := file_utils.WriteFileIfChanged(lockFilePath, tomlString); err != nil {
84 | return err
85 | }
86 | return nil
87 | }
88 |
89 | func getLockFile() *LockFile {
90 | if loadedLockFile == nil {
91 | log.Fatal("Lock file not loaded")
92 | }
93 | return loadedLockFile
94 | }
95 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_target_code.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 |
14 | [objects.parsed_resource]
15 | resource_type = "aws.s3.Bucket"
16 | logical_name = "AssumedRolesBucket"
17 | natural_language = """\
18 | Bucket: assumed-roles
19 | Versioning enabled: True"""
20 | referenced_resources = []
21 | referenced_variables = []
22 | source_file_path = "path/to/source_file"
23 | source_file_line = 1
24 |
25 | [[objects]]
26 | target_code = """\
27 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
28 | bucket = aws_s3_bucket.AssumedRolesBucket.id
29 |
30 | block_public_acls = true
31 | block_public_policy = false
32 | ignore_public_acls = true
33 | restrict_public_buckets = false
34 | }"""
35 |
36 | [objects.parsed_resource]
37 | resource_type = "aws.s3.BucketPublicAccessBlock"
38 | logical_name = "AssetsPublicAccessBlock"
39 | natural_language = """\
40 | Block public ACLs: True
41 | Block public policy: False
42 | Ignore public ACLs: True
43 | Restrict public buckets: False"""
44 | referenced_resources = ["AssumedRolesBucket"]
45 | referenced_variables = []
46 | source_file_path = "path/to/source_file"
47 | source_file_line = 8
48 |
49 | [[objects]]
50 | target_code = """\
51 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
52 | bucket = aws_s3_bucket.AssumedRolesBucket.id
53 |
54 | policy = jsonencode({
55 | Version = "2012-10-17"
56 | Statement = [
57 | {
58 | Action = "s3:GetObject"
59 | Effect = "Allow"
60 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
61 | Principal = "*"
62 | }
63 | ]
64 | })
65 | }"""
66 |
67 | [objects.parsed_resource]
68 | resource_type = "aws.s3.BucketPolicy"
69 | logical_name = "AssumedRolesBucketPolicy"
70 | natural_language = """\
71 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
72 | referenced_resources = ["AssumedRolesBucket"]
73 | referenced_variables = []
74 | source_file_path = "path/to/source_file"
75 | source_file_line = 17
76 |
77 | [[objects]]
78 | target_code = """\
79 | variable "server_container_name" {
80 | description = "Server container name"
81 | type = string
82 | default = "server-container"
83 | }"""
84 |
85 | [objects.parsed_variable]
86 | name = "server_container_name"
87 | natural_language = "Description: Server container name"
88 | type = "string"
89 | default = "server-container"
90 | source_file_path = "path/to/source_file"
91 | source_file_line = 24
92 |
--------------------------------------------------------------------------------
/frontend/parser/parse_constructor.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | commonTypes "salami/common/types"
5 | frontendTypes "salami/frontend/types"
6 | )
7 |
8 | func (p *Parser) parseConstructor() error {
9 | constructorNameToken := p.currentToken()
10 | var constructorArgTokens []*frontendTypes.Token
11 | p.advance()
12 | for p.currentToken().Type != frontendTypes.Newline && p.currentToken().Type != frontendTypes.EOF {
13 | if p.currentToken().Type == frontendTypes.ConstructorArg {
14 | constructorArgTokens = append(constructorArgTokens, p.currentToken())
15 | } else {
16 | return p.parseError(p.currentToken())
17 | }
18 | p.advance()
19 | }
20 |
21 | switch constructorNameToken.Value {
22 | case "@resource":
23 | err := p.parseResourceConstructor(constructorNameToken, constructorArgTokens)
24 | if err != nil {
25 | return err
26 | }
27 | case "@variable":
28 | err := p.parseVariableConstructor(constructorNameToken, constructorArgTokens)
29 | if err != nil {
30 | return err
31 | }
32 | default:
33 | return p.parseError(constructorNameToken)
34 | }
35 | if p.currentToken().Type != frontendTypes.Newline && p.currentToken().Type != frontendTypes.EOF {
36 | return p.parseError(p.currentToken())
37 | }
38 | return nil
39 | }
40 |
41 | func (p *Parser) parseResourceConstructor(
42 | constructorNameToken *frontendTypes.Token,
43 | constructorArgTokens []*frontendTypes.Token,
44 | ) error {
45 | if !p.currentObjectTypeIs(Unset) {
46 | return p.parseError(
47 | constructorNameToken,
48 | "@resource constructor must be the first line of a resource block",
49 | )
50 | }
51 | p.setCurrentObjectType(Resource)
52 | if len(constructorArgTokens) != 2 {
53 | return p.parseError(constructorNameToken, "exactly two arguments are expected for @resource constructor")
54 | }
55 | p.currentResource().ResourceType = commonTypes.ResourceType(constructorArgTokens[0].Value)
56 | p.currentResource().LogicalName = commonTypes.LogicalName(constructorArgTokens[1].Value)
57 | return nil
58 | }
59 |
60 | func (p *Parser) parseVariableConstructor(
61 | constructorNameToken *frontendTypes.Token,
62 | constructorArgTokens []*frontendTypes.Token,
63 | ) error {
64 | if !p.currentObjectTypeIs(Unset) {
65 | return p.parseError(
66 | constructorNameToken,
67 | "@variable constructor must be the first line of a variable block",
68 | )
69 | }
70 | p.setCurrentObjectType(Variable)
71 |
72 | if len(constructorArgTokens) > 3 || len(constructorArgTokens) < 2 {
73 | return p.parseError(constructorNameToken, "exactly two or three arguments are expected for @variable constructor")
74 | }
75 | p.currentVariable().Name = constructorArgTokens[0].Value
76 | p.currentVariable().Type = commonTypes.VariableType(constructorArgTokens[1].Value)
77 | if len(constructorArgTokens) == 3 {
78 | p.currentVariable().Default = constructorArgTokens[2].Value
79 | }
80 | return nil
81 | }
82 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/terraform/vpc.tf:
--------------------------------------------------------------------------------
1 | resource "aws_vpc" "MainVpc" {
2 | cidr_block = "10.0.0.0/16"
3 | enable_dns_support = true
4 | enable_dns_hostnames = true
5 |
6 | tags = {
7 | Name = "main-vpc"
8 | }
9 | }
10 |
11 | resource "aws_subnet" "PrivateSubnetA" {
12 | vpc_id = aws_vpc.MainVpc.id
13 | cidr_block = "10.0.1.0/24"
14 | availability_zone = "${var.aws_region}a"
15 | map_public_ip_on_launch = false
16 | tags = {
17 | Name = "private-subnet-a"
18 | }
19 | }
20 |
21 | resource "aws_subnet" "PrivateSubnetB" {
22 | vpc_id = aws_vpc.MainVpc.id
23 | cidr_block = "10.0.2.0/24"
24 | availability_zone = "${var.aws_region}c"
25 | map_public_ip_on_launch = false
26 | tags = {
27 | Name = "private-subnet-b"
28 | }
29 | }
30 |
31 | resource "aws_route_table" "PrivateRouteTable" {
32 | vpc_id = aws_vpc.MainVpc.id
33 |
34 | tags = {
35 | Name = "private-route-table"
36 | }
37 | }
38 |
39 | resource "aws_route_table_association" "PrivateSubnetARouteTableAssociation" {
40 | subnet_id = aws_subnet.PrivateSubnetA.id
41 | route_table_id = aws_route_table.PrivateRouteTable.id
42 | }
43 |
44 | resource "aws_route_table_association" "PrivateSubnetBRouteTableAssociation" {
45 | subnet_id = aws_subnet.PrivateSubnetB.id
46 | route_table_id = aws_route_table.PrivateRouteTable.id
47 | }
48 |
49 | resource "aws_subnet" "PublicSubnetA" {
50 | vpc_id = aws_vpc.MainVpc.id
51 | cidr_block = "10.0.3.0/24"
52 | availability_zone = "${var.aws_region}a"
53 | map_public_ip_on_launch = true
54 | tags = {
55 | Name = "public-subnet-a"
56 | }
57 | }
58 |
59 | resource "aws_subnet" "PublicSubnetB" {
60 | vpc_id = aws_vpc.MainVpc.id
61 | cidr_block = "10.0.4.0/24"
62 | availability_zone = "${var.aws_region}c"
63 | map_public_ip_on_launch = true
64 | tags = {
65 | Name = "public-subnet-b"
66 | }
67 | }
68 |
69 | resource "aws_internet_gateway" "InternetGateway" {
70 | vpc_id = aws_vpc.MainVpc.id
71 |
72 | tags = {
73 | Name = "internet-gateway"
74 | }
75 | }
76 |
77 | resource "aws_route_table" "PublicRouteTable" {
78 | vpc_id = aws_vpc.MainVpc.id
79 |
80 | route {
81 | cidr_block = "0.0.0.0/0"
82 | gateway_id = aws_internet_gateway.InternetGateway.id
83 | }
84 |
85 | tags = {
86 | Name = "public-route-table"
87 | }
88 | }
89 |
90 | resource "aws_route_table_association" "PublicSubnetARouteTableAssociation" {
91 | subnet_id = aws_subnet.PublicSubnetA.id
92 | route_table_id = aws_route_table.PublicRouteTable.id
93 | }
94 |
95 | resource "aws_route_table_association" "PublicSubnetBRouteTableAssociation" {
96 | subnet_id = aws_subnet.PublicSubnetB.id
97 | route_table_id = aws_route_table.PublicRouteTable.id
98 | }
--------------------------------------------------------------------------------
/common/lock_file_manager/type_mapper.go:
--------------------------------------------------------------------------------
1 | package lock_file_manager
2 |
3 | import "salami/common/types"
4 |
5 | func lockFileToCommonResource(lockFileObject Object) *types.ParsedResource {
6 | referencedResources := make([]types.LogicalName, len(lockFileObject.ParsedResource.ReferencedResources))
7 | for j, use := range lockFileObject.ParsedResource.ReferencedResources {
8 | referencedResources[j] = types.LogicalName(use)
9 | }
10 | return &types.ParsedResource{
11 | ResourceType: types.ResourceType(lockFileObject.ParsedResource.ResourceType),
12 | LogicalName: types.LogicalName(lockFileObject.ParsedResource.LogicalName),
13 | NaturalLanguage: lockFileObject.ParsedResource.NaturalLanguage,
14 | ReferencedResources: referencedResources,
15 | ReferencedVariables: lockFileObject.ParsedResource.ReferencedVariables,
16 | SourceFilePath: lockFileObject.ParsedResource.SourceFilePath,
17 | SourceFileLine: lockFileObject.ParsedResource.SourceFileLine,
18 | }
19 | }
20 |
21 | func lockFileToCommonVariable(lockFileObject Object) *types.ParsedVariable {
22 | return &types.ParsedVariable{
23 | Name: lockFileObject.ParsedVariable.Name,
24 | NaturalLanguage: lockFileObject.ParsedVariable.NaturalLanguage,
25 | Default: lockFileObject.ParsedVariable.Default,
26 | Type: types.VariableType(lockFileObject.ParsedVariable.VariableType),
27 | SourceFilePath: lockFileObject.ParsedVariable.SourceFilePath,
28 | SourceFileLine: lockFileObject.ParsedVariable.SourceFileLine,
29 | }
30 | }
31 |
32 | func commonToLockFileResource(commonResource *types.ParsedResource) *ParsedResource {
33 | if commonResource == nil {
34 | return nil
35 | }
36 | parsedResource := &ParsedResource{
37 | ResourceType: string(commonResource.ResourceType),
38 | LogicalName: string(commonResource.LogicalName),
39 | NaturalLanguage: commonResource.NaturalLanguage,
40 | ReferencedResources: make([]string, len(commonResource.ReferencedResources)),
41 | ReferencedVariables: commonResource.ReferencedVariables,
42 | SourceFilePath: commonResource.SourceFilePath,
43 | SourceFileLine: commonResource.SourceFileLine,
44 | }
45 | for j, referencedResource := range commonResource.ReferencedResources {
46 | parsedResource.ReferencedResources[j] = string(referencedResource)
47 | }
48 |
49 | return parsedResource
50 | }
51 |
52 | func commonToLockFileVariable(commonVariable *types.ParsedVariable) *ParsedVariable {
53 | if commonVariable == nil {
54 | return nil
55 | }
56 | parsedVariable := &ParsedVariable{
57 | Name: commonVariable.Name,
58 | NaturalLanguage: commonVariable.NaturalLanguage,
59 | VariableType: string(commonVariable.Type),
60 | Default: commonVariable.Default,
61 | SourceFilePath: commonVariable.SourceFilePath,
62 | SourceFileLine: commonVariable.SourceFileLine,
63 | }
64 |
65 | return parsedVariable
66 | }
67 |
--------------------------------------------------------------------------------
/frontend/semantic_analyzer/semantic_analyzer.go:
--------------------------------------------------------------------------------
1 | package semantic_analyzer
2 |
3 | import (
4 | "fmt"
5 | "salami/common/errors"
6 | "salami/common/symbol_table"
7 | )
8 |
9 | type SemanticAnalyzer struct {
10 | symbolTable *symbol_table.SymbolTable
11 | }
12 |
13 | func NewSemanticAnalyzer(symbolTable *symbol_table.SymbolTable) *SemanticAnalyzer {
14 | return &SemanticAnalyzer{
15 | symbolTable: symbolTable,
16 | }
17 | }
18 |
19 | func (sa *SemanticAnalyzer) Analyze() error {
20 | if err := sa.ensureResourcesHaveAllRequiredFields(); err != nil {
21 | return err
22 | }
23 | if err := sa.ensureVariablesHaveAllRequiredFields(); err != nil {
24 | return err
25 | }
26 | if err := sa.ensureReferencedVariablesAreDefined(); err != nil {
27 | return err
28 | }
29 | if err := sa.ensureUsedResourcesExist(); err != nil {
30 | return err
31 | }
32 | // TODO: Verify variable types
33 | return nil
34 | }
35 |
36 | func (sa *SemanticAnalyzer) ensureResourcesHaveAllRequiredFields() error {
37 | for _, resource := range sa.symbolTable.ResourceTable {
38 | if resource.ResourceType == "" {
39 | return &errors.SemanticError{
40 | SourceFilePath: resource.SourceFilePath,
41 | Message: "Resource type field on a resource object is missing or empty",
42 | }
43 | }
44 | if resource.LogicalName == "" {
45 | return &errors.SemanticError{
46 | SourceFilePath: resource.SourceFilePath,
47 | Message: "Logical name field on a resource object is missing or empty",
48 | }
49 | }
50 | }
51 | return nil
52 | }
53 |
54 | func (sa *SemanticAnalyzer) ensureVariablesHaveAllRequiredFields() error {
55 | for _, variable := range sa.symbolTable.VariableTable {
56 | if variable.Name == "" {
57 | return &errors.SemanticError{
58 | SourceFilePath: variable.SourceFilePath,
59 | Message: "Name field on a variable object is missing or empty",
60 | }
61 | }
62 | }
63 | return nil
64 | }
65 |
66 | func (sa *SemanticAnalyzer) ensureReferencedVariablesAreDefined() error {
67 | for _, resource := range sa.symbolTable.ResourceTable {
68 | for _, variableName := range resource.ReferencedVariables {
69 | if _, exists := sa.symbolTable.LookupVariable(variableName); !exists {
70 | return &errors.SemanticError{
71 | SourceFilePath: resource.SourceFilePath,
72 | Message: fmt.Sprintf("referenced variable '%s' is not defined", variableName),
73 | }
74 | }
75 | }
76 | }
77 | return nil
78 | }
79 |
80 | func (sa *SemanticAnalyzer) ensureUsedResourcesExist() error {
81 | for _, resource := range sa.symbolTable.ResourceTable {
82 | for _, logicalName := range resource.ReferencedResources {
83 | if _, exists := sa.symbolTable.LookupResource(logicalName); !exists {
84 | return &errors.SemanticError{
85 | SourceFilePath: resource.SourceFilePath,
86 | Message: fmt.Sprintf("referenced resource '%s' is not defined", logicalName),
87 | }
88 | }
89 | }
90 | }
91 | return nil
92 | }
93 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_variable_name.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 | target_code = """\
14 | resource "aws_s3_bucket" "AssumedRolesBucket" {
15 | bucket = "assumed-roles"
16 | versioning {
17 | enabled = true
18 | }
19 | }"""
20 |
21 | [objects.parsed_resource]
22 | resource_type = "aws.s3.Bucket"
23 | logical_name = "AssumedRolesBucket"
24 | natural_language = """\
25 | Bucket: assumed-roles
26 | Versioning enabled: True"""
27 | referenced_resources = []
28 | referenced_variables = []
29 | source_file_path = "path/to/source_file"
30 | source_file_line = 1
31 |
32 | [[objects]]
33 | target_code = """\
34 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
35 | bucket = aws_s3_bucket.AssumedRolesBucket.id
36 |
37 | block_public_acls = true
38 | block_public_policy = false
39 | ignore_public_acls = true
40 | restrict_public_buckets = false
41 | }"""
42 |
43 | [objects.parsed_resource]
44 | resource_type = "aws.s3.BucketPublicAccessBlock"
45 | logical_name = "AssetsPublicAccessBlock"
46 | natural_language = """\
47 | Block public ACLs: True
48 | Block public policy: False
49 | Ignore public ACLs: True
50 | Restrict public buckets: False"""
51 | referenced_resources = ["AssumedRolesBucket"]
52 | referenced_variables = []
53 | source_file_path = "path/to/source_file"
54 | source_file_line = 8
55 |
56 | [[objects]]
57 | target_code = """\
58 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
59 | bucket = aws_s3_bucket.AssumedRolesBucket.id
60 |
61 | policy = jsonencode({
62 | Version = "2012-10-17"
63 | Statement = [
64 | {
65 | Action = "s3:GetObject"
66 | Effect = "Allow"
67 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
68 | Principal = "*"
69 | }
70 | ]
71 | })
72 | }"""
73 |
74 | [objects.parsed_resource]
75 | resource_type = "aws.s3.BucketPolicy"
76 | logical_name = "AssumedRolesBucketPolicy"
77 | natural_language = """\
78 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
79 | referenced_resources = ["AssumedRolesBucket"]
80 | referenced_variables = []
81 | source_file_path = "path/to/source_file"
82 | source_file_line = 17
83 |
84 | [[objects]]
85 | target_code = """\
86 | variable "server_container_name" {
87 | description = "Server container name"
88 | type = string
89 | default = "server-container"
90 | }"""
91 |
92 | [objects.parsed_variable]
93 | description = "Server container name"
94 | type = "string"
95 | default = "server-container"
96 | source_file_path = "path/to/source_file"
97 | source_file_line = 24
98 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_target_file_checksum.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 |
11 | [[objects]]
12 | target_code = """\
13 | resource "aws_s3_bucket" "AssumedRolesBucket" {
14 | bucket = "assumed-roles"
15 | versioning {
16 | enabled = true
17 | }
18 | }"""
19 |
20 | [objects.parsed_resource]
21 | resource_type = "aws.s3.Bucket"
22 | logical_name = "AssumedRolesBucket"
23 | natural_language = """\
24 | Bucket: assumed-roles
25 | Versioning enabled: True"""
26 | referenced_resources = []
27 | referenced_variables = []
28 | source_file_path = "path/to/source_file"
29 | source_file_line = 1
30 |
31 | [[objects]]
32 | target_code = """\
33 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
34 | bucket = aws_s3_bucket.AssumedRolesBucket.id
35 |
36 | block_public_acls = true
37 | block_public_policy = false
38 | ignore_public_acls = true
39 | restrict_public_buckets = false
40 | }"""
41 |
42 | [objects.parsed_resource]
43 | resource_type = "aws.s3.BucketPublicAccessBlock"
44 | logical_name = "AssetsPublicAccessBlock"
45 | natural_language = """\
46 | Block public ACLs: True
47 | Block public policy: False
48 | Ignore public ACLs: True
49 | Restrict public buckets: False"""
50 | referenced_resources = ["AssumedRolesBucket"]
51 | referenced_variables = []
52 | source_file_path = "path/to/source_file"
53 | source_file_line = 8
54 |
55 | [[objects]]
56 | target_code = """\
57 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
58 | bucket = aws_s3_bucket.AssumedRolesBucket.id
59 |
60 | policy = jsonencode({
61 | Version = "2012-10-17"
62 | Statement = [
63 | {
64 | Action = "s3:GetObject"
65 | Effect = "Allow"
66 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
67 | Principal = "*"
68 | }
69 | ]
70 | })
71 | }"""
72 |
73 | [objects.parsed_resource]
74 | resource_type = "aws.s3.BucketPolicy"
75 | logical_name = "AssumedRolesBucketPolicy"
76 | natural_language = """\
77 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
78 | referenced_resources = ["AssumedRolesBucket"]
79 | referenced_variables = []
80 | source_file_path = "path/to/source_file"
81 | source_file_line = 17
82 |
83 | [[objects]]
84 | target_code = """\
85 | variable "server_container_name" {
86 | description = "Server container name"
87 | type = string
88 | default = "server-container"
89 | }"""
90 |
91 | [objects.parsed_variable]
92 | name = "server_container_name"
93 | natural_language = "Description: Server container name"
94 | type = "string"
95 | default = "server-container"
96 | source_file_path = "path/to/source_file"
97 | source_file_line = 24
98 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_resource_type.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 | target_code = """\
14 | resource "aws_s3_bucket" "AssumedRolesBucket" {
15 | bucket = "assumed-roles"
16 | versioning {
17 | enabled = true
18 | }
19 | }"""
20 |
21 | [objects.parsed_resource]
22 | logical_name = "AssumedRolesBucket"
23 | natural_language = """\
24 | Bucket: assumed-roles
25 | Versioning enabled: True"""
26 | referenced_resources = []
27 | referenced_variables = []
28 | source_file_path = "path/to/source_file"
29 | source_file_line = 1
30 |
31 | [[objects]]
32 | target_code = """\
33 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
34 | bucket = aws_s3_bucket.AssumedRolesBucket.id
35 |
36 | block_public_acls = true
37 | block_public_policy = false
38 | ignore_public_acls = true
39 | restrict_public_buckets = false
40 | }"""
41 |
42 | [objects.parsed_resource]
43 | resource_type = "aws.s3.BucketPublicAccessBlock"
44 | logical_name = "AssetsPublicAccessBlock"
45 | natural_language = """\
46 | Block public ACLs: True
47 | Block public policy: False
48 | Ignore public ACLs: True
49 | Restrict public buckets: False"""
50 | referenced_resources = ["AssumedRolesBucket"]
51 | referenced_variables = []
52 | source_file_path = "path/to/source_file"
53 | source_file_line = 8
54 |
55 | [[objects]]
56 | target_code = """\
57 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
58 | bucket = aws_s3_bucket.AssumedRolesBucket.id
59 |
60 | policy = jsonencode({
61 | Version = "2012-10-17"
62 | Statement = [
63 | {
64 | Action = "s3:GetObject"
65 | Effect = "Allow"
66 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
67 | Principal = "*"
68 | }
69 | ]
70 | })
71 | }"""
72 |
73 | [objects.parsed_resource]
74 | resource_type = "aws.s3.BucketPolicy"
75 | logical_name = "AssumedRolesBucketPolicy"
76 | natural_language = """\
77 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
78 | referenced_resources = ["AssumedRolesBucket"]
79 | referenced_variables = []
80 | source_file_path = "path/to/source_file"
81 | source_file_line = 17
82 |
83 | [[objects]]
84 | target_code = """\
85 | variable "server_container_name" {
86 | description = "Server container name"
87 | type = string
88 | default = "server-container"
89 | }"""
90 |
91 | [objects.parsed_variable]
92 | name = "server_container_name"
93 | natural_language = "Description: Server container name"
94 | type = "string"
95 | default = "server-container"
96 | source_file_path = "path/to/source_file"
97 | source_file_line = 24
98 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_target_file_path.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | checksum = "ea441ff260d926a935cf47abf698482d"
10 |
11 | [[objects]]
12 | target_code = """\
13 | resource "aws_s3_bucket" "AssumedRolesBucket" {
14 | bucket = "assumed-roles"
15 | versioning {
16 | enabled = true
17 | }
18 | }"""
19 |
20 | [objects.parsed_resource]
21 | resource_type = "aws.s3.Bucket"
22 | logical_name = "AssumedRolesBucket"
23 | natural_language = """\
24 | Bucket: assumed-roles
25 | Versioning enabled: True"""
26 | referenced_resources = []
27 | referenced_variables = []
28 | source_file_path = "path/to/source_file"
29 | source_file_line = 1
30 |
31 | [[objects]]
32 | target_code = """\
33 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
34 | bucket = aws_s3_bucket.AssumedRolesBucket.id
35 |
36 | block_public_acls = true
37 | block_public_policy = false
38 | ignore_public_acls = true
39 | restrict_public_buckets = false
40 | }"""
41 |
42 | [objects.parsed_resource]
43 | resource_type = "aws.s3.BucketPublicAccessBlock"
44 | logical_name = "AssetsPublicAccessBlock"
45 | natural_language = """\
46 | Block public ACLs: True
47 | Block public policy: False
48 | Ignore public ACLs: True
49 | Restrict public buckets: False"""
50 | referenced_resources = ["AssumedRolesBucket"]
51 | referenced_variables = []
52 | source_file_path = "path/to/source_file"
53 | source_file_line = 8
54 |
55 | [[objects]]
56 | target_code = """\
57 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
58 | bucket = aws_s3_bucket.AssumedRolesBucket.id
59 |
60 | policy = jsonencode({
61 | Version = "2012-10-17"
62 | Statement = [
63 | {
64 | Action = "s3:GetObject"
65 | Effect = "Allow"
66 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
67 | Principal = "*"
68 | }
69 | ]
70 | })
71 | }"""
72 |
73 | [objects.parsed_resource]
74 | resource_type = "aws.s3.BucketPolicy"
75 | logical_name = "AssumedRolesBucketPolicy"
76 | natural_language = """\
77 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
78 | referenced_resources = ["AssumedRolesBucket"]
79 | referenced_variables = []
80 | source_file_path = "path/to/source_file"
81 | source_file_line = 17
82 |
83 | [[objects]]
84 | target_code = """\
85 | variable "server_container_name" {
86 | description = "Server container name"
87 | type = string
88 | default = "server-container"
89 | }"""
90 |
91 | [objects.parsed_variable]
92 | name = "server_container_name"
93 | natural_language = "Description: Server container name"
94 | type = "string"
95 | default = "server-container"
96 | source_file_path = "path/to/source_file"
97 | source_file_line = 24
98 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_resource_logical_name.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 | target_code = """\
14 | resource "aws_s3_bucket" "AssumedRolesBucket" {
15 | bucket = "assumed-roles"
16 | versioning {
17 | enabled = true
18 | }
19 | }"""
20 |
21 | [objects.parsed_resource]
22 | resource_type = "aws.s3.Bucket"
23 | natural_language = """\
24 | Bucket: assumed-roles
25 | Versioning enabled: True"""
26 | referenced_resources = []
27 | referenced_variables = []
28 | source_file_path = "path/to/source_file"
29 | source_file_line = 1
30 |
31 | [[objects]]
32 | target_code = """\
33 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
34 | bucket = aws_s3_bucket.AssumedRolesBucket.id
35 |
36 | block_public_acls = true
37 | block_public_policy = false
38 | ignore_public_acls = true
39 | restrict_public_buckets = false
40 | }"""
41 |
42 | [objects.parsed_resource]
43 | resource_type = "aws.s3.BucketPublicAccessBlock"
44 | logical_name = "AssetsPublicAccessBlock"
45 | natural_language = """\
46 | Block public ACLs: True
47 | Block public policy: False
48 | Ignore public ACLs: True
49 | Restrict public buckets: False"""
50 | referenced_resources = ["AssumedRolesBucket"]
51 | referenced_variables = []
52 | source_file_path = "path/to/source_file"
53 | source_file_line = 8
54 |
55 | [[objects]]
56 | target_code = """\
57 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
58 | bucket = aws_s3_bucket.AssumedRolesBucket.id
59 |
60 | policy = jsonencode({
61 | Version = "2012-10-17"
62 | Statement = [
63 | {
64 | Action = "s3:GetObject"
65 | Effect = "Allow"
66 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
67 | Principal = "*"
68 | }
69 | ]
70 | })
71 | }"""
72 |
73 | [objects.parsed_resource]
74 | resource_type = "aws.s3.BucketPolicy"
75 | logical_name = "AssumedRolesBucketPolicy"
76 | natural_language = """\
77 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
78 | referenced_resources = ["AssumedRolesBucket"]
79 | referenced_variables = []
80 | source_file_path = "path/to/source_file"
81 | source_file_line = 17
82 |
83 | [[objects]]
84 | target_code = """\
85 | variable "server_container_name" {
86 | description = "Server container name"
87 | type = string
88 | default = "server-container"
89 | }"""
90 |
91 | [objects.parsed_variable]
92 | name = "server_container_name"
93 | natural_language = "Description: Server container name"
94 | type = "string"
95 | default = "server-container"
96 | source_file_path = "path/to/source_file"
97 | source_file_line = 24
98 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_resource_source_file_path.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 | target_code = """\
14 | resource "aws_s3_bucket" "AssumedRolesBucket" {
15 | bucket = "assumed-roles"
16 | versioning {
17 | enabled = true
18 | }
19 | }"""
20 |
21 | [objects.parsed_resource]
22 | resource_type = "aws.s3.Bucket"
23 | logical_name = "AssumedRolesBucket"
24 | natural_language = """\
25 | Bucket: assumed-roles
26 | Versioning enabled: True"""
27 | referenced_resources = []
28 | referenced_variables = []
29 | source_file_line = 1
30 |
31 | [[objects]]
32 | target_code = """\
33 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
34 | bucket = aws_s3_bucket.AssumedRolesBucket.id
35 |
36 | block_public_acls = true
37 | block_public_policy = false
38 | ignore_public_acls = true
39 | restrict_public_buckets = false
40 | }"""
41 |
42 | [objects.parsed_resource]
43 | resource_type = "aws.s3.BucketPublicAccessBlock"
44 | logical_name = "AssetsPublicAccessBlock"
45 | natural_language = """\
46 | Block public ACLs: True
47 | Block public policy: False
48 | Ignore public ACLs: True
49 | Restrict public buckets: False"""
50 | referenced_resources = ["AssumedRolesBucket"]
51 | referenced_variables = []
52 | source_file_path = "path/to/source_file"
53 | source_file_line = 8
54 |
55 | [[objects]]
56 | target_code = """\
57 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
58 | bucket = aws_s3_bucket.AssumedRolesBucket.id
59 |
60 | policy = jsonencode({
61 | Version = "2012-10-17"
62 | Statement = [
63 | {
64 | Action = "s3:GetObject"
65 | Effect = "Allow"
66 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
67 | Principal = "*"
68 | }
69 | ]
70 | })
71 | }"""
72 |
73 | [objects.parsed_resource]
74 | resource_type = "aws.s3.BucketPolicy"
75 | logical_name = "AssumedRolesBucketPolicy"
76 | natural_language = """\
77 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
78 | referenced_resources = ["AssumedRolesBucket"]
79 | referenced_variables = []
80 | source_file_path = "path/to/source_file"
81 | source_file_line = 17
82 |
83 | [[objects]]
84 | target_code = """\
85 | variable "server_container_name" {
86 | description = "Server container name"
87 | type = string
88 | default = "server-container"
89 | }"""
90 |
91 | [objects.parsed_variable]
92 | name = "server_container_name"
93 | natural_language = "Description: Server container name"
94 | type = "string"
95 | default = "server-container"
96 | source_file_path = "path/to/source_file"
97 | source_file_line = 24
98 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_variable_source_file_path.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 | target_code = """\
14 | resource "aws_s3_bucket" "AssumedRolesBucket" {
15 | bucket = "assumed-roles"
16 | versioning {
17 | enabled = true
18 | }
19 | }"""
20 |
21 | [objects.parsed_resource]
22 | resource_type = "aws.s3.Bucket"
23 | logical_name = "AssumedRolesBucket"
24 | natural_language = """\
25 | Bucket: assumed-roles
26 | Versioning enabled: True"""
27 | referenced_resources = []
28 | referenced_variables = []
29 | source_file_path = "path/to/source_file"
30 | source_file_line = 1
31 |
32 | [[objects]]
33 | target_code = """\
34 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
35 | bucket = aws_s3_bucket.AssumedRolesBucket.id
36 |
37 | block_public_acls = true
38 | block_public_policy = false
39 | ignore_public_acls = true
40 | restrict_public_buckets = false
41 | }"""
42 |
43 | [objects.parsed_resource]
44 | resource_type = "aws.s3.BucketPublicAccessBlock"
45 | logical_name = "AssetsPublicAccessBlock"
46 | natural_language = """\
47 | Block public ACLs: True
48 | Block public policy: False
49 | Ignore public ACLs: True
50 | Restrict public buckets: False"""
51 | referenced_resources = ["AssumedRolesBucket"]
52 | referenced_variables = []
53 | source_file_path = "path/to/source_file"
54 | source_file_line = 8
55 |
56 | [[objects]]
57 | target_code = """\
58 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
59 | bucket = aws_s3_bucket.AssumedRolesBucket.id
60 |
61 | policy = jsonencode({
62 | Version = "2012-10-17"
63 | Statement = [
64 | {
65 | Action = "s3:GetObject"
66 | Effect = "Allow"
67 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
68 | Principal = "*"
69 | }
70 | ]
71 | })
72 | }"""
73 |
74 | [objects.parsed_resource]
75 | resource_type = "aws.s3.BucketPolicy"
76 | logical_name = "AssumedRolesBucketPolicy"
77 | natural_language = """\
78 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
79 | referenced_resources = ["AssumedRolesBucket"]
80 | referenced_variables = []
81 | source_file_path = "path/to/source_file"
82 | source_file_line = 17
83 |
84 | [[objects]]
85 | target_code = """\
86 | variable "server_container_name" {
87 | description = "Server container name"
88 | type = string
89 | default = "server-container"
90 | }"""
91 |
92 | [objects.parsed_variable]
93 | name = "server_container_name"
94 | natural_language = "Description: Server container name"
95 | type = "string"
96 | default = "server-container"
97 | source_file_line = 24
98 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_version.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | [[target_files_meta]]
3 | file_path = "path/to/target_file_1"
4 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
5 |
6 | [[target_files_meta]]
7 | file_path = "path/to/target_file_2"
8 | checksum = "ea441ff260d926a935cf47abf698482d"
9 |
10 | [[objects]]
11 | target_code = """\
12 | resource "aws_s3_bucket" "AssumedRolesBucket" {
13 | bucket = "assumed-roles"
14 | versioning {
15 | enabled = true
16 | }
17 | }"""
18 |
19 | [objects.parsed_resource]
20 | resource_type = "aws.s3.Bucket"
21 | logical_name = "AssumedRolesBucket"
22 | natural_language = """\
23 | Bucket: assumed-roles
24 | Versioning enabled: True"""
25 | referenced_resources = []
26 | referenced_variables = []
27 | source_file_path = "path/to/source_file"
28 | source_file_line = 1
29 |
30 | [[objects]]
31 | target_code = """\
32 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
33 | bucket = aws_s3_bucket.AssumedRolesBucket.id
34 |
35 | block_public_acls = true
36 | block_public_policy = false
37 | ignore_public_acls = true
38 | restrict_public_buckets = false
39 | }"""
40 |
41 | [objects.parsed_resource]
42 | resource_type = "aws.s3.BucketPublicAccessBlock"
43 | logical_name = "AssetsPublicAccessBlock"
44 | natural_language = """\
45 | Block public ACLs: True
46 | Block public policy: False
47 | Ignore public ACLs: True
48 | Restrict public buckets: False"""
49 | referenced_resources = ["AssumedRolesBucket"]
50 | referenced_variables = []
51 | source_file_path = "path/to/source_file"
52 | source_file_line = 8
53 |
54 | [[objects]]
55 | target_code = """\
56 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
57 | bucket = aws_s3_bucket.AssumedRolesBucket.id
58 |
59 | policy = jsonencode({
60 | Version = "2012-10-17"
61 | Statement = [
62 | {
63 | Action = "s3:GetObject"
64 | Effect = "Allow"
65 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
66 | Principal = "*"
67 | }
68 | ]
69 | })
70 | }"""
71 |
72 | [objects.parsed_resource]
73 | resource_type = "aws.s3.BucketPolicy"
74 | logical_name = "AssumedRolesBucketPolicy"
75 | natural_language = """\
76 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
77 | referenced_resources = ["AssumedRolesBucket"]
78 | referenced_variables = []
79 | source_file_path = "path/to/source_file"
80 | source_file_line = 17
81 |
82 | [[objects]]
83 | target_code = """\
84 | variable "server_container_name" {
85 | description = "Server container name"
86 | type = string
87 | default = "server-container"
88 | }"""
89 |
90 | [objects.parsed_variable]
91 | name = "server_container_name"
92 | natural_language = "Description: Server container name"
93 | type = "string"
94 | default = "server-container"
95 | source_file_path = "path/to/source_file"
96 | source_file_line = 24
97 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/invalid_semver.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1a"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 | target_code = """\
14 | resource "aws_s3_bucket" "AssumedRolesBucket" {
15 | bucket = "assumed-roles"
16 | versioning {
17 | enabled = true
18 | }
19 | }"""
20 |
21 | [objects.parsed_resource]
22 | resource_type = "aws.s3.Bucket"
23 | logical_name = "AssumedRolesBucket"
24 | natural_language = """\
25 | Bucket: assumed-roles
26 | Versioning enabled: True"""
27 | referenced_resources = []
28 | referenced_variables = []
29 | source_file_path = "path/to/source_file"
30 | source_file_line = 1
31 |
32 | [[objects]]
33 | target_code = """\
34 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
35 | bucket = aws_s3_bucket.AssumedRolesBucket.id
36 |
37 | block_public_acls = true
38 | block_public_policy = false
39 | ignore_public_acls = true
40 | restrict_public_buckets = false
41 | }"""
42 |
43 | [objects.parsed_resource]
44 | resource_type = "aws.s3.BucketPublicAccessBlock"
45 | logical_name = "AssetsPublicAccessBlock"
46 | natural_language = """\
47 | Block public ACLs: True
48 | Block public policy: False
49 | Ignore public ACLs: True
50 | Restrict public buckets: False"""
51 | referenced_resources = ["AssumedRolesBucket"]
52 | referenced_variables = []
53 | source_file_path = "path/to/source_file"
54 | source_file_line = 8
55 |
56 | [[objects]]
57 | target_code = """\
58 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
59 | bucket = aws_s3_bucket.AssumedRolesBucket.id
60 |
61 | policy = jsonencode({
62 | Version = "2012-10-17"
63 | Statement = [
64 | {
65 | Action = "s3:GetObject"
66 | Effect = "Allow"
67 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
68 | Principal = "*"
69 | }
70 | ]
71 | })
72 | }"""
73 |
74 | [objects.parsed_resource]
75 | resource_type = "aws.s3.BucketPolicy"
76 | logical_name = "AssumedRolesBucketPolicy"
77 | natural_language = """\
78 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
79 | referenced_resources = ["AssumedRolesBucket"]
80 | referenced_variables = []
81 | source_file_path = "path/to/source_file"
82 | source_file_line = 17
83 |
84 | [[objects]]
85 | target_code = """\
86 | variable "server_container_name" {
87 | description = "Server container name"
88 | type = string
89 | default = "server-container"
90 | }"""
91 |
92 | [objects.parsed_variable]
93 | name = "server_container_name"
94 | natural_language = "Server container name"
95 | type = "string"
96 | default = "server-container"
97 | source_file_path = "path/to/source_file"
98 | source_file_line = 24
99 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_variable_type.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 | target_code = """\
14 | resource "aws_s3_bucket" "AssumedRolesBucket" {
15 | bucket = "assumed-roles"
16 | versioning {
17 | enabled = true
18 | }
19 | }"""
20 |
21 | [objects.parsed_resource]
22 | resource_type = "aws.s3.Bucket"
23 | logical_name = "AssumedRolesBucket"
24 | natural_language = """\
25 | Bucket: assumed-roles
26 | Versioning enabled: True"""
27 | referenced_resources = []
28 | referenced_variables = []
29 | source_file_path = "path/to/source_file"
30 | source_file_line = 1
31 |
32 | [[objects]]
33 | target_code = """\
34 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
35 | bucket = aws_s3_bucket.AssumedRolesBucket.id
36 |
37 | block_public_acls = true
38 | block_public_policy = false
39 | ignore_public_acls = true
40 | restrict_public_buckets = false
41 | }"""
42 |
43 | [objects.parsed_resource]
44 | resource_type = "aws.s3.BucketPublicAccessBlock"
45 | logical_name = "AssetsPublicAccessBlock"
46 | natural_language = """\
47 | Block public ACLs: True
48 | Block public policy: False
49 | Ignore public ACLs: True
50 | Restrict public buckets: False"""
51 | referenced_resources = ["AssumedRolesBucket"]
52 | referenced_variables = []
53 | source_file_path = "path/to/source_file"
54 | source_file_line = 8
55 |
56 | [[objects]]
57 | target_code = """\
58 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
59 | bucket = aws_s3_bucket.AssumedRolesBucket.id
60 |
61 | policy = jsonencode({
62 | Version = "2012-10-17"
63 | Statement = [
64 | {
65 | Action = "s3:GetObject"
66 | Effect = "Allow"
67 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
68 | Principal = "*"
69 | }
70 | ]
71 | })
72 | }"""
73 |
74 | [objects.parsed_resource]
75 | resource_type = "aws.s3.BucketPolicy"
76 | logical_name = "AssumedRolesBucketPolicy"
77 | natural_language = """\
78 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
79 | referenced_resources = ["AssumedRolesBucket"]
80 | referenced_variables = []
81 | source_file_path = "path/to/source_file"
82 | source_file_line = 17
83 |
84 | [[objects]]
85 | target_code = """\
86 | variable "server_container_name" {
87 | description = "Server container name"
88 | type = string
89 | default = "server-container"
90 | }"""
91 |
92 | [objects.parsed_variable]
93 | name = "server_container_name"
94 | natural_language = "Description: Server container name"
95 | default = "server-container"
96 | source_file_path = "path/to/source_file"
97 | source_file_line = 24
98 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_resource_source_file_line.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 | target_code = """\
14 | resource "aws_s3_bucket" "AssumedRolesBucket" {
15 | bucket = "assumed-roles"
16 | versioning {
17 | enabled = true
18 | }
19 | }"""
20 |
21 | [objects.parsed_resource]
22 | resource_type = "aws.s3.Bucket"
23 | logical_name = "AssumedRolesBucket"
24 | natural_language = """\
25 | Bucket: assumed-roles
26 | Versioning enabled: True"""
27 | referenced_resources = []
28 | referenced_variables = []
29 | source_file_path = "path/to/source_file"
30 |
31 | [[objects]]
32 | target_code = """\
33 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
34 | bucket = aws_s3_bucket.AssumedRolesBucket.id
35 |
36 | block_public_acls = true
37 | block_public_policy = false
38 | ignore_public_acls = true
39 | restrict_public_buckets = false
40 | }"""
41 |
42 | [objects.parsed_resource]
43 | resource_type = "aws.s3.BucketPublicAccessBlock"
44 | logical_name = "AssetsPublicAccessBlock"
45 | natural_language = """\
46 | Block public ACLs: True
47 | Block public policy: False
48 | Ignore public ACLs: True
49 | Restrict public buckets: False"""
50 | referenced_resources = ["AssumedRolesBucket"]
51 | referenced_variables = []
52 | source_file_path = "path/to/source_file"
53 | source_file_line = 8
54 |
55 | [[objects]]
56 | target_code = """\
57 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
58 | bucket = aws_s3_bucket.AssumedRolesBucket.id
59 |
60 | policy = jsonencode({
61 | Version = "2012-10-17"
62 | Statement = [
63 | {
64 | Action = "s3:GetObject"
65 | Effect = "Allow"
66 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
67 | Principal = "*"
68 | }
69 | ]
70 | })
71 | }"""
72 |
73 | [objects.parsed_resource]
74 | resource_type = "aws.s3.BucketPolicy"
75 | logical_name = "AssumedRolesBucketPolicy"
76 | natural_language = """\
77 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
78 | referenced_resources = ["AssumedRolesBucket"]
79 | referenced_variables = []
80 | source_file_path = "path/to/source_file"
81 | source_file_line = 17
82 |
83 | [[objects]]
84 | target_code = """\
85 | variable "server_container_name" {
86 | description = "Server container name"
87 | type = string
88 | default = "server-container"
89 | }"""
90 |
91 | [objects.parsed_variable]
92 | name = "server_container_name"
93 | natural_language = "Description: Server container name"
94 | type = "string"
95 | default = "server-container"
96 | source_file_path = "path/to/source_file"
97 | source_file_line = 24
98 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/missing_variable_source_file_line.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 | target_code = """\
14 | resource "aws_s3_bucket" "AssumedRolesBucket" {
15 | bucket = "assumed-roles"
16 | versioning {
17 | enabled = true
18 | }
19 | }"""
20 |
21 | [objects.parsed_resource]
22 | resource_type = "aws.s3.Bucket"
23 | logical_name = "AssumedRolesBucket"
24 | natural_language = """\
25 | Bucket: assumed-roles
26 | Versioning enabled: True"""
27 | referenced_resources = []
28 | referenced_variables = []
29 | source_file_path = "path/to/source_file"
30 | source_file_line = 1
31 |
32 | [[objects]]
33 | target_code = """\
34 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
35 | bucket = aws_s3_bucket.AssumedRolesBucket.id
36 |
37 | block_public_acls = true
38 | block_public_policy = false
39 | ignore_public_acls = true
40 | restrict_public_buckets = false
41 | }"""
42 |
43 | [objects.parsed_resource]
44 | resource_type = "aws.s3.BucketPublicAccessBlock"
45 | logical_name = "AssetsPublicAccessBlock"
46 | natural_language = """\
47 | Block public ACLs: True
48 | Block public policy: False
49 | Ignore public ACLs: True
50 | Restrict public buckets: False"""
51 | referenced_resources = ["AssumedRolesBucket"]
52 | referenced_variables = []
53 | source_file_path = "path/to/source_file"
54 | source_file_line = 8
55 |
56 | [[objects]]
57 | target_code = """\
58 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
59 | bucket = aws_s3_bucket.AssumedRolesBucket.id
60 |
61 | policy = jsonencode({
62 | Version = "2012-10-17"
63 | Statement = [
64 | {
65 | Action = "s3:GetObject"
66 | Effect = "Allow"
67 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
68 | Principal = "*"
69 | }
70 | ]
71 | })
72 | }"""
73 |
74 | [objects.parsed_resource]
75 | resource_type = "aws.s3.BucketPolicy"
76 | logical_name = "AssumedRolesBucketPolicy"
77 | natural_language = """\
78 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
79 | referenced_resources = ["AssumedRolesBucket"]
80 | referenced_variables = []
81 | source_file_path = "path/to/source_file"
82 | source_file_line = 17
83 |
84 | [[objects]]
85 | target_code = """\
86 | variable "server_container_name" {
87 | description = "Server container name"
88 | type = string
89 | default = "server-container"
90 | }"""
91 |
92 | [objects.parsed_variable]
93 | name = "server_container_name"
94 | natural_language = "Description: Server container name"
95 | type = "string"
96 | default = "server-container"
97 | source_file_path = "path/to/source_file"
98 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/valid.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 | target_code = """\
14 | resource "aws_s3_bucket" "AssumedRolesBucket" {
15 | bucket = "assumed-roles"
16 | versioning {
17 | enabled = true
18 | }
19 | }"""
20 |
21 | [objects.parsed_resource]
22 | resource_type = "aws.s3.Bucket"
23 | logical_name = "AssumedRolesBucket"
24 | natural_language = """\
25 | Bucket: assumed-roles
26 | Versioning enabled: True"""
27 | referenced_resources = []
28 | referenced_variables = []
29 | source_file_path = "path/to/source_file"
30 | source_file_line = 1
31 |
32 | [[objects]]
33 | target_code = """\
34 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
35 | bucket = aws_s3_bucket.AssumedRolesBucket.id
36 |
37 | block_public_acls = true
38 | block_public_policy = false
39 | ignore_public_acls = true
40 | restrict_public_buckets = false
41 | }"""
42 |
43 | [objects.parsed_resource]
44 | resource_type = "aws.s3.BucketPublicAccessBlock"
45 | logical_name = "AssetsPublicAccessBlock"
46 | natural_language = """\
47 | Block public ACLs: True
48 | Block public policy: False
49 | Ignore public ACLs: True
50 | Restrict public buckets: False"""
51 | referenced_resources = ["AssumedRolesBucket"]
52 | referenced_variables = []
53 | source_file_path = "path/to/source_file"
54 | source_file_line = 8
55 |
56 | [[objects]]
57 | target_code = """\
58 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
59 | bucket = aws_s3_bucket.AssumedRolesBucket.id
60 |
61 | policy = jsonencode({
62 | Version = "2012-10-17"
63 | Statement = [
64 | {
65 | Action = "s3:GetObject"
66 | Effect = "Allow"
67 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
68 | Principal = "*"
69 | }
70 | ]
71 | })
72 | }"""
73 |
74 | [objects.parsed_resource]
75 | resource_type = "aws.s3.BucketPolicy"
76 | logical_name = "AssumedRolesBucketPolicy"
77 | natural_language = """\
78 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
79 | referenced_resources = ["AssumedRolesBucket"]
80 | referenced_variables = []
81 | source_file_path = "path/to/source_file"
82 | source_file_line = 17
83 |
84 | [[objects]]
85 | target_code = """\
86 | variable "server_container_name" {
87 | description = "Server container name"
88 | type = string
89 | default = "server-container"
90 | }"""
91 |
92 | [objects.parsed_variable]
93 | name = "server_container_name"
94 | natural_language = "Description: Server container name"
95 | type = "string"
96 | default = "server-container"
97 | source_file_path = "path/to/source_file"
98 | source_file_line = 24
99 |
--------------------------------------------------------------------------------
/common/lock_file_manager/testdata/lock_files/invalid_variable_type.toml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by Salami. It is not meant to be edited manually.
2 | version = "0.0.1"
3 |
4 | [[target_files_meta]]
5 | file_path = "path/to/target_file_1"
6 | checksum = "e460d56360c0c4d1ff32fd5e5a56eb99"
7 |
8 | [[target_files_meta]]
9 | file_path = "path/to/target_file_2"
10 | checksum = "ea441ff260d926a935cf47abf698482d"
11 |
12 | [[objects]]
13 | target_code = """\
14 | resource "aws_s3_bucket" "AssumedRolesBucket" {
15 | bucket = "assumed-roles"
16 | versioning {
17 | enabled = true
18 | }
19 | }"""
20 |
21 | [objects.parsed_resource]
22 | resource_type = "aws.s3.Bucket"
23 | logical_name = "AssumedRolesBucket"
24 | natural_language = """\
25 | Bucket: assumed-roles
26 | Versioning enabled: True"""
27 | referenced_resources = []
28 | referenced_variables = []
29 | source_file_path = "path/to/source_file"
30 | source_file_line = 1
31 |
32 | [[objects]]
33 | target_code = """\
34 | resource "aws_s3_bucket_public_access_block" "AssetsPublicAccessBlock" {
35 | bucket = aws_s3_bucket.AssumedRolesBucket.id
36 |
37 | block_public_acls = true
38 | block_public_policy = false
39 | ignore_public_acls = true
40 | restrict_public_buckets = false
41 | }"""
42 |
43 | [objects.parsed_resource]
44 | resource_type = "aws.s3.BucketPublicAccessBlock"
45 | logical_name = "AssetsPublicAccessBlock"
46 | natural_language = """\
47 | Block public ACLs: True
48 | Block public policy: False
49 | Ignore public ACLs: True
50 | Restrict public buckets: False"""
51 | referenced_resources = ["AssumedRolesBucket"]
52 | referenced_variables = []
53 | source_file_path = "path/to/source_file"
54 | source_file_line = 8
55 |
56 | [[objects]]
57 | target_code = """\
58 | resource "aws_s3_bucket_policy" "AssumedRolesBucketPolicy" {
59 | bucket = aws_s3_bucket.AssumedRolesBucket.id
60 |
61 | policy = jsonencode({
62 | Version = "2012-10-17"
63 | Statement = [
64 | {
65 | Action = "s3:GetObject"
66 | Effect = "Allow"
67 | Resource = "${aws_s3_bucket.AssumedRolesBucket.arn}/*"
68 | Principal = "*"
69 | }
70 | ]
71 | })
72 | }"""
73 |
74 | [objects.parsed_resource]
75 | resource_type = "aws.s3.BucketPolicy"
76 | logical_name = "AssumedRolesBucketPolicy"
77 | natural_language = """\
78 | Policy: A JSON policy that allows all principals to perform the "s3:GetObject" action on all objects in the specified S3 bucket."""
79 | referenced_resources = ["AssumedRolesBucket"]
80 | referenced_variables = []
81 | source_file_path = "path/to/source_file"
82 | source_file_line = 17
83 |
84 | [[objects]]
85 | target_code = """\
86 | variable "server_container_name" {
87 | description = "Server container name"
88 | type = string
89 | default = "server-container"
90 | }"""
91 |
92 | [objects.parsed_variable]
93 | name = "server_container_name"
94 | natural_language = "Server container name"
95 | type = "unsupported"
96 | default = "server-container"
97 | source_file_path = "path/to/source_file"
98 | source_file_line = 24
99 |
--------------------------------------------------------------------------------
/frontend/parser/parse_natural_language.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "regexp"
5 | commonTypes "salami/common/types"
6 | "salami/frontend/types"
7 | "strings"
8 | )
9 |
10 | func (p *Parser) parseNaturalLanguage() error {
11 | freeTextToken := p.currentToken()
12 | p.advance()
13 | for p.currentToken().Type != types.Newline && p.currentToken().Type != types.EOF {
14 | return p.parseError(p.currentToken())
15 | }
16 |
17 | if p.currentObjectTypeIs(Unset) {
18 | return p.parseError(
19 | freeTextToken,
20 | "ambiguous object type. Object must start with a constructor",
21 | )
22 | }
23 | p.currentObject().AddNaturalLanguage(freeTextToken.Value)
24 | err := p.extractAndSetReferencedVariables(freeTextToken.Value)
25 | if err != nil {
26 | return err
27 | }
28 | err = p.extractAndSetReferencedResources(freeTextToken.Value)
29 | if err != nil {
30 | return err
31 | }
32 | return nil
33 | }
34 |
35 | func (p *Parser) extractAndSetReferencedVariables(text string) error {
36 | re := regexp.MustCompile(`\{([^{}]+)\}`)
37 | isAlphanumeric := regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
38 | matches := re.FindAllStringSubmatch(text, -1)
39 |
40 | for _, match := range matches {
41 | variable := strings.TrimSpace(match[1])
42 | if strings.Contains(variable, "{") || strings.Contains(variable, "}") {
43 | return p.parseError(
44 | p.currentToken(),
45 | "nested curly braces in referenced variables are not allowed",
46 | )
47 | }
48 | if !isAlphanumeric.MatchString(variable) {
49 | return p.parseError(
50 | p.currentToken(),
51 | "variable inside curly braces must be alphanumeric. Underscores are allowed.",
52 | )
53 | }
54 | isUnique := true
55 | for _, v := range p.currentResource().ReferencedVariables {
56 | if v == variable {
57 | isUnique = false
58 | break
59 | }
60 | }
61 | if isUnique {
62 | p.currentResource().ReferencedVariables = append(p.currentResource().ReferencedVariables, variable)
63 | }
64 | }
65 | return nil
66 | }
67 |
68 | func (p *Parser) extractAndSetReferencedResources(text string) error {
69 | re := regexp.MustCompile(`\$([a-zA-Z0-9_]+)`)
70 | matches := re.FindAllStringSubmatchIndex(text, -1)
71 |
72 | for _, match := range matches {
73 | // Ignore if the dollar sign is escaped
74 | if match[0] > 0 && text[match[0]-1] == '\\' {
75 | continue
76 | }
77 |
78 | resource := strings.TrimSpace(text[match[2]:match[3]])
79 | if strings.Contains(resource, "$") {
80 | return p.parseError(
81 | p.currentToken(),
82 | "Nested dollar signs in referenced resources are not allowed",
83 | )
84 | }
85 | isUnique := true
86 | for _, r := range p.currentResource().ReferencedResources {
87 | if r == commonTypes.LogicalName(resource) {
88 | isUnique = false
89 | break
90 | }
91 | }
92 | if isUnique {
93 | p.currentResource().ReferencedResources = append(
94 | p.currentResource().ReferencedResources,
95 | commonTypes.LogicalName(resource),
96 | )
97 | }
98 | }
99 | return nil
100 | }
101 |
--------------------------------------------------------------------------------
/frontend/parser/parser.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "salami/common/errors"
5 | commonTypes "salami/common/types"
6 | frontendTypes "salami/frontend/types"
7 | )
8 |
9 | type ObjectType int
10 |
11 | const (
12 | Unset ObjectType = iota
13 | Resource
14 | Variable
15 | )
16 |
17 | type Parser struct {
18 | tokens []*frontendTypes.Token
19 | resources []*commonTypes.ParsedResource
20 | variables []*commonTypes.ParsedVariable
21 | index int
22 | currentObjectType ObjectType
23 | filePath string
24 | }
25 |
26 | func NewParser(tokens []*frontendTypes.Token, filePath string) *Parser {
27 | return &Parser{
28 | tokens: tokens,
29 | resources: make([]*commonTypes.ParsedResource, 0),
30 | variables: make([]*commonTypes.ParsedVariable, 0),
31 | index: 0,
32 | currentObjectType: Unset,
33 | filePath: filePath,
34 | }
35 | }
36 |
37 | func (p *Parser) Parse() ([]*commonTypes.ParsedResource, []*commonTypes.ParsedVariable, error) {
38 | for p.index < len(p.tokens) {
39 | switch p.currentToken().Type {
40 | case frontendTypes.EOF:
41 | return p.resources, p.variables, nil
42 | case frontendTypes.Newline:
43 | p.advance()
44 | if p.currentToken().Type == frontendTypes.Newline {
45 | p.setCurrentObjectType(Unset)
46 | }
47 | case frontendTypes.ConstructorName:
48 | err := p.parseConstructor()
49 | if err != nil {
50 | return nil, nil, err
51 | }
52 | case frontendTypes.NaturalLanguage:
53 | err := p.parseNaturalLanguage()
54 | if err != nil {
55 | return nil, nil, err
56 | }
57 | default:
58 | return nil, nil, p.parseError(p.currentToken())
59 | }
60 | }
61 | return nil, nil, &errors.MissingEOFTokenError{FilePath: p.filePath}
62 | }
63 |
64 | func (p *Parser) currentResource() *commonTypes.ParsedResource {
65 | return p.resources[len(p.resources)-1]
66 | }
67 |
68 | func (p *Parser) currentVariable() *commonTypes.ParsedVariable {
69 | return p.variables[len(p.variables)-1]
70 | }
71 |
72 | func (p *Parser) setCurrentObjectType(t ObjectType) {
73 | switch t {
74 | case Resource:
75 | newResource := commonTypes.NewParsedResource(p.filePath, p.currentToken().Line)
76 | p.resources = append(p.resources, newResource)
77 | case Variable:
78 | newVariable := commonTypes.NewParsedVariable(p.filePath, p.currentToken().Line)
79 | p.variables = append(p.variables, newVariable)
80 | }
81 | p.currentObjectType = t
82 | }
83 |
84 | func (p *Parser) currentObjectTypeIs(objectType ObjectType) bool {
85 | return p.currentObjectType == objectType
86 | }
87 |
88 | func (p *Parser) currentObject() frontendTypes.ParsedObject {
89 | switch p.currentObjectType {
90 | case Resource:
91 | return p.currentResource()
92 | case Variable:
93 | return p.currentVariable()
94 | default:
95 | return nil
96 | }
97 | }
98 |
99 | func (p *Parser) currentToken() *frontendTypes.Token {
100 | return p.tokens[p.index]
101 | }
102 |
103 | func (p *Parser) advance() {
104 | p.index++
105 | }
106 |
107 | func (p *Parser) parseError(token *frontendTypes.Token, messages ...string) error {
108 | error := errors.ParseError{
109 | Token: token,
110 | FilePath: p.filePath,
111 | }
112 | if len(messages) > 0 {
113 | error.Message = messages[0]
114 | }
115 | return &error
116 | }
117 |
--------------------------------------------------------------------------------
/common/change_set/change_set_test.go:
--------------------------------------------------------------------------------
1 | package change_set_test
2 |
3 | import (
4 | "encoding/json"
5 | "io"
6 | "os"
7 | "salami/common/change_set"
8 | "salami/common/symbol_table"
9 | "salami/common/types"
10 | "salami/common/utils/object_utils"
11 | "sort"
12 | "testing"
13 |
14 | "github.com/stretchr/testify/require"
15 | )
16 |
17 | func TestNewChangeSet(t *testing.T) {
18 | t.Run("should return an empty change set when there are no changes", func(t *testing.T) {
19 | previousObjects := getObjects("testdata/previous_objects.json")
20 | newResources, newVariables := object_utils.ObjectsToParsedObjects(previousObjects)
21 | symbolTable, err := symbol_table.NewSymbolTable(newResources, newVariables)
22 | require.NoError(t, err)
23 | changeSet := change_set.NewChangeSet(previousObjects, symbolTable)
24 | require.Equal(t, changeSet, &types.ChangeSet{Diffs: []*types.ChangeSetDiff{}})
25 | })
26 |
27 | t.Run("should return a change set with additions, deletions, and changes when they exist", func(t *testing.T) {
28 | previousObjects := getObjects("testdata/previous_objects.json")
29 | newObjects := getObjects("testdata/new_objects.json")
30 | newResources, newVariables := object_utils.ObjectsToParsedObjects(newObjects)
31 | symbolTable, err := symbol_table.NewSymbolTable(newResources, newVariables)
32 |
33 | require.NoError(t, err)
34 | changeSet := change_set.NewChangeSet(previousObjects, symbolTable)
35 | changeSetDiffs := sortChangeSetDiffs(changeSet.Diffs)
36 | require.Equal(t, 5, len(changeSetDiffs))
37 | expectedDiffs := getChangeSetDiffs("testdata/change_set_diffs.json")
38 | for i, actualDiff := range changeSetDiffs {
39 | require.Equal(t, expectedDiffs[i], actualDiff)
40 | }
41 | })
42 | }
43 |
44 | func getObjects(filePaths string) []*types.Object {
45 | jsonFile, err := os.Open(filePaths)
46 | if err != nil {
47 | panic(err)
48 | }
49 | defer jsonFile.Close()
50 |
51 | byteValue, _ := io.ReadAll(jsonFile)
52 |
53 | var objects []*types.Object
54 | json.Unmarshal(byteValue, &objects)
55 | return objects
56 | }
57 |
58 | func getChangeSetDiffs(filePath string) []*types.ChangeSetDiff {
59 | jsonFile, err := os.Open(filePath)
60 | if err != nil {
61 | panic(err)
62 | }
63 | defer jsonFile.Close()
64 |
65 | byteValue, _ := io.ReadAll(jsonFile)
66 |
67 | var changeSetDiffs []*types.ChangeSetDiff
68 | json.Unmarshal(byteValue, &changeSetDiffs)
69 | return changeSetDiffs
70 | }
71 |
72 | func sortChangeSetDiffs(diffs []*types.ChangeSetDiff) []*types.ChangeSetDiff {
73 | getNameAndType := func(obj *types.Object) (string, bool) {
74 | if obj == nil {
75 | return "", false
76 | }
77 | isVar := obj.IsVariable()
78 | name := ""
79 | if isVar {
80 | name = obj.ParsedVariable.Name
81 | } else {
82 | name = string(obj.ParsedResource.LogicalName)
83 | }
84 | return name, isVar
85 | }
86 |
87 | sort.Slice(diffs, func(i, j int) bool {
88 | iOldName, iOldIsVar := getNameAndType(diffs[i].OldObject)
89 | iNewName, iNewIsVar := getNameAndType(diffs[i].NewObject)
90 | jOldName, jOldIsVar := getNameAndType(diffs[j].OldObject)
91 | jNewName, jNewIsVar := getNameAndType(diffs[j].NewObject)
92 |
93 | if iOldIsVar != jOldIsVar {
94 | return iOldIsVar
95 | }
96 |
97 | if iNewIsVar != jNewIsVar {
98 | return iNewIsVar
99 | }
100 |
101 | if iOldName != jOldName {
102 | return iOldName < jOldName
103 | }
104 |
105 | return iNewName < jNewName
106 | })
107 |
108 | return diffs
109 | }
110 |
--------------------------------------------------------------------------------
/common/config/validate_test.go:
--------------------------------------------------------------------------------
1 | package config_test
2 |
3 | import (
4 | "path/filepath"
5 | "salami/common/config"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestValidateConfig(t *testing.T) {
12 | testCases := getTestCases()
13 |
14 | for _, tc := range testCases {
15 | t.Run(tc.name, func(t *testing.T) {
16 | setConfigFile(t, tc.fileName)
17 | err := config.ValidateConfig()
18 | require.Equal(t, err != nil, tc.wantErr, "unexpected error status: got error = %v, wantErr %v", err, tc.wantErr)
19 | if err != nil {
20 | require.Equal(
21 | t,
22 | tc.expectedErrorMessage,
23 | err.Error(),
24 | "unexpected error message: got = %v, want = %v",
25 | err.Error(),
26 | tc.expectedErrorMessage,
27 | )
28 | }
29 | })
30 | }
31 | }
32 |
33 | func setConfigFile(t *testing.T, fileName string) {
34 | fixturePath := filepath.Join("testdata", "config_files", fileName)
35 | config.SetConfigFilePath(fixturePath)
36 | }
37 |
38 | type testCase struct {
39 | name string
40 | fileName string
41 | wantErr bool
42 | expectedErrorMessage string
43 | }
44 |
45 | func getTestCases() []testCase {
46 | return []testCase{
47 | {
48 | "Valid config with all required fields",
49 | "valid.yaml",
50 | false,
51 | "",
52 | },
53 | {
54 | "Non-existing source directory",
55 | "invalid_source_dir.yaml",
56 | true,
57 | "config error: 'testdata/non_existent_dir' directory could not be resolved",
58 | },
59 | {
60 | "Missing target",
61 | "missing_target.yaml",
62 | true,
63 | "config error: invalid target configuration",
64 | },
65 | {
66 | "Invalid target platform",
67 | "invalid_platform.yaml",
68 | true,
69 | "config error: invalid target configuration",
70 | },
71 | {
72 | "Missing target platform",
73 | "missing_target_platform.yaml",
74 | true,
75 | "config error: invalid target configuration",
76 | },
77 | {
78 | "Missing llm",
79 | "missing_llm.yaml",
80 | true,
81 | "config error: invalid llm configuration",
82 | },
83 | {
84 | "Invalid llm provider",
85 | "invalid_llm_provider.yaml",
86 | true,
87 | "config error: invalid llm configuration",
88 | },
89 | {
90 | "Missing llm provider",
91 | "missing_llm_provider.yaml",
92 | true,
93 | "config error: invalid llm configuration",
94 | },
95 | {
96 | "Invalid llm model",
97 | "invalid_llm_model.yaml",
98 | true,
99 | "config error: invalid llm configuration",
100 | },
101 | {
102 | "Missing llm model",
103 | "missing_llm_model.yaml",
104 | true,
105 | "config error: invalid llm configuration",
106 | },
107 | {
108 | "Missing llm api key",
109 | "missing_llm_api_key.yaml",
110 | true,
111 | "config error: invalid llm configuration",
112 | },
113 | {
114 | "Invalid yaml format",
115 | "invalid_yaml.yaml",
116 | true,
117 | "config error: could not parse config file. Ensure it is valid yaml format",
118 | },
119 | {
120 | "Target directory outside of program's root directory",
121 | "target_dir_outside_root.yaml",
122 | true,
123 | "config error: target directory must be a subdirectory inside the root of the project",
124 | },
125 | {
126 | "Target directory equals the program's root directory",
127 | "target_dir_equals_root.yaml",
128 | true,
129 | "config error: target directory must be a subdirectory inside the root of the project",
130 | },
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/terraform/ecs.tf:
--------------------------------------------------------------------------------
1 | resource "aws_ecs_cluster" "EcsCluster" {
2 | name = "cluster"
3 | }
4 |
5 | resource "aws_service_discovery_private_dns_namespace" "EcsPrivateDnsNamespace" {
6 | name = var.local_dns_namespace_name
7 | description = "Private namespace for ECS cluster"
8 | vpc = aws_vpc.MainVpc.id
9 | }
10 |
11 | resource "aws_ecs_service" "ServerEcsService" {
12 | name = "server"
13 | cluster = aws_ecs_cluster.EcsCluster.id
14 | task_definition = aws_ecs_task_definition.ServerTaskDefinition.arn
15 | desired_count = 1
16 | launch_type = "FARGATE"
17 |
18 | network_configuration {
19 | assign_public_ip = true
20 | subnets = [aws_subnet.PublicSubnetA.id, aws_subnet.PublicSubnetB.id]
21 | security_groups = [aws_security_group.ServerEcsSecurityGroup.id]
22 | }
23 |
24 | load_balancer {
25 | target_group_arn = aws_lb_target_group.ServerTargetGroup.arn
26 | container_name = var.server_container_name
27 | container_port = var.container_port
28 | }
29 |
30 | deployment_controller {
31 | type = "ECS"
32 | }
33 |
34 | deployment_circuit_breaker {
35 | enable = true
36 | rollback = true
37 | }
38 |
39 | wait_for_steady_state = true
40 | }
41 |
42 | resource "aws_service_discovery_service" "PythonExecEcsServiceDiscovery" {
43 | name = var.python_exec_local_service_name
44 |
45 | dns_config {
46 | namespace_id = aws_service_discovery_private_dns_namespace.EcsPrivateDnsNamespace.id
47 | dns_records {
48 | ttl = 10
49 | type = "A"
50 | }
51 | }
52 | }
53 |
54 | resource "aws_security_group" "ServerEcsSecurityGroup" {
55 | name = "server-ecs-security-group"
56 | description = "Security group for Server ECS service"
57 | vpc_id = aws_vpc.MainVpc.id
58 |
59 | egress {
60 | from_port = 0
61 | to_port = 0
62 | protocol = "-1"
63 | cidr_blocks = ["0.0.0.0/0"]
64 | }
65 |
66 | ingress {
67 | from_port = var.container_port
68 | to_port = var.container_port
69 | protocol = "tcp"
70 | security_groups = [aws_security_group.AlbSecurityGroup.id]
71 | }
72 | }
73 |
74 | resource "aws_ecs_service" "PythonExecEcsService" {
75 | name = "python-exec"
76 | cluster = aws_ecs_cluster.EcsCluster.id
77 | task_definition = aws_ecs_task_definition.PythonExecTaskDefinition.arn
78 | desired_count = 1
79 | launch_type = "FARGATE"
80 |
81 | deployment_controller {
82 | type = "ECS"
83 | }
84 |
85 | network_configuration {
86 | assign_public_ip = false
87 | subnets = [aws_subnet.PrivateSubnetA.id, aws_subnet.PrivateSubnetB.id]
88 | security_groups = [aws_security_group.PythonExecEcsSecurityGroup.id]
89 | }
90 |
91 | deployment_circuit_breaker {
92 | enable = true
93 | rollback = true
94 | }
95 |
96 | wait_for_steady_state = true
97 | }
98 |
99 | resource "aws_security_group" "PythonExecEcsSecurityGroup" {
100 | name = "python-exec-ecs-security-group"
101 | description = "Security group for python exec ECS service"
102 | vpc_id = aws_vpc.MainVpc.id
103 |
104 | egress {
105 | from_port = 443
106 | to_port = 443
107 | protocol = "tcp"
108 | cidr_blocks = ["0.0.0.0/0"]
109 | }
110 |
111 | ingress {
112 | from_port = var.container_port
113 | to_port = var.container_port
114 | protocol = "tcp"
115 | security_groups = [aws_security_group.ServerEcsSecurityGroup.id]
116 | }
117 | }
--------------------------------------------------------------------------------
/common/driver/frontend.go:
--------------------------------------------------------------------------------
1 | package driver
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "salami/common/config"
7 | "salami/common/constants"
8 | "salami/common/symbol_table"
9 | "salami/common/types"
10 | "salami/common/utils/file_utils"
11 | "salami/frontend/lexer"
12 | "salami/frontend/parser"
13 | "salami/frontend/semantic_analyzer"
14 | "sync"
15 | )
16 |
17 | func runFrontend() (*symbol_table.SymbolTable, []error) {
18 | sourceFilePaths, err := getSourceFilePaths()
19 | if err != nil {
20 | return nil, []error{err}
21 | }
22 |
23 | allResources, allVariables, errors := parseFiles(sourceFilePaths, config.GetSourceDir())
24 | if len(errors) > 0 {
25 | return nil, errors
26 | }
27 |
28 | symbolTable, err := symbol_table.NewSymbolTable(allResources, allVariables)
29 | if err != nil {
30 | return nil, []error{err}
31 | }
32 |
33 | semanticAnalyzer := semantic_analyzer.NewSemanticAnalyzer(symbolTable)
34 | if err = semanticAnalyzer.Analyze(); err != nil {
35 | return nil, []error{err}
36 | }
37 |
38 | return symbolTable, nil
39 | }
40 |
41 | func getSourceFilePaths() ([]string, error) {
42 | sourceFilePaths, err := file_utils.GetFilePaths(config.GetSourceDir(), func(path string) bool {
43 | return filepath.Ext(path) == constants.SalamiFileExtension
44 | })
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | relativeSourceFilePaths, err := file_utils.GetRelativeFilePaths(
50 | config.GetSourceDir(),
51 | sourceFilePaths,
52 | )
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | return relativeSourceFilePaths, nil
58 | }
59 |
60 | func parseFiles(filePaths []string, sourceDir string) ([]*types.ParsedResource, []*types.ParsedVariable, []error) {
61 | resourcesChan := make(chan []*types.ParsedResource, len(filePaths))
62 | variablesChan := make(chan []*types.ParsedVariable, len(filePaths))
63 | errorChan := make(chan error, len(filePaths))
64 |
65 | var wg sync.WaitGroup
66 | for _, filePath := range filePaths {
67 | wg.Add(1)
68 | go func(filePath string) {
69 | resources, variables, err := parseFile(filePath, sourceDir)
70 | if err != nil {
71 | errorChan <- err
72 | }
73 | if resources != nil {
74 | resourcesChan <- resources
75 | }
76 | if variables != nil {
77 | variablesChan <- variables
78 | }
79 | wg.Done()
80 | }(filePath)
81 | }
82 | wg.Wait()
83 | close(resourcesChan)
84 | close(variablesChan)
85 | close(errorChan)
86 |
87 | var allResources []*types.ParsedResource
88 | var allVariables []*types.ParsedVariable
89 | var allErrors []error
90 | for resources := range resourcesChan {
91 | allResources = append(allResources, resources...)
92 | }
93 | for variables := range variablesChan {
94 | allVariables = append(allVariables, variables...)
95 | }
96 | for err := range errorChan {
97 | allErrors = append(allErrors, err)
98 | }
99 |
100 | return allResources, allVariables, allErrors
101 | }
102 |
103 | func parseFile(filePath string, sourceDir string) (
104 | resources []*types.ParsedResource,
105 | variables []*types.ParsedVariable,
106 | err error,
107 | ) {
108 | fullRelativeFilePath := filepath.Join(sourceDir, filePath)
109 | content, err := os.ReadFile(fullRelativeFilePath)
110 | if err != nil {
111 | return nil, nil, err
112 | }
113 |
114 | lexerInstance := lexer.NewLexer(filePath, string(content))
115 | tokens, err := lexerInstance.Run()
116 |
117 | if err != nil {
118 | return nil, nil, err
119 | }
120 | parserInstance := parser.NewParser(tokens, filePath)
121 | resources, variables, parsingError := parserInstance.Parse()
122 | return resources, variables, parsingError
123 | }
124 |
--------------------------------------------------------------------------------
/common/config/validate.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "salami/common/types"
8 | "strings"
9 |
10 | "github.com/go-playground/validator/v10"
11 | )
12 |
13 | func ValidateConfig() error {
14 | err := LoadConfig()
15 | if err != nil {
16 | return err
17 | }
18 | validate := newValidator()
19 |
20 | if err := validate.Struct(getConfig()); err != nil {
21 | if _, ok := err.(*validator.InvalidValidationError); ok {
22 | return err
23 | }
24 |
25 | for _, err := range err.(validator.ValidationErrors) {
26 | fieldValue := err.Value()
27 | namespace := err.Namespace()
28 | switch err.Tag() {
29 | case "valid_target":
30 | return getInvalidFieldError(namespace, nil)
31 | case "valid_llm":
32 | return getInvalidFieldError(namespace, nil)
33 | case "dir_exists":
34 | return &ConfigError{Message: fmt.Sprintf("'%s' directory could not be resolved", fieldValue)}
35 | case "target_dir_valid":
36 | return &ConfigError{
37 | Message: "target directory must be a subdirectory inside the root of the project",
38 | }
39 | case "required":
40 | return getMissingFieldError(namespace)
41 | default:
42 | return err
43 | }
44 | }
45 | }
46 | return nil
47 | }
48 |
49 | type CompilerConfig struct {
50 | Target CompilerTargetConfig `yaml:"target" validate:"valid_target"`
51 | Llm CompilerLlmConfig `yaml:"llm" validate:"valid_llm"`
52 | SourceDir string `yaml:"source_dir" validate:"required,dir_exists"`
53 | TargetDir string `yaml:"target_dir" validate:"required,target_dir_valid"`
54 | }
55 |
56 | type ConfigType struct {
57 | Compiler CompilerConfig `yaml:"compiler" validate:"required"`
58 | }
59 |
60 | type CompilerTargetConfig struct {
61 | Platform string `yaml:"platform" validate:"required"`
62 | }
63 |
64 | type CompilerLlmConfig struct {
65 | Provider string `yaml:"provider"`
66 | Model string `yaml:"model"`
67 | ApiKey string `yaml:"api_key"`
68 | MaxConcurrentExecutions int `yaml:"max_concurrent"`
69 | }
70 |
71 | func validateTarget(fl validator.FieldLevel) bool {
72 | target, ok := fl.Field().Interface().(CompilerTargetConfig)
73 | if !ok {
74 | return false
75 | }
76 | return target.Platform == types.TerraformPlatform
77 | }
78 |
79 | func validateLlm(fl validator.FieldLevel) bool {
80 | llmConfig, ok := fl.Field().Interface().(CompilerLlmConfig)
81 | if !ok {
82 | return false
83 | }
84 |
85 | validLlmProvider := llmConfig.Provider == types.LlmOpenaiProvider
86 | validLlmModel := llmConfig.Model == types.LlmGpt4Model
87 | apiKeyExists := llmConfig.ApiKey != ""
88 |
89 | return validLlmProvider && validLlmModel && apiKeyExists
90 | }
91 |
92 | func validateDirExists(fl validator.FieldLevel) bool {
93 | dir := fl.Field().String()
94 | _, err := os.Stat(dir)
95 | return !os.IsNotExist(err)
96 | }
97 |
98 | func validateTargetDir(fl validator.FieldLevel) bool {
99 | targetDir := fl.Field().String()
100 | absTargetDir, err := filepath.Abs(targetDir)
101 | if err != nil {
102 | return false
103 | }
104 |
105 | rootDir, err := os.Getwd()
106 | if err != nil {
107 | return false
108 | }
109 |
110 | rel, err := filepath.Rel(rootDir, absTargetDir)
111 | if err != nil {
112 | return false
113 | }
114 |
115 | if rel == "." {
116 | return false
117 | }
118 |
119 | return !strings.HasPrefix(rel, "..")
120 | }
121 |
122 | func newValidator() *validator.Validate {
123 | validate := validator.New(validator.WithRequiredStructEnabled())
124 | validate.RegisterValidation("valid_target", validateTarget)
125 | validate.RegisterValidation("valid_llm", validateLlm)
126 | validate.RegisterValidation("dir_exists", validateDirExists)
127 | validate.RegisterValidation("target_dir_valid", validateTargetDir)
128 | return validate
129 | }
130 |
--------------------------------------------------------------------------------
/common/lock_file_manager/validate.go:
--------------------------------------------------------------------------------
1 | package lock_file_manager
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "regexp"
7 |
8 | "github.com/BurntSushi/toml"
9 | "github.com/go-playground/validator/v10"
10 | )
11 |
12 | func ValidateLockFile() error {
13 | lockFile := &LockFile{}
14 | decodeLockFile(lockFile)
15 |
16 | if isEmptyLockFile(lockFile) {
17 | return nil
18 | }
19 | validate := newValidator()
20 | if err := validate.Struct(lockFile); err != nil {
21 | if _, ok := err.(*validator.InvalidValidationError); ok {
22 | return err
23 | }
24 |
25 | for _, err := range err.(validator.ValidationErrors) {
26 | fieldValue := err.Value()
27 | namespace := err.Namespace()
28 | switch err.Tag() {
29 | case "semver":
30 | return &LockFileError{Message: fmt.Sprintf("'%s' is not a valid semver", fieldValue)}
31 | case "required":
32 | return getMissingFieldError(namespace)
33 | case "oneof":
34 | return &LockFileError{Message: fmt.Sprintf("'%s' is not a valid value", fieldValue)}
35 | default:
36 | return err
37 | }
38 | }
39 | }
40 | return nil
41 | }
42 |
43 | type LockFile struct {
44 | Version string `toml:"version" validate:"required,semver"`
45 | TargetFileMetas []TargetFileMeta `toml:"target_files_meta" validate:"dive"`
46 | Objects []Object `toml:"objects" validate:"dive"`
47 | }
48 |
49 | type TargetFileMeta struct {
50 | FilePath string `toml:"file_path" validate:"required"`
51 | Checksum string `toml:"checksum" validate:"required"`
52 | }
53 |
54 | type Object struct {
55 | ParsedResource *ParsedResource `toml:"parsed_resource" validate:"required_without=ParsedVariable"`
56 | ParsedVariable *ParsedVariable `toml:"parsed_variable" validate:"required_without=ParsedResource"`
57 | TargetCode string `toml:"target_code" validate:"required"`
58 | }
59 |
60 | func (o *Object) IsResource() bool {
61 | return o.ParsedResource != nil
62 | }
63 |
64 | func (o *Object) IsVariable() bool {
65 | return o.ParsedVariable != nil
66 | }
67 |
68 | type ParsedVariable struct {
69 | Name string `toml:"name" validate:"required"`
70 | NaturalLanguage string `toml:"natural_language"`
71 | VariableType string `toml:"type" validate:"required,oneof=string number boolean"`
72 | Default string `toml:"default"`
73 | SourceFilePath string `toml:"source_file_path" validate:"required"`
74 | SourceFileLine int `toml:"source_file_line" validate:"required"`
75 | }
76 |
77 | type ParsedResource struct {
78 | ResourceType string `toml:"resource_type" validate:"required"`
79 | LogicalName string `toml:"logical_name" validate:"required"`
80 | NaturalLanguage string `toml:"natural_language"`
81 | ReferencedResources []string `toml:"referenced_resources"`
82 | ReferencedVariables []string `toml:"referenced_variables"`
83 | SourceFilePath string `toml:"source_file_path" validate:"required"`
84 | SourceFileLine int `toml:"source_file_line" validate:"required"`
85 | }
86 |
87 | func decodeLockFile(lockFile *LockFile) error {
88 | if _, err := toml.DecodeFile(lockFilePath, lockFile); err != nil {
89 | if err != nil && !os.IsNotExist(err) {
90 | return &LockFileError{Message: "could not parse lock file"}
91 | }
92 | }
93 | loadedLockFile = lockFile
94 | return nil
95 | }
96 |
97 | func validateSemVer(fl validator.FieldLevel) bool {
98 | numericVersionRegex := `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$`
99 | match, _ := regexp.MatchString(numericVersionRegex, fl.Field().String())
100 | return match
101 | }
102 |
103 | func newValidator() *validator.Validate {
104 | validate := validator.New()
105 | validate.RegisterValidation("semver", validateSemVer)
106 | return validate
107 | }
108 |
109 | func isEmptyLockFile(lf *LockFile) bool {
110 | return lf.Version == "" &&
111 | len(lf.TargetFileMetas) == 0 &&
112 | len(lf.Objects) == 0
113 | }
114 |
--------------------------------------------------------------------------------
/common/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type ResourceType string
4 | type LogicalName string
5 |
6 | type ParsedResource struct {
7 | ResourceType ResourceType
8 | LogicalName LogicalName
9 | NaturalLanguage string
10 | ReferencedResources []LogicalName
11 | ReferencedVariables []string
12 | SourceFilePath string
13 | SourceFileLine int
14 | }
15 |
16 | func NewParsedResource(SourceFilePath string, SourceFileLine int) *ParsedResource {
17 | return &ParsedResource{
18 | ReferencedResources: []LogicalName{},
19 | ReferencedVariables: []string{},
20 | SourceFilePath: SourceFilePath,
21 | SourceFileLine: SourceFileLine,
22 | }
23 | }
24 |
25 | func (r *ParsedResource) AddNaturalLanguage(NaturalLanguage string) {
26 | if r.NaturalLanguage == "" {
27 | r.NaturalLanguage = NaturalLanguage
28 | } else {
29 | r.NaturalLanguage = r.NaturalLanguage + "\n" + NaturalLanguage
30 | }
31 | }
32 |
33 | type VariableType string
34 |
35 | type ParsedVariable struct {
36 | Name string
37 | NaturalLanguage string
38 | Default string
39 | Type VariableType
40 | SourceFilePath string
41 | SourceFileLine int
42 | }
43 |
44 | func NewParsedVariable(SourceFilePath string, SourceFileLine int) *ParsedVariable {
45 | return &ParsedVariable{
46 | SourceFilePath: SourceFilePath,
47 | SourceFileLine: SourceFileLine,
48 | }
49 | }
50 |
51 | func (v *ParsedVariable) AddNaturalLanguage(NaturalLanguage string) {
52 | if v.NaturalLanguage == "" {
53 | v.NaturalLanguage = NaturalLanguage
54 | } else {
55 | v.NaturalLanguage = v.NaturalLanguage + "\n" + NaturalLanguage
56 | }
57 | }
58 |
59 | type Object struct {
60 | ParsedResource *ParsedResource
61 | ParsedVariable *ParsedVariable
62 | TargetCode string
63 | }
64 |
65 | func (o *Object) IsResource() bool {
66 | return o.ParsedResource != nil
67 | }
68 |
69 | func (o *Object) IsVariable() bool {
70 | return o.ParsedVariable != nil
71 | }
72 |
73 | func (o *Object) GetSourceFilePath() string {
74 | if o.IsResource() {
75 | return o.ParsedResource.SourceFilePath
76 | } else if o.IsVariable() {
77 | return o.ParsedVariable.SourceFilePath
78 | }
79 | return ""
80 | }
81 |
82 | func (o *Object) GetSourceFileLine() int {
83 | if o.IsResource() {
84 | return o.ParsedResource.SourceFileLine
85 | } else if o.IsVariable() {
86 | return o.ParsedVariable.SourceFileLine
87 | }
88 | return 0
89 | }
90 |
91 | func (o *Object) SetTargetCode(targetCode string) {
92 | o.TargetCode = targetCode
93 | }
94 |
95 | const (
96 | DiffTypeAdd = "add"
97 | DiffTypeRemove = "remove"
98 | DiffTypeUpdate = "update"
99 | DiffTypeMove = "move"
100 | )
101 |
102 | type ChangeSetDiff struct {
103 | OldObject *Object
104 | NewObject *Object
105 | DiffType string
106 | }
107 |
108 | func (ch *ChangeSetDiff) IsUpdate() bool {
109 | return ch.DiffType == DiffTypeUpdate
110 | }
111 |
112 | func (ch *ChangeSetDiff) IsAdd() bool {
113 | return ch.DiffType == DiffTypeAdd
114 | }
115 |
116 | func (ch *ChangeSetDiff) IsRemove() bool {
117 | return ch.DiffType == DiffTypeRemove
118 | }
119 |
120 | func (ch *ChangeSetDiff) IsMove() bool {
121 | return ch.DiffType == DiffTypeMove
122 | }
123 |
124 | type ChangeSet struct {
125 | Diffs []*ChangeSetDiff
126 | }
127 |
128 | type TargetFileMeta struct {
129 | FilePath string
130 | Checksum string
131 | }
132 |
133 | type TargetConfig struct {
134 | Platform string
135 | }
136 |
137 | type LlmConfig struct {
138 | Provider string
139 | Model string
140 | ApiKey string
141 | MaxConcurrentExecutions int
142 | }
143 |
144 | const TerraformPlatform = "terraform"
145 | const LlmOpenaiProvider = "openai"
146 | const LlmGpt4Model = "gpt4"
147 | const LlmOpenaiGpt4 = LlmOpenaiProvider + "_" + LlmGpt4Model
148 |
149 | type TargetFile struct {
150 | FilePath string
151 | Content string
152 | }
153 |
--------------------------------------------------------------------------------
/examples/public_and_private_ecs_services/terraform/vpc_endpoints.tf:
--------------------------------------------------------------------------------
1 | resource "aws_vpc_endpoint" "EcrDkrVpcEndpoint" {
2 | vpc_id = aws_vpc.MainVpc.id
3 | service_name = "com.amazonaws.${var.aws_region}.ecr.dkr"
4 | vpc_endpoint_type = "Interface"
5 | private_dns_enabled = true
6 | subnet_ids = [
7 | aws_subnet.PrivateSubnetA.id,
8 | aws_subnet.PrivateSubnetB.id
9 | ]
10 | security_group_ids = [aws_security_group.EcrVpcEndpointSG.id]
11 |
12 | policy = <> $GITHUB_OUTPUT
52 |
53 | - name: Get binary name
54 | id: get-binary-name
55 | run: |
56 | echo "binary-name=salami-${{ steps.salami-version.outputs.version }}-${{ matrix.goos }}-${{ matrix.arch_name }}" >> $GITHUB_OUTPUT
57 |
58 | - name: Build binary
59 | run: |
60 | GOOS=${{ matrix.goos }} \
61 | GOARCH=${{ matrix.goarch }} \
62 | go build -o ${{ steps.get-binary-name.outputs.binary-name }}
63 | working-directory: ./cli
64 |
65 | - name: Upload binary artifact
66 | uses: actions/upload-artifact@v3
67 | with:
68 | name: ${{ steps.get-binary-name.outputs.binary-name }}
69 | path: cli/${{ steps.get-binary-name.outputs.binary-name }}
70 | if-no-files-found: error
71 |
72 | verify-version:
73 | name: Verify version
74 |
75 | needs: build
76 |
77 | runs-on: ubuntu-latest
78 |
79 | steps:
80 | - name: Get Salami version
81 | id: salami-version
82 | run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
83 |
84 | - name: Download binary artifacts
85 | uses: actions/download-artifact@v3
86 | with:
87 | name: salami-${{ steps.salami-version.outputs.version }}-linux-x64
88 |
89 | - name: Verify version
90 | run: |
91 | version_from_tag=${{ steps.salami-version.outputs.version }}
92 | chmod +x ./salami-$version_from_tag-linux-x64
93 | version_from_binary=$(./salami-$version_from_tag-linux-x64 version | awk '{print $3}')
94 |
95 | if [ "$version_from_binary" != "$version_from_tag" ]; then
96 | echo "Version mismatch: $version_from_binary != $version_from_tag"
97 | exit 1
98 | fi
99 |
100 | create-release:
101 | name: Create draft release
102 |
103 | needs:
104 | - build
105 | - verify-version
106 |
107 | runs-on: ubuntu-latest
108 |
109 | steps:
110 | - name: Download binary artifacts
111 | uses: actions/download-artifact@v3
112 |
113 | - name: Get Salami version
114 | id: salami-version
115 | run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
116 |
117 | - name: Create release
118 | uses: softprops/action-gh-release@v1
119 | env:
120 | VERSION: ${{ steps.salami-version.outputs.version }}
121 | with:
122 | draft: true
123 | prerelease: ${{ contains(github.ref, '-rc') }}
124 | fail_on_unmatched_files: true
125 | files: |
126 | salami-${{ env.VERSION }}-darwin-x64/salami-${{ env.VERSION }}-darwin-x64
127 | salami-${{ env.VERSION }}-darwin-arm64/salami-${{ env.VERSION }}-darwin-arm64
128 | salami-${{ env.VERSION }}-linux-x64/salami-${{ env.VERSION }}-linux-x64
129 | salami-${{ env.VERSION }}-linux-arm64/salami-${{ env.VERSION }}-linux-arm64
130 | salami-${{ env.VERSION }}-windows-x64/salami-${{ env.VERSION }}-windows-x64
131 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
2 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
7 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
8 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
9 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
10 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
11 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
12 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
13 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
14 | github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
15 | github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
16 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
17 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20 | github.com/sashabaranov/go-openai v1.15.3 h1:rzoNK9n+Cak+PM6OQ9puxDmFllxfnVea9StlmhglXqA=
21 | github.com/sashabaranov/go-openai v1.15.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
22 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
23 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
24 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
25 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
26 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
27 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
28 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
29 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
30 | go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
31 | go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
32 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
33 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
34 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
35 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
36 | golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
37 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
38 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
39 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
40 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
41 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
42 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
43 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
44 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
45 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
47 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
48 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
49 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
50 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
51 |
--------------------------------------------------------------------------------