├── .dockerignore ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── docker.yml │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .golangci.yml ├── .goreleaser.yml ├── CONTRIBUTING.md ├── Dockerfile ├── Formula └── hcledit.rb ├── LICENSE ├── README.md ├── cmd └── hcledit │ ├── README.md │ ├── internal │ ├── command │ │ ├── create.go │ │ ├── create_test.go │ │ ├── delete.go │ │ ├── delete_test.go │ │ ├── fixture │ │ │ └── file.tf │ │ ├── read.go │ │ ├── read_test.go │ │ ├── root.go │ │ ├── root_test.go │ │ ├── update.go │ │ ├── update_test.go │ │ └── version.go │ └── version │ │ └── version.go │ └── main.go ├── example_test.go ├── go.mod ├── go.sum ├── hcledit.go ├── hcledit_test.go ├── internal ├── ast │ ├── ast.go │ ├── object.go │ ├── object_attribute.go │ ├── parser.go │ └── peeker.go ├── converter │ └── converter.go ├── handler │ ├── block.go │ ├── cty.go │ ├── handler.go │ ├── raw.go │ └── read.go ├── query │ └── query.go └── walker │ └── walker.go ├── option.go ├── option_test.go ├── read.go ├── read_test.go ├── write.go └── write_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !*.go 4 | !/internal 5 | !/cmd 6 | !/go.mod 7 | !/go.sum 8 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @tcnksm @drlau @slewiskelly @micnncim @ryan-ph @dtan4 @yanolab @suzuki-shunsuke @ryotafuwa-dev 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## WHAT 2 | (Write the change being made with this pull request) 3 | 4 | ## WHY 5 | (Write the motivation why you submit this pull request) 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | open-pull-requests-limit: 5 9 | labels: 10 | - dependencies 11 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | pull_request: 8 | 9 | jobs: 10 | docker: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v1 19 | 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v1 22 | with: 23 | version: latest 24 | buildkitd-flags: --debug 25 | 26 | - name: Cache Docker layers 27 | uses: actions/cache@v2 28 | with: 29 | path: /tmp/.buildx/cache 30 | key: ${{ runner.os }}-buildx-${{ github.sha }} 31 | restore-keys: | 32 | ${{ runner.os }}-buildx- 33 | 34 | - name: Login to DockerHub 35 | uses: docker/login-action@v1 36 | # The secrets are not available in pull_requests. This step is only 37 | # needed to push, so it's fine to skip this in PR builds. 38 | if: ${{ github.event_name != 'pull_request' }} 39 | with: 40 | username: ${{ secrets.DOCKER_USERNAME }} 41 | password: ${{ secrets.DOCKER_PASSWORD }} 42 | 43 | - name: Output version 44 | id: version 45 | run: | 46 | echo ::set-output name=version::${GITHUB_REF##*/} 47 | 48 | - name: Build and push 49 | uses: docker/build-push-action@v2 50 | with: 51 | # Only push if there was a tag pushed 52 | push: ${{ github.event_name != 'pull_request' }} 53 | cache-from: type=local,src=/tmp/.buildx/cache 54 | cache-to: type=local,dest=/tmp/.buildx/cache,mode=max 55 | tags: | 56 | ${{ github.repository }}:latest 57 | ${{ github.repository }}:${{ steps.version.outputs.version }} 58 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | lint: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Lint 17 | uses: reviewdog/action-golangci-lint@v2 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | 11 | # These permissions are needed for Goreleaser to push a branch and open a 12 | # PR to update the tap. 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | packages: write 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v4 26 | with: 27 | go-version: '1.22' 28 | 29 | - name: Cache 30 | uses: actions/cache@v2 31 | with: 32 | path: ~/go/pkg/mod 33 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 34 | restore-keys: | 35 | ${{ runner.os }}-go- 36 | 37 | - name: Run GoReleaser 38 | uses: goreleaser/goreleaser-action@v4 39 | with: 40 | distribution: goreleaser 41 | version: latest 42 | args: release --clean 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | go: [1.22.x] 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: ${{ matrix.go }} 22 | 23 | - name: Test 24 | run: go test -v -race ./... 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # golangci-lint configuration file 2 | # see: https://github.com/golangci/golangci/wiki/Configuration 3 | 4 | # Options for analysis running 5 | run: 6 | # Which dirs to skip: they won't be analyzed; 7 | # Can use regexp here: generated.*, regexp is applied on full path; 8 | # Default value is empty list, but next dirs are always skipped independently 9 | skip-dirs: 10 | - bin 11 | 12 | # All available settings of specific linters 13 | linters-settings: 14 | 15 | govet: 16 | # Report about shadowed variables 17 | check-shadowing: true 18 | 19 | golint: 20 | # Minimal confidence for issues, default is 0.8 21 | min-confidence: 0 22 | 23 | gocyclo: 24 | # Minimal code complexity to report, 30 by default (but we recommend 10-20) 25 | min-complexity: 30 26 | 27 | dupl: 28 | # Tokens count to trigger issue, 150 by default 29 | threshold: 100 30 | 31 | misspell: 32 | # Correct spellings using locale preferences for US or UK. 33 | # Default is to use a neutral variety of English. 34 | # Setting locale to US will correct the British spelling of 'colour' to 'color'. 35 | locale: US 36 | 37 | nakedret: 38 | # Make an issue if func has more lines of code than this setting and it has naked returns; default is 30 39 | max-func-lines: 0 40 | 41 | gocritic: 42 | # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty 43 | disabled-checks: 44 | - whyNoLint 45 | - wrapperFunc 46 | - ifElseChain 47 | - paramTypeCombine 48 | - singleCaseSwitch 49 | - unnamedResult 50 | - hugeParam 51 | - octalLiteral 52 | - commentedOutCode 53 | 54 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks. 55 | # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 56 | enabled-tags: 57 | - performance 58 | - style 59 | - experimental 60 | 61 | # Settings for enabling and disabling linters 62 | linters: 63 | disable-all: true 64 | enable: 65 | - bodyclose 66 | - deadcode 67 | - depguard 68 | - dogsled 69 | - errcheck 70 | - gocritic 71 | - gocyclo 72 | - gofmt 73 | - goimports 74 | - goprintffuncname 75 | - gosec 76 | - gosimple 77 | - govet 78 | - ineffassign 79 | - misspell 80 | - nakedret 81 | - nolintlint 82 | - rowserrcheck 83 | - scopelint 84 | - staticcheck 85 | - structcheck 86 | - stylecheck 87 | - typecheck 88 | - unconvert 89 | - unused 90 | - varcheck 91 | - whitespace 92 | 93 | # Configuration of issue rules 94 | issues: 95 | # Excluding configuration per-path, per-linter, per-text and per-source 96 | exclude-rules: 97 | - linters: 98 | - staticcheck 99 | text: "SA1019:" 100 | # Exclude shadow checking on the variable named err 101 | - text: "shadow: declaration of \"err\"" 102 | linters: 103 | - govet 104 | 105 | # Exclude godox check for TODOs, FIXMEs, and BUGs 106 | - text: "Line contains TODO/BUG/FIXME:" 107 | linters: 108 | - godox 109 | 110 | # Exclude some linters from running on tests files 111 | - path: _test\.go 112 | linters: 113 | - gocyclo 114 | - goconst 115 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | - go mod download 5 | - go test ./... 6 | builds: 7 | - id: hcledit 8 | binary: bin/hcledit 9 | dir: cmd/hcledit 10 | ldflags: 11 | - -s -w 12 | - -X go.mercari.io/hcledit/cmd/hcledit/internal/version.Version={{.Tag}} 13 | - -X go.mercari.io/hcledit/cmd/hcledit/internal/version.Revision={{.ShortCommit}} 14 | goos: 15 | - linux 16 | - windows 17 | - darwin 18 | env: 19 | - CGO_ENABLED=0 20 | archives: 21 | - name_template: >- 22 | {{- .ProjectName }}_ 23 | {{- .Version }}_ 24 | {{- title .Os }}_ 25 | {{- if eq .Arch "amd64" }}x86_64 26 | {{- else if eq .Arch "386" }}i386 27 | {{- else }}{{ .Arch }}{{ end }} 28 | {{- if .Arm }}v{{ .Arm }}{{ end -}} 29 | checksum: 30 | name_template: 'checksums.txt' 31 | snapshot: 32 | name_template: "{{ .Tag }}-next" 33 | changelog: 34 | sort: asc 35 | filters: 36 | exclude: 37 | - '^docs:' 38 | - '^test:' 39 | - Merge pull request 40 | - Merge branch 41 | brews: 42 | - repository: 43 | owner: mercari 44 | name: hcledit 45 | branch: update-brew-formula 46 | pull_request: 47 | enabled: true 48 | base: 49 | owner: mercari 50 | name: hcledit 51 | branch: main 52 | directory: Formula 53 | homepage: https://github.com/mercari/hcledit 54 | description: CLI to edit HCL configurations 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please read the CLA carefully before submitting your contribution to Mercari. 4 | Under any circumstances, by submitting your contribution, you are deemed to accept and agree to be bound by the terms and conditions of the CLA. 5 | 6 | https://www.mercari.com/cla 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod . 6 | COPY go.sum . 7 | RUN go mod download 8 | 9 | COPY . . 10 | RUN go build -o /bin/hcledit ./cmd/hcledit 11 | 12 | FROM alpine:3.13 13 | 14 | COPY --from=builder /bin/hcledit /usr/bin 15 | CMD ["hcledit"] 16 | -------------------------------------------------------------------------------- /Formula/hcledit.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | # frozen_string_literal: true 3 | 4 | # This file was generated by GoReleaser. DO NOT EDIT. 5 | class Hcledit < Formula 6 | desc "CLI to edit HCL configurations" 7 | homepage "https://github.com/mercari/hcledit" 8 | version "0.0.17" 9 | 10 | on_macos do 11 | on_intel do 12 | url "https://github.com/mercari/hcledit/releases/download/v0.0.17/hcledit_0.0.17_Darwin_x86_64.tar.gz" 13 | sha256 "7034b5dddaf9ba8224848a65d0bb21118d9d79722576c00a7eb292c3d31d21f4" 14 | 15 | def install 16 | bin.install "bin/hcledit" 17 | end 18 | end 19 | on_arm do 20 | url "https://github.com/mercari/hcledit/releases/download/v0.0.17/hcledit_0.0.17_Darwin_arm64.tar.gz" 21 | sha256 "cd255a9d47b24783b0f9bb545e3cc54f8035a7d37eae4649e697b9e70dd367e6" 22 | 23 | def install 24 | bin.install "bin/hcledit" 25 | end 26 | end 27 | end 28 | 29 | on_linux do 30 | on_intel do 31 | if Hardware::CPU.is_64_bit? 32 | url "https://github.com/mercari/hcledit/releases/download/v0.0.17/hcledit_0.0.17_Linux_x86_64.tar.gz" 33 | sha256 "8e3e4dabf2091263c11747f14d17cb7e9216c21f080f60fd61c0499487ff5df2" 34 | 35 | def install 36 | bin.install "bin/hcledit" 37 | end 38 | end 39 | end 40 | on_arm do 41 | if Hardware::CPU.is_64_bit? 42 | url "https://github.com/mercari/hcledit/releases/download/v0.0.17/hcledit_0.0.17_Linux_arm64.tar.gz" 43 | sha256 "4d8c29629ab14e3a144c4e243db22508c9b8575fa0f40f36acdf63871e0d4440" 44 | 45 | def install 46 | bin.install "bin/hcledit" 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Mercari, Inc. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hcledit 2 | 3 | [![workflow-test][workflow-test-badge]][workflow-test] 4 | [![release][release-badge]][release] 5 | [![pkg.go.dev][pkg.go.dev-badge]][pkg.go.dev] 6 | [![license][license-badge]][license] 7 | 8 | `hcledit` is a wrapper around the [`hclwrite`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/hclwrite) package that adds the ability to edit and manipulate [HCL](https://github.com/hashicorp/hcl) documents using a [`jq`](https://github.com/stedolan/jq)-like query/selector syntax. 9 | 10 | We provide a Go package and a simple CLI application based on this package. See [`hcledit` command](cmd/hcledit/README.md). 11 | 12 | *NOTE*: This is still under heavy development and we don't have enough documentation and we are planing to add breaking changes. Please be careful when using it. 13 | 14 | ## Install 15 | 16 | Use `go install`: 17 | 18 | ```bash 19 | $ go install go.mercari.io/hcledit/cmd/hcledit@latest 20 | ``` 21 | 22 | ## Usage 23 | 24 | See [Go doc][pkg.go.dev]. 25 | 26 | ## Examples 27 | 28 | The following is an HCL configuration which we want to manipulate. 29 | 30 | ```hcl 31 | resource "google_container_node_pool" "nodes1" { 32 | name = "nodes1" 33 | 34 | node_config { 35 | preemptible = false 36 | machine_type = "e2-medium" 37 | } 38 | } 39 | ``` 40 | 41 | To create a new attribute, 42 | 43 | ```go 44 | editor, _ := hcledit.ReadFile(filename) 45 | editor.Create("resource.google_container_node_pool.*.node_config.image_type", "COS") 46 | editor.OverWriteFile() 47 | ``` 48 | 49 | ```diff 50 | resource "google_container_node_pool" "nodes1" { 51 | name = "nodes1" 52 | 53 | node_config { 54 | preemptible = false 55 | machine_type = "e2-medium" 56 | + image_type = "COS" 57 | } 58 | } 59 | ``` 60 | 61 | To update the existing attribute, 62 | 63 | ```go 64 | editor, _ := hcledit.ReadFile(filename) 65 | editor.Update("resource.google_container_node_pool.*.node_config.machine_type", "e2-highmem-2") 66 | editor.OverWriteFile() 67 | ``` 68 | 69 | ```diff 70 | resource "google_container_node_pool" "nodes1" { 71 | name = "nodes1" 72 | 73 | node_config { 74 | preemptible = false 75 | - machine_type = "e2-medium" 76 | + machine_type = "e2-highmem-2" 77 | } 78 | } 79 | ``` 80 | 81 | To delete the existing attribute, 82 | 83 | ```go 84 | editor, _ := hcledit.ReadFile(filename) 85 | editor.Delete("resource.google_container_node_pool.*.node_config.machine_type") 86 | editor.OverWriteFile() 87 | ``` 88 | 89 | ```diff 90 | resource "google_container_node_pool" "nodes1" { 91 | name = "nodes1" 92 | 93 | node_config { 94 | preemptible = false 95 | - machine_type = "e2-medium" 96 | } 97 | } 98 | ``` 99 | 100 | ## Contribution 101 | 102 | During the active development, we unlikely accept PRs for new features but welcome bug fixes and documentation. 103 | If you find issues, please submit an issue first. 104 | 105 | If you want to submit a PR for bug fixes or documentation, please read the [CONTRIBUTING.md](CONTRIBUTING.md) and follow the instruction beforehand. 106 | 107 | ## License 108 | 109 | The hcledit is released under the [MIT License](LICENSE). 110 | 111 | 112 | 113 | [workflow-test]: https://github.com/mercari/hcledit/actions?query=workflow%3ATest 114 | [workflow-test-badge]: https://img.shields.io/github/workflow/status/mercari/hcledit/Test?label=Test&style=for-the-badge&logo=github 115 | 116 | [release]: https://github.com/mercari/hcledit/releases 117 | [release-badge]: https://img.shields.io/github/v/release/mercari/hcledit?style=for-the-badge&logo=github 118 | 119 | [pkg.go.dev]: https://pkg.go.dev/go.mercari.io/hcledit 120 | [pkg.go.dev-badge]: http://bit.ly/pkg-go-dev-badge 121 | 122 | [license]: LICENSE 123 | [license-badge]: https://img.shields.io/github/license/mercari/hcledit?style=for-the-badge 124 | -------------------------------------------------------------------------------- /cmd/hcledit/README.md: -------------------------------------------------------------------------------- 1 | # hcledit command 2 | 3 | [![release][release-badge]][release] 4 | [![docker][docker-badge]][docker] 5 | 6 | `hcledit` command is a CLI tool to edit HCL configuration, which exposes [`hcledit`](https://pkg.go.dev/go.mercari.io/hcledit) API as command line interface. You can think this as a sample application built by the package. 7 | 8 | ## Install 9 | 10 | Install binaries via [GitHub Releases][release] or below: 11 | 12 | Homebrew: 13 | 14 | ```bash 15 | $ brew tap mercari/hcledit https://github.com/mercari/hcledit 16 | $ brew install hcledit 17 | ``` 18 | 19 | `go get`: 20 | 21 | ```bash 22 | $ go get -u go.mercari.io/hcledit/cmd/hcledit 23 | ``` 24 | 25 | Docker: 26 | 27 | ```bash 28 | $ docker run --rm -it mercari/hcledit hcledit 29 | ``` 30 | 31 | ## Examples 32 | 33 | The following is an HCL configuration which we want to manipulate. 34 | 35 | ```hcl 36 | resource "google_container_node_pool" "nodes1" { 37 | name = "nodes1" 38 | 39 | node_config { 40 | preemptible = false 41 | machine_type = "e2-medium" 42 | } 43 | } 44 | ``` 45 | 46 | To read an attribute, 47 | 48 | ```console 49 | $ hcledit read 'resource.google_container_node_pool.*.node_config.machine_type' /path/to/file.tf 50 | resource.google_container_node_pool.nodes1.node_config.machine_type e2-medium 51 | ``` 52 | 53 | To create a new attribute, 54 | 55 | ```console 56 | $ hcledit create 'resource.google_container_node_pool.*.node_config.image_type' 'COS' /path/to/file.tf 57 | ``` 58 | 59 | ```diff 60 | resource "google_container_node_pool" "nodes1" { 61 | name = "nodes1" 62 | 63 | node_config { 64 | preemptible = false 65 | machine_type = "e2-medium" 66 | + image_type = "COS" 67 | } 68 | } 69 | ``` 70 | 71 | To update the existing attribute, 72 | 73 | ```console 74 | $ hcledit update 'resource.google_container_node_pool.*.node_config.machine_type' 'e2-highmem-2' /path/to/file.tf 75 | ``` 76 | 77 | ```diff 78 | resource "google_container_node_pool" "nodes1" { 79 | name = "nodes1" 80 | 81 | node_config { 82 | preemptible = false 83 | - machine_type = "e2-medium" 84 | + machine_type = "e2-highmem-2" 85 | } 86 | } 87 | ``` 88 | 89 | To delete the existing attribute, 90 | 91 | ```console 92 | $ hcledit delete 'resource.google_container_node_pool.*.node_config.machine_type' /path/to/file.tf 93 | ``` 94 | 95 | ```diff 96 | resource "google_container_node_pool" "nodes1" { 97 | name = "nodes1" 98 | 99 | node_config { 100 | preemptible = false 101 | - machine_type = "e2-medium" 102 | } 103 | } 104 | ``` 105 | 106 | 107 | 108 | [release]: https://github.com/mercari/hcledit/releases 109 | [release-badge]: https://img.shields.io/github/v/release/mercari/hcledit?style=for-the-badge&logo=github 110 | 111 | [docker]: https://hub.docker.com/r/mercari/hcledit 112 | [docker-badge]: https://img.shields.io/docker/v/mercari/hcledit?label=docker&sort=semver&style=for-the-badge&logo=docker 113 | -------------------------------------------------------------------------------- /cmd/hcledit/internal/command/create.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "go.mercari.io/hcledit" 9 | ) 10 | 11 | type CreateOptions struct { 12 | Type string 13 | After string 14 | Comment string 15 | } 16 | 17 | func NewCmdCreate() *cobra.Command { 18 | opts := &CreateOptions{} 19 | cmd := &cobra.Command{ 20 | Use: "create ", 21 | Short: "Create a new field", 22 | Long: `Runs an address query on a hcl file and create new field with given value.`, 23 | Args: cobra.ExactArgs(3), 24 | RunE: func(_ *cobra.Command, args []string) error { 25 | return runCreate(opts, args) 26 | }, 27 | } 28 | 29 | cmd.Flags().StringVarP(&opts.Type, "type", "t", "string", "Type of the value") 30 | cmd.Flags().StringVarP(&opts.After, "after", "a", "", "Field key which before the value will be created") 31 | cmd.Flags().StringVarP(&opts.Comment, "comment", "c", "", "Comment to be inserted before the field added. Comment symbols like // are required") 32 | 33 | return cmd 34 | } 35 | 36 | func runCreate(opts *CreateOptions, args []string) error { 37 | query, valueStr, filePath := args[0], args[1], args[2] 38 | 39 | editor, err := hcledit.ReadFile(filePath) 40 | if err != nil { 41 | return fmt.Errorf("failed to read file: %s", err) 42 | } 43 | 44 | value, err := convert(valueStr, opts.Type) 45 | if err != nil { 46 | return fmt.Errorf("failed to convert input to specific type: %s", err) 47 | } 48 | 49 | if err := editor.Create(query, value, hcledit.WithAfter(opts.After), hcledit.WithComment(opts.Comment)); err != nil { 50 | return fmt.Errorf("failed to create: %s", err) 51 | } 52 | 53 | return editor.OverWriteFile() 54 | } 55 | -------------------------------------------------------------------------------- /cmd/hcledit/internal/command/create_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/google/go-cmp/cmp/cmpopts" 9 | ) 10 | 11 | func TestRunCreate(t *testing.T) { 12 | cases := map[string]struct { 13 | opts *CreateOptions 14 | want string 15 | }{ 16 | "WithoutAdditionalOptions": { 17 | opts: &CreateOptions{}, 18 | want: `resource "google_container_node_pool" "nodes1" { 19 | node_config { 20 | preemptible = false 21 | machine_type = "e2-medium" 22 | disk_size_gb = "100" 23 | } 24 | } 25 | `, 26 | }, 27 | "WithOptionWithAfter": { 28 | opts: &CreateOptions{ 29 | Type: "string", 30 | After: "preemptible", 31 | }, 32 | want: `resource "google_container_node_pool" "nodes1" { 33 | node_config { 34 | preemptible = false 35 | disk_size_gb = "100" 36 | machine_type = "e2-medium" 37 | } 38 | } 39 | `, 40 | }, 41 | "WithOptionComment": { 42 | opts: &CreateOptions{ 43 | Comment: "// TODO: Testing", 44 | }, 45 | want: `resource "google_container_node_pool" "nodes1" { 46 | node_config { 47 | preemptible = false 48 | machine_type = "e2-medium" 49 | // TODO: Testing 50 | disk_size_gb = "100" 51 | } 52 | } 53 | `, 54 | }, 55 | } 56 | 57 | for name, tc := range cases { 58 | tc := tc 59 | 60 | t.Run(name, func(t *testing.T) { 61 | filename := tempFile(t, `resource "google_container_node_pool" "nodes1" { 62 | node_config { 63 | preemptible = false 64 | machine_type = "e2-medium" 65 | } 66 | } 67 | `) 68 | 69 | tc.opts.Type = "string" // ensure default value 70 | 71 | err := runCreate(tc.opts, []string{ 72 | "resource.google_container_node_pool.nodes1.node_config.disk_size_gb", 73 | "100", 74 | filename, 75 | }) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | diff := cmp.Diff(tc.want, readFile(t, filename), cmpopts.AcyclicTransformer("multiline", func(s string) []string { 81 | return strings.Split(s, "\n") 82 | })) 83 | 84 | if diff != "" { 85 | t.Fatalf("Create mismatch (-want +got):\n%s", diff) 86 | } 87 | 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/hcledit/internal/command/delete.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "go.mercari.io/hcledit" 9 | ) 10 | 11 | func NewCmdDelete() *cobra.Command { 12 | return &cobra.Command{ 13 | Use: "delete ", 14 | Short: "Delete the given field and its value", 15 | Long: `Runs an address query on a hcl file and delete the given field and its value.`, 16 | Args: cobra.ExactArgs(2), 17 | RunE: func(_ *cobra.Command, args []string) error { 18 | return runDelete(args) 19 | }, 20 | } 21 | } 22 | 23 | func runDelete(args []string) error { 24 | query, filePath := args[0], args[1] 25 | 26 | editor, err := hcledit.ReadFile(filePath) 27 | if err != nil { 28 | return fmt.Errorf("failed to read file: %s", err) 29 | } 30 | 31 | if err := editor.Delete(query); err != nil { 32 | return fmt.Errorf("failed to delete: %s", err) 33 | } 34 | 35 | return editor.OverWriteFile() 36 | } 37 | -------------------------------------------------------------------------------- /cmd/hcledit/internal/command/delete_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/google/go-cmp/cmp/cmpopts" 10 | ) 11 | 12 | func TestRunDelete(t *testing.T) { 13 | filename := tempFile(t, ` 14 | resource "google_container_node_pool" "nodes1" { 15 | node_config { 16 | preemptible = false 17 | machine_type = "e2-medium" 18 | } 19 | } 20 | `) 21 | 22 | args := []string{ 23 | "resource.google_container_node_pool.*.node_config.machine_type", 24 | filename, 25 | } 26 | if err := runDelete(args); err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | got, err := ioutil.ReadFile(filename) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | want := ` 36 | resource "google_container_node_pool" "nodes1" { 37 | node_config { 38 | preemptible = false 39 | } 40 | } 41 | ` 42 | diff := cmp.Diff(want, string(got), cmpopts.AcyclicTransformer("multiline", func(s string) []string { 43 | return strings.Split(s, "\n") 44 | })) 45 | 46 | if diff != "" { 47 | t.Fatalf("Delete mismatch (-want +got):\n%s", diff) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd/hcledit/internal/command/fixture/file.tf: -------------------------------------------------------------------------------- 1 | module "my-module" { 2 | source = "source.tar.gz" 3 | 4 | bool_variable = true 5 | int_variable = 1 6 | string_variable = "string" 7 | 8 | # Comment 9 | array_variable = ["a", "b", "c"] 10 | empty_array = [] 11 | 12 | multiline_array_variable = [ 13 | "d", 14 | "e", 15 | "f", 16 | ] 17 | 18 | unevaluateable_reference = var.name 19 | unevaluateable_interpolation = "this-${local.reference}" 20 | 21 | map_variable = { 22 | bool_variable = true 23 | int_variable = 1 24 | string_variable = "string" 25 | 26 | # Comment 27 | array_variable = ["a", "b", "c"] 28 | 29 | multiline_array_variable = [ 30 | "d", 31 | "e", 32 | "f", 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/hcledit/internal/command/read.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/spf13/cobra" 11 | yaml "gopkg.in/yaml.v2" 12 | 13 | "go.mercari.io/hcledit" 14 | ) 15 | 16 | type ReadOptions struct { 17 | OutputFormat string 18 | Fallback bool 19 | } 20 | 21 | func NewCmdRead() *cobra.Command { 22 | opts := &ReadOptions{} 23 | cmd := &cobra.Command{ 24 | Use: "read ", 25 | Short: "Read a value", 26 | Long: `Runs an address query on a hcl file and prints the result`, 27 | Args: cobra.ExactArgs(2), 28 | RunE: func(_ *cobra.Command, args []string) error { 29 | result, err := runRead(opts, args) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | fmt.Print(result) 35 | return nil 36 | }, 37 | } 38 | 39 | cmd.Flags().StringVarP(&opts.OutputFormat, "output-format", "o", "go-template='{{.Value}}'", "format to print the value as") 40 | cmd.Flags().BoolVar(&opts.Fallback, "fallback", false, "falls back to reading the raw value if it cannot be evaluated") 41 | 42 | return cmd 43 | } 44 | 45 | func runRead(opts *ReadOptions, args []string) (string, error) { 46 | query, filePath := args[0], args[1] 47 | 48 | editor, err := hcledit.ReadFile(filePath) 49 | if err != nil { 50 | return "", fmt.Errorf("failed to read file: %s", err) 51 | } 52 | 53 | readOpts := []hcledit.Option{} 54 | if opts.Fallback { 55 | readOpts = append(readOpts, hcledit.WithReadFallbackToRawString()) 56 | } 57 | results, err := editor.Read(query, readOpts...) 58 | if err != nil && !opts.Fallback { 59 | return "", fmt.Errorf("failed to read file: %s", err) 60 | } 61 | 62 | if strings.HasPrefix(opts.OutputFormat, "go-template") { 63 | return displayTemplate(opts.OutputFormat, results) 64 | } 65 | 66 | switch opts.OutputFormat { 67 | case "json": 68 | j, err := json.Marshal(results) 69 | return string(j), err 70 | case "yaml": 71 | y, err := yaml.Marshal(results) 72 | return string(y), err 73 | default: 74 | return "", errors.New("invalid output-format") 75 | } 76 | } 77 | 78 | func displayTemplate(format string, results map[string]interface{}) (string, error) { 79 | split := strings.SplitN(format, "=", 2) 80 | 81 | if len(split) != 2 { 82 | return "", errors.New("go-template should be passed as go-template='