Skip to content

Commit

Permalink
✨ new: Adds the basic functionality to update plugins (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
bendoerr authored Aug 28, 2024
1 parent 4b9584a commit 44ca318
Show file tree
Hide file tree
Showing 11 changed files with 843 additions and 0 deletions.
340 changes: 340 additions & 0 deletions .golangci.yml

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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
88 changes: 88 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
25 changes: 25 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
46 changes: 46 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
100 changes: 100 additions & 0 deletions pkg/github/latest.go
Original file line number Diff line number Diff line change
@@ -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
}
40 changes: 40 additions & 0 deletions pkg/tflint/data.go
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 39 additions & 0 deletions pkg/tflint/find.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 44ca318

Please sign in to comment.