├── 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 | --------------------------------------------------------------------------------