Skip to content

Commit

Permalink
add resource_kubernetes_endpointslice (#2086)
Browse files Browse the repository at this point in the history
* initial resource creation

* initial structures commit

* add flatteners and expanders

* initial create/read functions

* update read function

* add to provider.go

* add finished schema with working tfplan

* fix expand on SliceEndpoints

* update to TypeList

* update to using TypeList

* update endpoints expander

* fix flattener errors

* add expander for objectref and flattener for conditions

* add changelog-entry

* initial tests

* add missing port change check and use proper read function

* get update working as well as adding ForceNew to address_type attribute

* finish endpoint_slice_v1_test.go

* add descriptions to attributes

* add docs

* website-lint-fix

* add validator functions to port, hostname, and addresstype

* add int check

* fix test format

* refactor validate functions

* set to correct port
  • Loading branch information
BBBmau committed Jun 22, 2023
1 parent 23f51c8 commit e9fab70
Show file tree
Hide file tree
Showing 8 changed files with 861 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .changelog/2086.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
`kubernetes/resource_kubernetes_endpointslice.go`: Add kubernetes_endpoint_slice resource
```
1 change: 1 addition & 0 deletions kubernetes/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ func Provider() *schema.Provider {
"kubernetes_pod_v1": resourceKubernetesPod(),
"kubernetes_endpoints": resourceKubernetesEndpoints(),
"kubernetes_endpoints_v1": resourceKubernetesEndpoints(),
"kubernetes_endpoint_slice_v1": resourceKubernetesEndpointSlice(),
"kubernetes_env": resourceKubernetesEnv(),
"kubernetes_limit_range": resourceKubernetesLimitRange(),
"kubernetes_limit_range_v1": resourceKubernetesLimitRange(),
Expand Down
189 changes: 189 additions & 0 deletions kubernetes/resource_kubernetes_endpointslice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package kubernetes

import (
"context"
"log"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
api "k8s.io/api/discovery/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
pkgApi "k8s.io/apimachinery/pkg/types"
)

func resourceKubernetesEndpointSlice() *schema.Resource {
return &schema.Resource{
CreateContext: resourceKubernetesEndpointSliceCreate,
ReadContext: resourceKubernetesEndpointSliceRead,
UpdateContext: resourceKubernetesEndpointSliceUpdate,
DeleteContext: resourceKubernetesEndpointSliceDelete,

Schema: map[string]*schema.Schema{
"metadata": namespacedMetadataSchema("endpoint_slice", true),
"address_type": {
Type: schema.TypeString,
Description: "address_type specifies the type of address carried by this EndpointSlice. All addresses in this slice must be the same type. This field is immutable after creation.",
Required: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{"IPv4", "IPv6", "FQDN"}, false),
},
"endpoint": {
Description: "endpoint is a list of unique endpoints in this slice. Each slice may include a maximum of 1000 endpoints.",
Type: schema.TypeList,
MaxItems: 1000,
Required: true,
Elem: schemaEndpointSliceSubsetEndpoints(),
},
"port": {
Description: "port specifies the list of network ports exposed by each endpoint in this slice. Each port must have a unique name. Each slice may include a maximum of 100 ports.",
Type: schema.TypeList,
MaxItems: 100,
Required: true,
Elem: schemaEndpointSliceSubsetPorts(),
},
},
}
}

func resourceKubernetesEndpointSliceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn, err := meta.(KubeClientsets).MainClientset()
if err != nil {
return diag.FromErr(err)
}

metadata := expandMetadata(d.Get("metadata").([]interface{}))
endpoint_slice := api.EndpointSlice{
ObjectMeta: metadata,
AddressType: api.AddressType(d.Get("address_type").(string)),
Endpoints: expandEndpointSliceEndpoints(d.Get("endpoint").([]interface{})),
Ports: expandEndpointSlicePorts(d.Get("port").([]interface{})),
}

log.Printf("[INFO] Creating new endpoint_slice: %#v", endpoint_slice)
out, err := conn.DiscoveryV1().EndpointSlices(metadata.Namespace).Create(ctx, &endpoint_slice, metav1.CreateOptions{})
if err != nil {
return diag.Errorf("Failed to create endpoint_slice because: %s", err)
}
log.Printf("[INFO] Submitted new endpoint_slice: %#v", out)
d.SetId(buildId(out.ObjectMeta))

return resourceKubernetesEndpointSliceRead(ctx, d, meta)
}

func resourceKubernetesEndpointSliceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn, err := meta.(KubeClientsets).MainClientset()
if err != nil {
return diag.FromErr(err)
}
namespace, name, err := idParts(d.Id())

log.Printf("[INFO] Reading endpoint slice %s", name)
endpoint, err := conn.DiscoveryV1().EndpointSlices(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return diag.Errorf("Failed to read endpoint_slice because: %s", err)
}
log.Printf("[INFO] Received endpoint slice: %#v", endpoint)

address_type := d.Get("address_type").(string)
log.Printf("[DEBUG] Default address type is %q", address_type)
d.Set("address_type", address_type)

err = d.Set("metadata", flattenMetadata(endpoint.ObjectMeta, d, meta))
if err != nil {
return diag.FromErr(err)
}

flattenedEndpoints := flattenEndpointSliceEndpoints(endpoint.Endpoints)
log.Printf("[DEBUG] Flattened EndpointSlice Endpoints: %#v", flattenedEndpoints)
err = d.Set("endpoint", flattenedEndpoints)
if err != nil {
return diag.FromErr(err)
}

flattenedPorts := flattenEndpointSlicePorts(endpoint.Ports)
log.Printf("[DEBUG] Flattened EndpointSlice Ports: %#v", flattenedPorts)
err = d.Set("port", flattenedPorts)
if err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceKubernetesEndpointSliceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn, err := meta.(KubeClientsets).MainClientset()
if err != nil {
return diag.FromErr(err)
}

namespace, name, err := idParts(d.Id())
if err != nil {
return diag.Errorf("Failed to update endpointSlice because: %s", err)
}

ops := patchMetadata("metadata.0.", "/metadata/", d)
if d.HasChange("address_type") {
address_type := d.Get("address_type").(string)
ops = append(ops, &ReplaceOperation{
Path: "/addressType",
Value: address_type,
})
}
if d.HasChange("endpoint") {
endpoints := expandEndpointSliceEndpoints(d.Get("endpoint").([]interface{}))
ops = append(ops, &ReplaceOperation{
Path: "/endpoints",
Value: endpoints,
})
}
if d.HasChange("port") {
ports := expandEndpointSlicePorts(d.Get("port").([]interface{}))
ops = append(ops, &ReplaceOperation{
Path: "/ports",
Value: ports,
})
}
data, err := ops.MarshalJSON()
if err != nil {
return diag.Errorf("Failed to marshal update operations: %s", err)
}
log.Printf("[INFO] Updating endpointSlice %q: %v", name, string(data))
out, err := conn.DiscoveryV1().EndpointSlices(namespace).Patch(ctx, name, pkgApi.JSONPatchType, data, metav1.PatchOptions{})
if err != nil {
return diag.Errorf("Failed to update endpointSlice: %s", err)
}
log.Printf("[INFO] Submitted updated endpointSlice: %#v", out)
d.SetId(buildId(out.ObjectMeta))

return resourceKubernetesEndpointSliceRead(ctx, d, meta)
}

func resourceKubernetesEndpointSliceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn, err := meta.(KubeClientsets).MainClientset()
if err != nil {
return diag.FromErr(err)
}

namespace, name, err := idParts(d.Id())
if err != nil {
return diag.Errorf("Failed to delete endpointSlice because: %s", err)
}
log.Printf("[INFO] Deleting endpointSlice: %#v", name)
err = conn.DiscoveryV1().EndpointSlices(namespace).Delete(ctx, name, metav1.DeleteOptions{})
if err != nil {
if statusErr, ok := err.(*errors.StatusError); ok && errors.IsNotFound(statusErr) {
return nil
}
return diag.Errorf("Failed to delete endpoints because: %s", err)
}
log.Printf("[INFO] EndpointSlice %s deleted", name)
d.SetId("")

return nil
}
190 changes: 190 additions & 0 deletions kubernetes/resource_kubernetes_endpointslice_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package kubernetes

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccKubernetesEndpointSlice_basic(t *testing.T) {
name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "kubernetes_endpoint_slice_v1.test",
IDRefreshIgnore: []string{"metadata.0.resource_version"},
ProviderFactories: testAccProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccKubernetesEndpointSliceConfig_basic(name),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "metadata.0.name", name),
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.uid"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.#", "1"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.condition.#", "1"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.addresses.#", "1"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.addresses.0", "129.144.50.56"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.#", "1"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.0.port", "90"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.0.name", "first"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.0.app_protocol", "test"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "address_type", "IPv4"),
),
},
{
Config: testAccKubernetesEndpointSliceConfig_modified(name),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "metadata.0.name", name),
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.uid"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.#", "1"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.condition.#", "1"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.condition.0.ready", "true"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.hostname", "test"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.node_name", "test"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.addresses.#", "2"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.target_ref.0.name", "test"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.addresses.0", "2001:db8:3333:4444:5555:6666:7777:8888"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.addresses.1", "2002:db8:3333:4444:5555:6666:7777:8888"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.#", "2"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.0.port", "90"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.0.name", "first"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.0.app_protocol", "test"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.1.port", "900"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.1.name", "second"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.1.app_protocol", "test"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "address_type", "IPv6"),
),
},
},
})
}

func TestAccKubernetesEndpointSlice_generatedName(t *testing.T) {
prefix := "tf-acc-test-gen-"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "kubernetes_endpoint_slice_v1.test",
IDRefreshIgnore: []string{"metadata.0.resource_version"},
ProviderFactories: testAccProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccKubernetesEndpointSliceConfig_generatedName(prefix),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.uid"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.#", "1"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.condition.#", "1"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.addresses.#", "1"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "endpoint.0.addresses.0", "129.144.50.56"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.#", "1"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.0.port", "90"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.0.name", "first"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "port.0.app_protocol", "test"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "address_type", "IPv4"),
resource.TestCheckResourceAttr("kubernetes_endpoint_slice_v1.test", "metadata.0.generate_name", prefix),
resource.TestMatchResourceAttr("kubernetes_endpoint_slice_v1.test", "metadata.0.name", regexp.MustCompile("^"+prefix)),
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.generation"),
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.resource_version"),
resource.TestCheckResourceAttrSet("kubernetes_endpoint_slice_v1.test", "metadata.0.uid"),
),
},
},
})
}

func testAccKubernetesEndpointSliceConfig_basic(name string) string {
return fmt.Sprintf(`resource "kubernetes_endpoint_slice_v1" "test" {
metadata {
name = "%s"
}
endpoint {
condition {
}
addresses = ["129.144.50.56"]
}
port {
port = "90"
name = "first"
app_protocol = "test"
}
address_type = "IPv4"
}
`, name)
}

func testAccKubernetesEndpointSliceConfig_modified(name string) string {
return fmt.Sprintf(`resource "kubernetes_endpoint_slice_v1" "test" {
metadata {
name = "%s"
}
endpoint {
condition {
ready = true
}
target_ref {
name = "test"
}
addresses = ["2001:db8:3333:4444:5555:6666:7777:8888", "2002:db8:3333:4444:5555:6666:7777:8888"]
hostname = "test"
node_name = "test"
zone = "us-west"
}
port {
port = "90"
name = "first"
app_protocol = "test"
}
port {
port = "900"
name = "second"
app_protocol = "test"
}
address_type = "IPv6"
}
`, name)
}

func testAccKubernetesEndpointSliceConfig_generatedName(prefix string) string {
return fmt.Sprintf(`resource "kubernetes_endpoint_slice_v1" "test" {
metadata {
generate_name = "%s"
}
endpoint {
condition {
}
addresses = ["129.144.50.56"]
}
port {
port = "90"
name = "first"
app_protocol = "test"
}
address_type = "IPv4"
}
`, prefix)
}
Loading

0 comments on commit e9fab70

Please sign in to comment.