provider/kubernetes: Add support for Service (#14554)

This commit is contained in:
Radek Simko 2017-05-17 14:43:53 +02:00 committed by GitHub
parent cebe0c9967
commit 65d47774f2
6 changed files with 1009 additions and 0 deletions

View File

@ -93,6 +93,7 @@ func Provider() terraform.ResourceProvider {
"kubernetes_persistent_volume_claim": resourceKubernetesPersistentVolumeClaim(), "kubernetes_persistent_volume_claim": resourceKubernetesPersistentVolumeClaim(),
"kubernetes_resource_quota": resourceKubernetesResourceQuota(), "kubernetes_resource_quota": resourceKubernetesResourceQuota(),
"kubernetes_secret": resourceKubernetesSecret(), "kubernetes_secret": resourceKubernetesSecret(),
"kubernetes_service": resourceKubernetesService(),
}, },
ConfigureFunc: providerConfigure, ConfigureFunc: providerConfigure,
} }

View File

@ -0,0 +1,225 @@
package kubernetes
import (
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
pkgApi "k8s.io/kubernetes/pkg/api"
"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 resourceKubernetesService() *schema.Resource {
return &schema.Resource{
Create: resourceKubernetesServiceCreate,
Read: resourceKubernetesServiceRead,
Exists: resourceKubernetesServiceExists,
Update: resourceKubernetesServiceUpdate,
Delete: resourceKubernetesServiceDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"metadata": namespacedMetadataSchema("service", true),
"spec": {
Type: schema.TypeList,
Description: "Spec defines the behavior of a service. http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status",
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"cluster_ip": {
Type: schema.TypeString,
Description: "The IP address of the service. It is usually assigned randomly by the master. If an address is specified manually and is not in use by others, it will be allocated to the service; otherwise, creation of the service will fail. `None` can be specified for headless services when proxying is not required. Ignored if type is `ExternalName`. More info: http://kubernetes.io/docs/user-guide/services#virtual-ips-and-service-proxies",
Optional: true,
ForceNew: true,
Computed: true,
},
"external_ips": {
Type: schema.TypeSet,
Description: "A list of IP addresses for which nodes in the cluster will also accept traffic for this service. These IPs are not managed by Kubernetes. The user is responsible for ensuring that traffic arrives at a node with this IP. A common example is external load-balancers that are not part of the Kubernetes system.",
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"external_name": {
Type: schema.TypeString,
Description: "The external reference that kubedns or equivalent will return as a CNAME record for this service. No proxying will be involved. Must be a valid DNS name and requires `type` to be `ExternalName`.",
Optional: true,
},
"load_balancer_ip": {
Type: schema.TypeString,
Description: "Only applies to `type = LoadBalancer`. LoadBalancer will get created with the IP specified in this field. This feature depends on whether the underlying cloud-provider supports specifying this field when a load balancer is created. This field will be ignored if the cloud-provider does not support the feature.",
Optional: true,
},
"load_balancer_source_ranges": {
Type: schema.TypeSet,
Description: "If specified and supported by the platform, this will restrict traffic through the cloud-provider load-balancer will be restricted to the specified client IPs. This field will be ignored if the cloud-provider does not support the feature. More info: http://kubernetes.io/docs/user-guide/services-firewalls",
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"port": {
Type: schema.TypeList,
Description: "The list of ports that are exposed by this service. More info: http://kubernetes.io/docs/user-guide/services#virtual-ips-and-service-proxies",
Required: true,
MinItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Description: "The name of this port within the service. All ports within the service must have unique names. Optional if only one ServicePort is defined on this service.",
Optional: true,
},
"node_port": {
Type: schema.TypeInt,
Description: "The port on each node on which this service is exposed when `type` is `NodePort` or `LoadBalancer`. Usually assigned by the system. If specified, it will be allocated to the service if unused or else creation of the service will fail. Default is to auto-allocate a port if the `type` of this service requires one. More info: http://kubernetes.io/docs/user-guide/services#type--nodeport",
Computed: true,
Optional: true,
},
"port": {
Type: schema.TypeInt,
Description: "The port that will be exposed by this service.",
Required: true,
},
"protocol": {
Type: schema.TypeString,
Description: "The IP protocol for this port. Supports `TCP` and `UDP`. Default is `TCP`.",
Optional: true,
Default: "TCP",
},
"target_port": {
Type: schema.TypeInt,
Description: "Number or name of the port to access on the pods targeted by the service. Number must be in the range 1 to 65535. This field is ignored for services with `cluster_ip = \"None\"`. More info: http://kubernetes.io/docs/user-guide/services#defining-a-service",
Required: true,
},
},
},
},
"selector": {
Type: schema.TypeMap,
Description: "Route service traffic to pods with label keys and values matching this selector. Only applies to types `ClusterIP`, `NodePort`, and `LoadBalancer`. More info: http://kubernetes.io/docs/user-guide/services#overview",
Optional: true,
},
"session_affinity": {
Type: schema.TypeString,
Description: "Used to maintain session affinity. Supports `ClientIP` and `None`. Defaults to `None`. More info: http://kubernetes.io/docs/user-guide/services#virtual-ips-and-service-proxies",
Optional: true,
Default: "None",
},
"type": {
Type: schema.TypeString,
Description: "Determines how the service is exposed. Defaults to `ClusterIP`. Valid options are `ExternalName`, `ClusterIP`, `NodePort`, and `LoadBalancer`. `ExternalName` maps to the specified `external_name`. More info: http://kubernetes.io/docs/user-guide/services#overview",
Optional: true,
Default: "ClusterIP",
},
},
},
},
},
}
}
func resourceKubernetesServiceCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)
metadata := expandMetadata(d.Get("metadata").([]interface{}))
svc := api.Service{
ObjectMeta: metadata,
Spec: expandServiceSpec(d.Get("spec").([]interface{})),
}
log.Printf("[INFO] Creating new service: %#v", svc)
out, err := conn.CoreV1().Services(metadata.Namespace).Create(&svc)
if err != nil {
return err
}
log.Printf("[INFO] Submitted new service: %#v", out)
d.SetId(buildId(out.ObjectMeta))
return resourceKubernetesServiceRead(d, meta)
}
func resourceKubernetesServiceRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)
namespace, name := idParts(d.Id())
log.Printf("[INFO] Reading service %s", name)
svc, err := conn.CoreV1().Services(namespace).Get(name)
if err != nil {
log.Printf("[DEBUG] Received error: %#v", err)
return err
}
log.Printf("[INFO] Received service: %#v", svc)
err = d.Set("metadata", flattenMetadata(svc.ObjectMeta))
if err != nil {
return err
}
flattened := flattenServiceSpec(svc.Spec)
log.Printf("[DEBUG] Flattened service spec: %#v", flattened)
err = d.Set("spec", flattened)
if err != nil {
return err
}
return nil
}
func resourceKubernetesServiceUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)
namespace, name := idParts(d.Id())
ops := patchMetadata("metadata.0.", "/metadata/", d)
if d.HasChange("spec") {
diffOps := patchServiceSpec("spec.0.", "/spec/", d)
ops = append(ops, diffOps...)
}
data, err := ops.MarshalJSON()
if err != nil {
return fmt.Errorf("Failed to marshal update operations: %s", err)
}
log.Printf("[INFO] Updating service %q: %v", name, string(data))
out, err := conn.CoreV1().Services(namespace).Patch(name, pkgApi.JSONPatchType, data)
if err != nil {
return fmt.Errorf("Failed to update service: %s", err)
}
log.Printf("[INFO] Submitted updated service: %#v", out)
d.SetId(buildId(out.ObjectMeta))
return resourceKubernetesServiceRead(d, meta)
}
func resourceKubernetesServiceDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)
namespace, name := idParts(d.Id())
log.Printf("[INFO] Deleting service: %#v", name)
err := conn.CoreV1().Services(namespace).Delete(name, &api.DeleteOptions{})
if err != nil {
return err
}
log.Printf("[INFO] Service %s deleted", name)
d.SetId("")
return nil
}
func resourceKubernetesServiceExists(d *schema.ResourceData, meta interface{}) (bool, error) {
conn := meta.(*kubernetes.Clientset)
namespace, name := idParts(d.Id())
log.Printf("[INFO] Checking service %s", name)
_, err := conn.CoreV1().Services(namespace).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)
}
return true, err
}

