From 44ca318380c4d2c745d5e0a203ae5f24224e4894 Mon Sep 17 00:00:00 2001 From: Ben Doerr Date: Wed, 28 Aug 2024 19:33:54 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20new:=20Adds=20the=20basic=20functio?= =?UTF-8?q?nality=20to=20update=20plugins=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .golangci.yml | 340 ++++++++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 34 ++++ cmd/main.go | 88 +++++++++++ go.mod | 25 +++ go.sum | 46 ++++++ pkg/github/latest.go | 100 ++++++++++++ pkg/tflint/data.go | 40 +++++ pkg/tflint/find.go | 39 +++++ pkg/tflint/open.go | 57 +++++++ pkg/tflint/plugin.go | 47 ++++++ pkg/tflint/write.go | 27 ++++ 11 files changed, 843 insertions(+) create mode 100644 .golangci.yml create mode 100644 .pre-commit-config.yaml create mode 100644 cmd/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/github/latest.go create mode 100644 pkg/tflint/data.go create mode 100644 pkg/tflint/find.go create mode 100644 pkg/tflint/open.go create mode 100644 pkg/tflint/plugin.go create mode 100644 pkg/tflint/write.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..895fea8 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,340 @@ +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 3m + +# This file contains only configs which differ from defaults. +# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml +linters-settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 30 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 + package-average: 10.0 + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Such cases aren't reported by default. + # Default: false + check-type-assertions: true + + exhaustive: + # Program elements to check for exhaustiveness. + # Default: [ switch ] + check: + - switch + - map + + exhaustruct: + # List of regular expressions to exclude struct packages and names from check. + # Default: [] + exclude: + # std libs + - "^net/http.Client$" + - "^net/http.Cookie$" + - "^net/http.Request$" + - "^net/http.Response$" + - "^net/http.Server$" + - "^net/http.Transport$" + - "^net/url.URL$" + - "^os/exec.Cmd$" + - "^reflect.StructField$" + # public libs + - "^github.com/Shopify/sarama.Config$" + - "^github.com/Shopify/sarama.ProducerMessage$" + - "^github.com/mitchellh/mapstructure.DecoderConfig$" + - "^github.com/prometheus/client_golang/.+Opts$" + - "^github.com/spf13/cobra.Command$" + - "^github.com/spf13/cobra.CompletionOptions$" + - "^github.com/stretchr/testify/mock.Mock$" + - "^github.com/testcontainers/testcontainers-go.+Request$" + - "^github.com/testcontainers/testcontainers-go.FromDockerfile$" + - "^golang.org/x/tools/go/analysis.Analyzer$" + - "^google.golang.org/protobuf/.+Options$" + - "^gopkg.in/yaml.v3.Node$" + + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 100 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 50 + # Ignore comments when counting lines. + # Default false + ignore-comments: true + + gocognit: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 30 + + gocritic: + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + mnd: + # List of function patterns to exclude from analysis. + # Values always ignored: `time.Date`, + # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, + # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. + # Default: [] + ignored-functions: + - flag.Arg + - flag.Duration.* + - flag.Float.* + - flag.Int.* + - flag.Uint.* + - os.Chmod + - os.Mkdir.* + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets.* + - prometheus.LinearBuckets + + gomodguard: + blocked: + # List of blocked modules. + # Default: [] + modules: + - github.com/golang/protobuf: + recommendations: + - google.golang.org/protobuf + reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" + - github.com/satori/go.uuid: + recommendations: + - github.com/google/uuid + reason: "satori's package is not maintained" + - github.com/gofrs/uuid: + recommendations: + - github.com/google/uuid + reason: "gofrs' package is not go module" + + govet: + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + # Settings per analyzer. + settings: + shadow: + # Whether to be strict about shadowing; can be noisy. + # Default: false + strict: true + + nakedret: + # Make an issue if func has more lines of code than this setting, and it has naked returns. + # Default: 30 + max-func-lines: 0 + + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [funlen, gocognit, lll] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + + rowserrcheck: + # database/sql is always checked + # Default: [] + packages: + - github.com/jmoiron/sqlx + + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + + gci: + # Section configuration to compare against. + # Section names are case-insensitive and may contain parameters in (). + # The default order of sections is `standard > default > custom > blank > dot`, + # If `custom-order` is `true`, it follows the order of `sections` option. + # Default: ["standard", "default"] + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + - prefix(github.com/bendoerr-terraform-modules) # Custom section: groups all imports with the specified Prefix. + - blank # Blank section: contains all blank imports. This section is not present unless explicitly enabled. + - dot # Dot section: contains all dot imports. This section is not present unless explicitly enabled. + # Skip generated files. + # Default: true + skip-generated: false + # Enable custom order of sections. + # If `true`, make the section order the same as the order of `sections`. + # Default: false + custom-order: true + +linters: + disable-all: true + enable: + ## enabled by default + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - gosimple # specializes in simplifying a code + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - typecheck # like the front-end of a Go compiler, parses and type-checks Go code + - unused # checks for unused constants, variables, functions and types + ## disabled by default + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - cyclop # checks function and package cyclomatic complexity + - dupl # tool for code clone detection + - durationcheck # checks for two durations multiplied together + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - exhaustive # checks exhaustiveness of enum switch statements + - copyloopvar # checks for pointers to enclosing loop variables + - forbidigo # forbids identifiers + - funlen # tool for detection of long functions + - gocheckcompilerdirectives # validates go compiler directive comments (//go:) + - gochecknoglobals # checks that no global variables exist + - gochecknoinits # checks that no init functions are present in Go code + - gochecksumtype # checks exhaustiveness on Go "sum types" + - gocognit # computes and checks the cognitive complexity of functions + - goconst # finds repeated strings that could be replaced by a constant + - gocritic # provides diagnostics that check for bugs, performance and style issues + - gocyclo # computes and checks the cyclomatic complexity of functions + - godot # checks if comments end in a period + - mnd # detects magic numbers + - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - lll # reports long lines + - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) + - makezero # finds slice declarations with non-zero initial length + - mirror # reports wrong mirror patterns of bytes/strings usage + - musttag # enforces field tags in (un)marshaled structs + - nakedret # finds naked returns in functions greater than a specified function length + - nestif # reports deeply nested if statements + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - nilnil # checks that there is no simultaneous return of nil error and an invalid value + - noctx # finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + - nonamedreturns # reports all named returns + - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter # checks Prometheus metrics naming via promlint + - protogetter # reports direct reads from proto message fields when getters should be used + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - rowserrcheck # checks whether Err of rows is checked successfully + - sloglint # ensure consistent code style when using log/slog + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - stylecheck # is a replacement for golint + - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 + - testableexamples # checks if examples are testable (have an expected output) + - testifylint # checks usage of github.com/stretchr/testify + - testpackage # makes you use a separate _test package + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + - wastedassign # finds wasted assignment statements + - whitespace # detects leading and trailing whitespace + - gci # controls golang package import order and makes it always deterministic + + ## you may want to enable + #- decorder # checks declaration order and count of types, constants, variables and functions + #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized + #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega + #- godox # detects FIXME, TODO and other comment keywords + #- goheader # checks is file header matches to pattern + #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters + #- interfacebloat # checks the number of methods inside an interface + #- ireturn # accept interfaces, return concrete types + #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated + #- tagalign # checks that struct tags are well aligned + #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope + #- wrapcheck # checks that errors returned from external packages are wrapped + #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event + + ## disabled + #- containedctx # detects struct contained context.Context field + #- contextcheck # [too many false positives] checks the function whether use a non-inherited context + #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages + #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + #- dupword # [useless without config] checks for duplicate words in the source code + #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted + #- forcetypeassert # [replaced by errcheck] finds forced type assertions + #- goerr113 # [too strict] checks the errors handling expressions + #- gofmt # [replaced by goimports] checks whether code was gofmt-ed + #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed + #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase + #- grouper # analyzes expression groups + #- importas # enforces consistent import aliases + #- maintidx # measures the maintainability index of each function + #- misspell # [useless] finds commonly misspelled English words in comments + #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity + #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test + #- tagliatelle # checks the struct tags + #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines + #- goimports # [use gci] in addition to fixing imports, goimports also formats your code in the same style as gofmt + + ## deprecated + #- deadcode # [deprecated, replaced by unused] finds unused code + #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized + #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes + #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible + #- interfacer # [deprecated] suggests narrower interface types + #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted + #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name + #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs + #- structcheck # [deprecated, replaced by unused] finds unused struct fields + #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants + #- gomnd # [deprecated, renamed to mnd] detects magic numbers + #- exportloopref [deprecated, replaced by copyloopvar] # checks for pointers to enclosing loop variables + #- execinquery [deprecated, archived by the owner] # checks query string in Query function which reads your Go src files and warning it finds + +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 50 + + exclude-rules: + - source: "(noinspection|TODO)" + linters: [godot] + - source: "//noinspection" + linters: [gocritic] + - path: "_test\\.go" + linters: + - bodyclose + - dupl + - funlen + - goconst + - gosec + - noctx + - wrapcheck + - text: 'shadow: declaration of "(err|ctx)" shadows declaration at' + linters: [govet] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a451927 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,34 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 2c9f875913ee60ca25ce70243dc24d5b6415598c # frozen: v4.6.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-json + - id: check-toml + - id: check-yaml + - id: detect-aws-credentials + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/gitleaks/gitleaks + rev: 77c3c6a34b2577d71083442326c60b8fd58926ec # frozen: v8.18.4 + hooks: + - id: gitleaks + + - repo: https://github.com/executablebooks/mdformat + rev: 08fba30538869a440b5059de90af03e3502e35fb # frozen: 0.7.17 + hooks: + - id: mdformat + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: f295829140d25717bc79368d3f966fc1f67a824f # frozen: v0.41.0 + hooks: + - id: markdownlint-fix-docker + + - repo: https://github.com/golangci/golangci-lint + rev: c2e095c022a97360f7fff5d49fbc11f273be929a # frozen: v1.60.3 + hooks: + - id: golangci-lint diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..527aa7b --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "os" + "strings" + + "github.com/alecthomas/kong" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/spf13/afero" + + "github.com/bendoerr-terraform-modules/tflint-plugin-version-update/pkg/github" + "github.com/bendoerr-terraform-modules/tflint-plugin-version-update/pkg/tflint" +) + +type Config struct { + Freeze bool `name:"freeze"` + Path string `name:"path" arg:"" type:"path"` +} + +func main() { + var err error + var cfg Config + + _ = kong.Parse(&cfg) + + tflFile, err := tflint.OpenConfig(afero.Afero{Fs: afero.NewOsFs()}, cfg.Path) + if err != nil { + panic(err) + } + + tflData, err := tflint.NewData(tflFile) + if err != nil { + panic(err) + } + + tflHcl, err := tflData.ParseForRead() + if err != nil { + panic(err) + } + + tflHclW, err := tflData.ParseForWrite() + if err != nil { + panic(err) + } + + plugins, err := tflint.FindPluginVersions(tflHcl) + if err != nil { + panic(err) + } + + runUpdate(plugins, cfg, tflHclW) + + _, _ = tflHclW.WriteTo(os.Stdout) +} + +func runUpdate(plugins []*tflint.PluginConfig, cfg Config, tflHclW *hclwrite.File) { + for _, plugin := range plugins { + latestVersion, err := github.LatestVersion(plugin.SourceOwner, plugin.SourceRepo) + if err != nil { + panic(err) + } + + if cfg.Freeze { + if plugin.Version == latestVersion.ReleaseSHA { + continue + } + } else { + if plugin.Version == latestVersion.ReleaseTag || "v"+plugin.Version == latestVersion.ReleaseTag { + continue + } + } + + if cfg.Freeze { + err = tflint.UpdatePluginVersion(plugin.Name, latestVersion.ReleaseSHA, latestVersion.ReleaseTag, tflHclW) + if err != nil { + panic(err) + } + } else { + // Stylistically tflint drops the 'v' in their documentation, + // so we'll follow that as well. + version := strings.TrimPrefix(latestVersion.ReleaseTag, "v") + err = tflint.UpdatePluginVersion(plugin.Name, version, "", tflHclW) + if err != nil { + panic(err) + } + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d55c0ca --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/bendoerr-terraform-modules/tflint-plugin-version-update + +go 1.23.0 + +require ( + github.com/alecthomas/kong v0.9.0 + github.com/hashicorp/hcl/v2 v2.22.0 + github.com/kr/pretty v0.3.1 + github.com/spf13/afero v1.11.0 +) + +require ( + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/zclconf/go-cty v1.13.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aa9e16f --- /dev/null +++ b/go.sum @@ -0,0 +1,46 @@ +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= +github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= +github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= +github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= +github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= diff --git a/pkg/github/latest.go b/pkg/github/latest.go new file mode 100644 index 0000000..f79e778 --- /dev/null +++ b/pkg/github/latest.go @@ -0,0 +1,100 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +type Latest struct { + ReleaseTag string + ReleaseSHA string + ReleaseDescription string +} + +type latestResponse struct { + TagName string `json:"tag_name"` + Description string `json:"body"` +} + +type tagResponse struct { + Object struct { + SHA string `json:"sha"` + } `json:"object"` +} + +func get(owner, repo, path string) (*http.Response, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/%s", owner, repo, path) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("failed create request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + _ = resp.Body.Close() + return nil, fmt.Errorf("failed request to Github API at URL='%s', %w", url, err) + } + + if resp.StatusCode >= http.StatusMultipleChoices { + _ = resp.Body.Close() + return nil, fmt.Errorf("failed request to Github API at URL='%s' with StatusCode='%d'", url, resp.StatusCode) + } + + return resp, nil +} + +func getLatest(owner, repo string) (*latestResponse, error) { + resp, err := get(owner, repo, "releases/latest") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + result := &latestResponse{} + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + return nil, fmt.Errorf("failed to decode latest releaes response: %w", err) + } + + return result, nil +} + +func getTag(owner, repo, tag string) (*tagResponse, error) { + resp, err := get(owner, repo, "git/ref/tags/"+tag) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + result := &tagResponse{} + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + return nil, fmt.Errorf("failed to decode tag response: %w", err) + } + + return result, nil +} + +func LatestVersion(owner, repo string) (*Latest, error) { + release, err := getLatest(owner, repo) + if err != nil { + return nil, err + } + + tag, err := getTag(owner, repo, release.TagName) + if err != nil { + return nil, err + } + + return &Latest{ + ReleaseTag: release.TagName, + ReleaseSHA: tag.Object.SHA, + ReleaseDescription: release.Description, + }, nil +} diff --git a/pkg/tflint/data.go b/pkg/tflint/data.go new file mode 100644 index 0000000..827910f --- /dev/null +++ b/pkg/tflint/data.go @@ -0,0 +1,40 @@ +package tflint + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/spf13/afero" +) + +type Data struct { + Bytes []byte + Filename string +} + +func NewData(file afero.File) (*Data, error) { + d := &Data{} + var err error + d.Filename = file.Name() + d.Bytes, err = afero.ReadAll(file) + if err != nil { + return nil, err + } + return d, nil +} + +func (d *Data) ParseForRead() (*hcl.File, error) { + f, diag := hclsyntax.ParseConfig(d.Bytes, d.Filename, hcl.InitialPos) + if diag.HasErrors() { + return nil, diag + } + return f, nil +} + +func (d *Data) ParseForWrite() (*hclwrite.File, error) { + f, diag := hclwrite.ParseConfig(d.Bytes, d.Filename, hcl.InitialPos) + if diag.HasErrors() { + return nil, diag + } + return f, nil +} diff --git a/pkg/tflint/find.go b/pkg/tflint/find.go new file mode 100644 index 0000000..b56aff9 --- /dev/null +++ b/pkg/tflint/find.go @@ -0,0 +1,39 @@ +package tflint + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" +) + +func FindPluginVersions(file *hcl.File) ([]*PluginConfig, error) { + var plugins []*PluginConfig + + var configSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "plugin", + LabelNames: []string{"name"}, + }, + }, + } + + content, diag := file.Body.Content(configSchema) + if diag.HasErrors() { + return nil, diag + } + + for _, block := range content.Blocks { + if block.Type == "plugin" { + pluginConfig := &PluginConfig{Name: block.Labels[0]} + if err := gohcl.DecodeBody(block.Body, nil, pluginConfig); err != nil { + return nil, err + } + if err := pluginConfig.Validate(); err != nil { + return nil, err + } + plugins = append(plugins, pluginConfig) + } + } + + return plugins, nil +} diff --git a/pkg/tflint/open.go b/pkg/tflint/open.go new file mode 100644 index 0000000..f6aa0e3 --- /dev/null +++ b/pkg/tflint/open.go @@ -0,0 +1,57 @@ +package tflint + +import ( + "errors" + "fmt" + "log" + "os" + + "github.com/spf13/afero" +) + +// OpenConfig loads a TFLint config file following the logic from tflint except +// it will not auto load from the home directory, to update that provide the +// path explictly. +// +// tflint's LoadConfig @ tflint/tflint/config.go +// +// The priority of the configuration files is as follows: +// +// 1. file passed by the --config option +// 2. file set by the TFLINT_CONFIG_FILE environment variable +// 3. current directory (./.tflint.hcl) +// +// For 1 and 2, if the file does not exist, an error will be returned immediately. +// If 3 fails then an error will be returned. +func OpenConfig(fs afero.Afero, file string) (afero.File, error) { + // Load the file passed by the --config option + if file != "" { + log.Printf("[INFO] Load config: %s", file) + f, err := fs.Open(file) + if err != nil { + return nil, fmt.Errorf("unable to open file='%s': %w", file, err) + } + return f, nil + } + + // Load the file set by the environment variable + envFile := os.Getenv("TFLINT_CONFIG_FILE") + if envFile != "" { + log.Printf("[INFO] Load config: %s", envFile) + f, err := fs.Open(envFile) + if err != nil { + return nil, fmt.Errorf("unable to open TFLINT_CONFIG_FILE='%s': %w", envFile, err) + } + return f, nil + } + + // Load the default config file + var defaultConfigFile = ".tflint.hcl" + log.Printf("[INFO] Load default config: %s", defaultConfigFile) + if f, err := fs.Open(defaultConfigFile); err == nil { + return f, nil + } + log.Printf("[INFO] Default config not found") + + return nil, errors.New("no config file found") +} diff --git a/pkg/tflint/plugin.go b/pkg/tflint/plugin.go new file mode 100644 index 0000000..c6962a0 --- /dev/null +++ b/pkg/tflint/plugin.go @@ -0,0 +1,47 @@ +package tflint + +import ( + "fmt" + "strings" + + "github.com/hashicorp/hcl/v2" +) + +// PluginConfig is a TFLint's plugin config. +type PluginConfig struct { + Name string `hcl:"name,label"` + Enabled bool `hcl:"enabled"` + Version string `hcl:"version,optional"` + Source string `hcl:"source,optional"` + SigningKey string `hcl:"signing_key,optional"` + + Body hcl.Body `hcl:",remain"` + + // Parsed source attributes + SourceHost string + SourceOwner string + SourceRepo string +} + +func (c *PluginConfig) Validate() error { + if c.Version != "" && c.Source == "" { + return fmt.Errorf(`plugin "%s": "source" attribute cannot be omitted when specifying "version"`, c.Name) + } + + if c.Source != "" { + if c.Version == "" { + return fmt.Errorf(`plugin "%s": "version" attribute cannot be omitted when specifying "source"`, c.Name) + } + + parts := strings.Split(c.Source, "/") + if len(parts) != 3 { //nolint:mnd // Expected `github.com/owner/repo` format + return fmt.Errorf(`plugin "%s": "source" is invalid. Must be in the format "${host}/${owner}/${repo}"`, c.Name) + } + + c.SourceHost = parts[0] + c.SourceOwner = parts[1] + c.SourceRepo = parts[2] + } + + return nil +} diff --git a/pkg/tflint/write.go b/pkg/tflint/write.go new file mode 100644 index 0000000..80de069 --- /dev/null +++ b/pkg/tflint/write.go @@ -0,0 +1,27 @@ +package tflint + +import ( + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" +) + +func UpdatePluginVersion(name, version, comment string, file *hclwrite.File) error { + for _, block := range file.Body().Blocks() { + if block.Type() == "plugin" && block.Labels()[0] == name { + if comment == "" { + block.Body().SetAttributeValue("version", cty.StringVal(version)) + } else { + tokens := append( + hclwrite.TokensForValue(cty.StringVal(version)), + &hclwrite.Token{ + Type: hclsyntax.TokenComment, + Bytes: []byte("# " + comment), + }, + ) + block.Body().SetAttributeRaw("version", tokens) + } + } + } + return nil +}