Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #1

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module tfversion-checker

go 1.20
159 changes: 159 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package main

import (
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"tfversion-checker/utils"
)

var terraformFilePath []string

func main() {
// Define CLI flags
scanFlag := flag.Bool("scan", false, "Initiate a scan of the current directory for Terraform configurations")
checkFlag := flag.Bool("check", false, "Perform version checks and display results")
pathFlag := flag.String("path", ".", "Specify the path to scan for Terraform configurations")
enforceFlag := flag.Bool("enforce", false, "Enforce version policies across projects")

// Parse command-line flags
flag.Parse()

// Validate flag combinations
flagCount := countFlags(*scanFlag, *checkFlag, *enforceFlag)
println(flagCount)
if flagCount == 0 {
fmt.Println("One of the flags must be specified.")
flag.Usage()
os.Exit(1)
}
// Check if file path is provided
if *pathFlag == "" {
fmt.Println("Error: Please provide the path to the Terraform configuration file using the -file flag.")
return
}

if *scanFlag {
scanAction(*pathFlag)
}

if *checkFlag {
checkAction()
}

if *enforceFlag {
enforceAction(*pathFlag)
}
}

// countFlags counts the number of specified flags
func countFlags(flags ...bool) int {
count := 0
for _, flag := range flags {
if flag {
count++
}
}
return count
}

// scanAction initiates a scan of the current directory for Terraform configurations

func scanAction(path string) {
fmt.Println("Scanning for Terraform configurations in the current directory...")
fileCount := 0
err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Printf("Error accessing path %s: %v\n", path, err)
return nil
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".tf") {
//store the path of the file in a list
fileCount++
terraformFilePath = append(terraformFilePath, path)

}
return nil
})
fmt.Printf("Found Terraform configuration file: %d\n", fileCount)
if err != nil {
fmt.Printf("Error scanning for Terraform configurations: %v\n", err)
}
}

// checkAction performs version checks and displays results
func checkAction() {
fmt.Println("Performing version checks and displaying results...")
for _, filePath := range terraformFilePath {
checkTerraformVersion(filePath)
}
}

// enforceAction enforces version policies across projects
func enforceAction(path string) {
fmt.Println("Enforcing version policies across projects...")
// Implement logic for enforcing version policies
}

// checkTerraformVersion reads the content of a Terraform configuration file and checks for the required_version attribute
func checkTerraformVersion(filePath string) {
file, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("Error opening file %s: %v\n", filePath, err)
return
}

str := string(file)

if match := regexp.MustCompile(`\brequired_version\s*=\s*"(.*?)"`).FindStringSubmatch(str); len(match) > 1 {
version := regexp.MustCompile(`[^~>]+`).FindString(match[1])
fmt.Printf("Terraform version specified in %s: %s\n", filePath, version)
// Compare with the latest stable release
latestVersion, err := utils.GetLatestTerraformVersion()
if err != nil {
fmt.Printf("Error retrieving latest Terraform version: %v\n", err)
return
}

if version != latestVersion {
fmt.Printf("Warning: The specified version is outdated. Latest version: %s\n", latestVersion)
}
}
// different option: required_providers\s*{(?:[^{}]+|{(?:[^{}]+|{[^{}]*})*})*}
//pattern := `required_providers\s*{(?s:(.*?))}`
pattern := `required_providers\s*{(?:[^{}]+|{(?:[^{}]+|{[^{}]*})*})*}`
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(str, -1)
for _, match := range matches {
parseRequiredProvidersBlock(match[0])
}

}

// parseRequiredProvidersBlock parses the required_providers block and checks versions
func parseRequiredProvidersBlock(block string) {
// Extract provider and version from the block
providerPattern := `\s*([a-zA-Z0-9_]+)\s*=\s*{\s*source\s*=\s*"([^"]+)"\s*version\s*=\s*"([^"]+)"`
providerMatches := regexp.MustCompile(providerPattern).FindAllStringSubmatch(block, -1)

for _, provider := range providerMatches {
if len(provider) > 0 {
fmt.Printf("Required Provider: %s\n", provider[1])
version := regexp.MustCompile(`[^~>]+`).FindString(provider[3])
fmt.Printf("Source: %s\n", provider[2])
fmt.Printf("Version: %s\n", version)
latestProviderVersion, err := utils.GetLatestProviderVersion(provider[1], provider[2])
if err != nil {
fmt.Printf("Error retrieving latest %s provider version: %v\n", provider, err)
return
}

if version != latestProviderVersion {
fmt.Printf("Warning: The specified %s provider version is outdated. Latest version: %s\n", provider[1], latestProviderVersion)
}
}
}
}
68 changes: 68 additions & 0 deletions utils/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package utils

import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)

// getLatestProviderVersion retrieves the latest stable provider version from GitHub releases
func GetLatestProviderVersion(provider string, source string) (string, error) {
splitSource := strings.Split(source, `/`)
providerReleasesURL := fmt.Sprintf("https://api.github.com/repos/%s/terraform-provider-%s/releases/latest", splitSource[0], provider)
resp, err := http.Get(providerReleasesURL)
if err != nil {
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("Failed to retrieve latest %s provider version. Status code: %d", provider, resp.StatusCode)
}

var releaseInfo map[string]interface{}
if err := ReadJSON(resp.Body, &releaseInfo); err != nil {
return "", err
}

tagName, ok := releaseInfo["tag_name"].(string)
if !ok {
return "", fmt.Errorf("Failed to extract latest %s provider version from the response", provider)
}

return strings.TrimPrefix(tagName, "v"), nil
}

// getLatestTerraformVersion retrieves the latest stable Terraform version from GitHub releases
func GetLatestTerraformVersion() (string, error) {
const terraformReleasesURL = "https://api.github.com/repos/hashicorp/terraform/releases/latest"
resp, err := http.Get(terraformReleasesURL)
if err != nil {
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("Failed to retrieve latest Terraform version. Status code: %d", resp.StatusCode)
}

var releaseInfo map[string]interface{}
if err := ReadJSON(resp.Body, &releaseInfo); err != nil {
return "", err
}

tagName, ok := releaseInfo["tag_name"].(string)
if !ok {
return "", fmt.Errorf("Failed to extract latest Terraform version from the response")
}

// Extract the version from the tag name (e.g., "v0.15.3" becomes "0.15.3")
return strings.TrimPrefix(tagName, "v"), nil
}

// readJSON reads JSON from a reader and unmarshals it into the provided interface
func ReadJSON(r io.Reader, v interface{}) error {
return json.NewDecoder(r).Decode(v)
}