Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions coreweave/networking/resource_vpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
networkingv1beta1 "buf.build/gen/go/coreweave/networking/protocolbuffers/go/coreweave/networking/v1beta1"
"connectrpc.com/connect"
"github.com/coreweave/terraform-provider-coreweave/coreweave"
"github.com/coreweave/terraform-provider-coreweave/internal/coretf"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/hashicorp/terraform-plugin-framework-nettypes/cidrtypes"
"github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator"
Expand Down Expand Up @@ -481,6 +482,9 @@ func (r *VpcResource) Schema(ctx context.Context, req resource.SchemaRequest, re
"vpc_prefixes": schema.SetNestedAttribute{
Optional: true,
MarkdownDescription: "A list of additional prefixes associated with the VPC. For example, CKS clusters use these prefixes for Pod and service CIDR ranges.",
Validators: []validator.Set{
coretf.ProtoSetValidator[VpcPrefixResourceModel](),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Expand Down Expand Up @@ -508,6 +512,7 @@ func (r *VpcResource) Schema(ctx context.Context, req resource.SchemaRequest, re
Computed: true,
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
coretf.ProtoSetValidator[HostPrefixResourceModel](),
},
PlanModifiers: []planmodifier.Set{
setplanmodifier.RequiresReplaceIfConfigured(),
Expand Down Expand Up @@ -551,6 +556,9 @@ func (r *VpcResource) Schema(ctx context.Context, req resource.SchemaRequest, re
Optional: true,
Computed: true,
MarkdownDescription: "Settings affecting traffic entering the VPC.",
Validators: []validator.Object{
coretf.ProtoValidator[VpcIngressResourceModel](),
},
Attributes: map[string]schema.Attribute{
"disable_public_services": schema.BoolAttribute{
Optional: true,
Expand All @@ -567,6 +575,9 @@ func (r *VpcResource) Schema(ctx context.Context, req resource.SchemaRequest, re
Optional: true,
Computed: true,
MarkdownDescription: "Settings affecting traffic leaving the VPC.",
Validators: []validator.Object{
coretf.ProtoValidator[VpcEgressResourceModel](),
},
Attributes: map[string]schema.Attribute{
"disable_public_access": schema.BoolAttribute{
Optional: true,
Expand Down
282 changes: 282 additions & 0 deletions coreweave/networking/resource_vpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1065,3 +1065,285 @@ resource "coreweave_networking_vpc" "test_vpc" {
assert.Equal(t, expected, networking.MustRenderVpcResource(ctx, resourceName, m))
})
}

// TestVpcValidation tests validation logic without making actual API calls.
// These tests use PlanOnly mode to verify validation behavior.
func TestVpcValidation(t *testing.T) {
t.Parallel()

t.Run("primary host prefix with IPAM should fail", func(t *testing.T) {
randomInt := rand.IntN(100)
resourceName := fmt.Sprintf("test_validation_primary_ipam_%x", randomInt)

invalidModel := &networking.VpcResourceModel{
Name: types.StringValue(fmt.Sprintf("test-validation-primary-ipam-%x", randomInt)),
Zone: types.StringValue("US-LAB-01A"),
HostPrefixes: hostPrefixesToSet(t, []networking.HostPrefixResourceModel{
{
Name: types.StringValue("primary-with-ipam"),
Type: types.StringValue(networkingv1beta1.HostPrefix_PRIMARY.String()),
Prefixes: []cidrtypes.IPPrefix{cidrtypes.NewIPPrefixValue("172.16.0.0/12")},
IPAM: &networking.IPAMPolicyResourceModel{
PrefixLength: types.Int32Value(64),
},
},
}),
}

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: provider.TestProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: networking.MustRenderVpcResource(t.Context(), resourceName, invalidModel),
PlanOnly: true,
ExpectError: regexp.MustCompile(`(?i)host prefix ipam must not be set`),
},
},
})
})

t.Run("attached host prefix without IPAM should fail", func(t *testing.T) {
randomInt := rand.IntN(100)
resourceName := fmt.Sprintf("test_validation_attached_no_ipam_%x", randomInt)

invalidModel := &networking.VpcResourceModel{
Name: types.StringValue(fmt.Sprintf("test-validation-attached-no-ipam-%x", randomInt)),
Zone: types.StringValue("US-LAB-01A"),
HostPrefixes: hostPrefixesToSet(t, []networking.HostPrefixResourceModel{
fixtureHostPrefixPrimary(),
{
Name: types.StringValue("attached-no-ipam"),
Type: types.StringValue(networkingv1beta1.HostPrefix_ATTACHED.String()),
Prefixes: []cidrtypes.IPPrefix{cidrtypes.NewIPPrefixValue("2601:db8:cccc::/48")},
IPAM: nil, // Missing IPAM
},
}),
}

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: provider.TestProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: networking.MustRenderVpcResource(t.Context(), resourceName, invalidModel),
PlanOnly: true,
ExpectError: regexp.MustCompile(`(?i)(ipam.*required|ipam must be set)`),
},
},
})
})

t.Run("routed host prefix without IPAM should fail", func(t *testing.T) {
randomInt := rand.IntN(100)
resourceName := fmt.Sprintf("test_validation_routed_no_ipam_%x", randomInt)

invalidModel := &networking.VpcResourceModel{
Name: types.StringValue(fmt.Sprintf("test-validation-routed-no-ipam-%x", randomInt)),
Zone: types.StringValue("US-LAB-01A"),
HostPrefixes: hostPrefixesToSet(t, []networking.HostPrefixResourceModel{
fixtureHostPrefixPrimary(),
{
Name: types.StringValue("routed-no-ipam"),
Type: types.StringValue(networkingv1beta1.HostPrefix_ROUTED.String()),
Prefixes: []cidrtypes.IPPrefix{cidrtypes.NewIPPrefixValue("2601:db8:bbbb::/48")},
IPAM: nil, // Missing IPAM
},
}),
}

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: provider.TestProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: networking.MustRenderVpcResource(t.Context(), resourceName, invalidModel),
PlanOnly: true,
ExpectError: regexp.MustCompile(`(?i)(ipam.*required|ipam must be set)`),
},
},
})
})

