Skip to content

Commit 971d169

Browse files
authored
Support custom Terraform backends (#291)
* Update the specification of the `spec.backend` to enable the end-users to custom the Terraform backend configuration Signed-off-by: loheagn <[email protected]>
1 parent 8601f66 commit 971d169

18 files changed

+1371
-204
lines changed

api/v1beta2/configuration_types.go

+23-4
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ type ConfigurationSpec struct {
3535
// +kubebuilder:pruning:PreserveUnknownFields
3636
Variable *runtime.RawExtension `json:"variable,omitempty"`
3737

38-
// Backend stores the state in a Kubernetes secret with locking done using a Lease resource.
39-
// TODO(zzxwill) If a backend exists in HCL/JSON, this can be optional. Currently, if Backend is not set by users, it
40-
// still will set by the controller, ignoring the settings in HCL/JSON backend
38+
// Backend describes the Terraform backend configuration.
39+
// This field is needed if the users use a git repo to provide the hcl files or
40+
// want to use their custom Terraform backend (instead of the default kubernetes backend type).
41+
// Notice: This field may cause two backend blocks in the final Terraform module and make the executor job failed.
42+
// So, please make sure that there are no backend configurations in your inline hcl code or the git repo.
4143
Backend *Backend `json:"backend,omitempty"`
4244

4345
// Path is the sub-directory of remote git repository.
@@ -104,12 +106,29 @@ type Property struct {
104106
Value string `json:"value,omitempty"`
105107
}
106108

107-
// Backend stores the state in a Kubernetes secret with locking done using a Lease resource.
109+
// Backend describes the Terraform backend configuration
108110
type Backend struct {
109111
// SecretSuffix used when creating secrets. Secrets will be named in the format: tfstate-{workspace}-{secretSuffix}
110112
SecretSuffix string `json:"secretSuffix,omitempty"`
111113
// InClusterConfig Used to authenticate to the cluster from inside a pod. Only `true` is allowed
112114
InClusterConfig bool `json:"inClusterConfig,omitempty"`
115+
116+
// Inline allows users to use raw hcl code to specify their Terraform backend
117+
Inline string `json:"inline,omitempty"`
118+
119+
// BackendType indicates which backend type to use. This field is needed for custom backend configuration.
120+
// +kubebuilder:validation:Enum=kubernetes
121+
BackendType string `json:"backendType,omitempty"`
122+
123+
// Kubernetes is needed for the Terraform `kubernetes` backend type.
124+
Kubernetes *KubernetesBackendConf `json:"kubernetes,omitempty"`
125+
}
126+
127+
// KubernetesBackendConf defines all options supported by the Terraform `kubernetes` backend type.
128+
// You can refer to https://www.terraform.io/language/settings/backends/kubernetes for the usage of each option.
129+
type KubernetesBackendConf struct {
130+
SecretSuffix string `json:"secret_suffix" hcl:"secret_suffix"`
131+
Namespace *string `json:"namespace,omitempty" hcl:"namespace"`
113132
}
114133

115134
// +kubebuilder:object:root=true

api/v1beta2/zz_generated.deepcopy.go

+26-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chart/crds/terraform.core.oam.dev_configurations.yaml

+28-5
Original file line numberDiff line numberDiff line change
@@ -188,16 +188,39 @@ spec:
188188
description: ConfigurationSpec defines the desired state of Configuration
189189
properties:
190190
backend:
191-
description: Backend stores the state in a Kubernetes secret with
192-
locking done using a Lease resource. TODO(zzxwill) If a backend
193-
exists in HCL/JSON, this can be optional. Currently, if Backend
194-
is not set by users, it still will set by the controller, ignoring
195-
the settings in HCL/JSON backend
191+
description: 'Backend describes the Terraform backend configuration.
192+
This field is needed if the users use a git repo to provide the
193+
hcl files or want to use their custom Terraform backend (instead
194+
of the default kubernetes backend type). Notice: This field may
195+
cause two backend blocks in the final Terraform module and make
196+
the executor job failed. So, please make sure that there are no
197+
backend configurations in your inline hcl code or the git repo.'
196198
properties:
199+
backendType:
200+
description: BackendType indicates which backend type to use.
201+
This field is needed for custom backend configuration.
202+
enum:
203+
- kubernetes
204+
type: string
197205
inClusterConfig:
198206
description: InClusterConfig Used to authenticate to the cluster
199207
from inside a pod. Only `true` is allowed
200208
type: boolean
209+
inline:
210+
description: Inline allows users to use raw hcl code to specify
211+
their Terraform backend
212+
type: string
213+
kubernetes:
214+
description: Kubernetes is needed for the Terraform `kubernetes`
215+
backend type.
216+
properties:
217+
namespace:
218+
type: string
219+
secret_suffix:
220+
type: string
221+
required:
222+
- secret_suffix
223+
type: object
201224
secretSuffix:
202225
description: 'SecretSuffix used when creating secrets. Secrets
203226
will be named in the format: tfstate-{workspace}-{secretSuffix}'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
Copyright 2022 The KubeVela Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package backend
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"reflect"
23+
"strings"
24+
25+
"github.com/hashicorp/hcl/v2"
26+
"github.com/hashicorp/hcl/v2/gohcl"
27+
"github.com/hashicorp/hcl/v2/hclparse"
28+
"github.com/hashicorp/hcl/v2/hclwrite"
29+
"github.com/oam-dev/terraform-controller/api/v1beta2"
30+
"github.com/pkg/errors"
31+
"github.com/zclconf/go-cty/cty"
32+
"github.com/zclconf/go-cty/cty/gocty"
33+
"sigs.k8s.io/controller-runtime/pkg/client"
34+
)
35+
36+
var backendInitFuncMap = map[string]*backendInitFunc{
37+
"kubernetes": {
38+
initFuncFromHCL: newK8SBackendFromInline,
39+
initFuncFromConf: newK8SBackendFromExplicit,
40+
},
41+
}
42+
43+
// Backend is an abstraction of what all backend types can do
44+
type Backend interface {
45+
// HCL can get the hcl code string
46+
HCL() string
47+
48+
// GetTFStateJSON is used to get the Terraform state json from backend
49+
GetTFStateJSON(ctx context.Context) ([]byte, error)
50+
51+
// CleanUp is used to clean up the backend when delete the configuration object
52+
// For example, if the configuration use kubernetes backend, CleanUp will delete the backend secret
53+
CleanUp(ctx context.Context) error
54+
}
55+
56+
type backendInitFunc struct {
57+
initFuncFromHCL func(*ParsedBackendConfig, client.Client) (Backend, error)
58+
initFuncFromConf func(interface{}, client.Client) (Backend, error)
59+
}
60+
61+
// ParsedBackendConfig is a struct parsed from the backend hcl block
62+
type ParsedBackendConfig struct {
63+
// Name is the label of the backend hcl block
64+
// It means which backend type the configuration will use
65+
Name string `hcl:"name,label"`
66+
// Attrs are the key-value pairs in the backend hcl block
67+
Attrs hcl.Body `hcl:",remain"`
68+
}
69+
70+
func (conf ParsedBackendConfig) getAttrValue(key string) (*cty.Value, error) {
71+
attrs, diags := conf.Attrs.JustAttributes()
72+
if diags.HasErrors() {
73+
return nil, diags
74+
}
75+
attr := attrs[key]
76+
if attr == nil {
77+
return nil, fmt.Errorf("cannot find attr %s", key)
78+
}
79+
v, diags := attr.Expr.Value(nil)
80+
if diags.HasErrors() {
81+
return nil, diags
82+
}
83+
return &v, nil
84+
}
85+
86+
func (conf ParsedBackendConfig) getAttrString(key string) (string, error) {
87+
v, err := conf.getAttrValue(key)
88+
if err != nil {
89+
return "", err
90+
}
91+
result := ""
92+
err = gocty.FromCtyValue(*v, &result)
93+
return result, err
94+
}
95+
96+
// ParseConfigurationBackend parses backend Conf from the v1beta2.Configuration
97+
func ParseConfigurationBackend(configuration *v1beta2.Configuration, k8sClient client.Client) (Backend, error) {
98+
backend := configuration.Spec.Backend
99+
100+
switch {
101+
102+
case backend == nil || (backend.Inline == "" && backend.BackendType == ""):
103+
// use the default k8s backend
104+
if configuration.Spec.Backend != nil {
105+
if configuration.Spec.Backend.SecretSuffix == "" {
106+
configuration.Spec.Backend.SecretSuffix = configuration.Name
107+
}
108+
configuration.Spec.Backend.InClusterConfig = true
109+
} else {
110+
configuration.Spec.Backend = &v1beta2.Backend{
111+
SecretSuffix: configuration.Name,
112+
InClusterConfig: true,
113+
}
114+
}
115+
return newDefaultK8SBackend(configuration.Spec.Backend.SecretSuffix, k8sClient), nil
116+
117+
case backend.Inline != "" && backend.BackendType != "":
118+
return nil, errors.New("it's not allowed to set `spec.backend.inline` and `spec.backend.backendType` at the same time")
119+
120+
case backend.Inline != "":
121+
// In this case, use the inline custom backend
122+
return handleInlineBackendHCL(backend.Inline, k8sClient)
123+
124+
case backend.BackendType != "":
125+
// In this case, use the explicit custom backend
126+
127+
// first, check if is valid custom backend
128+
backendType := backend.BackendType
129+
// fetch backendConfValue using reflection
130+
backendStructValue := reflect.ValueOf(backend)
131+
if backendStructValue.Kind() == reflect.Ptr {
132+
backendStructValue = backendStructValue.Elem()
133+
}
134+
backendField := backendStructValue.FieldByNameFunc(func(name string) bool {
135+
return strings.EqualFold(name, backendType)
136+
})
137+
if backendField.IsNil() {
138+
return nil, fmt.Errorf("there is no configuration for backendType %s", backend.BackendType)
139+
}
140+
backendConfValue := backendField.Interface()
141+
142+
// second, handle the backendConf
143+
return handleExplicitBackend(backendConfValue, backendType, k8sClient)
144+
}
145+
146+
return nil, nil
147+
}
148+
149+
func handleInlineBackendHCL(code string, k8sClient client.Client) (Backend, error) {
150+
151+
type BackendConfigWrap struct {
152+
Backend ParsedBackendConfig `hcl:"backend,block"`
153+
}
154+
155+
type TerraformConfig struct {
156+
Remain interface{} `hcl:",remain"`
157+
Terraform struct {
158+
Remain interface{} `hcl:",remain"`
159+
Backend ParsedBackendConfig `hcl:"backend,block"`
160+
} `hcl:"terraform,block"`
161+
}
162+
163+
hclFile, diags := hclparse.NewParser().ParseHCL([]byte(code), "backend")
164+
if diags.HasErrors() {
165+
return nil, fmt.Errorf("there are syntax errors in the inline backend hcl code: %w", diags)
166+
}
167+
168+
// try to parse hclFile to Config or BackendConfig
169+
config := &TerraformConfig{}
170+
// nolint:staticcheck
171+
backendConfig := &ParsedBackendConfig{}
172+
diags = gohcl.DecodeBody(hclFile.Body, nil, config)
173+
if diags.HasErrors() || config.Terraform.Backend.Name == "" {
174+
backendConfigWrap := &BackendConfigWrap{}
175+
diags = gohcl.DecodeBody(hclFile.Body, nil, backendConfigWrap)
176+
if diags.HasErrors() || backendConfigWrap.Backend.Name == "" {
177+
return nil, fmt.Errorf("the inline backend hcl code is not valid Terraform backend configuration: %w", diags)
178+
}
179+
backendConfig = &backendConfigWrap.Backend
180+
} else {
181+
backendConfig = &config.Terraform.Backend
182+
}
183+
184+
initFunc := backendInitFuncMap[backendConfig.Name]
185+
if initFunc == nil || initFunc.initFuncFromHCL == nil {
186+
return nil, fmt.Errorf("backend type (%s) is not supported", backendConfig.Name)
187+
}
188+
return initFunc.initFuncFromHCL(backendConfig, k8sClient)
189+
}
190+
191+
func handleExplicitBackend(backendConf interface{}, backendType string, k8sClient client.Client) (Backend, error) {
192+
hclFile := hclwrite.NewEmptyFile()
193+
gohcl.EncodeIntoBody(backendConf, hclFile.Body())
194+
195+
initFunc := backendInitFuncMap[backendType]
196+
if initFunc == nil || initFunc.initFuncFromConf == nil {
197+
return nil, fmt.Errorf("backend type (%s) is not supported", backendType)
198+
}
199+
return initFunc.initFuncFromConf(backendConf, k8sClient)
200+
}

0 commit comments

Comments
 (0)