View File

@ -0,0 +1,499 @@
package kubernetes
import (
"fmt"
"reflect"
"regexp"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
api "k8s.io/kubernetes/pkg/api/v1"
kubernetes "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
"k8s.io/kubernetes/pkg/util/intstr"
)
func TestAccKubernetesService_basic(t *testing.T) {
var conf api.Service
name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "kubernetes_service.test",
Providers: testAccProviders,
CheckDestroy: testAccCheckKubernetesServiceDestroy,
Steps: []resource.TestStep{
{
Config: testAccKubernetesServiceConfig_basic(name),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesServiceExists("kubernetes_service.test", &conf),
resource.TestCheckResourceAttr("kubernetes_service.test", "metadata.0.name", name),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.self_link"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.uid"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.#", "1"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.#", "1"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "spec.0.cluster_ip"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.name", ""),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.node_port", "0"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.port", "8080"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.protocol", "TCP"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.target_port", "80"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.session_affinity", "None"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.type", "ClusterIP"),
testAccCheckServicePorts(&conf, []api.ServicePort{
{
Port: int32(8080),
Protocol: api.ProtocolTCP,
TargetPort: intstr.FromInt(80),
},
}),
),
},
{
Config: testAccKubernetesServiceConfig_modified(name),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesServiceExists("kubernetes_service.test", &conf),
resource.TestCheckResourceAttr("kubernetes_service.test", "metadata.0.name", name),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.self_link"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.uid"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.#", "1"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.#", "1"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "spec.0.cluster_ip"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.name", ""),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.node_port", "0"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.port", "8081"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.protocol", "TCP"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.target_port", "80"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.session_affinity", "None"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.type", "ClusterIP"),
testAccCheckServicePorts(&conf, []api.ServicePort{
{
Port: int32(8081),
Protocol: api.ProtocolTCP,
TargetPort: intstr.FromInt(80),
},
}),
),
},
},
})
}
func TestAccKubernetesService_loadBalancer(t *testing.T) {
var conf api.Service
name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "kubernetes_service.test",
Providers: testAccProviders,
CheckDestroy: testAccCheckKubernetesServiceDestroy,
Steps: []resource.TestStep{
{
Config: testAccKubernetesServiceConfig_loadBalancer(name),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesServiceExists("kubernetes_service.test", &conf),
resource.TestCheckResourceAttr("kubernetes_service.test", "metadata.0.name", name),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.#", "1"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.#", "1"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.#", "1"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "spec.0.port.0.node_port"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.port", "8888"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.protocol", "TCP"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.target_port", "80"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "spec.0.cluster_ip"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_ips.#", "2"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_ips.1452553500", "10.0.0.4"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_ips.3371212991", "10.0.0.3"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_name", "ext-name-"+name),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.load_balancer_ip", "12.0.0.120"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.load_balancer_source_ranges.#", "2"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.load_balancer_source_ranges.138364083", "10.0.0.5/32"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.load_balancer_source_ranges.445311837", "10.0.0.6/32"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.selector.%", "1"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.selector.App", "MyApp"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.session_affinity", "ClientIP"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.type", "LoadBalancer"),
testAccCheckServicePorts(&conf, []api.ServicePort{
{
Port: int32(8888),
Protocol: api.ProtocolTCP,
TargetPort: intstr.FromInt(80),
},
}),
),
},
{
Config: testAccKubernetesServiceConfig_loadBalancer_modified(name),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesServiceExists("kubernetes_service.test", &conf),
resource.TestCheckResourceAttr("kubernetes_service.test", "metadata.0.name", name),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.#", "1"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "spec.0.cluster_ip"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_ips.#", "2"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_ips.1452553500", "10.0.0.4"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_ips.563283338", "10.0.0.5"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_name", "ext-name-modified-"+name),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.load_balancer_ip", "12.0.0.125"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.load_balancer_source_ranges.#", "2"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.load_balancer_source_ranges.2271073252", "10.0.0.1/32"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.load_balancer_source_ranges.2515041290", "10.0.0.2/32"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.#", "1"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "spec.0.port.0.node_port"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.port", "9999"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.protocol", "TCP"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.target_port", "81"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.selector.%", "2"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.selector.App", "MyModifiedApp"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.selector.NewSelector", "NewValue"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.session_affinity", "ClientIP"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.type", "LoadBalancer"),
testAccCheckServicePorts(&conf, []api.ServicePort{
{
Port: int32(9999),
Protocol: api.ProtocolTCP,
TargetPort: intstr.FromInt(81),
},
}),
),
},
},
})
}
func TestAccKubernetesService_nodePort(t *testing.T) {
var conf api.Service
name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "kubernetes_service.test",
Providers: testAccProviders,
CheckDestroy: testAccCheckKubernetesServiceDestroy,
Steps: []resource.TestStep{
{
Config: testAccKubernetesServiceConfig_nodePort(name),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesServiceExists("kubernetes_service.test", &conf),
resource.TestCheckResourceAttr("kubernetes_service.test", "metadata.0.name", name),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.#", "1"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.#", "1"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "spec.0.cluster_ip"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_ips.#", "2"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_ips.1452553500", "10.0.0.4"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_ips.563283338", "10.0.0.5"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.external_name", "ext-name-"+name),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.load_balancer_ip", "12.0.0.125"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.#", "2"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.name", "first"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "spec.0.port.0.node_port"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.port", "10222"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.protocol", "TCP"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.0.target_port", "22"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.1.name", "second"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "spec.0.port.1.node_port"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.1.port", "10333"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.1.protocol", "TCP"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.port.1.target_port", "33"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.selector.%", "1"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.selector.App", "MyApp"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.session_affinity", "ClientIP"),
resource.TestCheckResourceAttr("kubernetes_service.test", "spec.0.type", "NodePort"),
testAccCheckServicePorts(&conf, []api.ServicePort{
{
Name: "first",
Port: int32(10222),
Protocol: api.ProtocolTCP,
TargetPort: intstr.FromInt(22),
},
{
Name: "second",
Port: int32(10333),
Protocol: api.ProtocolTCP,
TargetPort: intstr.FromInt(33),
},
}),
),
},
},
})
}
func TestAccKubernetesService_importBasic(t *testing.T) {
resourceName := "kubernetes_service.test"
name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckKubernetesServiceDestroy,
Steps: []resource.TestStep{
{
Config: testAccKubernetesServiceConfig_basic(name),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
func TestAccKubernetesService_generatedName(t *testing.T) {
var conf api.Service
prefix := "tf-acc-test-gen-"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "kubernetes_service.test",
Providers: testAccProviders,
CheckDestroy: testAccCheckKubernetesServiceDestroy,
Steps: []resource.TestStep{
{
Config: testAccKubernetesServiceConfig_generatedName(prefix),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesServiceExists("kubernetes_service.test", &conf),
resource.TestCheckResourceAttr("kubernetes_service.test", "metadata.0.annotations.%", "0"),
testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}),
resource.TestCheckResourceAttr("kubernetes_service.test", "metadata.0.labels.%", "0"),
testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}),
resource.TestCheckResourceAttr("kubernetes_service.test", "metadata.0.generate_name", prefix),
resource.TestMatchResourceAttr("kubernetes_service.test", "metadata.0.name", regexp.MustCompile("^"+prefix)),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.self_link"),
resource.TestCheckResourceAttrSet("kubernetes_service.test", "metadata.0.uid"),
),
},
},
})
}
func TestAccKubernetesService_importGeneratedName(t *testing.T) {
resourceName := "kubernetes_service.test"
prefix := "tf-acc-test-gen-import-"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckKubernetesServiceDestroy,
Steps: []resource.TestStep{
{
Config: testAccKubernetesServiceConfig_generatedName(prefix),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
func testAccCheckServicePorts(svc *api.Service, expected []api.ServicePort) resource.TestCheckFunc {
return func(s *terraform.State) error {
if len(expected) == 0 && len(svc.Spec.Ports) == 0 {
return nil
}
ports := svc.Spec.Ports
// Ignore NodePorts as these are assigned randomly
for k, _ := range ports {
ports[k].NodePort = 0
}
if !reflect.DeepEqual(ports, expected) {
return fmt.Errorf("Service ports don't match.\nExpected: %#v\nGiven: %#v",
expected, svc.Spec.Ports)
}
return nil
}
}
func testAccCheckKubernetesServiceDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*kubernetes.Clientset)
for _, rs := range s.RootModule().Resources {
if rs.Type != "kubernetes_service" {
continue
}
namespace, name := idParts(rs.Primary.ID)
resp, err := conn.CoreV1().Services(namespace).Get(name)
if err == nil {
if resp.Name == rs.Primary.ID {
return fmt.Errorf("Service still exists: %s", rs.Primary.ID)
}
}
}
return nil
}
func testAccCheckKubernetesServiceExists(n string, obj *api.Service) 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 := idParts(rs.Primary.ID)
out, err := conn.CoreV1().Services(namespace).Get(name)
if err != nil {
return err
}
*obj = *out
return nil
}
}
func testAccKubernetesServiceConfig_basic(name string) string {
return fmt.Sprintf(`
resource "kubernetes_service" "test" {
metadata {
annotations {
TestAnnotationOne = "one"
TestAnnotationTwo = "two"
}
labels {
TestLabelOne = "one"
TestLabelTwo = "two"
TestLabelThree = "three"
}
name = "%s"
}
spec {
port {
port = 8080
target_port = 80
}
}
}`, name)
}
func testAccKubernetesServiceConfig_modified(name string) string {
return fmt.Sprintf(`
resource "kubernetes_service" "test" {
metadata {
annotations {
TestAnnotationOne = "one"
Different = "1234"
}
labels {
TestLabelOne = "one"
TestLabelThree = "three"
}
name = "%s"
}
spec {
port {
port = 8081
target_port = 80
}
}
}`, name)
}
func testAccKubernetesServiceConfig_loadBalancer(name string) string {
return fmt.Sprintf(`
resource "kubernetes_service" "test" {
metadata {
name = "%s"
}
spec {
external_name = "ext-name-%s"
external_ips = ["10.0.0.3", "10.0.0.4"]
load_balancer_ip = "12.0.0.120"
load_balancer_source_ranges = ["10.0.0.5/32", "10.0.0.6/32"]
selector {
App = "MyApp"
}
session_affinity = "ClientIP"
port {
port = 8888
target_port = 80
}
type = "LoadBalancer"
}
}`, name, name)
}
func testAccKubernetesServiceConfig_loadBalancer_modified(name string) string {
return fmt.Sprintf(`
resource "kubernetes_service" "test" {
metadata {
name = "%s"
}
spec {
external_name = "ext-name-modified-%s"
external_ips = ["10.0.0.4", "10.0.0.5"]
load_balancer_ip = "12.0.0.125"
load_balancer_source_ranges = ["10.0.0.1/32", "10.0.0.2/32"]
selector {
App = "MyModifiedApp"
NewSelector = "NewValue"
}
session_affinity = "ClientIP"
port {
port = 9999
target_port = 81
}
type = "LoadBalancer"
}
}`, name, name)
}
func testAccKubernetesServiceConfig_nodePort(name string) string {
return fmt.Sprintf(`
resource "kubernetes_service" "test" {
metadata {
name = "%s"
}
spec {
external_name = "ext-name-%s"
external_ips = ["10.0.0.4", "10.0.0.5"]
load_balancer_ip = "12.0.0.125"
selector {
App = "MyApp"
}
session_affinity = "ClientIP"
port {
name = "first"
port = 10222
target_port = 22
}
port {
name = "second"
port = 10333
target_port = 33
}
type = "NodePort"
}
}`, name, name)
}
func testAccKubernetesServiceConfig_generatedName(prefix string) string {
return fmt.Sprintf(`
resource "kubernetes_service" "test" {
metadata {
generate_name = "%s"
}
spec {
port {
port = 8080
target_port = 80
}
}
}`, prefix)
}

