Skip to content
This repository has been archived by the owner on Jun 29, 2022. It is now read-only.

Commit

Permalink
Refactor backend handling
Browse files Browse the repository at this point in the history
- Simplify Backend interface.
  - Rather than relying on the caller to call LoadConfig, expose new
    functions for creating a backend configuration and a backend
    struct. HCL unmarshaling happens inside NewConfig now so that
    it's impossible to construct an "unloaded" backend.
  - Replace Render() with String(). The actual rendering happens
    at backend construction time and the result is stored for
    retrieval by String().
  - Remove Validate() from the interface. Instead, validate the
    the config at creation time.
- Make region a required parameter. Ensuring that a region is
  specified only when no credentials file is specified doesn't make
  sense. A region is always required by the S3 Terraform backend. In
  addition, the AWS_SHARED_CREDENTIALS_FILE and AWS_DEFAULT_REGION
  env vars aren't documented in the S3 backend reference guide. We
  might as well just get rid of these.
- Get rid of the "registration" pattern. Storing all supported
  backend types in a global variable doesn't seem useful. Instead,
  each implementation of the Backend interface can construct a
  concrete type which implements the interface and return it.
- Fix whitespace problems in backend file.
  • Loading branch information
johananl committed Aug 24, 2020
1 parent 5eaf0b2 commit 74751c8
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 168 deletions.
75 changes: 51 additions & 24 deletions cli/cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (

"github.com/kinvolk/lokomotive/pkg/backend"
"github.com/kinvolk/lokomotive/pkg/backend/local"
"github.com/kinvolk/lokomotive/pkg/backend/s3"
"github.com/kinvolk/lokomotive/pkg/components/util"
"github.com/kinvolk/lokomotive/pkg/config"
"github.com/kinvolk/lokomotive/pkg/platform"
Expand Down Expand Up @@ -62,36 +63,68 @@ func initialize(ctxLogger *logrus.Entry) (*terraform.Executor, platform.Platform
ctxLogger.Fatal("No cluster configured")
}

// Get the configured backend for the cluster. Backend types currently supported: local, s3.
b, diags := getConfiguredBackend(lokoConfig)
if diags.HasErrors() {
for _, diagnostic := range diags {
ctxLogger.Error(diagnostic.Error())
}

ctxLogger.Fatal("Errors found while loading cluster configuration")
}
// Render backend configuration.
var b backend.Backend

// Use a local backend if no backend is configured.
if b == nil {
b = local.NewLocalBackend()
if lokoConfig.RootConfig.Backend != nil {
b = createBackend(ctxLogger, lokoConfig)
}

assetDir, err := homedir.Expand(p.Meta().AssetDir)
if err != nil {
ctxLogger.Fatalf("Error expanding path: %v", err)
}

// Validate backend configuration.
if err = b.Validate(); err != nil {
ctxLogger.Fatalf("Failed to validate backend configuration: %v", err)
}

ex := initializeTerraform(ctxLogger, p, b)

return ex, p, lokoConfig, assetDir
}

// createBackend constructs a Backend based on the provided cluster config and returns a pointer to
// it. If a backend with the provided name doesn't exist, an error is returned.
func createBackend(logger *logrus.Entry, config *config.Config) backend.Backend {
bn := config.RootConfig.Backend.Name

switch bn {
case backend.Local:
bc, diags := local.NewConfig(&config.RootConfig.Backend.Config, config.EvalContext)
if diags.HasErrors() {
for _, diagnostic := range diags {
logger.Error(diagnostic.Error())
}

logger.Fatal("Errors found while loading backend configuration")
}

b, err := local.NewBackend(bc)
if err != nil {
logger.Fatalf("Error constructing backend: %v", err)
}

return b
case backend.S3:
bc, diags := s3.NewConfig(&config.RootConfig.Backend.Config, config.EvalContext)
if diags.HasErrors() {
for _, diagnostic := range diags {
logger.Error(diagnostic.Error())
}

logger.Fatal("Errors found while loading backend configuration")
}

b, err := s3.NewBackend(bc)
if err != nil {
logger.Fatalf("Error constructing backend: %v", err)
}

return b
}

logger.Fatalf("Unknown backend %q", bn)

return nil
}

// initializeTerraform initialized Terraform directory using given backend and platform
// and returns configured executor.
func initializeTerraform(ctxLogger *logrus.Entry, p platform.Platform, b backend.Backend) *terraform.Executor {
Expand All @@ -100,14 +133,8 @@ func initializeTerraform(ctxLogger *logrus.Entry, p platform.Platform, b backend
ctxLogger.Fatalf("Error expanding path: %v", err)
}

// Render backend configuration.
renderedBackend, err := b.Render()
if err != nil {
ctxLogger.Fatalf("Failed to render backend configuration file: %v", err)
}

// Configure Terraform directory, module and backend.
if err := terraform.Configure(assetDir, renderedBackend); err != nil {
if err := terraform.Configure(assetDir, b); err != nil {
ctxLogger.Fatalf("Failed to configure Terraform : %v", err)
}

Expand Down
20 changes: 0 additions & 20 deletions cli/cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"github.com/mitchellh/go-homedir"
"github.com/spf13/viper"

"github.com/kinvolk/lokomotive/pkg/backend"
"github.com/kinvolk/lokomotive/pkg/config"
"github.com/kinvolk/lokomotive/pkg/platform"
)
Expand All @@ -34,25 +33,6 @@ const (
defaultKubeconfigPath = "~/.kube/config"
)

// getConfiguredBackend loads a backend from the given configuration file.
func getConfiguredBackend(lokoConfig *config.Config) (backend.Backend, hcl.Diagnostics) {
if lokoConfig.RootConfig.Backend == nil {
// No backend defined and no configuration error
return nil, hcl.Diagnostics{}
}

backend, err := backend.GetBackend(lokoConfig.RootConfig.Backend.Name)
if err != nil {
diag := &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: err.Error(),
}
return nil, hcl.Diagnostics{diag}
}

return backend, backend.LoadConfig(&lokoConfig.RootConfig.Backend.Config, lokoConfig.EvalContext)
}

// getConfiguredPlatform loads a platform from the given configuration file.
func getConfiguredPlatform() (platform.Platform, hcl.Diagnostics) {
lokoConfig, diags := getLokoConfig()
Expand Down
44 changes: 8 additions & 36 deletions pkg/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,15 @@

package backend

import (
"fmt"

"github.com/hashicorp/hcl/v2"
const (
// Local represents a local backend.
Local = "local"
// S3 represents an S3 backend.
S3 = "s3"
)

// Backend describes the Terraform state storage location.
// Backend describes a Terraform state storage location.
type Backend interface {
// LoadConfig loads the backend config provided by the user.
LoadConfig(*hcl.Body, *hcl.EvalContext) hcl.Diagnostics
// Render renders the backend template with user backend configuration.
Render() (string, error)
// Validate validates backend configuration.
Validate() error
}

// backends is a collection in which all Backends get automatically registered.
var backends map[string]Backend

// Initialize package's global variable on import.
func init() {
backends = make(map[string]Backend)
}

// Register registers Backend b in the internal backends map.
func Register(name string, b Backend) {
if _, exists := backends[name]; exists {
panic(fmt.Sprintf("backend with name %q registered already", name))
}
backends[name] = b
}

// GetBackend returns the Backend referred to by name.
func GetBackend(name string) (Backend, error) {
backend, exists := backends[name]
if !exists {
return nil, fmt.Errorf("no backend with name %q found", name)
}
return backend, nil
// String returns the Backend's configuration as rendered Terraform code.
String() string
}
50 changes: 30 additions & 20 deletions pkg/backend/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,50 @@
package local

import (
"fmt"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"

"github.com/kinvolk/lokomotive/internal/template"
"github.com/kinvolk/lokomotive/pkg/backend"
)

type local struct {
// Config represents the configuration of a local backend.
type Config struct {
Path string `hcl:"path,optional"`
}

// init registers local as a backend.
func init() {
backend.Register("local", NewLocalBackend())
}
// NewConfig creates a new Config and returns a pointer to it as well as any HCL diagnostics.
func NewConfig(b *hcl.Body, ctx *hcl.EvalContext) (*Config, hcl.Diagnostics) {
c := &Config{}

if b == nil {
return nil, hcl.Diagnostics{}
}

// LoadConfig loads the configuration for the local backend.
func (l *local) LoadConfig(configBody *hcl.Body, evalContext *hcl.EvalContext) hcl.Diagnostics {
if configBody == nil {
return hcl.Diagnostics{}
if d := gohcl.DecodeBody(*b, ctx, c); len(d) != 0 {
return nil, d
}
return gohcl.DecodeBody(*configBody, evalContext, l)

return c, hcl.Diagnostics{}
}

func NewLocalBackend() *local {
return &local{}
// Backend implements the Backend interface for a local backend.
type Backend struct {
config *Config
// A string containing the rendered Terraform code of the backend.
rendered string
}

// Render renders the Go template with local backend configuration.
func (l *local) Render() (string, error) {
return template.Render(backendConfigTmpl, l)
func (b *Backend) String() string {
return b.rendered
}

// Validate validates the local backend configuration.
func (l *local) Validate() error {
return nil
// NewBackend constructs a Backend based on the provided config and returns a pointer to it.
func NewBackend(c *Config) (*Backend, error) {
rendered, err := template.Render(backendConfigTmpl, c)
if err != nil {
return nil, fmt.Errorf("rendering backend: %v", err)
}

return &Backend{config: c, rendered: rendered}, nil
}
10 changes: 5 additions & 5 deletions pkg/backend/local/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
package local

var backendConfigTmpl = `
{{- if .Path }}
backend "local" {
path = "{{ .Path }}"
}
{{- end }}
backend "local" {
{{- if .Path }}
path = "{{ .Path }}"
{{- end }}
}
`
81 changes: 49 additions & 32 deletions pkg/backend/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,61 +15,78 @@
package s3

import (
"os"
"fmt"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/pkg/errors"

"github.com/kinvolk/lokomotive/internal/template"
"github.com/kinvolk/lokomotive/pkg/backend"
)

type s3 struct {
// Config represents the configuration of an S3 backend.
type Config struct {
Bucket string `hcl:"bucket"`
Key string `hcl:"key"`
Region string `hcl:"region,optional"`
Region string `hcl:"region"`
AWSCredsPath string `hcl:"aws_creds_path,optional"`
DynamoDBTable string `hcl:"dynamodb_table,optional"`
}

// init registers s3 as a backend.
func init() {
backend.Register("s3", NewS3Backend())
}
// validate returns an error if the Config is invalid.
func (c *Config) validate() error {
if c.Bucket == "" {
return fmt.Errorf("bucket cannot be empty")
}

if c.Key == "" {
return fmt.Errorf("key cannot be empty")
}

// LoadConfig loads the configuration for the s3 backend.
func (s *s3) LoadConfig(configBody *hcl.Body, evalContext *hcl.EvalContext) hcl.Diagnostics {
if configBody == nil {
return hcl.Diagnostics{}
if c.Region == "" {
return fmt.Errorf("region cannot be empty")
}
return gohcl.DecodeBody(*configBody, evalContext, s)

return nil
}

func NewS3Backend() *s3 {
return &s3{}
// NewConfig creates a new Config and returns a pointer to it as well as any HCL diagnostics.
func NewConfig(b *hcl.Body, ctx *hcl.EvalContext) (*Config, hcl.Diagnostics) {
diags := hcl.Diagnostics{}

c := &Config{}

if b == nil {
return nil, diags
}

if d := gohcl.DecodeBody(*b, ctx, c); len(d) != 0 {
diags = append(diags, d...)
return nil, diags
}

return c, diags
}

// Render renders the Go template with s3 backend configuration.
func (s *s3) Render() (string, error) {
return template.Render(backendConfigTmpl, s)
// Backend implements the Backend interface for an S3 backend.
type Backend struct {
config *Config
// A string containing the rendered Terraform code of the backend.
rendered string
}

// Validate validates the s3 backend configuration.
func (s *s3) Validate() error {
if s.Bucket == "" {
return errors.Errorf("no bucket specified")
}
func (b *Backend) String() string {
return b.rendered
}

if s.Key == "" {
return errors.Errorf("no key specified")
// NewBackend constructs a Backend based on the provided config and returns a pointer to it.
func NewBackend(c *Config) (*Backend, error) {
if err := c.validate(); err != nil {
return nil, fmt.Errorf("validating backend config: %w", err)
}

if s.AWSCredsPath == "" && os.Getenv("AWS_SHARED_CREDENTIALS_FILE") == "" {
if s.Region == "" && os.Getenv("AWS_DEFAULT_REGION") == "" {
return errors.Errorf("no region specified: use Region field in backend configuration or AWS_DEFAULT_REGION environment variable")
}
rendered, err := template.Render(backendConfigTmpl, c)
if err != nil {
return nil, fmt.Errorf("rendering backend: %v", err)
}

return nil
return &Backend{config: c, rendered: rendered}, nil
}
Loading

0 comments on commit 74751c8

Please sign in to comment.