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

Refactor backend handling #828

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
34 changes: 5 additions & 29 deletions cli/cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import (
"helm.sh/helm/v3/pkg/chart"
"sigs.k8s.io/yaml"

"github.com/kinvolk/lokomotive/pkg/backend"
"github.com/kinvolk/lokomotive/pkg/backend/local"
"github.com/kinvolk/lokomotive/pkg/components/util"
"github.com/kinvolk/lokomotive/pkg/config"
"github.com/kinvolk/lokomotive/pkg/platform"
Expand Down Expand Up @@ -58,52 +56,30 @@ func initialize(ctxLogger *logrus.Entry) (*terraform.Executor, platform.Platform
ctxLogger.Fatal("Errors found while loading cluster configuration")
}

// 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")
}

// Use a local backend if no backend is configured.
if b == nil {
b = local.NewLocalBackend()
if p == nil {
ctxLogger.Fatal("No cluster configured")
}

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)
ex := initializeTerraform(ctxLogger, p)

return ex, p, lokoConfig, assetDir
}

// 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 {
func initializeTerraform(ctxLogger *logrus.Entry, p platform.Platform) *terraform.Executor {
assetDir, err := homedir.Expand(p.Meta().AssetDir)
if err != nil {
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); err != nil {
ctxLogger.Fatalf("Failed to configure Terraform : %v", err)
}

Expand Down
22 changes: 1 addition & 21 deletions cli/cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"github.com/sirupsen/logrus"
"github.com/spf13/viper"

"github.com/kinvolk/lokomotive/pkg/backend"
"github.com/kinvolk/lokomotive/pkg/config"
"github.com/kinvolk/lokomotive/pkg/platform"
)
Expand All @@ -36,25 +35,6 @@ const (
kubeconfigTerraformOutputKey = "kubeconfig"
)

// 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(lokoConfig *config.Config, require bool) (platform.Platform, hcl.Diagnostics) {
if lokoConfig.RootConfig.Cluster == nil && !require {
Expand All @@ -80,7 +60,7 @@ func getConfiguredPlatform(lokoConfig *config.Config, require bool) (platform.Pl
return nil, hcl.Diagnostics{diag}
}

return platform, platform.LoadConfig(&lokoConfig.RootConfig.Cluster.Config, lokoConfig.EvalContext)
return platform, platform.LoadConfig(lokoConfig)
}

// getKubeconfig finds the right kubeconfig file to use for an action and returns it's content.
Expand Down
74 changes: 48 additions & 26 deletions pkg/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,61 @@ import (
"fmt"

"github.com/hashicorp/hcl/v2"
"github.com/kinvolk/lokomotive/pkg/backend/local"
"github.com/kinvolk/lokomotive/pkg/backend/s3"
"github.com/kinvolk/lokomotive/pkg/config"
)

// Backend describes the 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
const (
// Local represents a local backend.
Local = "local"
// S3 represents an S3 backend.
S3 = "s3"
)

// Backend describes a Terraform state storage location.
type Backend struct {
Type string
Config interface{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why empty interface if we know all the types we deal with?

}

// backends is a collection in which all Backends get automatically registered.
var backends map[string]Backend
// New creates a new Backend from the provided config and returns a pointer to it.
func New(c *config.Config) (*Backend, hcl.Diagnostics) {
if c == nil && c.RootConfig.Backend == nil {
return nil, hcl.Diagnostics{
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "nil backend config",
},
}
}

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

// 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))
var bc interface{}

var d hcl.Diagnostics

switch backendType {
case Local:
bc, d = local.NewConfig(&c.RootConfig.Backend.Config, c.EvalContext)
case S3:
bc, d = s3.NewConfig(&c.RootConfig.Backend.Config, c.EvalContext)
default:
return nil, hcl.Diagnostics{
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("unknown backend type %q", backendType),
},
}
}
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)
if d.HasErrors() {
return nil, d
}
return backend, nil

return &Backend{
Type: backendType,
Config: bc,
}, nil
}
36 changes: 11 additions & 25 deletions pkg/backend/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,24 @@ package local
import (
"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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe FromHCL would be a better name for this function? So it reads pkg/backend/s3.FromHCL(...) ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I'd get rid of this code completely and do the reading only once somewhere, as reading is always exactly the same regardless of the backend type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what we had with LoadConfig(), however I don't think this is a good approach - see #828 (comment).

c := &Config{}

// 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 b == nil {
return nil, hcl.Diagnostics{}
}
return gohcl.DecodeBody(*configBody, evalContext, l)
}

func NewLocalBackend() *local {
return &local{}
}

// Render renders the Go template with local backend configuration.
func (l *local) Render() (string, error) {
return template.Render(backendConfigTmpl, l)
}
if d := gohcl.DecodeBody(*b, ctx, c); len(d) != 0 {
return nil, d
}

// Validate validates the local backend configuration.
func (l *local) Validate() error {
return nil
return c, hcl.Diagnostics{}
}
23 changes: 0 additions & 23 deletions pkg/backend/local/template.go

This file was deleted.

65 changes: 32 additions & 33 deletions pkg/backend/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,60 +16,59 @@ package s3

import (
"fmt"
"os"

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

"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())
}
// 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{}

// 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 b == nil {
return nil, diags
}
return gohcl.DecodeBody(*configBody, evalContext, s)
}

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

if err := c.validate(); err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("validating backend config: %v", err),
})

return nil, diags
}

// Render renders the Go template with s3 backend configuration.
func (s *s3) Render() (string, error) {
return template.Render(backendConfigTmpl, s)
return c, diags
}

// Validate validates the s3 backend configuration.
func (s *s3) Validate() error {
if s.Bucket == "" {
johananl marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("no bucket specified")
// 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 s.Key == "" {
return fmt.Errorf("no key specified")
if c.Key == "" {
return fmt.Errorf("key cannot be empty")
}

if s.AWSCredsPath == "" && os.Getenv("AWS_SHARED_CREDENTIALS_FILE") == "" {
if s.Region == "" && os.Getenv("AWS_DEFAULT_REGION") == "" {
return fmt.Errorf("no region specified: use Region field in backend configuration or " +
"AWS_DEFAULT_REGION environment variable")
}
if c.Region == "" {
return fmt.Errorf("region cannot be empty")
}

return nil
Expand Down
29 changes: 0 additions & 29 deletions pkg/backend/s3/template.go

This file was deleted.

Loading