View File

@ -0,0 +1,188 @@
package kubernetes
import (
"github.com/hashicorp/terraform/helper/schema"
"k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/util/intstr"
)
// Flatteners
func flattenIntOrString(in intstr.IntOrString) int {
return in.IntValue()
}
func flattenServicePort(in []v1.ServicePort) []interface{} {
att := make([]interface{}, len(in), len(in))
for i, n := range in {
m := make(map[string]interface{})
m["name"] = n.Name
m["protocol"] = string(n.Protocol)
m["port"] = int(n.Port)
m["target_port"] = flattenIntOrString(n.TargetPort)
m["node_port"] = int(n.NodePort)
att[i] = m
}
return att
}
func flattenServiceSpec(in v1.ServiceSpec) []interface{} {
att := make(map[string]interface{})
if len(in.Ports) > 0 {
att["port"] = flattenServicePort(in.Ports)
}
if len(in.Selector) > 0 {
att["selector"] = in.Selector
}
if in.ClusterIP != "" {
att["cluster_ip"] = in.ClusterIP
}
if in.Type != "" {
att["type"] = string(in.Type)
}
if len(in.ExternalIPs) > 0 {
att["external_ips"] = newStringSet(schema.HashString, in.ExternalIPs)
}
if in.SessionAffinity != "" {
att["session_affinity"] = string(in.SessionAffinity)
}
if in.LoadBalancerIP != "" {
att["load_balancer_ip"] = in.LoadBalancerIP
}
if len(in.LoadBalancerSourceRanges) > 0 {
att["load_balancer_source_ranges"] = newStringSet(schema.HashString, in.LoadBalancerSourceRanges)
}
if in.ExternalName != "" {
att["external_name"] = in.ExternalName
}
return []interface{}{att}
}
// Expanders
func expandIntOrString(in int) intstr.IntOrString {
return intstr.FromInt(in)
}
func expandServicePort(l []interface{}) []v1.ServicePort {
if len(l) == 0 || l[0] == nil {
return []v1.ServicePort{}
}
obj := make([]v1.ServicePort, len(l), len(l))
for i, n := range l {
cfg := n.(map[string]interface{})
obj[i] = v1.ServicePort{
Port: int32(cfg["port"].(int)),
TargetPort: expandIntOrString(cfg["target_port"].(int)),
}
if v, ok := cfg["name"].(string); ok {
obj[i].Name = v
}
if v, ok := cfg["protocol"].(string); ok {
obj[i].Protocol = v1.Protocol(v)
}
if v, ok := cfg["node_port"].(int); ok {
obj[i].NodePort = int32(v)
}
}
return obj
}
func expandServiceSpec(l []interface{}) v1.ServiceSpec {
if len(l) == 0 || l[0] == nil {
return v1.ServiceSpec{}
}
in := l[0].(map[string]interface{})
obj := v1.ServiceSpec{}
if v, ok := in["port"].([]interface{}); ok && len(v) > 0 {
obj.Ports = expandServicePort(v)
}
if v, ok := in["selector"].(map[string]interface{}); ok && len(v) > 0 {
obj.Selector = expandStringMap(v)
}
if v, ok := in["cluster_ip"].(string); ok {
obj.ClusterIP = v
}
if v, ok := in["type"].(string); ok {
obj.Type = v1.ServiceType(v)
}
if v, ok := in["external_ips"].(*schema.Set); ok && v.Len() > 0 {
obj.ExternalIPs = sliceOfString(v.List())
}
if v, ok := in["session_affinity"].(string); ok {
obj.SessionAffinity = v1.ServiceAffinity(v)
}
if v, ok := in["load_balancer_ip"].(string); ok {
obj.LoadBalancerIP = v
}
if v, ok := in["load_balancer_source_ranges"].(*schema.Set); ok && v.Len() > 0 {
obj.LoadBalancerSourceRanges = sliceOfString(v.List())
}
if v, ok := in["external_name"].(string); ok {
obj.ExternalName = v
}
return obj
}
// Patch Ops
func patchServiceSpec(keyPrefix, pathPrefix string, d *schema.ResourceData) PatchOperations {
ops := make([]PatchOperation, 0, 0)
if d.HasChange(keyPrefix + "selector") {
ops = append(ops, &ReplaceOperation{
Path: pathPrefix + "selector",
Value: d.Get(keyPrefix + "selector").(map[string]interface{}),
})
}
if d.HasChange(keyPrefix + "type") {
ops = append(ops, &ReplaceOperation{
Path: pathPrefix + "type",
Value: d.Get(keyPrefix + "type").(string),
})
}
if d.HasChange(keyPrefix + "session_affinity") {
ops = append(ops, &ReplaceOperation{
Path: pathPrefix + "sessionAffinity",
Value: d.Get(keyPrefix + "session_affinity").(string),
})
}
if d.HasChange(keyPrefix + "load_balancer_ip") {
ops = append(ops, &ReplaceOperation{
Path: pathPrefix + "loadBalancerIP",
Value: d.Get(keyPrefix + "load_balancer_ip").(string),
})
}
if d.HasChange(keyPrefix + "load_balancer_source_ranges") {
ops = append(ops, &ReplaceOperation{
Path: pathPrefix + "loadBalancerSourceRanges",
Value: d.Get(keyPrefix + "load_balancer_source_ranges").(*schema.Set).List(),
})
}
if d.HasChange(keyPrefix + "port") {
ops = append(ops, &ReplaceOperation{
Path: pathPrefix + "ports",
Value: expandServicePort(d.Get(keyPrefix + "port").([]interface{})),
})
}
if d.HasChange(keyPrefix + "external_ips") {
// If we haven't done this the deprecated field would have priority
ops = append(ops, &ReplaceOperation{
Path: pathPrefix + "deprecatedPublicIPs",
Value: nil,
})
ops = append(ops, &ReplaceOperation{
Path: pathPrefix + "externalIPs",
Value: d.Get(keyPrefix + "external_ips").(*schema.Set).List(),
})
}
if d.HasChange(keyPrefix + "external_name") {
ops = append(ops, &ReplaceOperation{
Path: pathPrefix + "externalName",
Value: d.Get(keyPrefix + "external_name").(string),
})
}
return ops
}