t.Run("invalid VPC name format should fail", func(t *testing.T) {
randomInt := rand.IntN(100)
resourceName := fmt.Sprintf("test_validation_invalid_name_%x", randomInt)

invalidModel := &networking.VpcResourceModel{
Name: types.StringValue("INVALID_NAME_UPPERCASE"), // Invalid format
Zone: types.StringValue("US-LAB-01A"),
HostPrefixes: hostPrefixesToSet(t, []networking.HostPrefixResourceModel{fixtureHostPrefixPrimary()}),
}

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: provider.TestProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: networking.MustRenderVpcResource(t.Context(), resourceName, invalidModel),
PlanOnly: true,
ExpectError: regexp.MustCompile(`(?i)(does not match regex pattern|invalid.*name)`),
},
},
})
})

t.Run("empty vpc_prefix name should fail", func(t *testing.T) {
randomInt := rand.IntN(100)
resourceName := fmt.Sprintf("test_validation_empty_prefix_name_%x", randomInt)

invalidModel := &networking.VpcResourceModel{
Name: types.StringValue(fmt.Sprintf("test-validation-prefix-%x", randomInt)),
Zone: types.StringValue("US-LAB-01A"),
HostPrefixes: hostPrefixesToSet(t, []networking.HostPrefixResourceModel{fixtureHostPrefixPrimary()}),
VpcPrefixes: []networking.VpcPrefixResourceModel{
{
Name: types.StringValue(""), // Empty name
Value: types.StringValue("10.0.0.0/16"),
},
},
}

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: provider.TestProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: networking.MustRenderVpcResource(t.Context(), resourceName, invalidModel),
PlanOnly: true,
ExpectError: regexp.MustCompile(`(?i)(name.*length must be at least 1|value length must be at least 1 characters)`),
},
},
})
})

t.Run("invalid vpc_prefix CIDR should fail", func(t *testing.T) {
randomInt := rand.IntN(100)
resourceName := fmt.Sprintf("test_validation_invalid_cidr_%x", randomInt)

invalidModel := &networking.VpcResourceModel{
Name: types.StringValue(fmt.Sprintf("test-validation-cidr-%x", randomInt)),
Zone: types.StringValue("US-LAB-01A"),
HostPrefixes: hostPrefixesToSet(t, []networking.HostPrefixResourceModel{fixtureHostPrefixPrimary()}),
VpcPrefixes: []networking.VpcPrefixResourceModel{
{
Name: types.StringValue("invalid-cidr"),
Value: types.StringValue("not-a-valid-cidr"), // Invalid CIDR
},
},
}

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: provider.TestProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: networking.MustRenderVpcResource(t.Context(), resourceName, invalidModel),
PlanOnly: true,
ExpectError: regexp.MustCompile(`(?i)(not a valid IP prefix|invalid.*cidr)`),
},
},
})
})

