Skip to content

Commit 7769ced

Browse files
committed
Add kubernetes backend
1 parent 9266e94 commit 7769ced

File tree

668 files changed

+88914
-87397
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

668 files changed

+88914
-87397
lines changed

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
/backend/remote-state/pg Unmaintained
1616
/backend/remote-state/s3 @hashicorp/terraform-aws
1717
/backend/remote-state/swift Unmaintained
18+
/backend/remote-state/kubernetes @jrhouston @alexsomesan
1819

1920
# Provisioners
2021
builtin/provisioners/chef Unmaintained

backend/init/init.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
backendGCS "github.com/hashicorp/terraform/backend/remote-state/gcs"
2323
backendHTTP "github.com/hashicorp/terraform/backend/remote-state/http"
2424
backendInmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
25+
backendKubernetes "github.com/hashicorp/terraform/backend/remote-state/kubernetes"
2526
backendManta "github.com/hashicorp/terraform/backend/remote-state/manta"
2627
backendOSS "github.com/hashicorp/terraform/backend/remote-state/oss"
2728
backendPg "github.com/hashicorp/terraform/backend/remote-state/pg"
@@ -64,6 +65,7 @@ func Init(services *disco.Disco) {
6465
"gcs": func() backend.Backend { return backendGCS.New() },
6566
"http": func() backend.Backend { return backendHTTP.New() },
6667
"inmem": func() backend.Backend { return backendInmem.New() },
68+
"kubernetes": func() backend.Backend { return backendKubernetes.New() },
6769
"manta": func() backend.Backend { return backendManta.New() },
6870
"oss": func() backend.Backend { return backendOSS.New() },
6971
"pg": func() backend.Backend { return backendPg.New() },
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
package kubernetes
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"log"
8+
"os"
9+
10+
"github.com/hashicorp/terraform/backend"
11+
"github.com/hashicorp/terraform/helper/schema"
12+
"github.com/hashicorp/terraform/version"
13+
"github.com/mitchellh/cli"
14+
"github.com/mitchellh/go-homedir"
15+
k8sSchema "k8s.io/apimachinery/pkg/runtime/schema"
16+
"k8s.io/client-go/dynamic"
17+
restclient "k8s.io/client-go/rest"
18+
"k8s.io/client-go/tools/clientcmd"
19+
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
20+
)
21+
22+
// Modified from github.com/terraform-providers/terraform-provider-kubernetes
23+
24+
const (
25+
noConfigError = `
26+
27+
[Kubernetes backend] Neither service_account nor load_config_file were set to true,
28+
this could cause issues connecting to your Kubernetes cluster.
29+
`
30+
)
31+
32+
var (
33+
secretResource = k8sSchema.GroupVersionResource{
34+
Group: "",
35+
Version: "v1",
36+
Resource: "secrets",
37+
}
38+
)
39+
40+
// New creates a new backend for kubernetes remote state.
41+
func New() backend.Backend {
42+
s := &schema.Backend{
43+
Schema: map[string]*schema.Schema{
44+
"secret_suffix": {
45+
Type: schema.TypeString,
46+
Required: true,
47+
Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`.",
48+
},
49+
"labels": {
50+
Type: schema.TypeMap,
51+
Optional: true,
52+
Description: "Map of additional labels to be applied to the secret.",
53+
Elem: &schema.Schema{Type: schema.TypeString},
54+
},
55+
"namespace": {
56+
Type: schema.TypeString,
57+
Optional: true,
58+
DefaultFunc: schema.EnvDefaultFunc("KUBE_NAMESPACE", "default"),
59+
Description: "Namespace to store the secret in.",
60+
},
61+
"in_cluster_config": {
62+
Type: schema.TypeBool,
63+
Optional: true,
64+
DefaultFunc: schema.EnvDefaultFunc("KUBE_IN_CLUSTER_CONFIG", false),
65+
Description: "Used to authenticate to the cluster from inside a pod.",
66+
},
67+
"load_config_file": {
68+
Type: schema.TypeBool,
69+
Optional: true,
70+
DefaultFunc: schema.EnvDefaultFunc("KUBE_LOAD_CONFIG_FILE", true),
71+
Description: "Load local kubeconfig.",
72+
},
73+
"host": {
74+
Type: schema.TypeString,
75+
Optional: true,
76+
DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""),
77+
Description: "The hostname (in form of URI) of Kubernetes master.",
78+
},
79+
"username": {
80+
Type: schema.TypeString,
81+
Optional: true,
82+
DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", ""),
83+
Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
84+
},
85+
"password": {
86+
Type: schema.TypeString,
87+
Optional: true,
88+
DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", ""),
89+
Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
90+
},
91+
"insecure": {
92+
Type: schema.TypeBool,
93+
Optional: true,
94+
DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false),
95+
Description: "Whether server should be accessed without verifying the TLS certificate.",
96+
},
97+
"client_certificate": {
98+
Type: schema.TypeString,
99+
Optional: true,
100+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""),
101+
Description: "PEM-encoded client certificate for TLS authentication.",
102+
},
103+
"client_key": {
104+
Type: schema.TypeString,
105+
Optional: true,
106+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""),
107+
Description: "PEM-encoded client certificate key for TLS authentication.",
108+
},
109+
"cluster_ca_certificate": {
110+
Type: schema.TypeString,
111+
Optional: true,
112+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""),
113+
Description: "PEM-encoded root certificates bundle for TLS authentication.",
114+
},
115+
"config_path": {
116+
Type: schema.TypeString,
117+
Optional: true,
118+
DefaultFunc: schema.MultiEnvDefaultFunc(
119+
[]string{
120+
"KUBE_CONFIG",
121+
"KUBECONFIG",
122+
},
123+
"~/.kube/config"),
124+
Description: "Path to the kube config file, defaults to ~/.kube/config",
125+
},
126+
"config_context": {
127+
Type: schema.TypeString,
128+
Optional: true,
129+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""),
130+
},
131+
"config_context_auth_info": {
132+
Type: schema.TypeString,
133+
Optional: true,
134+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""),
135+
Description: "",
136+
},
137+
"config_context_cluster": {
138+
Type: schema.TypeString,
139+
Optional: true,
140+
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""),
141+
Description: "",
142+
},
143+
"token": {
144+
Type: schema.TypeString,
145+
Optional: true,
146+
DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""),
147+
Description: "Token to authentifcate a service account.",
148+
},
149+
"exec": {
150+
Type: schema.TypeList,
151+
Optional: true,
152+
MaxItems: 1,
153+
Elem: &schema.Resource{
154+
Schema: map[string]*schema.Schema{
155+
"api_version": {
156+
Type: schema.TypeString,
157+
Required: true,
158+
},
159+
"command": {
160+
Type: schema.TypeString,
161+
Required: true,
162+
},
163+
"env": {
164+
Type: schema.TypeMap,
165+
Optional: true,
166+
Elem: &schema.Schema{Type: schema.TypeString},
167+
},
168+
"args": {
169+
Type: schema.TypeList,
170+
Optional: true,
171+
Elem: &schema.Schema{Type: schema.TypeString},
172+
},
173+
},
174+
},
175+
Description: "Use a credential plugin to authenticate.",
176+
},
177+
},
178+
}
179+
180+
result := &Backend{Backend: s}
181+
result.Backend.ConfigureFunc = result.configure
182+
return result
183+
}
184+
185+
type Backend struct {
186+
*schema.Backend
187+
188+
// The fields below are set from configure
189+
kubernetesSecretClient dynamic.ResourceInterface
190+
config *restclient.Config
191+
namespace string
192+
labels map[string]string
193+
nameSuffix string
194+
}
195+
196+
func (b Backend) KubernetesSecretClient() (dynamic.ResourceInterface, error) {
197+
if b.kubernetesSecretClient != nil {
198+
return b.kubernetesSecretClient, nil
199+
}
200+
201+
client, err := dynamic.NewForConfig(b.config)
202+
if err != nil {
203+
return nil, fmt.Errorf("Failed to configure: %s", err)
204+
}
205+
206+
b.kubernetesSecretClient = client.Resource(secretResource).Namespace(b.namespace)
207+
return b.kubernetesSecretClient, nil
208+
}
209+
210+
func (b *Backend) configure(ctx context.Context) error {
211+
if b.config != nil {
212+
return nil
213+
}
214+
215+
// Grab the resource data
216+
data := schema.FromContextBackendConfig(ctx)
217+
218+
cfg, err := getInitialConfig(data)
219+
if err != nil {
220+
return err
221+
}
222+
223+
// Overriding with static configuration
224+
cfg.UserAgent = fmt.Sprintf("HashiCorp/1.0 Terraform/%s", version.String())
225+
226+
if v, ok := data.GetOk("host"); ok {
227+
cfg.Host = v.(string)
228+
}
229+
if v, ok := data.GetOk("username"); ok {
230+
cfg.Username = v.(string)
231+
}
232+
if v, ok := data.GetOk("password"); ok {
233+
cfg.Password = v.(string)
234+
}
235+
if v, ok := data.GetOk("insecure"); ok {
236+
cfg.Insecure = v.(bool)
237+
}
238+
if v, ok := data.GetOk("cluster_ca_certificate"); ok {
239+
cfg.CAData = bytes.NewBufferString(v.(string)).Bytes()
240+
}
241+
if v, ok := data.GetOk("client_certificate"); ok {
242+
cfg.CertData = bytes.NewBufferString(v.(string)).Bytes()
243+
}
244+
if v, ok := data.GetOk("client_key"); ok {
245+
cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes()
246+
}
247+
if v, ok := data.GetOk("token"); ok {
248+
cfg.BearerToken = v.(string)
249+
}
250+
251+
if v, ok := data.GetOk("labels"); ok {
252+
labels := map[string]string{}
253+
for k, vv := range v.(map[string]interface{}) {
254+
labels[k] = vv.(string)
255+
}
256+
b.labels = labels
257+
}
258+
259+
ns := data.Get("namespace").(string)
260+
b.namespace = ns
261+
b.nameSuffix = data.Get("secret_suffix").(string)
262+
b.config = cfg
263+
264+
return nil
265+
}
266+
267+
func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) {
268+
var cfg *restclient.Config
269+
var err error
270+
271+
c := &cli.BasicUi{Writer: os.Stdout}
272+
273+
inCluster := data.Get("in_cluster_config").(bool)
274+
cf := data.Get("load_config_file").(bool)
275+
276+
if !inCluster && !cf {
277+
c.Output(noConfigError)
278+
}
279+
280+
if inCluster {
281+
cfg, err = restclient.InClusterConfig()
282+
if err != nil {
283+
return nil, err
284+
}
285+
} else {
286+
cfg, err = tryLoadingConfigFile(data)
287+
if err != nil {
288+
return nil, err
289+
}
290+
}
291+
292+
if cfg == nil {
293+
cfg = &restclient.Config{}
294+
}
295+
return cfg, err
296+
}
297+
298+
func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) {
299+
path, err := homedir.Expand(d.Get("config_path").(string))
300+
if err != nil {
301+
return nil, err
302+
}
303+
304+
loader := &clientcmd.ClientConfigLoadingRules{
305+
ExplicitPath: path,
306+
}
307+
308+
overrides := &clientcmd.ConfigOverrides{}
309+
ctxSuffix := "; default context"
310+
311+
ctx, ctxOk := d.GetOk("config_context")
312+
authInfo, authInfoOk := d.GetOk("config_context_auth_info")
313+
cluster, clusterOk := d.GetOk("config_context_cluster")
314+
if ctxOk || authInfoOk || clusterOk {
315+
ctxSuffix = "; overriden context"
316+
if ctxOk {
317+
overrides.CurrentContext = ctx.(string)
318+
ctxSuffix += fmt.Sprintf("; config ctx: %s", overrides.CurrentContext)
319+
log.Printf("[DEBUG] Using custom current context: %q", overrides.CurrentContext)
320+
}
321+
322+
overrides.Context = clientcmdapi.Context{}
323+
if authInfoOk {
324+
overrides.Context.AuthInfo = authInfo.(string)
325+
ctxSuffix += fmt.Sprintf("; auth_info: %s", overrides.Context.AuthInfo)
326+
}
327+
if clusterOk {
328+
overrides.Context.Cluster = cluster.(string)
329+
ctxSuffix += fmt.Sprintf("; cluster: %s", overrides.Context.Cluster)
330+
}
331+
log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context)
332+
}
333+
334+
if v, ok := d.GetOk("exec"); ok {
335+
exec := &clientcmdapi.ExecConfig{}
336+
if spec, ok := v.([]interface{})[0].(map[string]interface{}); ok {
337+
exec.APIVersion = spec["api_version"].(string)
338+
exec.Command = spec["command"].(string)
339+
exec.Args = expandStringSlice(spec["args"].([]interface{}))
340+
for kk, vv := range spec["env"].(map[string]interface{}) {
341+
exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{Name: kk, Value: vv.(string)})
342+
}
343+
} else {
344+
return nil, fmt.Errorf("Failed to parse exec")
345+
}
346+
overrides.AuthInfo.Exec = exec
347+
}
348+
349+
cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides)
350+
cfg, err := cc.ClientConfig()
351+
if err != nil {
352+
if pathErr, ok := err.(*os.PathError); ok && os.IsNotExist(pathErr.Err) {
353+
log.Printf("[INFO] Unable to load config file as it doesn't exist at %q", path)
354+
return nil, nil
355+
}
356+
return nil, fmt.Errorf("Failed to load config (%s%s): %s", path, ctxSuffix, err)
357+
}
358+
359+
log.Printf("[INFO] Successfully loaded config file (%s%s)", path, ctxSuffix)
360+
return cfg, nil
361+
}
362+
363+
func expandStringSlice(s []interface{}) []string {
364+
result := make([]string, len(s), len(s))
365+
for k, v := range s {
366+
// Handle the Terraform parser bug which turns empty strings in lists to nil.
367+
if v == nil {
368+
result[k] = ""
369+
} else {
370+
result[k] = v.(string)
371+
}
372+
}
373+
return result
374+
}

0 commit comments

Comments
 (0)