View File

@ -0,0 +1,93 @@
---
layout: "kubernetes"
page_title: "Kubernetes: kubernetes_service"
sidebar_current: "docs-kubernetes-service"
description: |-
A Service is an abstraction which defines a logical set of pods and a policy by which to access them - sometimes called a micro-service.
---
# kubernetes_service
A Service is an abstraction which defines a logical set of pods and a policy by which to access them - sometimes called a micro-service.
## Example Usage
```hcl
resource "kubernetes_service" "example" {
metadata {
name = "terraform-example"
}
spec {
selector {
App = "MyApp"
}
session_affinity = "ClientIP"
port {
port = 8080
target_port = 80
}
type = "LoadBalancer"
}
}
```
## Argument Reference
The following arguments are supported:
* `metadata` - (Required) Standard service's metadata. More info: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#metadata
* `spec` - (Required) Spec defines the behavior of a service. http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status
## Nested Blocks
### `metadata`
#### Arguments
* `annotations` - (Optional) An unstructured key value map stored with the service that may be used to store arbitrary metadata. More info: http://kubernetes.io/docs/user-guide/annotations
* `generate_name` - (Optional) Prefix, used by the server, to generate a unique name ONLY IF the `name` field has not been provided. This value will also be combined with a unique suffix. Read more: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#idempotency
* `labels` - (Optional) Map of string keys and values that can be used to organize and categorize (scope and select) the service. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels
* `name` - (Optional) Name of the service, must be unique. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names
* `namespace` - (Optional) Namespace defines the space within which name of the service must be unique.
#### Attributes
* `generation` - A sequence number representing a specific generation of the desired state.
* `resource_version` - An opaque value that represents the internal version of this service that can be used by clients to determine when service has changed. Read more: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#concurrency-control-and-consistency
* `self_link` - A URL representing this service.
* `uid` - The unique in time and space value for this service. More info: http://kubernetes.io/docs/user-guide/identifiers#uids
### `spec`
#### Arguments
* `cluster_ip` - (Optional) The IP address of the service. It is usually assigned randomly by the master. If an address is specified manually and is not in use by others, it will be allocated to the service; otherwise, creation of the service will fail. `None` can be specified for headless services when proxying is not required. Ignored if type is `ExternalName`. More info: http://kubernetes.io/docs/user-guide/services#virtual-ips-and-service-proxies
* `external_ips` - (Optional) A list of IP addresses for which nodes in the cluster will also accept traffic for this service. These IPs are not managed by Kubernetes. The user is responsible for ensuring that traffic arrives at a node with this IP. A common example is external load-balancers that are not part of the Kubernetes system.
* `external_name` - (Optional) The external reference that kubedns or equivalent will return as a CNAME record for this service. No proxying will be involved. Must be a valid DNS name and requires `type` to be `ExternalName`.
* `load_balancer_ip` - (Optional) Only applies to `type = LoadBalancer`. LoadBalancer will get created with the IP specified in this field. This feature depends on whether the underlying cloud-provider supports specifying this field when a load balancer is created. This field will be ignored if the cloud-provider does not support the feature.
* `load_balancer_source_ranges` - (Optional) If specified and supported by the platform, this will restrict traffic through the cloud-provider load-balancer will be restricted to the specified client IPs. This field will be ignored if the cloud-provider does not support the feature. More info: http://kubernetes.io/docs/user-guide/services-firewalls
* `port` - (Required) The list of ports that are exposed by this service. More info: http://kubernetes.io/docs/user-guide/services#virtual-ips-and-service-proxies
* `selector` - (Optional) Route service traffic to pods with label keys and values matching this selector. Only applies to types `ClusterIP`, `NodePort`, and `LoadBalancer`. More info: http://kubernetes.io/docs/user-guide/services#overview
* `session_affinity` - (Optional) Used to maintain session affinity. Supports `ClientIP` and `None`. Defaults to `None`. More info: http://kubernetes.io/docs/user-guide/services#virtual-ips-and-service-proxies
* `type` - (Optional) Determines how the service is exposed. Defaults to `ClusterIP`. Valid options are `ExternalName`, `ClusterIP`, `NodePort`, and `LoadBalancer`. `ExternalName` maps to the specified `external_name`. More info: http://kubernetes.io/docs/user-guide/services#overview
### `port`
#### Arguments
* `name` - (Optional) The name of this port within the service. All ports within the service must have unique names. Optional if only one ServicePort is defined on this service.
* `node_port` - (Optional) The port on each node on which this service is exposed when `type` is `NodePort` or `LoadBalancer`. Usually assigned by the system. If specified, it will be allocated to the service if unused or else creation of the service will fail. Default is to auto-allocate a port if the `type` of this service requires one. More info: http://kubernetes.io/docs/user-guide/services#type--nodeport
* `port` - (Required) The port that will be exposed by this service.
* `protocol` - (Optional) The IP protocol for this port. Supports `TCP` and `UDP`. Default is `TCP`.
* `target_port` - (Required) Number or name of the port to access on the pods targeted by the service. Number must be in the range 1 to 65535. This field is ignored for services with `cluster_ip = "None"`. More info: http://kubernetes.io/docs/user-guide/services#defining-a-service
## Import
Service can be imported using its namespace and name, e.g.
```
$ terraform import kubernetes_service.example default/terraform-name
```

View File

@ -34,6 +34,9 @@
<li<%= sidebar_current("docs-kubernetes-resource-secret") %>> <li<%= sidebar_current("docs-kubernetes-resource-secret") %>>
<a href="/docs/providers/kubernetes/r/secret.html">kubernetes_secret</a> <a href="/docs/providers/kubernetes/r/secret.html">kubernetes_secret</a>
</li> </li>
<li<%= sidebar_current("docs-kubernetes-resource-service") %>>
<a href="/docs/providers/kubernetes/r/service.html">kubernetes_service</a>
</li>
</ul> </ul>
</li> </li>
</ul> </ul>