t.Run("empty vpc_prefix value should fail", func(t *testing.T) {
randomInt := rand.IntN(100)
resourceName := fmt.Sprintf("test_validation_empty_prefix_value_%x", randomInt)

invalidModel := &networking.VpcResourceModel{
Name: types.StringValue(fmt.Sprintf("test-validation-prefix-val-%x", randomInt)),
Zone: types.StringValue("US-LAB-01A"),
HostPrefixes: hostPrefixesToSet(t, []networking.HostPrefixResourceModel{fixtureHostPrefixPrimary()}),
VpcPrefixes: []networking.VpcPrefixResourceModel{
{
Name: types.StringValue("empty-value"),
Value: types.StringValue(""), // Empty value
},
},
}

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: provider.TestProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: networking.MustRenderVpcResource(t.Context(), resourceName, invalidModel),
PlanOnly: true,
ExpectError: regexp.MustCompile(`(?i)(value is empty|not a valid IP prefix)`),
},
},
})
})

t.Run("host_prefix and host_prefixes both set should fail", func(t *testing.T) {
randomInt := rand.IntN(100)
resourceName := fmt.Sprintf("test_validation_both_host_prefix_%x", randomInt)

invalidModel := &networking.VpcResourceModel{
Name: types.StringValue(fmt.Sprintf("test-validation-both-%x", randomInt)),
Zone: types.StringValue("US-LAB-01A"),
HostPrefix: types.StringValue("172.16.0.0/12"), // Both set
HostPrefixes: hostPrefixesToSet(t, []networking.HostPrefixResourceModel{fixtureHostPrefixPrimary()}),
}

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: provider.TestProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: networking.MustRenderVpcResource(t.Context(), resourceName, invalidModel),
PlanOnly: true,
ExpectError: regexp.MustCompile(`(?i)These attributes cannot be configured together`),
},
},
})
})

t.Run("valid VPC with all fields should pass", func(t *testing.T) {
randomInt := rand.IntN(100)
resourceName := fmt.Sprintf("test_validation_valid_full_%x", randomInt)
fullResourceName := fmt.Sprintf("coreweave_networking_vpc.%s", resourceName)

validModel := &networking.VpcResourceModel{
Name: types.StringValue(fmt.Sprintf("test-validation-valid-%x", randomInt)),
Zone: types.StringValue("US-LAB-01A"),
HostPrefixes: hostPrefixesToSet(t, []networking.HostPrefixResourceModel{
fixtureHostPrefixPrimary(),
fixtureHostPrefixAttached(),
fixtureHostPrefixRouted(),
}),
VpcPrefixes: []networking.VpcPrefixResourceModel{
{
Name: types.StringValue("pod-cidr"),
Value: types.StringValue("10.0.0.0/16"),
},
{
Name: types.StringValue("service-cidr"),
Value: types.StringValue("10.16.0.0/16"),
},
},
Ingress: &networking.VpcIngressResourceModel{
DisablePublicServices: types.BoolValue(false),
},
Egress: &networking.VpcEgressResourceModel{
DisablePublicAccess: types.BoolValue(false),
},
Dhcp: &networking.VpcDhcpResourceModel{
Dns: &networking.VpcDhcpDnsResourceModel{
Servers: types.SetValueMust(types.StringType, []attr.Value{
types.StringValue("1.1.1.1"),
types.StringValue("8.8.8.8"),
}),
},
},
}

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: provider.TestProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: networking.MustRenderVpcResource(t.Context(), resourceName, validModel),
PlanOnly: true,
ExpectNonEmptyPlan: true,
ConfigPlanChecks: resource.ConfigPlanChecks{
PostApplyPreRefresh: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(fullResourceName, plancheck.ResourceActionCreate),
},
},
},
},
})
})
}
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
buf.build/gen/go/coreweave/cwobject/protocolbuffers/go v1.36.10-20250604181649-b97f17b05d5b.1
buf.build/gen/go/coreweave/networking/connectrpc/go v1.19.1-20260121155637-a637e7777165.2
buf.build/gen/go/coreweave/networking/protocolbuffers/go v1.36.11-20260121155637-a637e7777165.1
buf.build/go/protovalidate v1.1.0
connectrpc.com/connect v1.19.1
github.com/aws/aws-sdk-go-v2 v1.37.0
github.com/aws/aws-sdk-go-v2/config v1.30.1
Expand All @@ -35,8 +36,10 @@ require (
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect
buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.36.10-20241220201140-4c5ba75caaf8.1 // indirect
cel.dev/expr v0.24.0 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/agext/levenshtein v1.2.2 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.0 // indirect
Expand All @@ -55,6 +58,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/cel-go v0.26.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
Expand All @@ -70,7 +74,6 @@ require (
github.com/hashicorp/terraform-registry-address v0.4.0 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
Expand All @@ -80,10 +83,12 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect
Expand Down
Loading
Loading