-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12372 from hashicorp/f-kubernetes
kubernetes: Add provider + namespace resource
- Loading branch information
Showing
14 changed files
with
1,055 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
package kubernetes | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"log" | ||
"os" | ||
|
||
"github.com/hashicorp/terraform/helper/schema" | ||
"github.com/hashicorp/terraform/terraform" | ||
"github.com/mitchellh/go-homedir" | ||
kubernetes "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" | ||
"k8s.io/kubernetes/pkg/client/restclient" | ||
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd" | ||
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" | ||
) | ||
|
||
func Provider() terraform.ResourceProvider { | ||
return &schema.Provider{ | ||
Schema: map[string]*schema.Schema{ | ||
"host": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""), | ||
Description: "The hostname (in form of URI) of Kubernetes master.", | ||
}, | ||
"username": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", ""), | ||
Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", | ||
}, | ||
"password": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", ""), | ||
Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", | ||
}, | ||
"insecure": { | ||
Type: schema.TypeBool, | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false), | ||
Description: "Whether server should be accessed without verifying the TLS certificate.", | ||
}, | ||
"client_certificate": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""), | ||
Description: "PEM-encoded client certificate for TLS authentication.", | ||
}, | ||
"client_key": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""), | ||
Description: "PEM-encoded client certificate key for TLS authentication.", | ||
}, | ||
"cluster_ca_certificate": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""), | ||
Description: "PEM-encoded root certificates bundle for TLS authentication.", | ||
}, | ||
"config_path": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG", "~/.kube/config"), | ||
Description: "Path to the kube config file, defaults to ~/.kube/config", | ||
}, | ||
"config_context_auth_info": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""), | ||
Description: "", | ||
}, | ||
"config_context_cluster": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""), | ||
Description: "", | ||
}, | ||
}, | ||
|
||
ResourcesMap: map[string]*schema.Resource{ | ||
"kubernetes_namespace": resourceKubernetesNamespace(), | ||
}, | ||
ConfigureFunc: providerConfigure, | ||
} | ||
} | ||
|
||
func providerConfigure(d *schema.ResourceData) (interface{}, error) { | ||
// Config file loading | ||
cfg, err := tryLoadingConfigFile(d) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if cfg == nil { | ||
cfg = &restclient.Config{} | ||
} | ||
|
||
// Overriding with static configuration | ||
cfg.UserAgent = fmt.Sprintf("HashiCorp/1.0 Terraform/%s", terraform.VersionString()) | ||
|
||
if v, ok := d.GetOk("host"); ok { | ||
cfg.Host = v.(string) | ||
} | ||
if v, ok := d.GetOk("username"); ok { | ||
cfg.Username = v.(string) | ||
} | ||
if v, ok := d.GetOk("password"); ok { | ||
cfg.Password = v.(string) | ||
} | ||
if v, ok := d.GetOk("insecure"); ok { | ||
cfg.Insecure = v.(bool) | ||
} | ||
if v, ok := d.GetOk("cluster_ca_certificate"); ok { | ||
cfg.CAData = bytes.NewBufferString(v.(string)).Bytes() | ||
} | ||
if v, ok := d.GetOk("client_certificate"); ok { | ||
cfg.CertData = bytes.NewBufferString(v.(string)).Bytes() | ||
} | ||
if v, ok := d.GetOk("client_key"); ok { | ||
cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes() | ||
} | ||
|
||
k, err := kubernetes.NewForConfig(cfg) | ||
if err != nil { | ||
return nil, fmt.Errorf("Failed to configure: %s", err) | ||
} | ||
|
||
return k, nil | ||
} | ||
|
||
func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { | ||
path, err := homedir.Expand(d.Get("config_path").(string)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
loader := &clientcmd.ClientConfigLoadingRules{ | ||
ExplicitPath: path, | ||
} | ||
overrides := &clientcmd.ConfigOverrides{} | ||
ctxSuffix := "; no context" | ||
authInfo, authInfoOk := d.GetOk("config_context_auth_info") | ||
cluster, clusterOk := d.GetOk("config_context_cluster") | ||
if authInfoOk || clusterOk { | ||
overrides.Context = clientcmdapi.Context{} | ||
if authInfoOk { | ||
overrides.Context.AuthInfo = authInfo.(string) | ||
} | ||
if clusterOk { | ||
overrides.Context.Cluster = cluster.(string) | ||
} | ||
ctxSuffix = fmt.Sprintf("; auth_info: %s, cluster: %s", | ||
overrides.Context.AuthInfo, overrides.Context.Cluster) | ||
} | ||
log.Printf("[DEBUG] Using override context: %#v", *overrides) | ||
|
||
cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides) | ||
cfg, err := cc.ClientConfig() | ||
if err != nil { | ||
if pathErr, ok := err.(*os.PathError); ok && os.IsNotExist(pathErr.Err) { | ||
log.Printf("[INFO] Unable to load config file as it doesn't exist at %q", path) | ||
return nil, nil | ||
} | ||
return nil, fmt.Errorf("Failed to load config (%s%s): %s", path, ctxSuffix, err) | ||
} | ||
|
||
log.Printf("[INFO] Successfully loaded config file (%s%s)", path, ctxSuffix) | ||
return cfg, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package kubernetes | ||
|
||
import ( | ||
"os" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform/helper/schema" | ||
"github.com/hashicorp/terraform/terraform" | ||
) | ||
|
||
var testAccProviders map[string]terraform.ResourceProvider | ||
var testAccProvider *schema.Provider | ||
|
||
func init() { | ||
testAccProvider = Provider().(*schema.Provider) | ||
testAccProviders = map[string]terraform.ResourceProvider{ | ||
"kubernetes": testAccProvider, | ||
} | ||
} | ||
|
||
func TestProvider(t *testing.T) { | ||
if err := Provider().(*schema.Provider).InternalValidate(); err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
} | ||
|
||
func TestProvider_impl(t *testing.T) { | ||
var _ terraform.ResourceProvider = Provider() | ||
} | ||
|
||
func testAccPreCheck(t *testing.T) { | ||
hasFileCfg := (os.Getenv("KUBE_CTX_AUTH_INFO") != "" && os.Getenv("KUBE_CTX_CLUSTER") != "") | ||
hasStaticCfg := (os.Getenv("KUBE_HOST") != "" && | ||
os.Getenv("KUBE_USER") != "" && | ||
os.Getenv("KUBE_PASSWORD") != "" && | ||
os.Getenv("KUBE_CLIENT_CERT_DATA") != "" && | ||
os.Getenv("KUBE_CLIENT_KEY_DATA") != "" && | ||
os.Getenv("KUBE_CLUSTER_CA_CERT_DATA") != "") | ||
|
||
if !hasFileCfg && !hasStaticCfg { | ||
t.Fatalf("File config (KUBE_CTX_AUTH_INFO and KUBE_CTX_CLUSTER) or static configuration"+ | ||
" (%s) must be set for acceptance tests", | ||
strings.Join([]string{ | ||
"KUBE_HOST", | ||
"KUBE_USER", | ||
"KUBE_PASSWORD", | ||
"KUBE_CLIENT_CERT_DATA", | ||
"KUBE_CLIENT_KEY_DATA", | ||
"KUBE_CLUSTER_CA_CERT_DATA", | ||
}, ", ")) | ||
} | ||
} |
143 changes: 143 additions & 0 deletions
143
builtin/providers/kubernetes/resource_kubernetes_namespace.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package kubernetes | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"time" | ||
|
||
"github.com/hashicorp/terraform/helper/resource" | ||
"github.com/hashicorp/terraform/helper/schema" | ||
"k8s.io/kubernetes/pkg/api/errors" | ||
api "k8s.io/kubernetes/pkg/api/v1" | ||
kubernetes "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" | ||
) | ||
|
||
func resourceKubernetesNamespace() *schema.Resource { | ||
return &schema.Resource{ | ||
Create: resourceKubernetesNamespaceCreate, | ||
Read: resourceKubernetesNamespaceRead, | ||
Exists: resourceKubernetesNamespaceExists, | ||
Update: resourceKubernetesNamespaceUpdate, | ||
Delete: resourceKubernetesNamespaceDelete, | ||
Importer: &schema.ResourceImporter{ | ||
State: schema.ImportStatePassthrough, | ||
}, | ||
|
||
Schema: map[string]*schema.Schema{ | ||
"metadata": metadataSchema("namespace"), | ||
}, | ||
} | ||
} | ||
|
||
func resourceKubernetesNamespaceCreate(d *schema.ResourceData, meta interface{}) error { | ||
conn := meta.(*kubernetes.Clientset) | ||
|
||
metadata := expandMetadata(d.Get("metadata").([]interface{})) | ||
namespace := api.Namespace{ | ||
ObjectMeta: metadata, | ||
} | ||
log.Printf("[INFO] Creating new namespace: %#v", namespace) | ||
out, err := conn.CoreV1().Namespaces().Create(&namespace) | ||
if err != nil { | ||
return err | ||
} | ||
log.Printf("[INFO] Submitted new namespace: %#v", out) | ||
d.SetId(out.Name) | ||
|
||
return resourceKubernetesNamespaceRead(d, meta) | ||
} | ||
|
||
func resourceKubernetesNamespaceRead(d *schema.ResourceData, meta interface{}) error { | ||
conn := meta.(*kubernetes.Clientset) | ||
|
||
name := d.Id() | ||
log.Printf("[INFO] Reading namespace %s", name) | ||
namespace, err := conn.CoreV1().Namespaces().Get(name) | ||
if err != nil { | ||
log.Printf("[DEBUG] Received error: %#v", err) | ||
return err | ||
} | ||
log.Printf("[INFO] Received namespace: %#v", namespace) | ||
err = d.Set("metadata", flattenMetadata(namespace.ObjectMeta)) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func resourceKubernetesNamespaceUpdate(d *schema.ResourceData, meta interface{}) error { | ||
conn := meta.(*kubernetes.Clientset) | ||
|
||
metadata := expandMetadata(d.Get("metadata").([]interface{})) | ||
// This is necessary in case the name is generated | ||
metadata.Name = d.Id() | ||
|
||
namespace := api.Namespace{ | ||
ObjectMeta: metadata, | ||
} | ||
log.Printf("[INFO] Updating namespace: %#v", namespace) | ||
out, err := conn.CoreV1().Namespaces().Update(&namespace) | ||
if err != nil { | ||
return err | ||
} | ||
log.Printf("[INFO] Submitted updated namespace: %#v", out) | ||
d.SetId(out.Name) | ||
|
||
return resourceKubernetesNamespaceRead(d, meta) | ||
} | ||
|
||
func resourceKubernetesNamespaceDelete(d *schema.ResourceData, meta interface{}) error { | ||
conn := meta.(*kubernetes.Clientset) | ||
|
||
name := d.Id() | ||
log.Printf("[INFO] Deleting namespace: %#v", name) | ||
err := conn.CoreV1().Namespaces().Delete(name, &api.DeleteOptions{}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
stateConf := &resource.StateChangeConf{ | ||
Target: []string{}, | ||
Pending: []string{"Terminating"}, | ||
Timeout: 5 * time.Minute, | ||
Refresh: func() (interface{}, string, error) { | ||
out, err := conn.CoreV1().Namespaces().Get(name) | ||
if err != nil { | ||
if statusErr, ok := err.(*errors.StatusError); ok && statusErr.ErrStatus.Code == 404 { | ||
return nil, "", nil | ||
} | ||
log.Printf("[ERROR] Received error: %#v", err) | ||
return out, "Error", err | ||
} | ||
|
||
statusPhase := fmt.Sprintf("%v", out.Status.Phase) | ||
log.Printf("[DEBUG] Namespace %s status received: %#v", out.Name, statusPhase) | ||
return out, statusPhase, nil | ||
}, | ||
} | ||
_, err = stateConf.WaitForState() | ||
if err != nil { | ||
return err | ||
} | ||
log.Printf("[INFO] Namespace %s deleted", name) | ||
|
||
d.SetId("") | ||
return nil | ||
} | ||
|
||
func resourceKubernetesNamespaceExists(d *schema.ResourceData, meta interface{}) (bool, error) { | ||
conn := meta.(*kubernetes.Clientset) | ||
|
||
name := d.Id() | ||
log.Printf("[INFO] Checking namespace %s", name) | ||
_, err := conn.CoreV1().Namespaces().Get(name) | ||
if err != nil { | ||
if statusErr, ok := err.(*errors.StatusError); ok && statusErr.ErrStatus.Code == 404 { | ||
return false, nil | ||
} | ||
log.Printf("[DEBUG] Received error: %#v", err) | ||
} | ||
log.Printf("[INFO] Namespace %s exists", name) | ||
return true, err | ||
} |
Oops, something went wrong.