From 9a2cb4fe3d17d2b78e80c83beaa2101a5fe808a8 Mon Sep 17 00:00:00 2001 From: Daniel Brooks Date: Fri, 9 Mar 2018 11:54:57 -0800 Subject: [PATCH 1/6] First pass at adding cron jobs --- kubernetes/provider.go | 1 + kubernetes/resource_kubernetes_cron_job.go | 211 +++++++++++++++++++++ kubernetes/schema_cron_job_spec.go | 27 +++ kubernetes/structure_cron_job.go | 59 ++++++ 4 files changed, 298 insertions(+) create mode 100644 kubernetes/resource_kubernetes_cron_job.go create mode 100644 kubernetes/schema_cron_job_spec.go create mode 100644 kubernetes/structure_cron_job.go diff --git a/kubernetes/provider.go b/kubernetes/provider.go index 6d3507d925..0146e76459 100755 --- a/kubernetes/provider.go +++ b/kubernetes/provider.go @@ -127,6 +127,7 @@ func Provider() terraform.ResourceProvider { "kubernetes_config_map": resourceKubernetesConfigMap(), "kubernetes_horizontal_pod_autoscaler": resourceKubernetesHorizontalPodAutoscaler(), "kubernetes_job": resourceKubernetesJob(), + "kubernetes_cron_job": resourceKubernetesCronJob(), "kubernetes_ingress": resourceKubernetesIngress(), "kubernetes_limit_range": resourceKubernetesLimitRange(), "kubernetes_namespace": resourceKubernetesNamespace(), diff --git a/kubernetes/resource_kubernetes_cron_job.go b/kubernetes/resource_kubernetes_cron_job.go new file mode 100644 index 0000000000..7bb2dbed3d --- /dev/null +++ b/kubernetes/resource_kubernetes_cron_job.go @@ -0,0 +1,211 @@ +package kubernetes + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + pkgApi "k8s.io/apimachinery/pkg/types" + kubernetes "k8s.io/client-go/kubernetes" + batchv2 "k8s.io/client-go/pkg/apis/batch/v2alpha1" +) + +func resourceKubernetesCronJob() *schema.Resource { + return &schema.Resource{ + Create: resourceKubernetesCronJobCreate, + Read: resourceKubernetesCronJobRead, + Update: resourceKubernetesCronJobUpdate, + Delete: resourceKubernetesCronJobDelete, + Exists: resourceKubernetesCronJobExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "metadata": namespacedMetadataSchema("cronjob", true), + "spec": { + Type: schema.TypeList, + Description: "Spec of the cron job owned by the cluster", + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: cronJobSpecFields(), + }, + }, + }, + } +} + +func resourceKubernetesCronJobCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + metadata := expandMetadata(d.Get("metadata").([]interface{})) + spec, err := expandCronJobSpec(d.Get("spec").([]interface{})) + if err != nil { + return err + } + spec.JobTemplate.ObjectMeta.Annotations = metadata.Annotations + + job := batchv2.CronJob{ + ObjectMeta: metadata, + Spec: spec, + } + + log.Printf("[INFO] Creating new cron job: %#v", job) + + out, err := conn.BatchV2alpha1().CronJobs(metadata.Namespace).Create(&job) + if err != nil { + return err + } + log.Printf("[INFO] Submitted new cron job: %#v", out) + + d.SetId(buildId(out.ObjectMeta)) + + return resourceKubernetesCronJobRead(d, meta) +} + +func resourceKubernetesCronJobUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + namespace, name, err := idParts(d.Id()) + if err != nil { + return err + } + + ops := patchMetadata("metadata.0.", "/metadata/", d) + + if d.HasChange("spec") { + specOps, err := patchCronJobSpec("/spec", "spec.0.", d) + if err != nil { + return err + } + ops = append(ops, specOps...) + } + + data, err := ops.MarshalJSON() + if err != nil { + return fmt.Errorf("Failed to marshal update operations: %s", err) + } + + log.Printf("[INFO] Updating cron job %s: %s", d.Id(), ops) + + out, err := conn.BatchV2alpha1().CronJobs(namespace).Patch(name, pkgApi.JSONPatchType, data) + if err != nil { + return err + } + log.Printf("[INFO] Submitted updated cron job: %#v", out) + + d.SetId(buildId(out.ObjectMeta)) + return resourceKubernetesCronJobRead(d, meta) +} + +func resourceKubernetesCronJobRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + namespace, name, err := idParts(d.Id()) + if err != nil { + return err + } + + log.Printf("[INFO] Reading cron job %s", name) + job, err := conn.BatchV2alpha1().CronJobs(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + log.Printf("[DEBUG] Received error: %#v", err) + return err + } + log.Printf("[INFO] Received cron job: %#v", job) + + // Remove server-generated labels unless using manual selector + if _, ok := d.GetOk("spec.0.manual_selector"); !ok { + labels := job.ObjectMeta.Labels + + if _, ok := labels["controller-uid"]; ok { + delete(labels, "controller-uid") + } + + if _, ok := labels["cron-job-name"]; ok { + delete(labels, "cron-job-name") + } + + labels = job.Spec.JobTemplate.Spec.Selector.MatchLabels + + if _, ok := labels["controller-uid"]; ok { + delete(labels, "controller-uid") + } + } + + err = d.Set("metadata", flattenMetadata(job.ObjectMeta, d)) + if err != nil { + return err + } + + jobSpec, err := flattenCronJobSpec(job.Spec) + if err != nil { + return err + } + + err = d.Set("spec", jobSpec) + if err != nil { + return err + } + + return nil +} + +func resourceKubernetesCronJobDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + namespace, name, err := idParts(d.Id()) + if err != nil { + return err + } + + log.Printf("[INFO] Deleting cron job: %#v", name) + err = conn.BatchV2alpha1().CronJobs(namespace).Delete(name, nil) + if err != nil { + return err + } + + err = resource.Retry(1*time.Minute, func() *resource.RetryError { + _, err := conn.BatchV2alpha1().CronJobs(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + if statusErr, ok := err.(*errors.StatusError); ok && statusErr.ErrStatus.Code == 404 { + return nil + } + return resource.NonRetryableError(err) + } + + e := fmt.Errorf("Cron Job %s still exists", name) + return resource.RetryableError(e) + }) + if err != nil { + return err + } + + log.Printf("[INFO] Cron Job %s deleted", name) + + d.SetId("") + return nil +} + +func resourceKubernetesCronJobExists(d *schema.ResourceData, meta interface{}) (bool, error) { + conn := meta.(*kubernetes.Clientset) + + namespace, name, err := idParts(d.Id()) + if err != nil { + return false, err + } + + log.Printf("[INFO] Checking cron job %s", name) + _, err = conn.BatchV2alpha1().CronJobs(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + if statusErr, ok := err.(*errors.StatusError); ok && statusErr.ErrStatus.Code == 404 { + return false, nil + } + log.Printf("[DEBUG] Received error: %#v", err) + } + return true, err +} diff --git a/kubernetes/schema_cron_job_spec.go b/kubernetes/schema_cron_job_spec.go new file mode 100644 index 0000000000..50b4046870 --- /dev/null +++ b/kubernetes/schema_cron_job_spec.go @@ -0,0 +1,27 @@ +package kubernetes + +import ( + "github.com/hashicorp/terraform/helper/schema" +) + +func cronJobSpecFields() map[string]*schema.Schema { + s := map[string]*schema.Schema{ + "schedule": { + Type: schema.TypeString, + Optional: true, + //ValidateFunc: validate, TODO: validate cron syntax.. + Description: "Cron format string, e.g. 0 * * * * or @hourly, as schedule time of its jobs to be created and executed.", + }, + "job_template": { + Type: schema.TypeList, + Description: "Describes the pod that will be created when executing a cron job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/", + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: jobSpecFields(), + }, + }, + } + + return s +} diff --git a/kubernetes/structure_cron_job.go b/kubernetes/structure_cron_job.go new file mode 100644 index 0000000000..172f2b85d5 --- /dev/null +++ b/kubernetes/structure_cron_job.go @@ -0,0 +1,59 @@ +package kubernetes + +import ( + "github.com/hashicorp/terraform/helper/schema" + batchv2 "k8s.io/client-go/pkg/apis/batch/v2alpha1" + "errors" +) + +func flattenCronJobSpec(in batchv2.CronJobSpec) ([]interface{}, error) { + att := make(map[string]interface{}) + + if in.Schedule != "" { + att["schedule"] = in.Schedule + } else { + return nil, errors.New("You need to define a schedule.") + } + + jobSpec, err := flattenJobSpec(in.JobTemplate.Spec) + if err != nil { + return nil, err + } + att["job_template"] = jobSpec + + return []interface{}{att}, nil +} + +func expandCronJobSpec(j []interface{}) (batchv2.CronJobSpec, error) { + obj := batchv2.CronJobSpec{} + + if len(j) == 0 || j[0] == nil { + return obj, nil + } + + in := j[0].(map[string]interface{}) + + if v, ok := in["schedule"].(string); ok && len(v) > 0 { + obj.Schedule = *ptrToString(string(v)) + } else { + return obj, errors.New("You need to define a schedule.") + } + + podSpec, err := expandJobSpec(in["job_template"].([]interface{})) + if err != nil { + return obj, err + } + + + obj.JobTemplate = batchv2.JobTemplateSpec { + Spec: podSpec, + } + + return obj, nil +} + +func patchCronJobSpec(pathPrefix, prefix string, d *schema.ResourceData) (PatchOperations, error) { + ops := make([]PatchOperation, 0) + + return ops, nil +} From dfa2066afcac126d45a5b39d35ef76ec165b7d55 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 12 Mar 2018 19:22:33 -0700 Subject: [PATCH 2/6] Making pull request modifications --- kubernetes/schema_cron_job_spec.go | 2 +- kubernetes/structure_cron_job.go | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/kubernetes/schema_cron_job_spec.go b/kubernetes/schema_cron_job_spec.go index 50b4046870..5d2a34187b 100644 --- a/kubernetes/schema_cron_job_spec.go +++ b/kubernetes/schema_cron_job_spec.go @@ -8,7 +8,7 @@ func cronJobSpecFields() map[string]*schema.Schema { s := map[string]*schema.Schema{ "schedule": { Type: schema.TypeString, - Optional: true, + Required: true, //ValidateFunc: validate, TODO: validate cron syntax.. Description: "Cron format string, e.g. 0 * * * * or @hourly, as schedule time of its jobs to be created and executed.", }, diff --git a/kubernetes/structure_cron_job.go b/kubernetes/structure_cron_job.go index 172f2b85d5..cbc055318c 100644 --- a/kubernetes/structure_cron_job.go +++ b/kubernetes/structure_cron_job.go @@ -9,11 +9,7 @@ import ( func flattenCronJobSpec(in batchv2.CronJobSpec) ([]interface{}, error) { att := make(map[string]interface{}) - if in.Schedule != "" { - att["schedule"] = in.Schedule - } else { - return nil, errors.New("You need to define a schedule.") - } + att["schedule"] = in.Schedule jobSpec, err := flattenJobSpec(in.JobTemplate.Spec) if err != nil { @@ -33,11 +29,7 @@ func expandCronJobSpec(j []interface{}) (batchv2.CronJobSpec, error) { in := j[0].(map[string]interface{}) - if v, ok := in["schedule"].(string); ok && len(v) > 0 { - obj.Schedule = *ptrToString(string(v)) - } else { - return obj, errors.New("You need to define a schedule.") - } + obj.Schedule = in["schedule"].(string) podSpec, err := expandJobSpec(in["job_template"].([]interface{})) if err != nil { From 72a3a09c2cad05f9686c52101e59f48985ac6c28 Mon Sep 17 00:00:00 2001 From: Matt Morrison Date: Thu, 5 Apr 2018 23:53:54 +1200 Subject: [PATCH 3/6] Complete out CronJob schema and add tests --- kubernetes/provider_test.go | 4 - kubernetes/resource_kubernetes_cron_job.go | 11 +- .../resource_kubernetes_cron_job_test.go | 163 ++++++++++++++++++ kubernetes/resource_kubernetes_job.go | 10 +- kubernetes/resource_kubernetes_job_test.go | 29 ++-- kubernetes/schema_cron_job_spec.go | 53 +++++- kubernetes/schema_job_spec.go | 7 +- kubernetes/schema_metadata.go | 8 + kubernetes/schema_pod_spec.go | 16 ++ kubernetes/structure_cron_job.go | 80 ++++++++- kubernetes/structure_job.go | 31 ++-- kubernetes/structures_deployment.go | 12 +- kubernetes/structures_pod.go | 32 ++++ 13 files changed, 400 insertions(+), 56 deletions(-) create mode 100644 kubernetes/resource_kubernetes_cron_job_test.go diff --git a/kubernetes/provider_test.go b/kubernetes/provider_test.go index 51853a2c24..8e4992d8c0 100644 --- a/kubernetes/provider_test.go +++ b/kubernetes/provider_test.go @@ -9,8 +9,6 @@ import ( "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" - "github.com/terraform-providers/terraform-provider-aws/aws" - "github.com/terraform-providers/terraform-provider-google/google" api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -22,8 +20,6 @@ func init() { testAccProvider = Provider().(*schema.Provider) testAccProviders = map[string]terraform.ResourceProvider{ "kubernetes": testAccProvider, - "google": google.Provider(), - "aws": aws.Provider(), } } diff --git a/kubernetes/resource_kubernetes_cron_job.go b/kubernetes/resource_kubernetes_cron_job.go index 7bb2dbed3d..618e77b8c2 100644 --- a/kubernetes/resource_kubernetes_cron_job.go +++ b/kubernetes/resource_kubernetes_cron_job.go @@ -92,7 +92,7 @@ func resourceKubernetesCronJobUpdate(d *schema.ResourceData, meta interface{}) e log.Printf("[INFO] Updating cron job %s: %s", d.Id(), ops) - out, err := conn.BatchV2alpha1().CronJobs(namespace).Patch(name, pkgApi.JSONPatchType, data) + out, err := conn.BatchV2alpha1().CronJobs(namespace).Patch(name, pkgApi.JSONPatchType, data) if err != nil { return err } @@ -111,7 +111,7 @@ func resourceKubernetesCronJobRead(d *schema.ResourceData, meta interface{}) err } log.Printf("[INFO] Reading cron job %s", name) - job, err := conn.BatchV2alpha1().CronJobs(namespace).Get(name, metav1.GetOptions{}) + job, err := conn.BatchV2alpha1().CronJobs(namespace).Get(name, metav1.GetOptions{}) if err != nil { log.Printf("[DEBUG] Received error: %#v", err) return err @@ -130,7 +130,10 @@ func resourceKubernetesCronJobRead(d *schema.ResourceData, meta interface{}) err delete(labels, "cron-job-name") } - labels = job.Spec.JobTemplate.Spec.Selector.MatchLabels + if job.Spec.JobTemplate.Spec.Selector != nil && + job.Spec.JobTemplate.Spec.Selector.MatchLabels != nil { + labels = job.Spec.JobTemplate.Spec.Selector.MatchLabels + } if _, ok := labels["controller-uid"]; ok { delete(labels, "controller-uid") @@ -142,7 +145,7 @@ func resourceKubernetesCronJobRead(d *schema.ResourceData, meta interface{}) err return err } - jobSpec, err := flattenCronJobSpec(job.Spec) + jobSpec, err := flattenCronJobSpec(job.Spec, d) if err != nil { return err } diff --git a/kubernetes/resource_kubernetes_cron_job_test.go b/kubernetes/resource_kubernetes_cron_job_test.go new file mode 100644 index 0000000000..16f9234b13 --- /dev/null +++ b/kubernetes/resource_kubernetes_cron_job_test.go @@ -0,0 +1,163 @@ +package kubernetes + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kubernetes "k8s.io/client-go/kubernetes" + batchv2 "k8s.io/client-go/pkg/apis/batch/v2alpha1" +) + +func TestAccKubernetesCronJob_basic(t *testing.T) { + var conf batchv2.CronJob + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "kubernetes_cron_job.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesCronJobDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesCronJobConfig_basic(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesCronJobExists("kubernetes_cron_job.test", &conf), + resource.TestCheckResourceAttr("kubernetes_cron_job.test", "metadata.0.name", name), + resource.TestCheckResourceAttrSet("kubernetes_cron_job.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_cron_job.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_cron_job.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_cron_job.test", "metadata.0.uid"), + resource.TestCheckResourceAttrSet("kubernetes_cron_job.test", "spec.0.schedule"), + resource.TestCheckResourceAttr("kubernetes_cron_job.test", "spec.#", "1"), + resource.TestCheckResourceAttr("kubernetes_cron_job.test", "spec.0.job_template.0.spec.0.parallelism", "1"), + resource.TestCheckResourceAttr("kubernetes_cron_job.test", "spec.0.job_template.0.spec.0.template.0.spec.0.container.0.name", "hello"), + ), + }, + { + Config: testAccKubernetesCronJobConfig_modified(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesCronJobExists("kubernetes_cron_job.test", &conf), + resource.TestCheckResourceAttr("kubernetes_cron_job.test", "metadata.0.name", name), + resource.TestCheckResourceAttrSet("kubernetes_cron_job.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_cron_job.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_cron_job.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_cron_job.test", "metadata.0.uid"), + resource.TestCheckResourceAttr("kubernetes_cron_job.test", "spec.#", "1"), + resource.TestCheckResourceAttr("kubernetes_cron_job.test", "spec.0.job_template.0.spec.0.parallelism", "2"), + resource.TestCheckResourceAttr("kubernetes_cron_job.test", "spec.0.job_template.0.spec.0.template.0.spec.0.container.0.name", "hello"), + resource.TestCheckResourceAttr("kubernetes_cron_job.test", "spec.0.job_template.0.spec.0.template.0.metadata.#", "1"), + resource.TestCheckResourceAttr("kubernetes_cron_job.test", "spec.0.job_template.0.spec.0.template.0.metadata.0.labels.%", "2"), + ), + }, + }, + }) +} + +func testAccCheckKubernetesCronJobDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*kubernetes.Clientset) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "kubernetes_cron_job" { + continue + } + + namespace, name, err := idParts(rs.Primary.ID) + if err != nil { + return err + } + + resp, err := conn.CronJobs(namespace).Get(name, meta_v1.GetOptions{}) + if err == nil { + if resp.Name == rs.Primary.ID { + return fmt.Errorf("CronJob still exists: %s", rs.Primary.ID) + } + } + } + + return nil +} + +func testAccCheckKubernetesCronJobExists(n string, obj *batchv2.CronJob) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*kubernetes.Clientset) + + namespace, name, err := idParts(rs.Primary.ID) + if err != nil { + return err + } + + out, err := conn.CronJobs(namespace).Get(name, meta_v1.GetOptions{}) + if err != nil { + return err + } + + *obj = *out + return nil + } +} + +func testAccKubernetesCronJobConfig_basic(name string) string { + return fmt.Sprintf(` +resource "kubernetes_cron_job" "test" { + metadata { + name = "%s" + } + spec { + schedule = "1 0 * * *" + job_template { + spec { + template { + spec { + container { + name = "hello" + image = "alpine" + command = ["echo", "'hello'"] + } + } + } + } + } + } +}`, name) +} + +func testAccKubernetesCronJobConfig_modified(name string) string { + return fmt.Sprintf(` +resource "kubernetes_cron_job" "test" { + metadata { + name = "%s" + } + spec { + schedule = "1 0 * * *" + job_template { + spec { + parallelism = 2 + template { + metadata { + labels { + foo = "bar" + baz = "foo" + } + } + spec { + container { + name = "hello" + image = "alpine" + command = ["echo", "'abcdef'"] + } + } + } + } + } + } +}`, name) +} diff --git a/kubernetes/resource_kubernetes_job.go b/kubernetes/resource_kubernetes_job.go index 63c2d58018..613a1e97d5 100644 --- a/kubernetes/resource_kubernetes_job.go +++ b/kubernetes/resource_kubernetes_job.go @@ -37,9 +37,6 @@ func resourceKubernetesJob() *schema.Resource { }, }, } - s.Schema["spec"].Elem.(*schema.Resource). - Schema["template"].Elem.(*schema.Resource). - Schema["restart_policy"].Default = "OnFailure" return s } @@ -163,12 +160,17 @@ func resourceKubernetesJobRead(d *schema.ResourceData, meta interface{}) error { } } + job.ObjectMeta.Labels = reconcileTopLevelLabels( + job.ObjectMeta.Labels, + expandMetadata(d.Get("metadata").([]interface{})), + expandMetadata(d.Get("spec.0.template.0.metadata").([]interface{})), + ) err = d.Set("metadata", flattenMetadata(job.ObjectMeta, d)) if err != nil { return err } - jobSpec, err := flattenJobSpec(job.Spec) + jobSpec, err := flattenJobSpec(job.Spec, d) if err != nil { return err } diff --git a/kubernetes/resource_kubernetes_job_test.go b/kubernetes/resource_kubernetes_job_test.go index beacc26a5a..c649c0b919 100644 --- a/kubernetes/resource_kubernetes_job_test.go +++ b/kubernetes/resource_kubernetes_job_test.go @@ -32,7 +32,7 @@ func TestAccKubernetesJob_basic(t *testing.T) { resource.TestCheckResourceAttrSet("kubernetes_job.test", "metadata.0.uid"), resource.TestCheckResourceAttr("kubernetes_job.test", "spec.#", "1"), resource.TestCheckResourceAttr("kubernetes_job.test", "spec.0.parallelism", "2"), - resource.TestCheckResourceAttr("kubernetes_job.test", "spec.0.template.0.container.0.name", "hello"), + resource.TestCheckResourceAttr("kubernetes_job.test", "spec.0.template.0.spec.0.container.0.name", "hello"), ), }, { @@ -46,7 +46,7 @@ func TestAccKubernetesJob_basic(t *testing.T) { resource.TestCheckResourceAttrSet("kubernetes_job.test", "metadata.0.uid"), resource.TestCheckResourceAttr("kubernetes_job.test", "spec.#", "1"), resource.TestCheckResourceAttr("kubernetes_job.test", "spec.0.parallelism", "2"), - resource.TestCheckResourceAttr("kubernetes_job.test", "spec.0.template.0.container.0.name", "hello"), + resource.TestCheckResourceAttr("kubernetes_job.test", "spec.0.template.0.spec.0.container.0.name", "hello"), ), }, }, @@ -110,10 +110,17 @@ resource "kubernetes_job" "test" { spec { parallelism = 2 template { - container { - name = "hello" - image = "alpine" - command = ["echo", "'hello'"] + metadata { + labels { + job = "one" + } + } + spec { + container { + name = "hello" + image = "alpine" + command = ["echo", "'hello'"] + } } } } @@ -129,10 +136,12 @@ resource "kubernetes_job" "test" { spec { parallelism = 2 template { - container { - name = "hello" - image = "alpine" - command = ["echo", "'world'"] + spec { + container { + name = "hello" + image = "alpine" + command = ["echo", "'world'"] + } } } } diff --git a/kubernetes/schema_cron_job_spec.go b/kubernetes/schema_cron_job_spec.go index 5d2a34187b..f8de4347c7 100644 --- a/kubernetes/schema_cron_job_spec.go +++ b/kubernetes/schema_cron_job_spec.go @@ -2,15 +2,23 @@ package kubernetes import ( "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" ) func cronJobSpecFields() map[string]*schema.Schema { s := map[string]*schema.Schema{ - "schedule": { + "concurrency_policy": { Type: schema.TypeString, - Required: true, - //ValidateFunc: validate, TODO: validate cron syntax.. - Description: "Cron format string, e.g. 0 * * * * or @hourly, as schedule time of its jobs to be created and executed.", + Optional: true, + Default: "Allow", + ValidateFunc: validation.StringInSlice([]string{"Allow", "Forbid", "Replace"}, false), + Description: "Specifies how to treat concurrent executions of a Job. Defaults to Allow.", + }, + "failed_jobs_history_limit": { + Type: schema.TypeInt, + Optional: true, + Default: "1", + Description: "The number of failed finished jobs to retain. This is a pointer to distinguish between explicit zero and not specified. Defaults to 1.", }, "job_template": { Type: schema.TypeList, @@ -18,9 +26,44 @@ func cronJobSpecFields() map[string]*schema.Schema { Required: true, MaxItems: 1, Elem: &schema.Resource{ - Schema: jobSpecFields(), + Schema: map[string]*schema.Schema{ + "metadata": metadataSchema("jobTemplateSpec", true), + "spec": { + Type: schema.TypeList, + Description: "Specification of the desired behavior of the job", + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: jobSpecFields(), + }, + }, + }, }, }, + "schedule": { + Type: schema.TypeString, + Required: true, + //ValidateFunc: validate, TODO: validate cron syntax.. + Description: "Cron format string, e.g. 0 * * * * or @hourly, as schedule time of its jobs to be created and executed.", + }, + "starting_deadline_seconds": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: "Optional deadline in seconds for starting the job if it misses scheduled time for any reason. Missed jobs executions will be counted as failed ones.", + }, + "successful_jobs_history_limit": { + Type: schema.TypeInt, + Optional: true, + Default: 3, + Description: "The number of successful finished jobs to retain. Defaults to 3.", + }, + "suspend": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "This flag tells the controller to suspend subsequent executions, it does not apply to already started executions. Defaults to false.", + }, } return s diff --git a/kubernetes/schema_job_spec.go b/kubernetes/schema_job_spec.go index 3de087af21..5d4bc0d8e7 100644 --- a/kubernetes/schema_job_spec.go +++ b/kubernetes/schema_job_spec.go @@ -83,10 +83,15 @@ func jobSpecFields() map[string]*schema.Schema { Required: true, MaxItems: 1, Elem: &schema.Resource{ - Schema: podSpecFields(false), + Schema: podTemplateSpecFields(false), }, }, } + // fix restart_policy for job resources + s["template"].Elem.(*schema.Resource). + Schema["spec"].Elem.(*schema.Resource). + Schema["restart_policy"].Default = "OnFailure" + return s } diff --git a/kubernetes/schema_metadata.go b/kubernetes/schema_metadata.go index 5df58567f2..a88a436087 100755 --- a/kubernetes/schema_metadata.go +++ b/kubernetes/schema_metadata.go @@ -70,9 +70,16 @@ func metadataSchema(objectName string, generatableName bool) *schema.Schema { } metadataRequired := true + metadataComputed := false switch objectName { case "deploymentSpec": metadataRequired = false + case "podTemplateSpec": + metadataRequired = false + metadataComputed = true + case "jobTemplateSpec": + metadataRequired = false + metadataComputed = true } return &schema.Schema{ @@ -80,6 +87,7 @@ func metadataSchema(objectName string, generatableName bool) *schema.Schema { Description: fmt.Sprintf("Standard %s's metadata. More info: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#metadata", objectName), Required: metadataRequired, Optional: !metadataRequired, + Computed: metadataComputed, MaxItems: 1, Elem: &schema.Resource{ Schema: fields, diff --git a/kubernetes/schema_pod_spec.go b/kubernetes/schema_pod_spec.go index 276b559450..b1e9c39bc1 100755 --- a/kubernetes/schema_pod_spec.go +++ b/kubernetes/schema_pod_spec.go @@ -4,6 +4,22 @@ import ( "github.com/hashicorp/terraform/helper/schema" ) +func podTemplateSpecFields(isUpdatable bool) map[string]*schema.Schema { + s := map[string]*schema.Schema{ + "metadata": metadataSchema("podTemplateSpec", true), + "spec": { + Type: schema.TypeList, + Description: "Specification of the desired behavior of the job", + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: podSpecFields(isUpdatable), + }, + }, + } + return s +} + func podSpecFields(isUpdatable bool) map[string]*schema.Schema { s := map[string]*schema.Schema{ "active_deadline_seconds": { diff --git a/kubernetes/structure_cron_job.go b/kubernetes/structure_cron_job.go index cbc055318c..05b25e84eb 100644 --- a/kubernetes/structure_cron_job.go +++ b/kubernetes/structure_cron_job.go @@ -3,19 +3,52 @@ package kubernetes import ( "github.com/hashicorp/terraform/helper/schema" batchv2 "k8s.io/client-go/pkg/apis/batch/v2alpha1" - "errors" ) -func flattenCronJobSpec(in batchv2.CronJobSpec) ([]interface{}, error) { +func flattenCronJobSpec(in batchv2.CronJobSpec, d *schema.ResourceData) ([]interface{}, error) { att := make(map[string]interface{}) + att["concurrency_policy"] = in.ConcurrencyPolicy + if in.FailedJobsHistoryLimit != nil { + att["failed_jobs_history_limit"] = int(*in.FailedJobsHistoryLimit) + } else { + att["failed_jobs_history_limit"] = 1 + } + att["schedule"] = in.Schedule - jobSpec, err := flattenJobSpec(in.JobTemplate.Spec) + jobTemplate, err := flattenJobTemplate(in.JobTemplate, d) if err != nil { return nil, err } - att["job_template"] = jobSpec + att["job_template"] = jobTemplate + + if in.StartingDeadlineSeconds != nil { + att["starting_deadline_seconds"] = int(*in.StartingDeadlineSeconds) + } else { + att["starting_deadline_seconds"] = 0 + } + + if in.SuccessfulJobsHistoryLimit != nil { + att["successful_jobs_history_limit"] = int(*in.SuccessfulJobsHistoryLimit) + } else { + att["successful_jobs_history_limit"] = 3 + } + + return []interface{}{att}, nil +} + +func flattenJobTemplate(in batchv2.JobTemplateSpec, d *schema.ResourceData) ([]interface{}, error) { + att := make(map[string]interface{}) + + meta := flattenMetadata(in.ObjectMeta, d) + att["metadata"] = meta + + jobSpec, err := flattenJobSpec(in.Spec, d) + if err != nil { + return nil, err + } + att["spec"] = jobSpec return []interface{}{att}, nil } @@ -29,16 +62,49 @@ func expandCronJobSpec(j []interface{}) (batchv2.CronJobSpec, error) { in := j[0].(map[string]interface{}) + obj.ConcurrencyPolicy = batchv2.ConcurrencyPolicy(in["concurrency_policy"].(string)) + + if v, ok := in["failed_jobs_history_limit"].(int); ok && v != 1 { + obj.FailedJobsHistoryLimit = ptrToInt32(int32(v)) + } + obj.Schedule = in["schedule"].(string) - podSpec, err := expandJobSpec(in["job_template"].([]interface{})) + jtSpec, err := expandJobTemplate(in["job_template"].([]interface{})) if err != nil { return obj, err } + obj.JobTemplate = jtSpec + + if v, ok := in["starting_deadline_seconds"].(int); ok && v > 0 { + obj.StartingDeadlineSeconds = ptrToInt64(int64(v)) + } + + if v, ok := in["successful_jobs_history_limit"].(int); ok && v != 3 { + obj.StartingDeadlineSeconds = ptrToInt64(int64(v)) + } + + if v, ok := in["suspend"].(bool); ok { + obj.Suspend = ptrToBool(v) + } + return obj, nil +} + +func expandJobTemplate(in []interface{}) (batchv2.JobTemplateSpec, error) { + obj := batchv2.JobTemplateSpec{} + + tpl := in[0].(map[string]interface{}) + + spec, err := expandJobSpec(tpl["spec"].([]interface{})) + if err != nil { + return obj, err + } + obj.Spec = spec - obj.JobTemplate = batchv2.JobTemplateSpec { - Spec: podSpec, + if metaCfg, ok := tpl["metadata"]; ok { + metadata := expandMetadata(metaCfg.([]interface{})) + obj.ObjectMeta = metadata } return obj, nil diff --git a/kubernetes/structure_job.go b/kubernetes/structure_job.go index c78481c336..c34f17b4c6 100644 --- a/kubernetes/structure_job.go +++ b/kubernetes/structure_job.go @@ -3,10 +3,9 @@ package kubernetes import ( "github.com/hashicorp/terraform/helper/schema" batchv1 "k8s.io/api/batch/v1" - "k8s.io/api/core/v1" ) -func flattenJobSpec(in batchv1.JobSpec) ([]interface{}, error) { +func flattenJobSpec(in batchv1.JobSpec, d *schema.ResourceData) ([]interface{}, error) { att := make(map[string]interface{}) if in.ActiveDeadlineSeconds != nil { @@ -29,7 +28,18 @@ func flattenJobSpec(in batchv1.JobSpec) ([]interface{}, error) { att["selector"] = flattenLabelSelector(in.Selector) } - podSpec, err := flattenPodSpec(in.Template.Spec) + // Remove server-generated labels + labels := in.Template.ObjectMeta.Labels + + if _, ok := labels["controller-uid"]; ok { + delete(labels, "controller-uid") + } + + if _, ok := labels["job-name"]; ok { + delete(labels, "job-name") + } + + podSpec, err := flattenPodTemplateSpec(in.Template, d) if err != nil { return nil, err } @@ -67,13 +77,13 @@ func expandJobSpec(j []interface{}) (batchv1.JobSpec, error) { obj.Selector = expandLabelSelector(v) } - podSpec, err := expandPodSpec(in["template"].([]interface{})) - if err != nil { - return obj, err - } - - obj.Template = v1.PodTemplateSpec{ - Spec: podSpec, + for _, v := range in["template"].([]interface{}) { + template := v.(map[string]interface{}) + pts, err := expandPodTemplateSpec(template) + if err != nil { + return obj, err + } + obj.Template = pts } return obj, nil @@ -83,7 +93,6 @@ func patchJobSpec(pathPrefix, prefix string, d *schema.ResourceData) (PatchOpera ops := make([]PatchOperation, 0) if d.HasChange(prefix + "active_deadline_seconds") { - v := d.Get(prefix + "active_deadline_seconds").(int) ops = append(ops, &ReplaceOperation{ Path: pathPrefix + "/activeDeadlineSeconds", diff --git a/kubernetes/structures_deployment.go b/kubernetes/structures_deployment.go index ef16b64c02..7d8147846c 100755 --- a/kubernetes/structures_deployment.go +++ b/kubernetes/structures_deployment.go @@ -5,7 +5,6 @@ import ( "github.com/hashicorp/terraform/helper/schema" appsv1 "k8s.io/api/apps/v1" - "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -97,18 +96,11 @@ func expandDeploymentSpec(deployment []interface{}) (appsv1.DeploymentSpec, erro for _, v := range in["template"].([]interface{}) { template := v.(map[string]interface{}) - podSpec, err := expandPodSpec(template["spec"].([]interface{})) + pts, err := expandPodTemplateSpec(template) if err != nil { return obj, err } - obj.Template = v1.PodTemplateSpec{ - Spec: podSpec, - } - - if metaCfg, ok := template["metadata"]; ok { - metadata := expandMetadata(metaCfg.([]interface{})) - obj.Template.ObjectMeta = metadata - } + obj.Template = pts } return obj, nil diff --git a/kubernetes/structures_pod.go b/kubernetes/structures_pod.go index 2c0cafa8a6..caa7a69859 100755 --- a/kubernetes/structures_pod.go +++ b/kubernetes/structures_pod.go @@ -76,6 +76,21 @@ func flattenPodSpec(in v1.PodSpec) ([]interface{}, error) { return []interface{}{att}, nil } +func flattenPodTemplateSpec(in v1.PodTemplateSpec, d *schema.ResourceData) ([]interface{}, error) { + att := make(map[string]interface{}) + + meta := flattenMetadata(in.ObjectMeta, d) + att["metadata"] = meta + + podSpec, err := flattenPodSpec(in.Spec) + if err != nil { + return nil, err + } + att["spec"] = podSpec + + return []interface{}{att}, nil +} + func flattenPodSecurityContext(in *v1.PodSecurityContext) []interface{} { att := make(map[string]interface{}) if in.FSGroup != nil { @@ -317,6 +332,23 @@ func flattenSecretVolumeSource(in *v1.SecretVolumeSource) []interface{} { // Expanders +func expandPodTemplateSpec(template map[string]interface{}) (v1.PodTemplateSpec, error) { + obj := v1.PodTemplateSpec{} + + podSpec, err := expandPodSpec(template["spec"].([]interface{})) + if err != nil { + return obj, err + } + obj.Spec = podSpec + + if metaCfg, ok := template["metadata"]; ok { + metadata := expandMetadata(metaCfg.([]interface{})) + obj.ObjectMeta = metadata + } + + return obj, nil +} + func expandPodSpec(p []interface{}) (v1.PodSpec, error) { obj := v1.PodSpec{} if len(p) == 0 || p[0] == nil { From 3bf981fe4f21c3f917941b162ddb2644b32fdae4 Mon Sep 17 00:00:00 2001 From: Matt Morrison Date: Tue, 1 May 2018 22:07:43 +1200 Subject: [PATCH 4/6] Add CronJob support across multiple API versions: - batch/v1beta1 - batch/v2alpha1 --- kubernetes/api_versions.go | 6 + kubernetes/resource_kubernetes_cron_job.go | 127 +++++++++++++++--- .../resource_kubernetes_cron_job_test.go | 16 +-- kubernetes/structure_cron_job.go | 2 +- 4 files changed, 124 insertions(+), 27 deletions(-) diff --git a/kubernetes/api_versions.go b/kubernetes/api_versions.go index e3a38ad3b8..d24e01b152 100644 --- a/kubernetes/api_versions.go +++ b/kubernetes/api_versions.go @@ -19,6 +19,8 @@ const ( appsV1 appsV1beta1 appsV1beta2 + batchV1beta1 + batchV2alpha1 extensionsV1beta1 ) @@ -32,6 +34,10 @@ func (g APIGroup) String() string { return "apps/v1beta2" case extensionsV1beta1: return "extensions/v1beta1" + case batchV1beta1: + return "batch/v1beta1" + case batchV2alpha1: + return "batch/v2alpha1" default: return "none" } diff --git a/kubernetes/resource_kubernetes_cron_job.go b/kubernetes/resource_kubernetes_cron_job.go index 618e77b8c2..0f2a62a3b5 100644 --- a/kubernetes/resource_kubernetes_cron_job.go +++ b/kubernetes/resource_kubernetes_cron_job.go @@ -7,13 +7,19 @@ import ( "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" + "k8s.io/api/batch/v1beta1" + "k8s.io/api/batch/v2alpha1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" pkgApi "k8s.io/apimachinery/pkg/types" - kubernetes "k8s.io/client-go/kubernetes" - batchv2 "k8s.io/client-go/pkg/apis/batch/v2alpha1" ) +const cronJobResourceGroupName = "cronjobs" + +var cronJobAPIGroups = []APIGroup{batchV1beta1, batchV2alpha1} + +var cronJobNotSupportedError = fmt.Errorf("could not find Kubernetes API group that supports CronJob resources") + func resourceKubernetesCronJob() *schema.Resource { return &schema.Resource{ Create: resourceKubernetesCronJobCreate, @@ -40,7 +46,8 @@ func resourceKubernetesCronJob() *schema.Resource { } func resourceKubernetesCronJobCreate(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*kubernetes.Clientset) + kp := meta.(*kubernetesProvider) + conn := kp.conn metadata := expandMetadata(d.Get("metadata").([]interface{})) spec, err := expandCronJobSpec(d.Get("spec").([]interface{})) @@ -49,26 +56,46 @@ func resourceKubernetesCronJobCreate(d *schema.ResourceData, meta interface{}) e } spec.JobTemplate.ObjectMeta.Annotations = metadata.Annotations - job := batchv2.CronJob{ + job := v1beta1.CronJob{ ObjectMeta: metadata, Spec: spec, } - log.Printf("[INFO] Creating new cron job: %#v", job) + created := &v1beta1.CronJob{} - out, err := conn.BatchV2alpha1().CronJobs(metadata.Namespace).Create(&job) + log.Printf("[INFO] Creating new cron job: %#v", job) + apiGroup, err := kp.highestSupportedAPIGroup(cronJobResourceGroupName, cronJobAPIGroups...) if err != nil { return err } - log.Printf("[INFO] Submitted new cron job: %#v", out) + switch apiGroup { + case batchV1beta1: + created, err = conn.BatchV1beta1().CronJobs(metadata.Namespace).Create(&job) + + case batchV2alpha1: + beta := &v2alpha1.CronJob{} + Convert(job, beta) + out, err2 := conn.BatchV2alpha1().CronJobs(metadata.Namespace).Create(beta) + if err2 != nil { + err = err2 + break + } + Convert(out, created) - d.SetId(buildId(out.ObjectMeta)) + default: + err = cronJobNotSupportedError + } + + log.Printf("[INFO] Submitted new cron job: %#v", created) + + d.SetId(buildId(created.ObjectMeta)) return resourceKubernetesCronJobRead(d, meta) } func resourceKubernetesCronJobUpdate(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*kubernetes.Clientset) + kp := meta.(*kubernetesProvider) + conn := kp.conn namespace, name, err := idParts(d.Id()) if err != nil { @@ -92,7 +119,28 @@ func resourceKubernetesCronJobUpdate(d *schema.ResourceData, meta interface{}) e log.Printf("[INFO] Updating cron job %s: %s", d.Id(), ops) - out, err := conn.BatchV2alpha1().CronJobs(namespace).Patch(name, pkgApi.JSONPatchType, data) + out := &v1beta1.CronJob{} + apiGroup, err := kp.highestSupportedAPIGroup(cronJobResourceGroupName, cronJobAPIGroups...) + if err != nil { + return err + } + switch apiGroup { + case batchV1beta1: + out, err = conn.BatchV1beta1().CronJobs(namespace).Patch(name, pkgApi.JSONPatchType, data) + + case batchV2alpha1: + beta, err2 := conn.BatchV2alpha1().CronJobs(namespace).Patch(name, pkgApi.JSONPatchType, data) + if err2 != nil { + err = err2 + break + } + Convert(beta, out) + + default: + err = cronJobNotSupportedError + } + + //out, err := conn.BatchV2alpha1().CronJobs(namespace).Patch(name, pkgApi.JSONPatchType, data) if err != nil { return err } @@ -103,7 +151,7 @@ func resourceKubernetesCronJobUpdate(d *schema.ResourceData, meta interface{}) e } func resourceKubernetesCronJobRead(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*kubernetes.Clientset) + kp := meta.(*kubernetesProvider) namespace, name, err := idParts(d.Id()) if err != nil { @@ -111,7 +159,7 @@ func resourceKubernetesCronJobRead(d *schema.ResourceData, meta interface{}) err } log.Printf("[INFO] Reading cron job %s", name) - job, err := conn.BatchV2alpha1().CronJobs(namespace).Get(name, metav1.GetOptions{}) + job, err := readCronJob(kp, namespace, name) if err != nil { log.Printf("[DEBUG] Received error: %#v", err) return err @@ -159,7 +207,8 @@ func resourceKubernetesCronJobRead(d *schema.ResourceData, meta interface{}) err } func resourceKubernetesCronJobDelete(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*kubernetes.Clientset) + kp := meta.(*kubernetesProvider) + conn := kp.conn namespace, name, err := idParts(d.Id()) if err != nil { @@ -167,13 +216,26 @@ func resourceKubernetesCronJobDelete(d *schema.ResourceData, meta interface{}) e } log.Printf("[INFO] Deleting cron job: %#v", name) - err = conn.BatchV2alpha1().CronJobs(namespace).Delete(name, nil) + apiGroup, err := kp.highestSupportedAPIGroup(cronJobResourceGroupName, cronJobAPIGroups...) + if err != nil { + return err + } + switch apiGroup { + case batchV1beta1: + err = conn.BatchV1beta1().CronJobs(namespace).Delete(name, nil) + + case batchV2alpha1: + err = conn.BatchV2alpha1().CronJobs(namespace).Delete(name, nil) + + default: + err = cronJobNotSupportedError + } if err != nil { return err } err = resource.Retry(1*time.Minute, func() *resource.RetryError { - _, err := conn.BatchV2alpha1().CronJobs(namespace).Get(name, metav1.GetOptions{}) + _, err := readCronJob(kp, namespace, name) if err != nil { if statusErr, ok := err.(*errors.StatusError); ok && statusErr.ErrStatus.Code == 404 { return nil @@ -195,7 +257,7 @@ func resourceKubernetesCronJobDelete(d *schema.ResourceData, meta interface{}) e } func resourceKubernetesCronJobExists(d *schema.ResourceData, meta interface{}) (bool, error) { - conn := meta.(*kubernetes.Clientset) + kp := meta.(*kubernetesProvider) namespace, name, err := idParts(d.Id()) if err != nil { @@ -203,7 +265,7 @@ func resourceKubernetesCronJobExists(d *schema.ResourceData, meta interface{}) ( } log.Printf("[INFO] Checking cron job %s", name) - _, err = conn.BatchV2alpha1().CronJobs(namespace).Get(name, metav1.GetOptions{}) + _, err = readCronJob(kp, namespace, name) if err != nil { if statusErr, ok := err.(*errors.StatusError); ok && statusErr.ErrStatus.Code == 404 { return false, nil @@ -212,3 +274,34 @@ func resourceKubernetesCronJobExists(d *schema.ResourceData, meta interface{}) ( } return true, err } + +func readCronJob(kp *kubernetesProvider, namespace, name string) (cj *v1beta1.CronJob, err error) { + conn := kp.conn + + log.Printf("[INFO] Reading CronJob %s", name) + cj = &v1beta1.CronJob{} + + apiGroup, err := kp.highestSupportedAPIGroup(cronJobResourceGroupName, cronJobAPIGroups...) + if err != nil { + return nil, err + } + log.Printf("[INFO] Reading CronJob using %s API Group", apiGroup) + + switch apiGroup { + case batchV1beta1: + cj, err = conn.BatchV1beta1().CronJobs(namespace).Get(name, metav1.GetOptions{}) + return cj, err + + case batchV2alpha1: + out, err := conn.BatchV2alpha1().CronJobs(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + err = Convert(out, cj) + + default: + return nil, cronJobNotSupportedError + } + + return cj, err +} diff --git a/kubernetes/resource_kubernetes_cron_job_test.go b/kubernetes/resource_kubernetes_cron_job_test.go index 16f9234b13..79efa7db50 100644 --- a/kubernetes/resource_kubernetes_cron_job_test.go +++ b/kubernetes/resource_kubernetes_cron_job_test.go @@ -7,13 +7,11 @@ import ( "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - kubernetes "k8s.io/client-go/kubernetes" - batchv2 "k8s.io/client-go/pkg/apis/batch/v2alpha1" + "k8s.io/api/batch/v1beta1" ) func TestAccKubernetesCronJob_basic(t *testing.T) { - var conf batchv2.CronJob + var conf v1beta1.CronJob name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) resource.Test(t, resource.TestCase{ @@ -58,7 +56,7 @@ func TestAccKubernetesCronJob_basic(t *testing.T) { } func testAccCheckKubernetesCronJobDestroy(s *terraform.State) error { - conn := testAccProvider.Meta().(*kubernetes.Clientset) + kp := testAccProvider.Meta().(*kubernetesProvider) for _, rs := range s.RootModule().Resources { if rs.Type != "kubernetes_cron_job" { @@ -70,7 +68,7 @@ func testAccCheckKubernetesCronJobDestroy(s *terraform.State) error { return err } - resp, err := conn.CronJobs(namespace).Get(name, meta_v1.GetOptions{}) + resp, err := readCronJob(kp, namespace, name) if err == nil { if resp.Name == rs.Primary.ID { return fmt.Errorf("CronJob still exists: %s", rs.Primary.ID) @@ -81,21 +79,21 @@ func testAccCheckKubernetesCronJobDestroy(s *terraform.State) error { return nil } -func testAccCheckKubernetesCronJobExists(n string, obj *batchv2.CronJob) resource.TestCheckFunc { +func testAccCheckKubernetesCronJobExists(n string, obj *v1beta1.CronJob) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { return fmt.Errorf("Not found: %s", n) } - conn := testAccProvider.Meta().(*kubernetes.Clientset) + kp := testAccProvider.Meta().(*kubernetesProvider) namespace, name, err := idParts(rs.Primary.ID) if err != nil { return err } - out, err := conn.CronJobs(namespace).Get(name, meta_v1.GetOptions{}) + out, err := readCronJob(kp, namespace, name) if err != nil { return err } diff --git a/kubernetes/structure_cron_job.go b/kubernetes/structure_cron_job.go index 05b25e84eb..7c68403a09 100644 --- a/kubernetes/structure_cron_job.go +++ b/kubernetes/structure_cron_job.go @@ -2,7 +2,7 @@ package kubernetes import ( "github.com/hashicorp/terraform/helper/schema" - batchv2 "k8s.io/client-go/pkg/apis/batch/v2alpha1" + batchv2 "k8s.io/api/batch/v1beta1" ) func flattenCronJobSpec(in batchv2.CronJobSpec, d *schema.ResourceData) ([]interface{}, error) { From 82119312b17ec0bdafbdfb8edd5abb885db744df Mon Sep 17 00:00:00 2001 From: Matt Morrison Date: Wed, 2 May 2018 11:15:12 +1200 Subject: [PATCH 5/6] Re-add Google + AWS test providers. (only Google tested). --- kubernetes/provider_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kubernetes/provider_test.go b/kubernetes/provider_test.go index 8e4992d8c0..51853a2c24 100644 --- a/kubernetes/provider_test.go +++ b/kubernetes/provider_test.go @@ -9,6 +9,8 @@ import ( "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws" + "github.com/terraform-providers/terraform-provider-google/google" api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -20,6 +22,8 @@ func init() { testAccProvider = Provider().(*schema.Provider) testAccProviders = map[string]terraform.ResourceProvider{ "kubernetes": testAccProvider, + "google": google.Provider(), + "aws": aws.Provider(), } } From 9bf4d0b44cb27919f26cedb973003ccdb5a34a64 Mon Sep 17 00:00:00 2001 From: Matt Morrison Date: Wed, 2 May 2018 14:19:49 +1200 Subject: [PATCH 6/6] Fix: CronJob orphaned in state if err encountered during create. --- kubernetes/resource_kubernetes_cron_job.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kubernetes/resource_kubernetes_cron_job.go b/kubernetes/resource_kubernetes_cron_job.go index 0f2a62a3b5..9c9e566388 100644 --- a/kubernetes/resource_kubernetes_cron_job.go +++ b/kubernetes/resource_kubernetes_cron_job.go @@ -85,6 +85,9 @@ func resourceKubernetesCronJobCreate(d *schema.ResourceData, meta interface{}) e default: err = cronJobNotSupportedError } + if err != nil { + return err + } log.Printf("[INFO] Submitted new cron job: %#v", created) @@ -139,8 +142,6 @@ func resourceKubernetesCronJobUpdate(d *schema.ResourceData, meta interface{}) e default: err = cronJobNotSupportedError } - - //out, err := conn.BatchV2alpha1().CronJobs(namespace).Patch(name, pkgApi.JSONPatchType, data) if err != nil { return err }