Skip to content

Commit

Permalink
Add support for custom s3 backend (kubevela#316)
Browse files Browse the repository at this point in the history
* Add support for custom s3 backend

Signed-off-by: loheagn <loheagn@icloud.com>

* Rename some examples

Signed-off-by: loheagn <loheagn@icloud.com>

* Rename `S3Backend.Token` to `S3Backend.SessionToken`

Signed-off-by: loheagn <loheagn@icloud.com>

* Fix go.mod

Signed-off-by: loheagn <loheagn@icloud.com>

* Refactor the `backend.Backend` initialization logic

Signed-off-by: loheagn <loheagn@icloud.com>

* lint code

Signed-off-by: loheagn <loheagn@icloud.com>

* lint code

Signed-off-by: loheagn <loheagn@icloud.com>

* lint code

Signed-off-by: loheagn <loheagn@icloud.com>

* lint code

Signed-off-by: loheagn <loheagn@icloud.com>

* e2e test

Signed-off-by: loheagn <loheagn@icloud.com>

* e2e test retry

Signed-off-by: loheagn <loheagn@icloud.com>

* e2e test

Signed-off-by: loheagn <loheagn@icloud.com>

* e2e test retry

Signed-off-by: loheagn <loheagn@icloud.com>

* e2e test try again

Signed-off-by: loheagn <loheagn@icloud.com>

* e2e test try again

Signed-off-by: loheagn <loheagn@icloud.com>

* e2e test try again

Signed-off-by: loheagn <loheagn@icloud.com>

* e2e test try again

Signed-off-by: loheagn <loheagn@icloud.com>

* e2e test try again

Signed-off-by: loheagn <loheagn@icloud.com>

* e2e test

Signed-off-by: loheagn <loheagn@icloud.com>

* Reuse the meta.Credentials to build `backend.Backend`

Signed-off-by: loheagn <loheagn@icloud.com>

* make "kubernetes" and "s3" constants

Signed-off-by: loheagn <loheagn@icloud.com>
  • Loading branch information
loheagn authored Jun 9, 2022
1 parent 6a3bc1a commit 487809a
Show file tree
Hide file tree
Showing 19 changed files with 704 additions and 196 deletions.
13 changes: 12 additions & 1 deletion api/v1beta2/configuration_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,14 @@ type Backend struct {
Inline string `json:"inline,omitempty"`

// BackendType indicates which backend type to use. This field is needed for custom backend configuration.
// +kubebuilder:validation:Enum=kubernetes
// +kubebuilder:validation:Enum=kubernetes;s3
BackendType string `json:"backendType,omitempty"`

// Kubernetes is needed for the Terraform `kubernetes` backend type.
Kubernetes *KubernetesBackendConf `json:"kubernetes,omitempty"`

// S3 is needed for the Terraform `s3` backend type.
S3 *S3BackendConf `json:"s3,omitempty"`
}

// KubernetesBackendConf defines all options supported by the Terraform `kubernetes` backend type.
Expand All @@ -134,6 +137,14 @@ type KubernetesBackendConf struct {
Namespace *string `json:"namespace,omitempty" hcl:"namespace"`
}

// S3BackendConf defines all options supported by the Terraform `s3` backend type.
// You can refer to https://www.terraform.io/language/settings/backends/s3 for the usage of each option.
type S3BackendConf struct {
Region string `json:"region" hcl:"region"`
Bucket string `json:"bucket" hcl:"bucket"`
Key string `json:"key" hcl:"key"`
}

// +kubebuilder:object:root=true

// Configuration is the Schema for the configurations API
Expand Down
25 changes: 25 additions & 0 deletions api/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions chart/crds/terraform.core.oam.dev_configurations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ spec:
This field is needed for custom backend configuration.
enum:
- kubernetes
- s3
type: string
inClusterConfig:
description: InClusterConfig Used to authenticate to the cluster
Expand All @@ -221,6 +222,20 @@ spec:
required:
- secret_suffix
type: object
s3:
description: S3 is needed for the Terraform `s3` backend type.
properties:
bucket:
type: string
key:
type: string
region:
type: string
required:
- bucket
- key
- region
type: object
secretSuffix:
description: 'SecretSuffix used when creating secrets. Secrets
will be named in the format: tfstate-{workspace}-{secretSuffix}'
Expand Down
190 changes: 80 additions & 110 deletions controllers/configuration/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,15 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/oam-dev/terraform-controller/api/v1beta2"
"github.com/pkg/errors"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var backendInitFuncMap = map[string]*backendInitFunc{
"kubernetes": {
initFuncFromHCL: newK8SBackendFromInline,
initFuncFromConf: newK8SBackendFromExplicit,
},
}
const (
backendTypeK8S = "kubernetes"
backendTypeS3 = "s3"
)

// Backend is an abstraction of what all backend types can do
type Backend interface {
Expand All @@ -53,148 +48,123 @@ type Backend interface {
CleanUp(ctx context.Context) error
}

type backendInitFunc struct {
initFuncFromHCL func(*ParsedBackendConfig, client.Client) (Backend, error)
initFuncFromConf func(interface{}, client.Client) (Backend, error)
}

// ParsedBackendConfig is a struct parsed from the backend hcl block
type ParsedBackendConfig struct {
// Name is the label of the backend hcl block
// It means which backend type the configuration will use
Name string `hcl:"name,label"`
// Attrs are the key-value pairs in the backend hcl block
Attrs hcl.Body `hcl:",remain"`
}

func (conf ParsedBackendConfig) getAttrValue(key string) (*cty.Value, error) {
attrs, diags := conf.Attrs.JustAttributes()
if diags.HasErrors() {
return nil, diags
}
attr := attrs[key]
if attr == nil {
return nil, fmt.Errorf("cannot find attr %s", key)
}
v, diags := attr.Expr.Value(nil)
if diags.HasErrors() {
return nil, diags
}
return &v, nil
}
type backendInitFunc func(k8sClient client.Client, backendConf interface{}, credentials map[string]string) (Backend, error)

func (conf ParsedBackendConfig) getAttrString(key string) (string, error) {
v, err := conf.getAttrValue(key)
if err != nil {
return "", err
}
result := ""
err = gocty.FromCtyValue(*v, &result)
return result, err
var backendInitFuncMap = map[string]backendInitFunc{
backendTypeK8S: newK8SBackend,
backendTypeS3: newS3Backend,
}

// ParseConfigurationBackend parses backend Conf from the v1beta2.Configuration
func ParseConfigurationBackend(configuration *v1beta2.Configuration, k8sClient client.Client) (Backend, error) {
func ParseConfigurationBackend(configuration *v1beta2.Configuration, k8sClient client.Client, credentials map[string]string) (Backend, error) {
backend := configuration.Spec.Backend

switch {
var (
backendType string
backendConf interface{}
err error
)

switch {
case backend == nil || (backend.Inline == "" && backend.BackendType == ""):
// use the default k8s backend
if configuration.Spec.Backend != nil {
if configuration.Spec.Backend.SecretSuffix == "" {
configuration.Spec.Backend.SecretSuffix = configuration.Name
}
configuration.Spec.Backend.InClusterConfig = true
} else {
configuration.Spec.Backend = &v1beta2.Backend{
SecretSuffix: configuration.Name,
InClusterConfig: true,
}
}
return newDefaultK8SBackend(configuration.Spec.Backend.SecretSuffix, k8sClient), nil
return handleDefaultBackend(configuration, k8sClient)

case backend.Inline != "" && backend.BackendType != "":
return nil, errors.New("it's not allowed to set `spec.backend.inline` and `spec.backend.backendType` at the same time")

case backend.Inline != "":
// In this case, use the inline custom backend
return handleInlineBackendHCL(backend.Inline, k8sClient)
backendType, backendConf, err = handleInlineBackendHCL(backend.Inline)

case backend.BackendType != "":
// In this case, use the explicit custom backend

// first, check if is valid custom backend
backendType := backend.BackendType
// fetch backendConfValue using reflection
backendStructValue := reflect.ValueOf(backend)
if backendStructValue.Kind() == reflect.Ptr {
backendStructValue = backendStructValue.Elem()
}
backendField := backendStructValue.FieldByNameFunc(func(name string) bool {
return strings.EqualFold(name, backendType)
})
if backendField.IsNil() {
return nil, fmt.Errorf("there is no configuration for backendType %s", backend.BackendType)
}
backendConfValue := backendField.Interface()

// second, handle the backendConf
return handleExplicitBackend(backendConfValue, backendType, k8sClient)
backendType, backendConf, err = handleExplicitBackend(backend)
}
if err != nil {
return nil, err
}

return nil, nil
initFunc := backendInitFuncMap[backendType]
if initFunc == nil {
return nil, fmt.Errorf("backend type (%s) is not supported", backendType)
}
return initFunc(k8sClient, backendConf, credentials)
}

func handleInlineBackendHCL(code string, k8sClient client.Client) (Backend, error) {

type BackendConfigWrap struct {
Backend ParsedBackendConfig `hcl:"backend,block"`
func handleDefaultBackend(configuration *v1beta2.Configuration, k8sClient client.Client) (Backend, error) {
if configuration.Spec.Backend != nil {
if configuration.Spec.Backend.SecretSuffix == "" {
configuration.Spec.Backend.SecretSuffix = configuration.Name
}
configuration.Spec.Backend.InClusterConfig = true
} else {
configuration.Spec.Backend = &v1beta2.Backend{
SecretSuffix: configuration.Name,
InClusterConfig: true,
}
}
return newDefaultK8SBackend(configuration.Spec.Backend.SecretSuffix, k8sClient), nil
}

func handleInlineBackendHCL(hclCode string) (string, interface{}, error) {
type TerraformConfig struct {
Remain interface{} `hcl:",remain"`
Terraform struct {
Remain interface{} `hcl:",remain"`
Backend ParsedBackendConfig `hcl:"backend,block"`
Backend struct {
Name string `hcl:"name,label"`
Attrs hcl.Body `hcl:",remain"`
} `hcl:"backend,block"`
} `hcl:"terraform,block"`
}

hclFile, diags := hclparse.NewParser().ParseHCL([]byte(code), "backend")
hclFile, diags := hclparse.NewParser().ParseHCL([]byte(hclCode), "backend")
if diags.HasErrors() {
return nil, fmt.Errorf("there are syntax errors in the inline backend hcl code: %w", diags)
return "", nil, fmt.Errorf("there are syntax errors in the inline backend hcl code: %w", diags)
}

// try to parse hclFile to Config or BackendConfig
// try to parse hclFile to TerraformConfig or TerraformConfig.Terraform
config := &TerraformConfig{}
// nolint:staticcheck
backendConfig := &ParsedBackendConfig{}
diags = gohcl.DecodeBody(hclFile.Body, nil, config)
if diags.HasErrors() || config.Terraform.Backend.Name == "" {
backendConfigWrap := &BackendConfigWrap{}
diags = gohcl.DecodeBody(hclFile.Body, nil, backendConfigWrap)
if diags.HasErrors() || backendConfigWrap.Backend.Name == "" {
return nil, fmt.Errorf("the inline backend hcl code is not valid Terraform backend configuration: %w", diags)
diags = gohcl.DecodeBody(hclFile.Body, nil, &config.Terraform)
if diags.HasErrors() || config.Terraform.Backend.Name == "" {
return "", nil, fmt.Errorf("the inline backend hcl code is not valid Terraform backend configuration: %w", diags)
}
backendConfig = &backendConfigWrap.Backend
} else {
backendConfig = &config.Terraform.Backend
}

initFunc := backendInitFuncMap[backendConfig.Name]
if initFunc == nil || initFunc.initFuncFromHCL == nil {
return nil, fmt.Errorf("backend type (%s) is not supported", backendConfig.Name)
backendType := config.Terraform.Backend.Name

var backendConf interface{}
switch strings.ToLower(backendType) {
case backendTypeK8S:
backendConf = &v1beta2.KubernetesBackendConf{}
case backendTypeS3:
backendConf = &v1beta2.S3BackendConf{}
default:
return "", nil, fmt.Errorf("backend type (%s) is not supported", backendType)
}
diags = gohcl.DecodeBody(config.Terraform.Backend.Attrs, nil, backendConf)
if diags.HasErrors() {
return "", nil, fmt.Errorf("the inline backend hcl code is not valid Terraform backend configuration: %w", diags)
}
return initFunc.initFuncFromHCL(backendConfig, k8sClient)

return backendType, backendConf, nil
}

func handleExplicitBackend(backendConf interface{}, backendType string, k8sClient client.Client) (Backend, error) {
hclFile := hclwrite.NewEmptyFile()
gohcl.EncodeIntoBody(backendConf, hclFile.Body())
func handleExplicitBackend(backend *v1beta2.Backend) (string, interface{}, error) {
// check if is valid custom backend
backendType := backend.BackendType

initFunc := backendInitFuncMap[backendType]
if initFunc == nil || initFunc.initFuncFromConf == nil {
return nil, fmt.Errorf("backend type (%s) is not supported", backendType)
// fetch backendConfValue using reflection
backendStructValue := reflect.ValueOf(backend)
if backendStructValue.Kind() == reflect.Ptr {
backendStructValue = backendStructValue.Elem()
}
backendField := backendStructValue.FieldByNameFunc(func(name string) bool {
return strings.EqualFold(name, backendType)
})
if backendField.Kind() != reflect.Ptr || backendField.IsNil() {
return "", nil, fmt.Errorf("there is no configuration for backendType %s", backend.BackendType)
}
return initFunc.initFuncFromConf(backendConf, k8sClient)
return backendType, backendField.Interface(), nil
}
Loading

0 comments on commit 487809a

Please sign in to comment.