From c433295aeb54c9046cf7e4ab5d43a10dc633ae38 Mon Sep 17 00:00:00 2001 From: Martin Proffitt Date: Mon, 27 May 2024 12:37:46 +0200 Subject: [PATCH] Add functionality to parse multiple prefixes This change introduces a new function `multiprefixloop` as a convenience function for splitting multiple cidr prefixes govorned by the number of bits and required offset inside each prefix. This function might be used in instances such as dividing additional VPC cidrs into subnets without requiring multiple instances of the funtion to be declared in a given composition. Additionally this contributes towards issue #2 by introducing happy path unit tests for each function. --- Makefile | 4 + README.md | 101 +++-- apis/composition.yaml | 4 +- apis/definition.yaml | 12 + examples/xr-cidrhost.yaml | 1 - examples/xr-cidrnetmask.yaml | 1 - examples/xr-cidrsubnet.yaml | 1 - examples/xr-multicidrsubnetsloop.yaml | 20 + fn.go | 110 +++-- fn_test.go | 380 +++++++++++++++++- input/v1beta1/parameters.go | 141 ++++++- input/v1beta1/zz_generated.deepcopy.go | 27 ++ .../cidr.fn.crossplane.io_parameters.yaml | 153 +++++-- validate.go | 58 ++- 14 files changed, 887 insertions(+), 126 deletions(-) create mode 100644 examples/xr-multicidrsubnetsloop.yaml diff --git a/Makefile b/Makefile index ebc0c8c..c30b35b 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,12 @@ render: @for file in examples/xr-*.yaml; do \ + echo ""; \ echo "Rendering $$file..."; \ crossplane beta render \ "$$file" \ apis/composition.yaml \ examples/functions.yaml; \ done + +debug: + go run . --insecure --debug \ No newline at end of file diff --git a/README.md b/README.md index b3ef38d..5e1c5e3 100644 --- a/README.md +++ b/README.md @@ -5,98 +5,119 @@ A [Crossplane](https://www.crossplane.io/) for calculating Classless Inter-Domain Routing ([CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing)) numbers. + A CIDR is an IP address allocation method that is used to improve data routing efficiency on the internet. ## Overview -This composition function offers 4 HashiCorp compatible -IP Network Functions plus one custom wrapper. Follow the -function links for detailed explanations of the function -semantics. +This composition function offers 4 HashiCorp compatible IP Network Functions +plus two custom wrappers. Follow the function links for detailed explanations of +the function semantics. + - [cidrhost](https://developer.hashicorp.com/terraform/language/functions/cidrhost) - [cidrnetmask](https://developer.hashicorp.com/terraform/language/functions/cidrnetmask) - [cidrsubnet](https://developer.hashicorp.com/terraform/language/functions/cidrsubnet) - [cidrsubnets](https://developer.hashicorp.com/terraform/language/functions/cidrsubnets) - cidrsubnetloop wraps [cidrsubnet](https://developer.hashicorp.com/terraform/language/functions/cidrsubnet) +- multiprefixloop wraps [cidrsubnets](https://developer.hashicorp.com/terraform/language/functions/cidrsubnets) To use this function, apply the following [functions.yaml](examples/functions.yaml) to your Crossplane management cluster. -``` + +```bash cat < 0 { - prefix, err = oxr.Resource.GetString(input.PrefixField) + cidrFunc := input.CidrFunc + if len(input.CidrFuncField) > 0 { + cidrFunc, err = oxr.Resource.GetString(input.CidrFuncField) if err != nil { - response.Fatal(rsp, errors.Wrapf(err, "cannot get prefix from field %s for %s", input.PrefixField, oxr.Resource.GetKind())) + response.Fatal(rsp, errors.Wrapf(err, "cannot get cidrFunc from field %s for %s", input.CidrFunc, oxr.Resource.GetKind())) return rsp, nil } } + log.Info("Running function", "cidrFunc", cidrFunc) - cidrFunc := input.CidrFunc - if len(input.CidrFunc) > 0 { - cidrFunc, err = oxr.Resource.GetString(input.CidrFunc) + var prefix string = input.Prefix + if cidrFunc != "multiprefixloop" && len(input.PrefixField) > 0 { + prefix, err = oxr.Resource.GetString(input.PrefixField) if err != nil { - response.Fatal(rsp, errors.Wrapf(err, "cannot get cidrFunc from field %s for %s", input.CidrFunc, oxr.Resource.GetKind())) + response.Fatal(rsp, errors.Wrapf(err, "cannot get prefix from field %s for %s", input.PrefixField, oxr.Resource.GetKind())) return rsp, nil } } + var field string = input.OutputField + if field == "" { + field = "status.atFunction.cidr" + } + switch cidrFunc { // cidrhost calculates the host CIDR from a prefix and a host number. // https://developer.hashicorp.com/terraform/language/functions/cidrhost @@ -99,11 +98,6 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ return rsp, nil } - field, err := oxr.Resource.GetString(input.OutputField) - if err != nil { - field = "status.atFunction.cidr.host" - } - err = dxr.Resource.SetString(field, host) if err != nil { response.Fatal(rsp, errors.Wrapf(err, "cannot set field %s to %s for %s", field, host, oxr.Resource.GetKind())) @@ -118,10 +112,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ response.Fatal(rsp, errors.Wrapf(err, "cannot calculate CIDR netmask for %s", oxr.Resource.GetKind())) return rsp, nil } - field, err := oxr.Resource.GetString(input.OutputField) - if err != nil { - field = "status.atFunction.cidr.netmask" - } + err = dxr.Resource.SetString(field, netmask) if err != nil { response.Fatal(rsp, errors.Wrapf(err, "cannot set field %s to %s for %s", field, netmask, oxr.Resource.GetKind())) @@ -154,10 +145,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ response.Fatal(rsp, errors.Wrapf(err, "cannot calculate subnet CIDR for %s", oxr.Resource.GetKind())) return rsp, nil } - field, err := oxr.Resource.GetString(input.OutputField) - if err != nil { - field = "status.atFunction.cidr.subnet" - } + err = dxr.Resource.SetString(field, string(cidr)) if err != nil { response.Fatal(rsp, errors.Wrapf(err, "cannot set field %s to %s for %s", field, string(cidr), oxr.Resource.GetKind())) @@ -187,10 +175,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ for _, cidr := range cidrs { cidrSubnetsStringArray = append(cidrSubnetsStringArray, string(cidr)) } - field, err := oxr.Resource.GetString(input.OutputField) - if err != nil { - field = "status.atFunction.cidr.subnets" - } + err = dxr.Resource.SetValue(field, cidrSubnetsStringArray) if err != nil { response.Fatal(rsp, errors.Wrapf(err, "cannot set field %s to %s for %s", field, cidrSubnetsStringArray, oxr.Resource.GetKind())) @@ -244,6 +229,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ return rsp, nil } } + for netNum = 0; netNum < netNumCount; netNum++ { cidr, cidrSubnetErr := CidrSubnet(prefix, newBits[0], netNum+offset) if cidrSubnetErr != nil { @@ -252,16 +238,66 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ } cidrSubnetLoopStringArray = append(cidrSubnetLoopStringArray, string(cidr)) } - field, err := oxr.Resource.GetString(input.OutputField) - if err != nil { - field = "status.atFunction.cidr.subnets" - } + err = dxr.Resource.SetValue(field, cidrSubnetLoopStringArray) if err != nil { response.Fatal(rsp, errors.Wrapf(err, "cannot set field %s to %s for %s", field, cidrSubnetLoopStringArray, oxr.Resource.GetKind())) return rsp, nil } + // multiprefix is a convenience wrapper around cidrsubnets that loops over a + // range of prefixes to create a list of subnets for each prefix. + case "multiprefixloop": + var subnetsByCidr map[string][]string = make(map[string][]string) + var multiPrefixes []v1beta1.MultiPrefix + + multiPrefixes = input.MultiPrefix + if len(input.MultiPrefixField) > 0 { + err = oxr.Resource.GetValueInto(input.MultiPrefixField, &multiPrefixes) + if err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot get multiprefix from field %s for %s", input.MultiPrefixField, oxr.Resource.GetKind())) + return rsp, nil + } + } + + for _, multiPrefix := range multiPrefixes { + prefix := multiPrefix.Prefix + if len(prefix) == 0 { + continue + } + + newBits := multiPrefix.NewBits + if len(newBits) == 0 { + continue + } + + if multiPrefix.Offset > 0 { + newBits = append([]int{multiPrefix.Offset}, newBits...) + } + + cidrs, err := CidrSubnets(prefix, newBits...) + if err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot calculate Subnet CIDRs for %s", oxr.Resource.GetKind())) + return rsp, nil + } + + var cidrSubnetsStringArray []string + for _, cidr := range cidrs { + cidrSubnetsStringArray = append(cidrSubnetsStringArray, string(cidr)) + } + + subnetsByCidr[prefix] = cidrSubnetsStringArray + if multiPrefix.Offset > 0 { + subnetsByCidr[prefix] = cidrSubnetsStringArray[1:] + } + } + + err = dxr.Resource.SetValue(field, subnetsByCidr) + if err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot set field %s to %s for %s", field, subnetsByCidr, oxr.Resource.GetKind())) + return rsp, nil + } + default: log.Info("internal error: sub function not supported: ", "cidrFunc", input.CidrFunc) } diff --git a/fn_test.go b/fn_test.go index 7fb969d..081d165 100644 --- a/fn_test.go +++ b/fn_test.go @@ -7,8 +7,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/structpb" "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/function-sdk-go/resource" fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" ) @@ -27,7 +30,382 @@ func TestRunFunction(t *testing.T) { reason string args args want want - }{} + }{ + "cidr-host": { + reason: "should return the CIDR host of the request", + args: args{ + ctx: context.Background(), + req: &fnv1beta1.RunFunctionRequest{ + Input: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "cidrFunc": { + Kind: &structpb.Value_StringValue{ + StringValue: "cidrhost", + }, + }, + "prefix": { + Kind: &structpb.Value_StringValue{ + StringValue: "127.0.0.0/24", + }, + }, + "hostNum": { + Kind: &structpb.Value_NumberValue{ + NumberValue: 111, + }, + }, + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"","kind":"","status": {"atFunction": {"cidr": "127.0.0.111"}}}`), + }, + }, + Meta: &fnv1beta1.ResponseMeta{ + Ttl: &durationpb.Duration{ + Seconds: 60, + }, + }, + }, + err: nil, + }, + }, + + "cidr-subnet": { + reason: "should return the cidr subnet of the request", + args: args{ + ctx: context.Background(), + req: &fnv1beta1.RunFunctionRequest{ + Input: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "cidrFunc": { + Kind: &structpb.Value_StringValue{ + StringValue: "cidrsubnet", + }, + }, + "prefix": { + Kind: &structpb.Value_StringValue{ + StringValue: "127.0.0.0/24", + }, + }, + "newBits": { + Kind: &structpb.Value_ListValue{ + ListValue: &structpb.ListValue{ + Values: []*structpb.Value{ + { + Kind: &structpb.Value_NumberValue{ + NumberValue: 8, + }, + }, + }, + }, + }, + }, + "netNum": { + Kind: &structpb.Value_NumberValue{ + NumberValue: 3, + }, + }, + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"","kind":"","status": {"atFunction": {"cidr": "127.0.0.3/32"}}}`), + }, + }, + Meta: &fnv1beta1.ResponseMeta{ + Ttl: &durationpb.Duration{ + Seconds: 60, + }, + }, + }, + err: nil, + }, + }, + "cidr-netmask": { + reason: "should return the CIDR netmask of the request", + args: args{ + ctx: context.Background(), + req: &fnv1beta1.RunFunctionRequest{ + Input: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "cidrFunc": { + Kind: &structpb.Value_StringValue{ + StringValue: "cidrnetmask", + }, + }, + "prefix": { + Kind: &structpb.Value_StringValue{ + StringValue: "127.0.0.0/24", + }, + }, + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"","kind":"","status": {"atFunction": {"cidr": "255.255.255.0"}}}`), + }, + }, + Meta: &fnv1beta1.ResponseMeta{ + Ttl: &durationpb.Duration{ + Seconds: 60, + }, + }, + }, + err: nil, + }, + }, + "cidr-subnets": { + reason: "should return the cidr subnet of the request", + args: args{ + ctx: context.Background(), + req: &fnv1beta1.RunFunctionRequest{ + Input: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "cidrFunc": { + Kind: &structpb.Value_StringValue{ + StringValue: "cidrsubnets", + }, + }, + "prefix": { + Kind: &structpb.Value_StringValue{ + StringValue: "127.0.0.0/24", + }, + }, + "newBits": { + Kind: &structpb.Value_ListValue{ + ListValue: &structpb.ListValue{ + Values: []*structpb.Value{ + { + Kind: &structpb.Value_NumberValue{ + NumberValue: 8, + }, + }, + { + Kind: &structpb.Value_NumberValue{ + NumberValue: 4, + }, + }, + { + Kind: &structpb.Value_NumberValue{ + NumberValue: 2, + }, + }, + }, + }, + }, + }, + "netNum": { + Kind: &structpb.Value_NumberValue{ + NumberValue: 3, + }, + }, + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"","kind":"","status": {"atFunction": {"cidr": ["127.0.0.0/32", "127.0.0.16/28", "127.0.0.64/26"]}}}`), + }, + }, + Meta: &fnv1beta1.ResponseMeta{ + Ttl: &durationpb.Duration{ + Seconds: 60, + }, + }, + }, + err: nil, + }, + }, + "cidr-subnetloop": { + reason: "should return the cidr subnet of the request", + args: args{ + ctx: context.Background(), + req: &fnv1beta1.RunFunctionRequest{ + Input: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "cidrFunc": { + Kind: &structpb.Value_StringValue{ + StringValue: "cidrsubnetloop", + }, + }, + "prefix": { + Kind: &structpb.Value_StringValue{ + StringValue: "10.0.0.0/24", + }, + }, + "newBits": { + Kind: &structpb.Value_ListValue{ + ListValue: &structpb.ListValue{ + Values: []*structpb.Value{ + { + Kind: &structpb.Value_NumberValue{ + NumberValue: 8, + }, + }, + }, + }, + }, + }, + "netNumCount": { + Kind: &structpb.Value_NumberValue{ + NumberValue: 3, + }, + }, + "offset": { + Kind: &structpb.Value_NumberValue{ + NumberValue: 48, + }, + }, + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"","kind":"","status": {"atFunction": {"cidr": ["10.0.0.48/32", "10.0.0.49/32", "10.0.0.50/32"]}}}`), + }, + }, + Meta: &fnv1beta1.ResponseMeta{ + Ttl: &durationpb.Duration{ + Seconds: 60, + }, + }, + }, + err: nil, + }, + }, + "multi-prefix-loop": { + reason: "should return multiple cidr subnets for the request", + args: args{ + ctx: context.Background(), + req: &fnv1beta1.RunFunctionRequest{ + Input: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "cidrFunc": { + Kind: &structpb.Value_StringValue{ + StringValue: "multiprefixloop", + }, + }, + "multiPrefix": { + Kind: &structpb.Value_ListValue{ + ListValue: &structpb.ListValue{ + Values: []*structpb.Value{ + { + Kind: &structpb.Value_StructValue{ + StructValue: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "prefix": { + Kind: &structpb.Value_StringValue{ + StringValue: "10.10.0.0/24", + }, + }, + "newBits": { + Kind: &structpb.Value_ListValue{ + ListValue: &structpb.ListValue{ + Values: []*structpb.Value{ + { + Kind: &structpb.Value_NumberValue{ + NumberValue: 8, + }, + }, + { + Kind: &structpb.Value_NumberValue{ + NumberValue: 4, + }, + }, + { + Kind: &structpb.Value_NumberValue{ + NumberValue: 2, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Kind: &structpb.Value_StructValue{ + StructValue: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "prefix": { + Kind: &structpb.Value_StringValue{ + StringValue: "10.12.0.0/24", + }, + }, + "newBits": { + Kind: &structpb.Value_ListValue{ + ListValue: &structpb.ListValue{ + Values: []*structpb.Value{ + { + Kind: &structpb.Value_NumberValue{ + NumberValue: 4, + }, + }, + { + Kind: &structpb.Value_NumberValue{ + NumberValue: 4, + }, + }, + { + Kind: &structpb.Value_NumberValue{ + NumberValue: 4, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"","kind":"","status": {"atFunction": ` + + `{"cidr": {"10.10.0.0/24": ["10.10.0.0/32", "10.10.0.16/28", "10.10.0.64/26"],` + + `"10.12.0.0/24": ["10.12.0.0/28", "10.12.0.16/28", "10.12.0.32/28"]}}}}`), + }, + }, + Meta: &fnv1beta1.ResponseMeta{ + Ttl: &durationpb.Duration{ + Seconds: 60, + }, + }, + }, + err: nil, + }, + }, + } for name, tc := range cases { t.Run(name, func(t *testing.T) { diff --git a/input/v1beta1/parameters.go b/input/v1beta1/parameters.go index 28103e0..6f48383 100644 --- a/input/v1beta1/parameters.go +++ b/input/v1beta1/parameters.go @@ -11,7 +11,40 @@ import ( // This isn't a custom resource, in the sense that we never install its CRD. // It is a KRM-like object, so we generate a CRD to describe its schema. +// MultiPrefix defines an item in a list of CIDR blocks to NewBits mappings +type MultiPrefix struct { + // Prefix is a CIDR block that is used as input for CIDR calculations + // + // +required + // +kubebuilder:validation:Pattern="^([0-9]{1,3}.){3}[0-9]{1,3}/[0-9]{1,2}$" + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Required + Prefix string `json:"prefix"` + + // NewBits is a list of bits to allocate to the subnet + // + // +required + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:Required + // +listType=atomic + NewBits []int `json:"newBits"` + + // Offset is the number of bits to offset the subnet mask by when generating + // subnets. + // + // +optional + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=32 + // +kubebuilder:default=0 + Offset int `json:"offset,omitempty"` +} + // Parameters can be used to provide input to this Function. +// +// Almost all parameters can be provided as literals or as references to +// fields on the claim, allowing defaults to be set in the composition and then +// overridden by the claim. +// // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:resource:categories=crossplane @@ -19,51 +52,127 @@ type Parameters struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // cidrfunc is one of cidrhost, cidrnetmast, cidesubnet, cidrsubnets, cidrsubnetloop + // cidrFunc is the name of the function to call + // + // +optional + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Enum={cidrhost,cidrnetmask,cidrsubnet,cidrsubnets,cidrsubnetloop,multiprefixloop} CidrFunc string `json:"cidrFunc"` - // prefix field + // cidrFuncField is a reference to a location on the claim specifying the + // cidrFunc to call + // + // +optional + // +kubebuilder:validation:Type=string + CidrFuncField string `json:"cidrFuncField,omitempty"` + + // multiPrefix is a list of CIDR blocks to NewBits mappings that are used as + // input for the `multiprefixloop` function. + // + // +optional + MultiPrefix []MultiPrefix `json:"multiPrefix,omitempty"` + + // multiPrefixField describes a location on the claim that contains the + // multiPrefix to use as input for the `multiprefixloop` function. + // + // The location referenced should contain a list of MultiPrefix objects. + // + // +optional + MultiPrefixField string `json:"multiPrefixField,omitempty"` + + // prefixField defines a location on the claim to take the prefix from + // + // +optional PrefixField string `json:"prefixField,omitempty"` // prefix is a CIDR block that is used as input for CIDR calculations - Prefix string `json:"prefix"` + // + // +optional + Prefix string `json:"prefix,omitempty"` - // hostnum field + // hostNumField points to a field on the claim that contains the hostNum + // + // +optional HostNumField string `json:"hostNumField,omitempty"` - // hostnum + // hostNum is a whole number that can be represented as a binary integer + // with no more than the number of digits remaining in the address after + // the given prefix. + // + // +optional HostNum int `json:"hostNum,omitempty"` - // newbits field + // newbitsField points to a field on the claim that contains the newBits + // + // +optional NewBitsField string `json:"newBitsField,omitempty"` - // newbits + // newbits is the number of additional bits with which to extend the prefix. + // For example, if given a prefix ending in /16 and a newbits value of 4, + // the resulting subnet address will have length /20. + // + // +optional NewBits []int `json:"newBits,omitempty"` - // netnum field + // netNumField points to a field on the claim that contains the netNum + // + // +optional NetNumField string `json:"netNumField,omitempty"` - // netnum + // netNum is a whole number that can be represented as a binary integer with + // no more than newbits binary digits, which will be used to populate the + // additional bits added to the prefix. + // + // +optional NetNum int64 `json:"netNum,omitempty"` - // netnumcount field + // netNumCountField points to a field on the claim that contains the + // netNumCount + // + // +optional NetNumCountField string `json:"netNumCountField,omitempty"` - // netnumcount + // netNumCount defines how many networks to create from the given prefix + // + // +optional NetNumCount int64 `json:"netNumCount,omitempty"` - // netnumitems field + // netNumItemsField points to a field on the claim that contains the + // netNumItems + // + // +optional NetNumItemsField string `json:"netNumItemsField,omitempty"` - // netnumitems + // netNumItems is an array of items whose length may be used to determine + // how many networks to create from the given prefix. + // + // When this field is defined, its length is compared against `netNumCount` + // and the larger of the two values is used. + // + // +optional NetNumItems []string `json:"netNumItems,omitempty"` - // offset field + // offsetField defines a location on the claim to take the offset from + // + // This field is mutually exclusive with netNumCount and netNumItems + // + // +optional OffsetField string `json:"offsetField,omitempty"` - // offset is only used by cidrsubnetloop + // offset defines a starting point in the cidr block to start allocating + // subnets from. If 0, will start from the beginning of the prefix. + // + // This field is mutually exclusive with netNumCount and netNumItems + // + // +optional Offset int `json:"offset,omitempty"` - // output field + // outputField specifies a location on the XR to patch the results of the + // function call to. + // + // If this field is not specified, the results will be patched to the status + // field `status.atFunction.cidr`. + // + // +optional OutputField string `json:"outputField,omitempty"` } diff --git a/input/v1beta1/zz_generated.deepcopy.go b/input/v1beta1/zz_generated.deepcopy.go index 6d8e5d9..3f8565e 100644 --- a/input/v1beta1/zz_generated.deepcopy.go +++ b/input/v1beta1/zz_generated.deepcopy.go @@ -8,11 +8,38 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MultiPrefix) DeepCopyInto(out *MultiPrefix) { + *out = *in + if in.NewBits != nil { + in, out := &in.NewBits, &out.NewBits + *out = make([]int, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MultiPrefix. +func (in *MultiPrefix) DeepCopy() *MultiPrefix { + if in == nil { + return nil + } + out := new(MultiPrefix) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Parameters) DeepCopyInto(out *Parameters) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.MultiPrefix != nil { + in, out := &in.MultiPrefix, &out.MultiPrefix + *out = make([]MultiPrefix, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.NewBits != nil { in, out := &in.NewBits, &out.NewBits *out = make([]int, len(*in)) diff --git a/package/input/cidr.fn.crossplane.io_parameters.yaml b/package/input/cidr.fn.crossplane.io_parameters.yaml index 5dcdb77..fa43cf8 100644 --- a/package/input/cidr.fn.crossplane.io_parameters.yaml +++ b/package/input/cidr.fn.crossplane.io_parameters.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.13.0 + controller-gen.kubebuilder.io/version: v0.14.0 name: parameters.cidr.fn.crossplane.io spec: group: cidr.fn.crossplane.io @@ -19,78 +19,177 @@ spec: - name: v1beta1 schema: openAPIV3Schema: - description: Parameters can be used to provide input to this Function. + description: |- + Parameters can be used to provide input to this Function. + + + Almost all parameters can be provided as literals or as references to + fields on the claim, allowing defaults to be set in the composition and then + overridden by the claim. properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string cidrFunc: - description: cidrfunc is one of cidrhost, cidrnetmast, cidesubnet, cidrsubnets, - cidrsubnetloop + description: cidrFunc is the name of the function to call + enum: + - cidrhost + - cidrnetmask + - cidrsubnet + - cidrsubnets + - cidrsubnetloop + - multiprefixloop + type: string + cidrFuncField: + description: |- + cidrFuncField is a reference to a location on the claim specifying the + cidrFunc to call type: string hostNum: - description: hostnum + description: |- + hostNum is a whole number that can be represented as a binary integer + with no more than the number of digits remaining in the address after + the given prefix. type: integer hostNumField: - description: hostnum field + description: hostNumField points to a field on the claim that contains + the hostNum type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object + multiPrefix: + description: |- + multiPrefix is a list of CIDR blocks to NewBits mappings that are used as + input for the `multiprefixloop` function. + items: + description: MultiPrefix defines an item in a list of CIDR blocks to + NewBits mappings + properties: + newBits: + description: NewBits is a list of bits to allocate to the subnet + items: + type: integer + minItems: 1 + type: array + x-kubernetes-list-type: atomic + offset: + default: 0 + description: |- + Offset is the number of bits to offset the subnet mask by when generating + subnets. + maximum: 32 + minimum: 0 + type: integer + prefix: + description: Prefix is a CIDR block that is used as input for CIDR + calculations + pattern: ^([0-9]{1,3}.){3}[0-9]{1,3}/[0-9]{1,2}$ + type: string + required: + - newBits + - prefix + type: object + type: array + multiPrefixField: + description: |- + multiPrefixField describes a location on the claim that contains the + multiPrefix to use as input for the `multiprefixloop` function. + + + The location referenced should contain a list of MultiPrefix objects. + type: string netNum: - description: netnum + description: |- + netNum is a whole number that can be represented as a binary integer with + no more than newbits binary digits, which will be used to populate the + additional bits added to the prefix. format: int64 type: integer netNumCount: - description: netnumcount + description: netNumCount defines how many networks to create from the + given prefix format: int64 type: integer netNumCountField: - description: netnumcount field + description: |- + netNumCountField points to a field on the claim that contains the + netNumCount type: string netNumField: - description: netnum field + description: netNumField points to a field on the claim that contains + the netNum type: string netNumItems: - description: netnumitems + description: |- + netNumItems is an array of items whose length may be used to determine + how many networks to create from the given prefix. + + + When this field is defined, its length is compared against `netNumCount` + and the larger of the two values is used. items: type: string type: array netNumItemsField: - description: netnumitems field + description: |- + netNumItemsField points to a field on the claim that contains the + netNumItems type: string newBits: - description: newbits + description: |- + newbits is the number of additional bits with which to extend the prefix. + For example, if given a prefix ending in /16 and a newbits value of 4, + the resulting subnet address will have length /20. items: type: integer type: array newBitsField: - description: newbits field + description: newbitsField points to a field on the claim that contains + the newBits type: string offset: - description: offset is only used by cidrsubnetloop + description: |- + offset defines a starting point in the cidr block to start allocating + subnets from. If 0, will start from the beginning of the prefix. + + + This field is mutually exclusive with netNumCount and netNumItems type: integer offsetField: - description: offset field + description: |- + offsetField defines a location on the claim to take the offset from + + + This field is mutually exclusive with netNumCount and netNumItems type: string outputField: - description: output field + description: |- + outputField specifies a location on the XR to patch the results of the + function call to. + + + If this field is not specified, the results will be patched to the status + field `status.atFunction.cidr`. type: string prefix: description: prefix is a CIDR block that is used as input for CIDR calculations type: string prefixField: - description: prefix field + description: prefixField defines a location on the claim to take the prefix + from type: string - required: - - cidrFunc - - prefix type: object served: true storage: true diff --git a/validate.go b/validate.go index 2e7765d..5198d2c 100644 --- a/validate.go +++ b/validate.go @@ -127,23 +127,59 @@ func ValidateCidrSubnetloopParameters(p *v1beta1.Parameters) *field.Error { return nil } +func ValidateMultiCidrPrefixParameter(p *v1beta1.Parameters, oxr *resource.Composite) *field.Error { + if len(p.MultiPrefix) > 0 && len(p.MultiPrefixField) > 0 { + return field.Required(field.NewPath("parameters"), "specify only one of multiPrefix or multiPrefixField to avoid ambiguous function input") + } + + if len(p.MultiPrefix) == 0 && p.MultiPrefixField == "" { + return field.Required(field.NewPath("parameters"), "either multiPrefix or multiPrefixField function input is required") + } + + var multiPrefixes []v1beta1.MultiPrefix = p.MultiPrefix + if len(p.MultiPrefix) == 0 { + err := oxr.Resource.GetValueInto(p.MultiPrefixField, &multiPrefixes) + if err != nil { + return field.Required(field.NewPath("parameters"), "cannot get multiPrefixes at multiPrefixField "+p.MultiPrefixField) + } + } + + for _, mp := range multiPrefixes { + _, _, err := net.ParseCIDR(mp.Prefix) + if err != nil { + return field.Required(field.NewPath("parameters"), "invalid CIDR prefix address "+mp.Prefix) + } + + if len(mp.NewBits) == 0 { + return field.Required(field.NewPath("parameters"), "newBits is required for each prefix in multiPrefixField") + } + } + + return nil +} + // ValidateParameters validates the Parameters object. func ValidateParameters(p *v1beta1.Parameters, oxr *resource.Composite) *field.Error { - if p.CidrFunc == "" { - return field.Required(field.NewPath("parameters"), "cidrFunc is required") - } + var cidrFunc string = p.CidrFunc + var err error - fieldError := ValidatePrefixParameter(p.Prefix, p.PrefixField, oxr) - if fieldError != nil { - return fieldError + if p.CidrFuncField != "" { + cidrFunc, err = oxr.Resource.GetString(p.CidrFuncField) + if err != nil { + return field.Required(field.NewPath("parameters"), "cannot get cidrFunc at cidrFuncField "+p.CidrFuncField) + } } - cidrFunc, err := oxr.Resource.GetString(p.CidrFunc) - if err != nil { - return field.Required(field.NewPath("parameters"), "cidrFunc is required") + if cidrFunc != "multiprefixloop" { + fieldError := ValidatePrefixParameter(p.Prefix, p.PrefixField, oxr) + if fieldError != nil { + return fieldError + } } switch cidrFunc { + case "": + return field.Required(field.NewPath("parameters"), "cidrFunc is required") case "cidrhost": return ValidateCidrHostParameters(p, *oxr) case "cidrnetmask": @@ -154,7 +190,9 @@ func ValidateParameters(p *v1beta1.Parameters, oxr *resource.Composite) *field.E return ValidateCidrSubnetsParameters(p, *oxr) case "cidrsubnetloop": return ValidateCidrSubnetloopParameters(p) + case "multiprefixloop": + return ValidateMultiCidrPrefixParameter(p, oxr) default: - return field.Required(field.NewPath("parameters"), "unexpected cidrFunc "+p.CidrFunc) + return field.Required(field.NewPath("parameters"), "unexpected cidrFunc "+cidrFunc) } }