Skip to content

Commit

Permalink
Introduce scaffold command
Browse files Browse the repository at this point in the history
  • Loading branch information
mraerino committed Apr 28, 2024
1 parent 4f2923b commit 03d0bae
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 31 deletions.
2 changes: 2 additions & 0 deletions cmd/tf-preview-gh/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import (
"os/signal"
"syscall"

"github.com/nimbolus/terraform-backend/pkg/scaffold"
"github.com/nimbolus/terraform-backend/pkg/speculative"
)

func main() {
rootCmd := speculative.NewCommand()
rootCmd.AddCommand(scaffold.NewCommand())

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ require (
github.com/go-git/go-git/v5 v5.12.0
github.com/go-redsync/redsync/v4 v4.11.0
github.com/gomodule/redigo v1.9.2
github.com/google/go-github/v57 v57.0.0
github.com/google/go-github/v61 v61.0.0
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
Expand All @@ -26,6 +25,7 @@ require (
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.9.0
github.com/whilp/git-urls v1.0.0
github.com/zclconf/go-cty v1.13.1
go.uber.org/multierr v1.11.0
)

Expand Down Expand Up @@ -58,6 +58,7 @@ require (
github.com/go-test/deep v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
Expand Down Expand Up @@ -113,7 +114,6 @@ require (
github.com/tmccombs/hcl2json v0.5.0 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/zclconf/go-cty v1.13.1 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
Expand Down
175 changes: 175 additions & 0 deletions pkg/scaffold/scaffold.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package scaffold

import (
"bufio"
"context"
"errors"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"slices"
"strings"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/nimbolus/terraform-backend/pkg/git"
"github.com/nimbolus/terraform-backend/pkg/tfcontext"
"github.com/spf13/cobra"
"github.com/zclconf/go-cty/cty"
)

var (
backendAddress string
)

func NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "scaffold",
Short: "scaffold the necessary config to use the GitHub Actions Terraform workflow",
RunE: func(cmd *cobra.Command, args []string) error {
return run(cmd.Context())
},
}

cmd.Flags().StringVar(&backendAddress, "backend-url", "https://ffddorf-terraform-backend.fly.dev/", "URL to use as the backend address")

return cmd
}

func run(ctx context.Context) error {
cwd, err := os.Getwd()
if err != nil {
return err
}

if err := writeBackendConfig(cwd); err != nil {
return err
}

// todo: create github actions workflows

return nil
}

func prompt(text string) (string, error) {
stdout := os.Stderr
fmt.Fprint(stdout, text)

rdr := bufio.NewReader(os.Stdin)
answer, err := rdr.ReadBytes('\n')
if err != nil {
return "", err
}
return string(answer[:len(answer)-1]), nil
}

func writeBackendConfig(dir string) (reterr error) {
var file *hclwrite.File
var outFile io.WriteCloser
var backendBlock *hclwrite.Block

_, filename, err := tfcontext.FindBackendBlock(dir)
if err == nil {
relPath, _ := filepath.Rel(dir, filename)
answer, err := prompt(fmt.Sprintf("There is an existing backend config at %s. Do you want to replace it? [y/N] ", relPath))
if err != nil {
return
}
if !strings.EqualFold(answer, "y") {
return errors.New("aborting")
}

b, err := os.ReadFile(filename)
if err != nil {
return err
}

var diags hcl.Diagnostics
file, diags = hclwrite.ParseConfig(b, filename, hcl.Pos{})
if len(diags) > 0 {
return errors.Join(diags)
}
var tfBlock *hclwrite.Block
for _, block := range file.Body().Blocks() {
if block.Type() != "terraform" {
continue
}
tfBlock = block
for _, innerBlock := range block.Body().Blocks() {
if innerBlock.Type() == "backend" {
backendBlock = innerBlock
}
}
}
if backendBlock == nil {
return errors.New("backend block not found anymore")
}
if backendBlock.Labels()[0] != "http" {
tfBlock.Body().RemoveBlock(backendBlock)
backendBlock = tfBlock.Body().AppendNewBlock("backend", nil)
}

outFile, err = os.Create(filename)
if err != nil {
return err
}
defer func() {
if reterr != nil {
// restore original content
_, _ = outFile.Write(b)
}
_ = outFile.Close()
}()
} else {
file = hclwrite.NewEmptyFile()
tfBlock := file.Body().AppendNewBlock("terraform", nil)
backendBlock = tfBlock.Body().AppendNewBlock("backend", nil)
filename = filepath.Join(dir, "backend.tf")
outFile, err = os.Create(filename)
if err != nil {
return err
}
defer outFile.Close()
}

origin, err := git.RepoOrigin()
if err != nil {
return err
}
segments := strings.Split(origin.Path, "/")
if len(segments) < 2 {
return fmt.Errorf("invalid repo path: %s", origin.Path)
}
repo := segments[1]

backendURL, err := url.Parse(backendAddress)
if err != nil {
return err
}
backendURL.Path = filepath.Join(backendURL.Path, "state", repo, "default")
address := backendURL.String()

backendBlock.SetLabels([]string{"http"})
backendBody := backendBlock.Body()
backendAttributes := []string{"address", "lock_address", "unlock_address", "username"}
for name := range backendBody.Attributes() {
if slices.Contains(backendAttributes, name) {
continue
}
backendBody.RemoveAttribute(name)
}
backendBody.SetAttributeValue("address", cty.StringVal(address))
backendBody.SetAttributeValue("lock_address", cty.StringVal(address))
backendBody.SetAttributeValue("unlock_address", cty.StringVal(address))
backendBody.SetAttributeValue("username", cty.StringVal("github_pat"))

if _, err := file.WriteTo(outFile); err != nil {
return err
}

relPath, _ := filepath.Rel(dir, filename)
fmt.Printf("Wrote backend config to: %s\n", relPath)
return nil
}
66 changes: 37 additions & 29 deletions pkg/tfcontext/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,19 @@ func readAttribute(attrs hcl.Attributes, name string) (string, error) {
return val.AsString(), nil
}

func FindBackend(dir string) (*BackendConfig, error) {
func FindBackendBlock(dir string) (*hcl.Block, string, error) {
parser := hclparse.NewParser()

tfFiles, err := files(dir)
if err != nil {
return nil, err
return nil, "", err
}

var file *hcl.File
for _, filename := range tfFiles {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
return nil, "", err
}

file, _ = parser.ParseHCL(b, filename)
Expand All @@ -108,35 +108,43 @@ func FindBackend(dir string) (*BackendConfig, error) {

content, _, _ := block.Body.PartialContent(terraformBlockSchema)
for _, innerBlock := range content.Blocks {
if innerBlock.Type != "backend" {
continue
}
if innerBlock.Labels[0] != "http" {
continue
}

content, _, _ := innerBlock.Body.PartialContent(backendSchema)
address, err := readAttribute(content.Attributes, "address")
if err != nil {
return nil, err
}
username, err := readAttribute(content.Attributes, "username")
if err != nil {
return nil, err
}
password, err := readAttribute(content.Attributes, "password")
if err != nil {
return nil, err
if innerBlock.Type == "backend" {
return innerBlock, filename, nil
}

return &BackendConfig{
Address: address,
Username: username,
Password: password,
}, nil
}
}
}

return nil, errors.New("backend config not found")
return nil, "", errors.New("backend block not found")
}

func FindBackend(dir string) (*BackendConfig, error) {
backend, _, err := FindBackendBlock(dir)
if err != nil {
return nil, err
}

if backend.Labels[0] != "http" {
return nil, errors.New("not using http backend")
}

content, _, _ := backend.Body.PartialContent(backendSchema)
address, err := readAttribute(content.Attributes, "address")
if err != nil {
return nil, err
}
username, err := readAttribute(content.Attributes, "username")
if err != nil {
return nil, err
}
password, err := readAttribute(content.Attributes, "password")
if err != nil {
return nil, err
}

return &BackendConfig{
Address: address,
Username: username,
Password: password,
}, nil
}

0 comments on commit 03d0bae

Please sign in to comment.