Skip to content

Commit

Permalink
Refactor preview runner to use new backend capability
Browse files Browse the repository at this point in the history
  • Loading branch information
mraerino committed Apr 28, 2024
1 parent 5b81c2d commit 25e4466
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 46 deletions.
85 changes: 39 additions & 46 deletions cmd/tf-preview-gh/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"io"
Expand All @@ -11,81 +12,73 @@ import (
"os"
"os/exec"
"os/signal"
"path/filepath"
"slices"
"strings"
"syscall"
"time"

"github.com/cenkalti/backoff"
"github.com/ffddorf/tf-preview-github/pkg/terraform"
"github.com/google/go-github/v57/github"
"github.com/google/uuid"
"github.com/hashicorp/go-slug"
"golang.ngrok.com/ngrok"
"golang.ngrok.com/ngrok/config"
)

type LocalContent struct {
dir string
}

func (c *LocalContent) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Content-Type", "application/octet-stream")

_, err := slug.Pack(c.dir, rw, true)
func serveWorkspace(ctx context.Context) (string, error) {
cwd, err := os.Getwd()
if err != nil {
fmt.Printf("failed to pack contents: %+v\n", err)
return
return "", err
}

fmt.Println("workspace was downloaded")
}

func startServer(ctx context.Context) (string, error) {
listenerCtx, cancelListener := context.WithCancel(context.Background())

connected := make(chan struct{})
go func() {
select {
case <-connected:
case <-time.After(10 * time.Second):
cancelListener()
}
}()

listener, err := ngrok.Listen(listenerCtx, config.HTTPEndpoint(), ngrok.WithAuthtokenFromEnv())
backend, err := terraform.FindBackend(cwd)
if err != nil {
return "", err
}
close(connected)

cwd, err := os.Getwd()
backendURL, err := url.Parse(backend.Address)
if err != nil {
listener.Close()
return "", err
return "", fmt.Errorf("failed to parse backend url: %s, %w", backend.Address, err)
}
handler := &LocalContent{
dir: cwd,
if backend.Password == "" {
backendPassword, ok := os.LookupEnv("TF_HTTP_PASSWORD")
if !ok || backendPassword == "" {
return "", errors.New("missing backend password")
}
backend.Password = backendPassword
}

server := &http.Server{
Handler: handler,
id := uuid.New()
backendURL.Path = filepath.Join(backendURL.Path, "/share/", id.String())

pr, pw := io.Pipe()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, backendURL.String(), pr)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/octet-stream")
req.SetBasicAuth(backend.Username, backend.Password)

go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
fmt.Printf("failed to shutdown server: %+v\n", err)
_, err := slug.Pack(cwd, pw, true)
if err != nil {
fmt.Printf("failed to pack workspace: %v\n", err)
pw.CloseWithError(err)
} else {
pw.Close()
}
}()

go func() {
if err := server.Serve(listener); err != http.ErrServerClosed {
fmt.Printf("server failed: %+v\n", err)
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("failed to stream workspace: %v\n", err)
} else if resp.StatusCode/100 != 2 {
fmt.Printf("invalid status code after streaming workspace: %d\n", resp.StatusCode)
}
fmt.Println("done streaming workspace")
}()

return listener.URL(), nil
return backendURL.String(), nil
}

type countingReader struct {
Expand Down Expand Up @@ -168,7 +161,7 @@ func main() {
panic("Missing flag: -github-repo")
}

serverURL, err := startServer(ctx)
serverURL, err := serveWorkspace(ctx)
if err != nil {
panic(err)
}
Expand Down
142 changes: 142 additions & 0 deletions pkg/terraform/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package terraform

import (
"errors"
"os"
"path/filepath"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
)

var rootSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "terraform",
LabelNames: nil,
},
},
}

var terraformBlockSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "backend",
LabelNames: []string{"name"},
},
},
}

var backendSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{Name: "address"},
{Name: "username"},
{Name: "password"},
},
}

func files(dir string) ([]string, error) {
infos, err := os.ReadDir(dir)
if err != nil {
return nil, err
}

var files []string
for _, info := range infos {
if info.IsDir() {
continue
}

name := info.Name()
ext := filepath.Ext(name)
if ext != ".tf" {
continue
}

fullPath := filepath.Join(dir, name)
files = append(files, fullPath)
}

return files, nil
}

type BackendConfig struct {
Address string
Username string
Password string
}

func readAttribute(attrs hcl.Attributes, name string) (string, error) {
raw, ok := attrs[name]
if !ok {
return "", nil
}

val, err := raw.Expr.Value(nil)
if err != nil {
return "", err
}

return val.AsString(), nil
}

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

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

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

file, _ = parser.ParseHCL(b, filename)
if file == nil {
continue
}

content, _, _ := file.Body.PartialContent(rootSchema)
for _, block := range content.Blocks {
if block.Type != "terraform" {
continue
}

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
}

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

return nil, errors.New("backend config not found")
}
18 changes: 18 additions & 0 deletions pkg/terraform/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package terraform_test

import (
"testing"

"github.com/ffddorf/tf-preview-github/pkg/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFindBackend(t *testing.T) {
be, err := terraform.FindBackend("./testdata")
require.NoError(t, err)

assert.Equal(t, "https://dummy-backend.example.com/state", be.Address)
assert.Equal(t, "my_user", be.Username)
assert.Empty(t, be.Password)
}
3 changes: 3 additions & 0 deletions pkg/terraform/testdata/another.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
locals {
foo = "bar"
}
8 changes: 8 additions & 0 deletions pkg/terraform/testdata/backend.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
backend "http" {
address = "https://dummy-backend.example.com/state"
lock_address = "https://dummy-backend.example.com/state"
unlock_address = "https://dummy-backend.example.com/state"
username = "my_user"
}
}

0 comments on commit 25e4466

Please sign in to comment.