Repository: kubernetes-sigs/ingress2gateway
Pull request #54 (2023-08-22 15:51:24)

Add releases workflows
**What type of PR is this?**

**What this PR does / why we need it**:
This PR adds initial build and release workflows using go releaser

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, please enter a release note below:
-->
```release-note
NONE
```

check https://github.com/LiorLieberman/ingress2gateway/releases/tag/v0.1.7 to see how an example release look like



Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 0000000..1cf3ee6
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,34 @@
+name: build
+on:
+  pull_request:
+    branches:
+      - main
+  push:
+    branches:
+      - main
+permissions:
+  contents: read
+jobs:
+  goreleaser:
+    runs-on: ubuntu-latest
+    environment: release
+    steps:
+      - name: Set up Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: '1.18'
+        id: go
+
+      - name: Check out code into the Go module directory
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - name: GoReleaser - Release
+        uses: goreleaser/goreleaser-action@v4
+        with:
+          version: latest
+          args: build --snapshot --clean
+
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000..7d27f3d
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,31 @@
+name: release
+on:
+  push:
+    tags:
+      - "v*.*.*"
+permissions:
+  contents: write
+jobs:
+  goreleaser:
+    runs-on: ubuntu-latest
+    environment: release
+    steps:
+      - name: Set up Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: '1.18'
+        id: go
+
+      - name: Check out code into the Go module directory
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - name: GoReleaser - Release
+        uses: goreleaser/goreleaser-action@v4
+        with:
+          version: latest
+          args: release --clean
+
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
new file mode 100644
index 0000000..47fd4b2
--- /dev/null
+++ b/.goreleaser.yaml
@@ -0,0 +1,56 @@
+# This is an example .goreleaser.yml file with some sensible defaults.
+# Make sure to check the documentation at https://goreleaser.com
+before:
+  hooks:
+    # You may remove this if you don't use go modules.
+    - go mod tidy
+    # you may remove this if you don't need go generate
+    # - go generate ./...
+builds:
+  - env:
+      - CGO_ENABLED=0
+    goos:
+      - linux
+      - darwin
+      - windows
+
+archives:
+  - format: tar.gz
+    # this name template makes the OS and Arch compatible with the results of uname.
+    name_template: >-
+      {{ .ProjectName }}_
+      {{- title .Os }}_
+      {{- if eq .Arch "amd64" }}x86_64
+      {{- else if eq .Arch "386" }}i386
+      {{- else }}{{ .Arch }}{{ end }}
+      {{- if .Arm }}v{{ .Arm }}{{ end }}
+    # use zip for windows archives
+    format_overrides:
+    - goos: windows
+      format: zip
+checksum:
+  name_template: 'checksums.txt'
+snapshot:
+  name_template: "{{ incpatch .Version }}-next"
+release:
+  name_template: "Release-v{{.Version}}"
+  header: |
+    ## Installation
+
+    To install this release, refer to the instructions at https://github.com/kubernetes-sigs/ingress2gateway/blob/v{{.Version}}/README.md
+
+    (ingress2gateway v{{.Version}} was tagged on {{.Date}}.)
+
+changelog:
+  # leaving this github-native option in case we will want to use the GitHub release notes generation API in the future.
+  # use: github-native
+  sort: asc
+  filters:
+    exclude:
+      - '^docs:'
+      - '^test:'
+
+# The lines beneath this are called `modelines`. See `:help modeline`
+# Feel free to remove those if you don't want/use them.
+# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
+# vim: set ts=2 sw=2 tw=0 fo=cnqoj
diff --git a/cmd/print.go b/cmd/print.go
index fde8cf2..a16d941 100644
--- a/cmd/print.go
+++ b/cmd/print.go
@@ -21,15 +21,16 @@ import (
 	"os"
 	"strings"
 
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
 	"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw"
 	"github.com/spf13/cobra"
+	networkingv1 "k8s.io/api/networking/v1"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/cli-runtime/pkg/printers"
 	"k8s.io/client-go/tools/clientcmd"
+	"sigs.k8s.io/controller-runtime/pkg/client/config"
 	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
-
-	// Call init function for the providers
-	_ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/ingressnginx"
 )
 
 type PrintRunner struct {
@@ -57,9 +58,9 @@ type PrintRunner struct {
 }
 
 // PrintGatewaysAndHTTPRoutes performs necessary steps to digest and print
-// converted Gateways and HTTP Routes. The steps include reading from the source,
+// converted Gateways and HTTP Routes. The steps includes reading from the source,
 // construct ingresses, convert them, then print them out.
-func (pr *PrintRunner) PrintGatewaysAndHTTPRoutes(cmd *cobra.Command, _ []string) error {
+func (pr *PrintRunner) PrintGatewaysAndHTTPRoutes(cmd *cobra.Command, args []string) error {
 	err := pr.initializeResourcePrinter()
 	if err != nil {
 		return fmt.Errorf("failed to initialize resrouce printer: %w", err)
@@ -69,9 +70,18 @@ func (pr *PrintRunner) PrintGatewaysAndHTTPRoutes(cmd *cobra.Command, _ []string
 		return fmt.Errorf("failed to initialize namespace filter: %w", err)
 	}
 
-	httpRoutes, gateways, err := i2gw.ToGatewayAPIResources(cmd.Context(), pr.namespaceFilter, pr.inputFile)
+	ingressList, err := getIngessList(pr.namespaceFilter, pr.inputFile)
 	if err != nil {
-		return err
+		return fmt.Errorf("failed to get ingresses from source: %w", err)
+	}
+
+	httpRoutes, gateways, errList := i2gw.Ingresses2GatewaysAndHTTPRoutes(ingressList.Items)
+	if len(errList) > 0 {
+		errMsg := fmt.Errorf("\n# Encountered %d errors", len(errList))
+		for _, err := range errList {
+			errMsg = fmt.Errorf("\n%w # %s", errMsg, err)
+		}
+		return errMsg
 	}
 
 	pr.outputResult(httpRoutes, gateways)
@@ -79,16 +89,42 @@ func (pr *PrintRunner) PrintGatewaysAndHTTPRoutes(cmd *cobra.Command, _ []string
 	return nil
 }
 
-func (pr *PrintRunner) outputResult(httpRoutes []gatewayv1beta1.HTTPRoute, gateways []gatewayv1beta1.Gateway) {
-	if len(httpRoutes)+len(gateways) == 0 {
+func getIngessList(namespaceFilter string, inputFile string) (*networkingv1.IngressList, error) {
+	ingressList := &networkingv1.IngressList{}
+	if inputFile != "" {
+		err := i2gw.ConstructIngressesFromFile(ingressList, inputFile, namespaceFilter)
+		if err != nil {
+			return nil, fmt.Errorf("failed to open input file: %w", err)
+		}
+	} else {
+		conf, err := config.GetConfig()
+		if err != nil {
+			return nil, fmt.Errorf("failed to get client config: %w", err)
+		}
+
+		cl, err := client.New(conf, client.Options{})
+		if err != nil {
+			return nil, fmt.Errorf("failed to create client: %w", err)
+		}
+		cl = client.NewNamespacedClient(cl, namespaceFilter)
+
+		err = i2gw.ConstructIngressesFromCluster(cl, ingressList)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get ingress resources from kubenetes cluster: %w", err)
+		}
+	}
+
+	if len(ingressList.Items) == 0 {
 		msg := "No resources found"
-		if pr.namespaceFilter != "" {
-			msg = fmt.Sprintf("%s in %s namespace", msg, pr.namespaceFilter)
+		if namespaceFilter != "" {
+			return nil, fmt.Errorf("%s in %s namespace", msg, namespaceFilter)
 		}
-		fmt.Println(msg)
-		return
+		return nil, fmt.Errorf(msg)
 	}
+	return ingressList, nil
+}
 
+func (pr *PrintRunner) outputResult(httpRoutes []gatewayv1beta1.HTTPRoute, gateways []gatewayv1beta1.Gateway) {
 	for i := range gateways {
 		err := pr.resourcePrinter.PrintObj(&gateways[i], os.Stdout)
 		if err != nil {
diff --git a/go.mod b/go.mod
index 511d1de..d62bb7a 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
 	k8s.io/apimachinery v0.25.2
 	k8s.io/cli-runtime v0.25.2
 	k8s.io/client-go v0.25.2
+	k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed
 	sigs.k8s.io/controller-runtime v0.13.0
 	sigs.k8s.io/gateway-api v0.5.0
 )
@@ -61,7 +62,6 @@ require (
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/klog/v2 v2.70.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
-	k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
 	sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
 	sigs.k8s.io/kustomize/api v0.12.1 // indirect
 	sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
diff --git a/pkg/i2gw/providers/common/converter.go b/pkg/i2gw/aggregator.go
similarity index 69%
rename from pkg/i2gw/providers/common/converter.go
rename to pkg/i2gw/aggregator.go
index 198350b..4cf3466 100644
--- a/pkg/i2gw/providers/common/converter.go
+++ b/pkg/i2gw/aggregator.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2023 The Kubernetes Authors.
+Copyright 2022 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -14,66 +14,31 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package common
+package i2gw
 
 import (
 	"fmt"
+	"regexp"
+	"strconv"
 	"strings"
 
-	"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw"
 	networkingv1 "k8s.io/api/networking/v1"
 	networkingv1beta1 "k8s.io/api/networking/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/validation/field"
+	"k8s.io/utils/pointer"
 	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 )
 
-// ToGateway converts the received ingresses to i2gw.GatewayResources,
-// without taking into consideration any provider specific logic.
-func ToGateway(ingresses []networkingv1.Ingress) (i2gw.GatewayResources, field.ErrorList) {
-	aggregator := ingressAggregator{ruleGroups: map[ruleGroupKey]*ingressRuleGroup{}}
-
-	var errs field.ErrorList
-	for _, ingress := range ingresses {
-		errs = append(errs, aggregator.addIngress(ingress)...)
-	}
-	if len(errs) > 0 {
-		return i2gw.GatewayResources{}, errs
-	}
-
-	routes, gateways, errs := aggregator.toHTTPRoutesAndGateways()
-	if len(errs) > 0 {
-		return i2gw.GatewayResources{}, errs
-	}
-
-	routeByKey := make(map[types.NamespacedName]gatewayv1beta1.HTTPRoute)
-	for _, route := range routes {
-		key := types.NamespacedName{Namespace: route.Namespace, Name: route.Name}
-		routeByKey[key] = route
-	}
-
-	gatewayByKey := make(map[types.NamespacedName]gatewayv1beta1.Gateway)
-	for _, gateway := range gateways {
-		key := types.NamespacedName{Namespace: gateway.Namespace, Name: gateway.Name}
-		gatewayByKey[key] = gateway
-	}
-
-	return i2gw.GatewayResources{
-		Gateways:   gatewayByKey,
-		HTTPRoutes: routeByKey,
-	}, nil
-}
-
 var (
-	GatewayGVK = schema.GroupVersionKind{
+	gatewayGVK = schema.GroupVersionKind{
 		Group:   "gateway.networking.k8s.io",
 		Version: "v1beta1",
 		Kind:    "Gateway",
 	}
 
-	HTTPRouteGVK = schema.GroupVersionKind{
+	httpRouteGVK = schema.GroupVersionKind{
 		Group:   "gateway.networking.k8s.io",
 		Version: "v1beta1",
 		Kind:    "HTTPRoute",
@@ -98,7 +63,8 @@ type ingressRuleGroup struct {
 }
 
 type ingressRule struct {
-	rule networkingv1.IngressRule
+	rule  networkingv1.IngressRule
+	extra *extra
 }
 
 type ingressDefaultBackend struct {
@@ -113,6 +79,20 @@ type ingressPath struct {
 	pathIdx  int
 	ruleType string
 	path     networkingv1.HTTPIngressPath
+	extra    *extra
+}
+
+type extra struct {
+	canary *canary
+}
+
+type canary struct {
+	enable           bool
+	headerKey        string
+	headerValue      string
+	headerRegexMatch bool
+	weight           int
+	weightTotal      int
 }
 
 func (a *ingressAggregator) addIngress(ingress networkingv1.Ingress) field.ErrorList {
@@ -124,8 +104,12 @@ func (a *ingressAggregator) addIngress(ingress networkingv1.Ingress) field.Error
 	} else {
 		ingressClass = ingress.Name
 	}
+	e, errs := getExtra(ingress)
+	if len(errs) > 0 {
+		return errs
+	}
 	for _, rule := range ingress.Spec.Rules {
-		a.addIngressRule(ingress.Namespace, ingressClass, rule, ingress.Spec)
+		a.addIngressRule(ingress.Namespace, ingressClass, rule, ingress.Spec, e)
 	}
 	if ingress.Spec.DefaultBackend != nil {
 		a.defaultBackends = append(a.defaultBackends, ingressDefaultBackend{
@@ -138,7 +122,7 @@ func (a *ingressAggregator) addIngress(ingress networkingv1.Ingress) field.Error
 	return nil
 }
 
-func (a *ingressAggregator) addIngressRule(namespace, ingressClass string, rule networkingv1.IngressRule, iSpec networkingv1.IngressSpec) {
+func (a *ingressAggregator) addIngressRule(namespace, ingressClass string, rule networkingv1.IngressRule, iSpec networkingv1.IngressSpec, e *extra) {
 	rgKey := ruleGroupKey(fmt.Sprintf("%s/%s/%s", namespace, ingressClass, rule.Host))
 	rg, ok := a.ruleGroups[rgKey]
 	if !ok {
@@ -152,7 +136,7 @@ func (a *ingressAggregator) addIngressRule(namespace, ingressClass string, rule
 	if len(iSpec.TLS) > 0 {
 		rg.tls = append(rg.tls, iSpec.TLS...)
 	}
-	rg.rules = append(rg.rules, ingressRule{rule: rule})
+	rg.rules = append(rg.rules, ingressRule{rule: rule, extra: e})
 }
 
 func (a *ingressAggregator) toHTTPRoutesAndGateways() ([]gatewayv1beta1.HTTPRoute, []gatewayv1beta1.Gateway, field.ErrorList) {
@@ -200,7 +184,7 @@ func (a *ingressAggregator) toHTTPRoutesAndGateways() ([]gatewayv1beta1.HTTPRout
 				},
 			},
 		}
-		httpRoute.SetGroupVersionKind(HTTPRouteGVK)
+		httpRoute.SetGroupVersionKind(httpRouteGVK)
 
 		backendRef, err := toBackendRef(db.backend, field.NewPath(db.name, "paths", "backends").Index(i))
 		if err != nil {
@@ -232,13 +216,13 @@ func (a *ingressAggregator) toHTTPRoutesAndGateways() ([]gatewayv1beta1.HTTPRout
 					GatewayClassName: gatewayv1beta1.ObjectName(parts[1]),
 				},
 			}
-			gateway.SetGroupVersionKind(GatewayGVK)
+			gateway.SetGroupVersionKind(gatewayGVK)
 			gatewaysByKey[gwKey] = gateway
 		}
 		for _, listener := range listeners {
 			var listenerNamePrefix string
 			if listener.Hostname != nil && *listener.Hostname != "" {
-				listenerNamePrefix = fmt.Sprintf("%s-", NameFromHost(string(*listener.Hostname)))
+				listenerNamePrefix = fmt.Sprintf("%s-", nameFromHost(string(*listener.Hostname)))
 			}
 
 			gateway.Spec.Listeners = append(gateway.Spec.Listeners, gatewayv1beta1.Listener{
@@ -273,7 +257,7 @@ func (rg *ingressRuleGroup) toHTTPRoute() (gatewayv1beta1.HTTPRoute, field.Error
 
 	for i, ir := range rg.rules {
 		for j, path := range ir.rule.HTTP.Paths {
-			ip := ingressPath{ruleIdx: i, pathIdx: j, ruleType: "http", path: path}
+			ip := ingressPath{ruleIdx: i, pathIdx: j, ruleType: "http", path: path, extra: ir.extra}
 			pmKey := getPathMatchKey(ip)
 			pathsByMatchGroup[pmKey] = append(pathsByMatchGroup[pmKey], ip)
 		}
@@ -281,7 +265,7 @@ func (rg *ingressRuleGroup) toHTTPRoute() (gatewayv1beta1.HTTPRoute, field.Error
 
 	httpRoute := gatewayv1beta1.HTTPRoute{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      NameFromHost(rg.host),
+			Name:      nameFromHost(rg.host),
 			Namespace: rg.namespace,
 		},
 		Spec: gatewayv1beta1.HTTPRouteSpec{},
@@ -291,7 +275,7 @@ func (rg *ingressRuleGroup) toHTTPRoute() (gatewayv1beta1.HTTPRoute, field.Error
 			},
 		},
 	}
-	httpRoute.SetGroupVersionKind(HTTPRouteGVK)
+	httpRoute.SetGroupVersionKind(httpRouteGVK)
 
 	if rg.ingressClass != "" {
 		httpRoute.Spec.ParentRefs = []gatewayv1beta1.ParentReference{{Name: gatewayv1beta1.ObjectName(rg.ingressClass)}}
@@ -312,7 +296,7 @@ func (rg *ingressRuleGroup) toHTTPRoute() (gatewayv1beta1.HTTPRoute, field.Error
 			Matches: []gatewayv1beta1.HTTPRouteMatch{*match},
 		}
 
-		backendRefs, errs := rg.configureBackendRef(paths)
+		backendRefs, errs := rg.calculateBackendRefWeight(paths)
 		errors = append(errors, errs...)
 		hrRule.BackendRefs = backendRefs
 
@@ -322,18 +306,47 @@ func (rg *ingressRuleGroup) toHTTPRoute() (gatewayv1beta1.HTTPRoute, field.Error
 	return httpRoute, errors
 }
 
-func (rg *ingressRuleGroup) configureBackendRef(paths []ingressPath) ([]gatewayv1beta1.HTTPBackendRef, field.ErrorList) {
+func (rg *ingressRuleGroup) calculateBackendRefWeight(paths []ingressPath) ([]gatewayv1beta1.HTTPBackendRef, field.ErrorList) {
 	var errors field.ErrorList
 	var backendRefs []gatewayv1beta1.HTTPBackendRef
 
+	var numWeightedBackends, totalWeightSet int32
+
+	// This is the default value for nginx annotation nginx.ingress.kubernetes.io/canary-weight-total
+	var weightTotal = 100
+
 	for i, path := range paths {
 		backendRef, err := toBackendRef(path.path.Backend, field.NewPath("paths", "backends").Index(i))
 		if err != nil {
 			errors = append(errors, err)
 			continue
 		}
+		if path.extra != nil && path.extra.canary != nil && path.extra.canary.weight != 0 {
+			weight := int32(path.extra.canary.weight)
+			backendRef.Weight = &weight
+			totalWeightSet += weight
+			numWeightedBackends++
+			if path.extra.canary.weightTotal > 0 {
+				weightTotal = path.extra.canary.weightTotal
+			}
+		}
 		backendRefs = append(backendRefs, gatewayv1beta1.HTTPBackendRef{BackendRef: *backendRef})
 	}
+	if numWeightedBackends > 0 && numWeightedBackends < int32(len(backendRefs)) {
+		weightToSet := (int32(weightTotal) - totalWeightSet) / (int32(len(backendRefs)) - numWeightedBackends)
+		if weightToSet < 0 {
+			weightToSet = 0
+		}
+		for i := range backendRefs {
+			if backendRefs[i].Weight == nil {
+				backendRefs[i].Weight = &weightToSet
+			}
+			if *backendRefs[i].Weight > int32(weightTotal) {
+				backendRefs[i].Weight = pointer.Int32(int32(weightTotal))
+
+			}
+		}
+	}
 
 	return backendRefs, errors
 }
@@ -343,12 +356,18 @@ func getPathMatchKey(ip ingressPath) pathMatchKey {
 	if ip.path.PathType != nil {
 		pathType = string(*ip.path.PathType)
 	}
-	return pathMatchKey(fmt.Sprintf("%s/%s", pathType, ip.path.Path))
+	var canaryHeaderKey string
+	if ip.extra != nil && ip.extra.canary != nil && ip.extra.canary.headerKey != "" {
+		canaryHeaderKey = ip.extra.canary.headerKey
+	}
+	return pathMatchKey(fmt.Sprintf("%s/%s/%s", pathType, ip.path.Path, canaryHeaderKey))
 }
 
 func toHTTPRouteMatch(ip ingressPath, path *field.Path) (*gatewayv1beta1.HTTPRouteMatch, *field.Error) {
 	pmPrefix := gatewayv1beta1.PathMatchPathPrefix
 	pmExact := gatewayv1beta1.PathMatchExact
+	hmExact := gatewayv1beta1.HeaderMatchExact
+	hmRegex := gatewayv1beta1.HeaderMatchRegularExpression
 
 	match := &gatewayv1beta1.HTTPRouteMatch{Path: &gatewayv1beta1.HTTPPathMatch{Value: &ip.path.Path}}
 	//exhaustive:ignore -explicit-exhaustive-switch
@@ -362,6 +381,18 @@ func toHTTPRouteMatch(ip ingressPath, path *field.Path) (*gatewayv1beta1.HTTPRou
 		return nil, field.Invalid(path.Child("pathType"), ip.path.PathType, fmt.Sprintf("unsupported path match type: %s", *ip.path.PathType))
 	}
 
+	if ip.extra != nil && ip.extra.canary != nil && ip.extra.canary.headerKey != "" {
+		headerMatch := gatewayv1beta1.HTTPHeaderMatch{
+			Name:  gatewayv1beta1.HTTPHeaderName(ip.extra.canary.headerKey),
+			Value: ip.extra.canary.headerValue,
+			Type:  &hmExact,
+		}
+		if ip.extra.canary.headerRegexMatch {
+			headerMatch.Type = &hmRegex
+		}
+		match.Headers = []gatewayv1beta1.HTTPHeaderMatch{headerMatch}
+	}
+
 	return match, nil
 }
 
@@ -386,3 +417,54 @@ func toBackendRef(ib networkingv1.IngressBackend, path *field.Path) (*gatewayv1b
 		},
 	}, nil
 }
+
+func nameFromHost(host string) string {
+	// replace all special chars with -
+	reg, _ := regexp.Compile("[^a-zA-Z0-9]+")
+	step1 := reg.ReplaceAllString(host, "-")
+	// remove all - at start of string
+	reg2, _ := regexp.Compile("^[^a-zA-Z0-9]+")
+	step2 := reg2.ReplaceAllString(step1, "")
+	// if nothing left, return "all-hosts"
+	if len(host) == 0 {
+		return "all-hosts"
+	}
+	return step2
+}
+
+func getExtra(ingress networkingv1.Ingress) (*extra, field.ErrorList) {
+	var errs field.ErrorList
+	var err error
+
+	fieldPath := field.NewPath(ingress.Name).Child("metadata").Child("annotations")
+
+	e := &extra{}
+	if c := ingress.Annotations["nginx.ingress.kubernetes.io/canary"]; c == "true" {
+		e.canary = &canary{enable: true}
+		if cHeader := ingress.Annotations["nginx.ingress.kubernetes.io/canary-by-header"]; cHeader != "" {
+			e.canary.headerKey = cHeader
+			e.canary.headerValue = "always"
+		}
+		if cHeaderVal := ingress.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"]; cHeaderVal != "" {
+			e.canary.headerValue = cHeaderVal
+		}
+		if cHeaderRegex := ingress.Annotations["nginx.ingress.kubernetes.io/canary-by-header-pattern"]; cHeaderRegex != "" {
+			e.canary.headerValue = cHeaderRegex
+			e.canary.headerRegexMatch = true
+		}
+		if cHeaderWeight := ingress.Annotations["nginx.ingress.kubernetes.io/canary-weight"]; cHeaderWeight != "" {
+			e.canary.weight, err = strconv.Atoi(cHeaderWeight)
+			if err != nil {
+				errs = append(errs, field.TypeInvalid(fieldPath, "nginx.ingress.kubernetes.io/canary-weight", err.Error()))
+			}
+			e.canary.weightTotal = 100
+		}
+		if cHeaderWeightTotal := ingress.Annotations["nginx.ingress.kubernetes.io/canary-weight-total"]; cHeaderWeightTotal != "" {
+			e.canary.weightTotal, err = strconv.Atoi(cHeaderWeightTotal)
+			if err != nil {
+				errs = append(errs, field.TypeInvalid(fieldPath, "nginx.ingress.kubernetes.io/canary-weight-total", err.Error()))
+			}
+		}
+	}
+	return e, errs
+}
diff --git a/pkg/i2gw/ingress2gateway.go b/pkg/i2gw/ingress2gateway.go
index 44b8921..1453c0d 100644
--- a/pkg/i2gw/ingress2gateway.go
+++ b/pkg/i2gw/ingress2gateway.go
@@ -30,106 +30,29 @@ import (
 	"k8s.io/apimachinery/pkg/util/validation/field"
 	kubeyaml "k8s.io/apimachinery/pkg/util/yaml"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/controller-runtime/pkg/client/config"
 	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 )
 
-func ToGatewayAPIResources(ctx context.Context, namespace string, inputFile string) ([]gatewayv1beta1.HTTPRoute, []gatewayv1beta1.Gateway, error) {
-	conf, err := config.GetConfig()
-	if err != nil {
-		return nil, nil, fmt.Errorf("failed to get client config: %w", err)
-	}
-
-	cl, err := client.New(conf, client.Options{})
-	if err != nil {
-		return nil, nil, fmt.Errorf("failed to create client: %w", err)
-	}
-	cl = client.NewNamespacedClient(cl, namespace)
-
-	var ingresses networkingv1.IngressList
-	var gateways []gatewayv1beta1.Gateway
-	var httpRoutes []gatewayv1beta1.HTTPRoute
-
-	providerByName := constructProviders(&ProviderConf{
-		Client: cl,
-	})
-
-	resources := InputResources{}
-
-	if inputFile != "" {
-		if err = ConstructIngressesFromFile(&ingresses, inputFile, namespace); err != nil {
-			return nil, nil, fmt.Errorf("failed to read ingresses from file: %w", err)
-		}
-		resources.Ingresses = ingresses.Items
-		if err = readProviderResourcesFromFile(ctx, providerByName, &resources, inputFile); err != nil {
-			return nil, nil, err
-		}
-	} else {
-		if err = ConstructIngressesFromCluster(ctx, cl, &ingresses); err != nil {
-			return nil, nil, fmt.Errorf("failed to read ingresses from cluster: %w", err)
-		}
-		resources.Ingresses = ingresses.Items
-		if err = readProviderResourcesFromCluster(ctx, providerByName, &resources); err != nil {
-			return nil, nil, err
-		}
-	}
-
-	var errs field.ErrorList
-	for _, provider := range providerByName {
-		gatewayResources, conversionErrs := provider.ToGatewayAPI(resources)
-		errs = append(errs, conversionErrs...)
-		for _, gateway := range gatewayResources.Gateways {
-			gateways = append(gateways, gateway)
-		}
-		for _, route := range gatewayResources.HTTPRoutes {
-			httpRoutes = append(httpRoutes, route)
-		}
-	}
-	if len(errs) > 0 {
-		return nil, nil, aggregatedErrs(errs)
-	}
-
-	return httpRoutes, gateways, nil
-}
-
-func readProviderResourcesFromFile(ctx context.Context, providerByName map[ProviderName]Provider, resources *InputResources, inputFile string) error {
-	for name, provider := range providerByName {
-		if err := provider.ReadResourcesFromFiles(ctx, resources.CustomResources, inputFile); err != nil {
-			return fmt.Errorf("failed to read %s resources from file: %w", name, err)
-		}
-	}
-	return nil
-}
-
-func readProviderResourcesFromCluster(ctx context.Context, providerByName map[ProviderName]Provider, resources *InputResources) error {
-	for name, provider := range providerByName {
-		if err := provider.ReadResourcesFromCluster(ctx, resources.CustomResources); err != nil {
-			return fmt.Errorf("failed to read %s resources from the cluster: %w", name, err)
-		}
-	}
-	return nil
-}
-
-func ConstructIngressesFromCluster(ctx context.Context, cl client.Client, ingressList *networkingv1.IngressList) error {
-	err := cl.List(ctx, ingressList)
+func ConstructIngressesFromCluster(cl client.Client, ingressList *networkingv1.IngressList) error {
+	err := cl.List(context.Background(), ingressList)
 	if err != nil {
 		return fmt.Errorf("failed to get ingresses from the cluster: %w", err)
 	}
 	return nil
 }
 
-// constructProviders constructs a map of concrete Provider implementations
-// by their ProviderName.
-//
-// TODO (levikobi): Issue #45 - let users filter by provider name.
-func constructProviders(conf *ProviderConf) map[ProviderName]Provider {
-	providerByName := make(map[ProviderName]Provider, len(ProviderConstructorByName))
+func Ingresses2GatewaysAndHTTPRoutes(ingresses []networkingv1.Ingress) ([]gatewayv1beta1.HTTPRoute, []gatewayv1beta1.Gateway, field.ErrorList) {
+	aggregator := ingressAggregator{ruleGroups: map[ruleGroupKey]*ingressRuleGroup{}}
 
-	for name, newProviderFunc := range ProviderConstructorByName {
-		providerByName[name] = newProviderFunc(conf)
+	var errs field.ErrorList
+	for _, ingress := range ingresses {
+		errs = append(errs, aggregator.addIngress(ingress)...)
+	}
+	if len(errs) > 0 {
+		return nil, nil, errs
 	}
 
-	return providerByName
+	return aggregator.toHTTPRoutesAndGateways()
 }
 
 // extractObjectsFromReader extracts all objects from a reader,
@@ -208,11 +131,3 @@ func ConstructIngressesFromFile(l *networkingv1.IngressList, inputFile string, n
 	}
 	return nil
 }
-
-func aggregatedErrs(errs field.ErrorList) error {
-	errMsg := fmt.Errorf("\n# Encountered %d errors", len(errs))
-	for _, err := range errs {
-		errMsg = fmt.Errorf("\n%w # %s", errMsg, err)
-	}
-	return errMsg
-}
diff --git a/pkg/i2gw/provider.go b/pkg/i2gw/provider.go
deleted file mode 100644
index 6dcf9e8..0000000
--- a/pkg/i2gw/provider.go
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
-Copyright 2023 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package i2gw
-
-import (
-	"context"
-
-	networkingv1 "k8s.io/api/networking/v1"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/validation/field"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-	gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
-	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
-)
-
-// ProviderConstructorByName is a map of ProviderConstructor functions by a
-// provider name. Different Provider implementations should add their construction
-// func at startup.
-var ProviderConstructorByName = map[ProviderName]ProviderConstructor{}
-
-// ProviderName is a string alias that stores the concrete Provider name.
-type ProviderName string
-
-// ProviderConstructor is a construction function that constructs concrete
-// implementations of the Provider interface.
-type ProviderConstructor func(conf *ProviderConf) Provider
-
-// ProviderConf contains all the configuration required for every concrete
-// Provider implementation.
-type ProviderConf struct {
-	Client client.Client
-}
-
-// The Provider interface specifies the required functionality which needs to be
-// implemented by every concrete Ingress/Gateway-API provider, in order for it to
-// be used.
-type Provider interface {
-	CustomResourceReader
-	ResourceConverter
-}
-
-type CustomResourceReader interface {
-
-	// ReadResourcesFromCluster reads custom resources associated with
-	// the underlying Provider implementation from the kubernetes cluster.
-	ReadResourcesFromCluster(ctx context.Context, customResources interface{}) error
-
-	// ReadResourcesFromFiles reads custom resources associated with
-	// the underlying Provider implementation from the files.
-	ReadResourcesFromFiles(ctx context.Context, customResources interface{}, filename string) error
-}
-
-// The ResourceConverter interface specifies all the implemented Gateway API resource
-// conversion functions.
-type ResourceConverter interface {
-
-	// ToGatewayAPIResources converts the received InputResources associated
-	// with the Provider into GatewayResources.
-	ToGatewayAPI(resources InputResources) (GatewayResources, field.ErrorList)
-}
-
-// InputResources contains all Ingress objects, and Provider specific
-// custom resources.
-type InputResources struct {
-	Ingresses       []networkingv1.Ingress
-	CustomResources interface{}
-}
-
-// GatewayResources contains all Gateway-API objects.
-type GatewayResources struct {
-	Gateways       map[types.NamespacedName]gatewayv1beta1.Gateway
-	GatewayClasses map[types.NamespacedName]gatewayv1beta1.GatewayClass
-
-	HTTPRoutes map[types.NamespacedName]gatewayv1beta1.HTTPRoute
-	TLSRoutes  map[types.NamespacedName]gatewayv1alpha2.TLSRoute
-	TCPRoutes  map[types.NamespacedName]gatewayv1alpha2.TCPRoute
-	UDPRoutes  map[types.NamespacedName]gatewayv1alpha2.UDPRoute
-
-	ReferenceGrants map[types.NamespacedName]gatewayv1alpha2.ReferenceGrant
-}
-
-// FeatureParser is a function that reads the InputResources, and applies
-// the appropriate modifications to the GatewayResources.
-//
-// Different FeatureParsers will run in undetermined order. The function must
-// modify / create only the required fields of the gateway resources and nothing else.
-type FeatureParser func(InputResources, *GatewayResources) field.ErrorList
diff --git a/pkg/i2gw/providers/common/utils.go b/pkg/i2gw/providers/common/utils.go
deleted file mode 100644
index 44e90e7..0000000
--- a/pkg/i2gw/providers/common/utils.go
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
-Copyright 2023 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package common
-
-import (
-	"fmt"
-	"regexp"
-
-	networkingv1 "k8s.io/api/networking/v1"
-	networkingv1beta1 "k8s.io/api/networking/v1beta1"
-	"k8s.io/apimachinery/pkg/util/validation/field"
-	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
-)
-
-func GetIngressClass(ingress networkingv1.Ingress) string {
-	var ingressClass string
-
-	if ingress.Spec.IngressClassName != nil && *ingress.Spec.IngressClassName != "" {
-		ingressClass = *ingress.Spec.IngressClassName
-	} else if _, ok := ingress.Annotations[networkingv1beta1.AnnotationIngressClass]; ok {
-		ingressClass = ingress.Annotations[networkingv1beta1.AnnotationIngressClass]
-	} else {
-		ingressClass = ingress.Name
-	}
-
-	return ingressClass
-}
-
-type IngressRuleGroup struct {
-	Namespace    string
-	IngressClass string
-	Host         string
-	TLS          []networkingv1.IngressTLS
-	Rules        []Rule
-}
-
-type Rule struct {
-	Ingress     networkingv1.Ingress
-	IngressRule networkingv1.IngressRule
-}
-
-func GetRuleGroups(ingresses []networkingv1.Ingress) map[string]IngressRuleGroup {
-	ruleGroups := make(map[string]IngressRuleGroup)
-
-	for _, ingress := range ingresses {
-		ingressClass := GetIngressClass(ingress)
-
-		for _, rule := range ingress.Spec.Rules {
-
-			rgKey := fmt.Sprintf("%s/%s/%s", ingress.Namespace, ingressClass, rule.Host)
-			rg, ok := ruleGroups[rgKey]
-			if !ok {
-				rg = IngressRuleGroup{
-					Namespace:    ingress.Namespace,
-					IngressClass: ingressClass,
-					Host:         rule.Host,
-				}
-				ruleGroups[rgKey] = rg
-			}
-			rg.TLS = append(rg.TLS, ingress.Spec.TLS...)
-			rg.Rules = append(rg.Rules, Rule{
-				Ingress:     ingress,
-				IngressRule: rule,
-			})
-
-			ruleGroups[rgKey] = rg
-		}
-
-	}
-
-	return ruleGroups
-}
-
-func NameFromHost(host string) string {
-	// replace all special chars with -
-	reg, _ := regexp.Compile("[^a-zA-Z0-9]+")
-	step1 := reg.ReplaceAllString(host, "-")
-	// remove all - at start of string
-	reg2, _ := regexp.Compile("^[^a-zA-Z0-9]+")
-	step2 := reg2.ReplaceAllString(step1, "")
-	// if nothing left, return "all-hosts"
-	if len(host) == 0 {
-		return "all-hosts"
-	}
-	return step2
-}
-
-func ToBackendRef(ib networkingv1.IngressBackend, path *field.Path) (*gatewayv1beta1.BackendRef, *field.Error) {
-	if ib.Service != nil {
-		if ib.Service.Port.Name != "" {
-			fieldPath := path.Child("service", "port")
-			return nil, field.Invalid(fieldPath, "name", fmt.Sprintf("named ports not supported: %s", ib.Service.Port.Name))
-		}
-		return &gatewayv1beta1.BackendRef{
-			BackendObjectReference: gatewayv1beta1.BackendObjectReference{
-				Name: gatewayv1beta1.ObjectName(ib.Service.Name),
-				Port: (*gatewayv1beta1.PortNumber)(&ib.Service.Port.Number),
-			},
-		}, nil
-	}
-	return &gatewayv1beta1.BackendRef{
-		BackendObjectReference: gatewayv1beta1.BackendObjectReference{
-			Group: (*gatewayv1beta1.Group)(ib.Resource.APIGroup),
-			Kind:  (*gatewayv1beta1.Kind)(&ib.Resource.Kind),
-			Name:  gatewayv1beta1.ObjectName(ib.Resource.Name),
-		},
-	}, nil
-}
diff --git a/pkg/i2gw/providers/ingressnginx/canary.go b/pkg/i2gw/providers/ingressnginx/canary.go
deleted file mode 100644
index 0688ca0..0000000
--- a/pkg/i2gw/providers/ingressnginx/canary.go
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
-Copyright 2023 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package ingressnginx
-
-import (
-	"fmt"
-	"strconv"
-
-	"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw"
-	"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common"
-	networkingv1 "k8s.io/api/networking/v1"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/validation/field"
-	"k8s.io/utils/pointer"
-	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
-)
-
-func canaryFeature(ingressResources i2gw.InputResources, gatewayResources *i2gw.GatewayResources) field.ErrorList {
-	ruleGroups := common.GetRuleGroups(ingressResources.Ingresses)
-
-	for _, rg := range ruleGroups {
-		pathsByMatchGroup, errs := getPathsByMatchGroups(rg)
-		if len(errs) > 0 {
-			return errs
-		}
-
-		for _, paths := range pathsByMatchGroup {
-			path := paths[0]
-
-			backendRefs, calculationErrs := calculateBackendRefWeight(paths)
-			errs = append(errs, calculationErrs...)
-
-			key := types.NamespacedName{Namespace: path.ingress.Namespace, Name: common.NameFromHost(rg.Host)}
-			httpRoute, ok := gatewayResources.HTTPRoutes[key]
-			if !ok {
-				panic("HTTPRoute not exists - this should never happen")
-			}
-
-			patchHTTPRouteWithBackendRefs(&httpRoute, backendRefs)
-		}
-		if len(errs) > 0 {
-			return errs
-		}
-	}
-
-	return nil
-}
-
-func getPathsByMatchGroups(rg common.IngressRuleGroup) (map[pathMatchKey][]ingressPath, field.ErrorList) {
-	pathsByMatchGroup := map[pathMatchKey][]ingressPath{}
-
-	for _, ir := range rg.Rules {
-
-		ingress := ir.Ingress
-		annotations, errs := parseCanaryAnnotations(ingress)
-		if len(errs) > 0 {
-			return nil, errs
-		}
-
-		extraFeatures := extra{canary: &annotations}
-
-		for _, path := range ir.IngressRule.HTTP.Paths {
-			ip := ingressPath{ingress: ingress, ruleType: "http", path: path, extra: &extraFeatures}
-			pmKey := getPathMatchKey(ip)
-			pathsByMatchGroup[pmKey] = append(pathsByMatchGroup[pmKey], ip)
-		}
-	}
-
-	return pathsByMatchGroup, nil
-}
-
-func patchHTTPRouteWithBackendRefs(httpRoute *gatewayv1beta1.HTTPRoute, backendRefs []gatewayv1beta1.HTTPBackendRef) {
-	for _, backendRef := range backendRefs {
-
-		ruleExists := false
-
-		for j, rule := range httpRoute.Spec.Rules {
-			foundBackendRef := false
-			for i := range rule.BackendRefs {
-				if backendRef.Name == rule.BackendRefs[i].Name {
-					rule.BackendRefs[i].Weight = backendRef.Weight
-					foundBackendRef = true
-					ruleExists = true
-					break
-				}
-			}
-
-			if !foundBackendRef {
-				httpRoute.Spec.Rules[j].BackendRefs = append(rule.BackendRefs, backendRef)
-			}
-		}
-
-		if !ruleExists {
-			httpRoute.Spec.Rules = append(httpRoute.Spec.Rules, gatewayv1beta1.HTTPRouteRule{
-				BackendRefs: []gatewayv1beta1.HTTPBackendRef{backendRef},
-			})
-		}
-	}
-}
-
-func calculateBackendRefWeight(paths []ingressPath) ([]gatewayv1beta1.HTTPBackendRef, field.ErrorList) {
-	var errors field.ErrorList
-	var backendRefs []gatewayv1beta1.HTTPBackendRef
-
-	var numWeightedBackends, totalWeightSet int32
-
-	// This is the default value for nginx annotation nginx.ingress.kubernetes.io/canary-weight-total
-	var weightTotal = 100
-
-	for i, path := range paths {
-		backendRef, err := common.ToBackendRef(path.path.Backend, field.NewPath("paths", "backends").Index(i))
-		if err != nil {
-			errors = append(errors, err)
-			continue
-		}
-		if path.extra != nil && path.extra.canary != nil && path.extra.canary.weight != 0 {
-			weight := int32(path.extra.canary.weight)
-			backendRef.Weight = &weight
-			totalWeightSet += weight
-			numWeightedBackends++
-			if path.extra.canary.weightTotal > 0 {
-				weightTotal = path.extra.canary.weightTotal
-			}
-		}
-		backendRefs = append(backendRefs, gatewayv1beta1.HTTPBackendRef{BackendRef: *backendRef})
-	}
-	if numWeightedBackends > 0 && numWeightedBackends < int32(len(backendRefs)) {
-		weightToSet := (int32(weightTotal) - totalWeightSet) / (int32(len(backendRefs)) - numWeightedBackends)
-		if weightToSet < 0 {
-			weightToSet = 0
-		}
-		for i := range backendRefs {
-			if backendRefs[i].Weight == nil {
-				backendRefs[i].Weight = &weightToSet
-			}
-			if *backendRefs[i].Weight > int32(weightTotal) {
-				backendRefs[i].Weight = pointer.Int32(int32(weightTotal))
-
-			}
-		}
-	}
-
-	return backendRefs, errors
-}
-
-type canaryAnnotations struct {
-	enable           bool
-	headerKey        string
-	headerValue      string
-	headerRegexMatch bool
-	weight           int
-	weightTotal      int
-}
-
-func parseCanaryAnnotations(ingress networkingv1.Ingress) (canaryAnnotations, field.ErrorList) {
-	var errs field.ErrorList
-	var err error
-
-	fieldPath := field.NewPath(ingress.Name).Child("metadata").Child("annotations")
-
-	var annotations canaryAnnotations
-	if c := ingress.Annotations["nginx.ingress.kubernetes.io/canary"]; c == "true" {
-		annotations.enable = true
-		if cHeader := ingress.Annotations["nginx.ingress.kubernetes.io/canary-by-header"]; cHeader != "" {
-			annotations.headerKey = cHeader
-			annotations.headerValue = "always"
-		}
-		if cHeaderVal := ingress.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"]; cHeaderVal != "" {
-			annotations.headerValue = cHeaderVal
-		}
-		if cHeaderRegex := ingress.Annotations["nginx.ingress.kubernetes.io/canary-by-header-pattern"]; cHeaderRegex != "" {
-			annotations.headerValue = cHeaderRegex
-			annotations.headerRegexMatch = true
-		}
-		if cHeaderWeight := ingress.Annotations["nginx.ingress.kubernetes.io/canary-weight"]; cHeaderWeight != "" {
-			annotations.weight, err = strconv.Atoi(cHeaderWeight)
-			if err != nil {
-				errs = append(errs, field.TypeInvalid(fieldPath, "nginx.ingress.kubernetes.io/canary-weight", err.Error()))
-			}
-			annotations.weightTotal = 100
-		}
-		if cHeaderWeightTotal := ingress.Annotations["nginx.ingress.kubernetes.io/canary-weight-total"]; cHeaderWeightTotal != "" {
-			annotations.weightTotal, err = strconv.Atoi(cHeaderWeightTotal)
-			if err != nil {
-				errs = append(errs, field.TypeInvalid(fieldPath, "nginx.ingress.kubernetes.io/canary-weight-total", err.Error()))
-			}
-		}
-	}
-	return annotations, errs
-}
-
-func getPathMatchKey(ip ingressPath) pathMatchKey {
-	var pathType string
-	if ip.path.PathType != nil {
-		pathType = string(*ip.path.PathType)
-	}
-	var canaryHeaderKey string
-	if ip.extra != nil && ip.extra.canary != nil && ip.extra.canary.headerKey != "" {
-		canaryHeaderKey = ip.extra.canary.headerKey
-	}
-	return pathMatchKey(fmt.Sprintf("%s/%s/%s", pathType, ip.path.Path, canaryHeaderKey))
-}
-
-type pathMatchKey string
-
-type ingressPath struct {
-	ingress networkingv1.Ingress
-
-	ruleType string
-	path     networkingv1.HTTPIngressPath
-	extra    *extra
-}
-
-type extra struct {
-	canary *canaryAnnotations
-}
diff --git a/pkg/i2gw/providers/ingressnginx/converter.go b/pkg/i2gw/providers/ingressnginx/converter.go
deleted file mode 100644
index e3cc751..0000000
--- a/pkg/i2gw/providers/ingressnginx/converter.go
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
-Copyright 2023 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package ingressnginx
-
-import (
-	"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw"
-	"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common"
-	"k8s.io/apimachinery/pkg/util/validation/field"
-)
-
-// converter implements the ConvertHTTPRoutes function of i2gw.ResourceConverter interface.
-type converter struct {
-	conf *i2gw.ProviderConf
-
-	featureParsers []i2gw.FeatureParser
-}
-
-// newConverter returns an ingress-nginx converter instance.
-func newConverter(conf *i2gw.ProviderConf) *converter {
-	return &converter{
-		conf: conf,
-		featureParsers: []i2gw.FeatureParser{
-			canaryFeature,
-		},
-	}
-}
-
-// ToGatewayAPIResources converts the received i2gw.InputResources to i2gw.GatewayResources
-// including the ingress-nginx specific features.
-func (c *converter) ToGatewayAPI(resources i2gw.InputResources) (i2gw.GatewayResources, field.ErrorList) {
-
-	// Convert plain ingress resources to gateway resources, ignoring all
-	// provider-specific features.
-	gatewayResources, errs := common.ToGateway(resources.Ingresses)
-	if len(errs) > 0 {
-		return i2gw.GatewayResources{}, errs
-	}
-
-	for _, parseFeatureFunc := range c.featureParsers {
-		// Apply the feature parsing function to the gateway resources, one by one.
-		parseErrs := parseFeatureFunc(resources, &gatewayResources)
-		// Append the parsing errors to the error list.
-		errs = append(errs, parseErrs...)
-	}
-
-	return gatewayResources, errs
-}
diff --git a/pkg/i2gw/providers/ingressnginx/ingressnginx.go b/pkg/i2gw/providers/ingressnginx/ingressnginx.go
deleted file mode 100644
index 56acaf1..0000000
--- a/pkg/i2gw/providers/ingressnginx/ingressnginx.go
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
-Copyright 2023 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package ingressnginx
-
-import (
-	"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw"
-)
-
-// The Name of the provider.
-const Name = "ingress-nginx"
-
-func init() {
-	i2gw.ProviderConstructorByName[Name] = NewProvider
-}
-
-// Provider implements the i2gw.Provider interface.
-type Provider struct {
-	conf *i2gw.ProviderConf
-
-	*resourceReader
-	*converter
-}
-
-// NewProvider constructs and returns the ingress-nginx implementation of i2gw.Provider.
-func NewProvider(conf *i2gw.ProviderConf) i2gw.Provider {
-	return &Provider{
-		conf:           conf,
-		resourceReader: newResourceReader(conf),
-		converter:      newConverter(conf),
-	}
-}
diff --git a/pkg/i2gw/providers/ingressnginx/resource_reader.go b/pkg/i2gw/providers/ingressnginx/resource_reader.go
deleted file mode 100644
index 71dac83..0000000
--- a/pkg/i2gw/providers/ingressnginx/resource_reader.go
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
-Copyright 2023 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package ingressnginx
-
-import (
-	"context"
-
-	"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw"
-)
-
-// converter implements the i2gw.CustomResourceReader interface.
-type resourceReader struct {
-	conf *i2gw.ProviderConf
-}
-
-// newResourceReader returns a resourceReader instance.
-func newResourceReader(conf *i2gw.ProviderConf) *resourceReader {
-	return &resourceReader{
-		conf: conf,
-	}
-}
-
-func (r *resourceReader) ReadResourcesFromCluster(ctx context.Context, customResources interface{}) error {
-	// ingress-nginx does not have any CRDs.
-	return nil
-}
-
-func (r *resourceReader) ReadResourcesFromFiles(ctx context.Context, customResources interface{}, filename string) error {
-	// ingress-nginx does not have any CRDs.
-	return nil
-}


Test patch:
diff --git a/cmd/print_test.go b/cmd/print_test.go
index 3c95dcc..a4f62b3 100644
--- a/cmd/print_test.go
+++ b/cmd/print_test.go
@@ -127,7 +127,7 @@ func Test_getNamespaceFilter(t *testing.T) {
 				namespace:     tc.namespace,
 				allNamespaces: tc.allNamespaces,
 			}
-			err = pr.initializeNamespaceFilter()
+			err := pr.initializeNamespaceFilter()
 
 			if tc.expectingError && err == nil {
 				t.Errorf("Expected error but got none")
diff --git a/pkg/i2gw/aggregator_test.go b/pkg/i2gw/aggregator_test.go
new file mode 100644
index 0000000..624e1e8
--- /dev/null
+++ b/pkg/i2gw/aggregator_test.go
@@ -0,0 +1,592 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package i2gw
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	corev1 "k8s.io/api/core/v1"
+	networkingv1 "k8s.io/api/networking/v1"
+	apiequality "k8s.io/apimachinery/pkg/api/equality"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+)
+
+func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
+	iPrefix := networkingv1.PathTypePrefix
+	iExact := networkingv1.PathTypeExact
+	gPathPrefix := gatewayv1beta1.PathMatchPathPrefix
+	gExact := gatewayv1beta1.PathMatchExact
+
+	testCases := []struct {
+		name             string
+		ingresses        []networkingv1.Ingress
+		expectGateways   []gatewayv1beta1.Gateway
+		expectHTTPRoutes []gatewayv1beta1.HTTPRoute
+		expectErrors     []error
+	}{{
+		name:             "empty",
+		ingresses:        []networkingv1.Ingress{},
+		expectGateways:   []gatewayv1beta1.Gateway{},
+		expectHTTPRoutes: []gatewayv1beta1.HTTPRoute{},
+		expectErrors:     []error{},
+	}, {
+		name: "simple ingress",
+		ingresses: []networkingv1.Ingress{{
+			ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "test"},
+			Spec: networkingv1.IngressSpec{
+				Rules: []networkingv1.IngressRule{{
+					Host: "example.com",
+					IngressRuleValue: networkingv1.IngressRuleValue{
+						HTTP: &networkingv1.HTTPIngressRuleValue{
+							Paths: []networkingv1.HTTPIngressPath{{
+								Path:     "/foo",
+								PathType: &iPrefix,
+								Backend: networkingv1.IngressBackend{
+									Service: &networkingv1.IngressServiceBackend{
+										Name: "example",
+										Port: networkingv1.ServiceBackendPort{
+											Number: 3000,
+										},
+									},
+								},
+							}},
+						},
+					},
+				}},
+			},
+		}},
+		expectGateways: []gatewayv1beta1.Gateway{{
+			ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "test"},
+			Spec: gatewayv1beta1.GatewaySpec{
+				GatewayClassName: "example",
+				Listeners: []gatewayv1beta1.Listener{{
+					Name:     "example-com-http",
+					Port:     80,
+					Protocol: gatewayv1beta1.HTTPProtocolType,
+					Hostname: gatewayHostnamePtr("example.com"),
+				}},
+			},
+		}},
+		expectHTTPRoutes: []gatewayv1beta1.HTTPRoute{{
+			ObjectMeta: metav1.ObjectMeta{Name: "example-com", Namespace: "test"},
+			Spec: gatewayv1beta1.HTTPRouteSpec{
+				CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
+					ParentRefs: []gatewayv1beta1.ParentReference{{
+						Name: "example",
+					}},
+				},
+				Hostnames: []gatewayv1beta1.Hostname{"example.com"},
+				Rules: []gatewayv1beta1.HTTPRouteRule{{
+					Matches: []gatewayv1beta1.HTTPRouteMatch{{
+						Path: &gatewayv1beta1.HTTPPathMatch{
+							Type:  &gPathPrefix,
+							Value: stringPtr("/foo"),
+						},
+					}},
+					BackendRefs: []gatewayv1beta1.HTTPBackendRef{{
+						BackendRef: gatewayv1beta1.BackendRef{
+							BackendObjectReference: gatewayv1beta1.BackendObjectReference{
+								Name: "example",
+								Port: portNumberPtr(3000),
+							},
+						},
+					}},
+				}},
+			},
+		}},
+		expectErrors: []error{},
+	}, {
+		name: "ingress with TLS",
+		ingresses: []networkingv1.Ingress{{
+			ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "test"},
+			Spec: networkingv1.IngressSpec{
+				TLS: []networkingv1.IngressTLS{{
+					Hosts:      []string{"example.com"},
+					SecretName: "example-cert",
+				}},
+				Rules: []networkingv1.IngressRule{{
+					Host: "example.com",
+					IngressRuleValue: networkingv1.IngressRuleValue{
+						HTTP: &networkingv1.HTTPIngressRuleValue{
+							Paths: []networkingv1.HTTPIngressPath{{
+								Path:     "/foo",
+								PathType: &iPrefix,
+								Backend: networkingv1.IngressBackend{
+									Service: &networkingv1.IngressServiceBackend{
+										Name: "example",
+										Port: networkingv1.ServiceBackendPort{
+											Number: 3000,
+										},
+									},
+								},
+							}},
+						},
+					},
+				}},
+			},
+		}},
+		expectGateways: []gatewayv1beta1.Gateway{{
+			ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "test"},
+			Spec: gatewayv1beta1.GatewaySpec{
+				GatewayClassName: "example",
+				Listeners: []gatewayv1beta1.Listener{{
+					Name:     "example-com-http",
+					Port:     80,
+					Protocol: gatewayv1beta1.HTTPProtocolType,
+					Hostname: gatewayHostnamePtr("example.com"),
+				}, {
+					Name:     "example-com-https",
+					Port:     443,
+					Protocol: gatewayv1beta1.HTTPSProtocolType,
+					Hostname: gatewayHostnamePtr("example.com"),
+					TLS: &gatewayv1beta1.GatewayTLSConfig{
+						CertificateRefs: []gatewayv1beta1.SecretObjectReference{{
+							Name: "example-cert",
+						}},
+					},
+				}},
+			},
+		}},
+		expectHTTPRoutes: []gatewayv1beta1.HTTPRoute{{
+			ObjectMeta: metav1.ObjectMeta{Name: "example-com", Namespace: "test"},
+			Spec: gatewayv1beta1.HTTPRouteSpec{
+				CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
+					ParentRefs: []gatewayv1beta1.ParentReference{{
+						Name: "example",
+					}},
+				},
+				Hostnames: []gatewayv1beta1.Hostname{"example.com"},
+				Rules: []gatewayv1beta1.HTTPRouteRule{{
+					Matches: []gatewayv1beta1.HTTPRouteMatch{{
+						Path: &gatewayv1beta1.HTTPPathMatch{
+							Type:  &gPathPrefix,
+							Value: stringPtr("/foo"),
+						},
+					}},
+					BackendRefs: []gatewayv1beta1.HTTPBackendRef{{
+						BackendRef: gatewayv1beta1.BackendRef{
+							BackendObjectReference: gatewayv1beta1.BackendObjectReference{
+								Name: "example",
+								Port: portNumberPtr(3000),
+							},
+						},
+					}},
+				}},
+			},
+		}},
+		expectErrors: []error{},
+	}, {
+		name: "ingress with custom and default backend",
+		ingresses: []networkingv1.Ingress{{
+			ObjectMeta: metav1.ObjectMeta{Name: "net", Namespace: "different"},
+			Spec: networkingv1.IngressSpec{
+				IngressClassName: stringPtr("example-proxy"),
+				Rules: []networkingv1.IngressRule{{
+					Host: "example.net",
+					IngressRuleValue: networkingv1.IngressRuleValue{
+						HTTP: &networkingv1.HTTPIngressRuleValue{
+							Paths: []networkingv1.HTTPIngressPath{{
+								Path:     "/bar",
+								PathType: &iExact,
+								Backend: networkingv1.IngressBackend{
+									Resource: &corev1.TypedLocalObjectReference{
+										Name:     "custom",
+										Kind:     "StorageBucket",
+										APIGroup: stringPtr("vendor.example.com"),
+									},
+								},
+							}},
+						},
+					},
+				}},
+				DefaultBackend: &networkingv1.IngressBackend{
+					Service: &networkingv1.IngressServiceBackend{
+						Name: "default",
+						Port: networkingv1.ServiceBackendPort{
+							Number: 8080,
+						},
+					},
+				},
+			},
+		}},
+		expectGateways: []gatewayv1beta1.Gateway{{
+			ObjectMeta: metav1.ObjectMeta{Name: "example-proxy", Namespace: "different"},
+			Spec: gatewayv1beta1.GatewaySpec{
+				GatewayClassName: "example-proxy",
+				Listeners: []gatewayv1beta1.Listener{{
+					Name:     "example-net-http",
+					Port:     80,
+					Protocol: gatewayv1beta1.HTTPProtocolType,
+					Hostname: gatewayHostnamePtr("example.net"),
+				}},
+			},
+		}},
+		expectHTTPRoutes: []gatewayv1beta1.HTTPRoute{{
+			ObjectMeta: metav1.ObjectMeta{Name: "example-net", Namespace: "different"},
+			Spec: gatewayv1beta1.HTTPRouteSpec{
+				CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
+					ParentRefs: []gatewayv1beta1.ParentReference{{
+						Name: "example-proxy",
+					}},
+				},
+				Hostnames: []gatewayv1beta1.Hostname{"example.net"},
+				Rules: []gatewayv1beta1.HTTPRouteRule{{
+					Matches: []gatewayv1beta1.HTTPRouteMatch{{
+						Path: &gatewayv1beta1.HTTPPathMatch{
+							Type:  &gExact,
+							Value: stringPtr("/bar"),
+						},
+					}},
+					BackendRefs: []gatewayv1beta1.HTTPBackendRef{{
+						BackendRef: gatewayv1beta1.BackendRef{
+							BackendObjectReference: gatewayv1beta1.BackendObjectReference{
+								Name:  "custom",
+								Group: apiGroupPtr("vendor.example.com"),
+								Kind:  apiKindPtr("StorageBucket"),
+							},
+						},
+					}},
+				}},
+			},
+		}, {
+			ObjectMeta: metav1.ObjectMeta{Name: "net-default-backend", Namespace: "different"},
+			Spec: gatewayv1beta1.HTTPRouteSpec{
+				CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
+					ParentRefs: []gatewayv1beta1.ParentReference{{
+						Name: "example-proxy",
+					}},
+				},
+				Rules: []gatewayv1beta1.HTTPRouteRule{{
+					BackendRefs: []gatewayv1beta1.HTTPBackendRef{{
+						BackendRef: gatewayv1beta1.BackendRef{
+							BackendObjectReference: gatewayv1beta1.BackendObjectReference{
+								Name: "default",
+								Port: portNumberPtr(8080),
+							},
+						}},
+					}},
+				},
+			},
+		}},
+	}}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			aggregator := ingressAggregator{ruleGroups: map[ruleGroupKey]*ingressRuleGroup{}}
+
+			for _, ingress := range tc.ingresses {
+				aggregator.addIngress(ingress)
+			}
+
+			httpRoutes, gateways, errs := aggregator.toHTTPRoutesAndGateways()
+
+			if len(httpRoutes) != len(tc.expectHTTPRoutes) {
+				t.Errorf("Expected %d HTTPRoutes, got %d: %+v", len(tc.expectHTTPRoutes), len(httpRoutes), httpRoutes)
+			} else {
+				for i, got := range httpRoutes {
+					want := tc.expectHTTPRoutes[i]
+					want.SetGroupVersionKind(httpRouteGVK)
+					if !apiequality.Semantic.DeepEqual(got, want) {
+						t.Errorf("Expected HTTPRoute %d to be %+v\n Got: %+v\n Diff: %s", i, want, got, cmp.Diff(want, got))
+					}
+				}
+			}
+
+			if len(gateways) != len(tc.expectGateways) {
+				t.Errorf("Expected %d Gateways, got %d: %+v", len(tc.expectGateways), len(gateways), gateways)
+			} else {
+				for i, got := range gateways {
+					want := tc.expectGateways[i]
+					want.SetGroupVersionKind(gatewayGVK)
+					if !apiequality.Semantic.DeepEqual(got, want) {
+						t.Errorf("Expected Gateway %d to be %+v\n Got: %+v\n Diff: %s", i, want, got, cmp.Diff(want, got))
+					}
+				}
+			}
+
+			if len(errs) != len(tc.expectErrors) {
+				t.Errorf("Expected %d errors, got %d: %+v", len(tc.expectErrors), len(errs), errs)
+			} else {
+				for i, e := range errs {
+					if errors.Is(e, tc.expectErrors[i]) {
+						t.Errorf("Unexpected error message at %d index. Got %s, want: %s", i, e, tc.expectErrors[i])
+					}
+				}
+			}
+		})
+	}
+}
+
+func int32Ptr(n int32) *int32 {
+	return &n
+}
+
+func stringPtr(s string) *string {
+	return &s
+}
+
+// TODO: Replace these with Gateway API util funcs.
+func apiGroupPtr(s string) *gatewayv1beta1.Group {
+	g := gatewayv1beta1.Group(s)
+	return &g
+}
+
+func apiKindPtr(s string) *gatewayv1beta1.Kind {
+	k := gatewayv1beta1.Kind(s)
+	return &k
+}
+
+func portNumberPtr(p int) *gatewayv1beta1.PortNumber {
+	pn := gatewayv1beta1.PortNumber(p)
+	return &pn
+}
+
+func gatewayHostnamePtr(s string) *gatewayv1beta1.Hostname {
+	h := gatewayv1beta1.Hostname(s)
+	return &h
+}
+
+func Test_getExtra(t *testing.T) {
+	testCases := []struct {
+		name          string
+		ingress       networkingv1.Ingress
+		expectedExtra *extra
+		expectedError field.ErrorList
+	}{
+		{
+			name: "actually get weights",
+			ingress: networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"nginx.ingress.kubernetes.io/canary":              "true",
+						"nginx.ingress.kubernetes.io/canary-weight":       "50",
+						"nginx.ingress.kubernetes.io/canary-weight-total": "100",
+					},
+				},
+			},
+			expectedExtra: &extra{
+				canary: &canary{
+					enable:      true,
+					weight:      50,
+					weightTotal: 100,
+				},
+			},
+		},
+		{
+			name: "assigns default weight total",
+			ingress: networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"nginx.ingress.kubernetes.io/canary":        "true",
+						"nginx.ingress.kubernetes.io/canary-weight": "50",
+					},
+				},
+			},
+			expectedExtra: &extra{
+				canary: &canary{
+					enable:      true,
+					weight:      50,
+					weightTotal: 100,
+				},
+			},
+		},
+		{
+			name: "errors on non integer weight",
+			ingress: networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"nginx.ingress.kubernetes.io/canary":        "true",
+						"nginx.ingress.kubernetes.io/canary-weight": "50.5",
+					},
+				},
+			},
+			expectedExtra: &extra{},
+			expectedError: field.ErrorList{field.TypeInvalid(field.NewPath(""), "", "")},
+		},
+		{
+			name: "errors on non integer weight total",
+			ingress: networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"nginx.ingress.kubernetes.io/canary":              "true",
+						"nginx.ingress.kubernetes.io/canary-weight-total": "50.5",
+					},
+				},
+			},
+			expectedExtra: &extra{},
+			expectedError: field.ErrorList{field.TypeInvalid(field.NewPath(""), "", "")},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+
+			actualExtra, errs := getExtra(tc.ingress)
+			if len(errs) != len(tc.expectedError) {
+				t.Fatalf("expected %d errors, got %d", len(tc.expectedError), len(errs))
+			}
+
+			if len(tc.expectedError) > 0 {
+				return
+			}
+
+			actualCanary := actualExtra.canary
+			expectedCanary := tc.expectedExtra.canary
+
+			if diff := cmp.Diff(*actualCanary, *expectedCanary, cmp.AllowUnexported(canary{})); diff != "" {
+				t.Fatalf("getExtra() mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func Test_ingressRuleGroup_calculateBackendRefWeight(t *testing.T) {
+	testCases := []struct {
+		name                string
+		paths               []ingressPath
+		expectedBackendRefs []gatewayv1beta1.HTTPBackendRef
+		expectedErrors      field.ErrorList
+	}{
+		{
+			name: "respect weight boundaries",
+			paths: []ingressPath{
+				{
+					path: networkingv1.HTTPIngressPath{
+						Backend: networkingv1.IngressBackend{
+							Resource: &corev1.TypedLocalObjectReference{
+								Name:     "canary",
+								Kind:     "StorageBucket",
+								APIGroup: stringPtr("vendor.example.com"),
+							},
+						},
+					},
+					extra: &extra{canary: &canary{
+						weight: 101,
+					}},
+				},
+				{
+					path: networkingv1.HTTPIngressPath{
+						Backend: networkingv1.IngressBackend{
+							Resource: &corev1.TypedLocalObjectReference{
+								Name:     "prod",
+								Kind:     "StorageBucket",
+								APIGroup: stringPtr("vendor.example.com"),
+							},
+						},
+					},
+				},
+			},
+			expectedBackendRefs: []gatewayv1beta1.HTTPBackendRef{
+				{BackendRef: gatewayv1beta1.BackendRef{Weight: int32Ptr(100)}},
+				{BackendRef: gatewayv1beta1.BackendRef{Weight: int32Ptr(0)}},
+			},
+		},
+		{
+			name: "default total weight",
+			paths: []ingressPath{
+				{
+					path: networkingv1.HTTPIngressPath{
+						Backend: networkingv1.IngressBackend{
+							Resource: &corev1.TypedLocalObjectReference{
+								Name:     "canary",
+								Kind:     "StorageBucket",
+								APIGroup: stringPtr("vendor.example.com"),
+							},
+						},
+					},
+					extra: &extra{canary: &canary{
+						weight: 30,
+					}},
+				},
+				{
+					path: networkingv1.HTTPIngressPath{
+						Backend: networkingv1.IngressBackend{
+							Resource: &corev1.TypedLocalObjectReference{
+								Name:     "prod",
+								Kind:     "StorageBucket",
+								APIGroup: stringPtr("vendor.example.com"),
+							},
+						},
+					},
+				},
+			},
+			expectedBackendRefs: []gatewayv1beta1.HTTPBackendRef{
+				{BackendRef: gatewayv1beta1.BackendRef{Weight: int32Ptr(30)}},
+				{BackendRef: gatewayv1beta1.BackendRef{Weight: int32Ptr(70)}},
+			},
+		},
+		{
+			name: "weight total assigned",
+			paths: []ingressPath{
+				{
+					path: networkingv1.HTTPIngressPath{
+						Backend: networkingv1.IngressBackend{
+							Resource: &corev1.TypedLocalObjectReference{
+								Name:     "canary",
+								Kind:     "StorageBucket",
+								APIGroup: stringPtr("vendor.example.com"),
+							},
+						},
+					},
+					extra: &extra{canary: &canary{
+						weight:      50,
+						weightTotal: 200,
+					}},
+				},
+				{
+					path: networkingv1.HTTPIngressPath{
+						Backend: networkingv1.IngressBackend{
+							Resource: &corev1.TypedLocalObjectReference{
+								Name:     "prod",
+								Kind:     "StorageBucket",
+								APIGroup: stringPtr("vendor.example.com"),
+							},
+						},
+					},
+				},
+			},
+			expectedBackendRefs: []gatewayv1beta1.HTTPBackendRef{
+				{BackendRef: gatewayv1beta1.BackendRef{Weight: int32Ptr(50)}},
+				{BackendRef: gatewayv1beta1.BackendRef{Weight: int32Ptr(150)}},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+
+			var irg ingressRuleGroup
+			actualBackendRefs, errs := irg.calculateBackendRefWeight(tc.paths)
+			if len(errs) != len(tc.expectedErrors) {
+				t.Fatalf("expected %d errors, got %d", len(tc.expectedErrors), len(errs))
+			}
+
+			if len(actualBackendRefs) != len(tc.expectedBackendRefs) {
+				t.Fatalf("expected %d backend refs, got %d", len(tc.expectedBackendRefs), len(actualBackendRefs))
+			}
+			for i := 0; i < len(tc.expectedBackendRefs); i++ {
+				if *tc.expectedBackendRefs[i].Weight != *actualBackendRefs[i].Weight {
+					t.Fatalf("%s backendRef expected weight is %d, actual %d",
+						actualBackendRefs[i].Name, *tc.expectedBackendRefs[i].Weight, *actualBackendRefs[i].Weight)
+				}
+			}
+		})
+	}
+}
diff --git a/pkg/i2gw/ingress2gateway_test.go b/pkg/i2gw/ingress2gateway_test.go
index 1b92f9b..8d436f9 100644
--- a/pkg/i2gw/ingress2gateway_test.go
+++ b/pkg/i2gw/ingress2gateway_test.go
@@ -17,7 +17,6 @@ limitations under the License.
 package i2gw
 
 import (
-	"context"
 	"fmt"
 	"testing"
 
@@ -39,23 +38,22 @@ func Test_constructIngressesFromFile(t *testing.T) {
 		filePath        string
 		namespace       string
 		wantIngressList []networkingv1.Ingress
-	}{
-		{
-			name:            "Test yaml input file with multiple resources with no namespace flag",
-			filePath:        "testdata/input-file.yaml",
-			namespace:       "",
-			wantIngressList: []networkingv1.Ingress{ingress1, ingress2, ingressNoNamespace},
-		}, {
-			name:            "Test json input file with multiple resources with no namespace flag",
-			filePath:        "testdata/input-file.json",
-			namespace:       "",
-			wantIngressList: []networkingv1.Ingress{ingress1, ingress2, ingressNoNamespace},
-		}, {
-			name:            "Test yaml input file with multiple resources with namespace1 flag",
-			filePath:        "testdata/input-file.yaml",
-			namespace:       "namespace1",
-			wantIngressList: []networkingv1.Ingress{ingress1},
-		},
+	}{{
+		name:            "Test yaml input file with multiple resources with no namespace flag",
+		filePath:        "testdata/input-file.yaml",
+		namespace:       "",
+		wantIngressList: []networkingv1.Ingress{ingress1, ingress2, ingressNoNamespace},
+	}, {
+		name:            "Test json input file with multiple resources with no namespace flag",
+		filePath:        "testdata/input-file.json",
+		namespace:       "",
+		wantIngressList: []networkingv1.Ingress{ingress1, ingress2, ingressNoNamespace},
+	}, {
+		name:            "Test yaml input file with multiple resources with namespace1 flag",
+		filePath:        "testdata/input-file.yaml",
+		namespace:       "namespace1",
+		wantIngressList: []networkingv1.Ingress{ingress1},
+	},
 	}
 
 	for _, tc := range testCases {
@@ -142,7 +140,7 @@ func Test_constructIngressesFromCluster(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			gotIngresses := &networkingv1.IngressList{}
 			cl := fake.NewClientBuilder().WithRuntimeObjects(tc.runtimeObjs...).Build()
-			err := ConstructIngressesFromCluster(context.Background(), cl, gotIngresses)
+			err := ConstructIngressesFromCluster(cl, gotIngresses)
 			if err != nil {
 				t.Errorf("test failed unexpectedly: %v", err)
 			}
diff --git a/pkg/i2gw/providers/common/converter_test.go b/pkg/i2gw/providers/common/converter_test.go
deleted file mode 100644
index 4fe7b45..0000000
--- a/pkg/i2gw/providers/common/converter_test.go
+++ /dev/null
@@ -1,364 +0,0 @@
-/*
-Copyright 2023 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package common
-
-import (
-	"errors"
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-	"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw"
-	corev1 "k8s.io/api/core/v1"
-	networkingv1 "k8s.io/api/networking/v1"
-	apiequality "k8s.io/apimachinery/pkg/api/equality"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/validation/field"
-	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
-)
-
-func Test_ingresses2GatewaysAndHttpRoutes(t *testing.T) {
-	iPrefix := networkingv1.PathTypePrefix
-	iExact := networkingv1.PathTypeExact
-	gPathPrefix := gatewayv1beta1.PathMatchPathPrefix
-	gExact := gatewayv1beta1.PathMatchExact
-
-	testCases := []struct {
-		name                     string
-		ingresses                []networkingv1.Ingress
-		expectedGatewayResources i2gw.GatewayResources
-		expectedErrors           field.ErrorList
-	}{
-		{
-			name:                     "empty",
-			ingresses:                []networkingv1.Ingress{},
-			expectedGatewayResources: i2gw.GatewayResources{},
-			expectedErrors:           field.ErrorList{},
-		},
-		{
-			name: "simple ingress",
-			ingresses: []networkingv1.Ingress{{
-				ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "test"},
-				Spec: networkingv1.IngressSpec{
-					Rules: []networkingv1.IngressRule{{
-						Host: "example.com",
-						IngressRuleValue: networkingv1.IngressRuleValue{
-							HTTP: &networkingv1.HTTPIngressRuleValue{
-								Paths: []networkingv1.HTTPIngressPath{{
-									Path:     "/foo",
-									PathType: &iPrefix,
-									Backend: networkingv1.IngressBackend{
-										Service: &networkingv1.IngressServiceBackend{
-											Name: "example",
-											Port: networkingv1.ServiceBackendPort{
-												Number: 3000,
-											},
-										},
-									},
-								}},
-							},
-						},
-					}},
-				},
-			}},
-			expectedGatewayResources: i2gw.GatewayResources{
-				Gateways: map[types.NamespacedName]gatewayv1beta1.Gateway{
-					{Namespace: "test", Name: "example"}: {
-						ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "test"},
-						Spec: gatewayv1beta1.GatewaySpec{
-							GatewayClassName: "example",
-							Listeners: []gatewayv1beta1.Listener{{
-								Name:     "example-com-http",
-								Port:     80,
-								Protocol: gatewayv1beta1.HTTPProtocolType,
-								Hostname: ptrTo(gatewayv1beta1.Hostname("example.com")),
-							}},
-						},
-					},
-				},
-				HTTPRoutes: map[types.NamespacedName]gatewayv1beta1.HTTPRoute{
-					{Namespace: "test", Name: "example-com"}: {
-						ObjectMeta: metav1.ObjectMeta{Name: "example-com", Namespace: "test"},
-						Spec: gatewayv1beta1.HTTPRouteSpec{
-							CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
-								ParentRefs: []gatewayv1beta1.ParentReference{{
-									Name: "example",
-								}},
-							},
-							Hostnames: []gatewayv1beta1.Hostname{"example.com"},
-							Rules: []gatewayv1beta1.HTTPRouteRule{{
-								Matches: []gatewayv1beta1.HTTPRouteMatch{{
-									Path: &gatewayv1beta1.HTTPPathMatch{
-										Type:  &gPathPrefix,
-										Value: ptrTo("/foo"),
-									},
-								}},
-								BackendRefs: []gatewayv1beta1.HTTPBackendRef{{
-									BackendRef: gatewayv1beta1.BackendRef{
-										BackendObjectReference: gatewayv1beta1.BackendObjectReference{
-											Name: "example",
-											Port: ptrTo(gatewayv1beta1.PortNumber(3000)),
-										},
-									},
-								}},
-							}},
-						},
-					},
-				},
-			},
-			expectedErrors: field.ErrorList{},
-		},
-		{
-			name: "ingress with TLS",
-			ingresses: []networkingv1.Ingress{{
-				ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "test"},
-				Spec: networkingv1.IngressSpec{
-					TLS: []networkingv1.IngressTLS{{
-						Hosts:      []string{"example.com"},
-						SecretName: "example-cert",
-					}},
-					Rules: []networkingv1.IngressRule{{
-						Host: "example.com",
-						IngressRuleValue: networkingv1.IngressRuleValue{
-							HTTP: &networkingv1.HTTPIngressRuleValue{
-								Paths: []networkingv1.HTTPIngressPath{{
-									Path:     "/foo",
-									PathType: &iPrefix,
-									Backend: networkingv1.IngressBackend{
-										Service: &networkingv1.IngressServiceBackend{
-											Name: "example",
-											Port: networkingv1.ServiceBackendPort{
-												Number: 3000,
-											},
-										},
-									},
-								}},
-							},
-						},
-					}},
-				},
-			}},
-			expectedGatewayResources: i2gw.GatewayResources{
-				Gateways: map[types.NamespacedName]gatewayv1beta1.Gateway{
-					{Namespace: "test", Name: "example"}: {
-						ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "test"},
-						Spec: gatewayv1beta1.GatewaySpec{
-							GatewayClassName: "example",
-							Listeners: []gatewayv1beta1.Listener{{
-								Name:     "example-com-http",
-								Port:     80,
-								Protocol: gatewayv1beta1.HTTPProtocolType,
-								Hostname: ptrTo(gatewayv1beta1.Hostname("example.com")),
-							}, {
-								Name:     "example-com-https",
-								Port:     443,
-								Protocol: gatewayv1beta1.HTTPSProtocolType,
-								Hostname: ptrTo(gatewayv1beta1.Hostname("example.com")),
-								TLS: &gatewayv1beta1.GatewayTLSConfig{
-									CertificateRefs: []gatewayv1beta1.SecretObjectReference{{
-										Name: "example-cert",
-									}},
-								},
-							}},
-						},
-					},
-				},
-				HTTPRoutes: map[types.NamespacedName]gatewayv1beta1.HTTPRoute{
-					{Namespace: "test", Name: "example-com"}: {
-						ObjectMeta: metav1.ObjectMeta{Name: "example-com", Namespace: "test"},
-						Spec: gatewayv1beta1.HTTPRouteSpec{
-							CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
-								ParentRefs: []gatewayv1beta1.ParentReference{{
-									Name: "example",
-								}},
-							},
-							Hostnames: []gatewayv1beta1.Hostname{"example.com"},
-							Rules: []gatewayv1beta1.HTTPRouteRule{{
-								Matches: []gatewayv1beta1.HTTPRouteMatch{{
-									Path: &gatewayv1beta1.HTTPPathMatch{
-										Type:  &gPathPrefix,
-										Value: ptrTo("/foo"),
-									},
-								}},
-								BackendRefs: []gatewayv1beta1.HTTPBackendRef{{
-									BackendRef: gatewayv1beta1.BackendRef{
-										BackendObjectReference: gatewayv1beta1.BackendObjectReference{
-											Name: "example",
-											Port: ptrTo(gatewayv1beta1.PortNumber(3000)),
-										},
-									},
-								}},
-							}},
-						},
-					},
-				},
-			},
-			expectedErrors: field.ErrorList{},
-		},
-		{
-			name: "ingress with custom and default backend",
-			ingresses: []networkingv1.Ingress{{
-				ObjectMeta: metav1.ObjectMeta{Name: "net", Namespace: "different"},
-				Spec: networkingv1.IngressSpec{
-					IngressClassName: ptrTo("example-proxy"),
-					Rules: []networkingv1.IngressRule{{
-						Host: "example.net",
-						IngressRuleValue: networkingv1.IngressRuleValue{
-							HTTP: &networkingv1.HTTPIngressRuleValue{
-								Paths: []networkingv1.HTTPIngressPath{{
-									Path:     "/bar",
-									PathType: &iExact,
-									Backend: networkingv1.IngressBackend{
-										Resource: &corev1.TypedLocalObjectReference{
-											Name:     "custom",
-											Kind:     "StorageBucket",
-											APIGroup: ptrTo("vendor.example.com"),
-										},
-									},
-								}},
-							},
-						},
-					}},
-					DefaultBackend: &networkingv1.IngressBackend{
-						Service: &networkingv1.IngressServiceBackend{
-							Name: "default",
-							Port: networkingv1.ServiceBackendPort{
-								Number: 8080,
-							},
-						},
-					},
-				},
-			}},
-			expectedGatewayResources: i2gw.GatewayResources{
-				Gateways: map[types.NamespacedName]gatewayv1beta1.Gateway{
-					{Namespace: "different", Name: "example-proxy"}: {
-						ObjectMeta: metav1.ObjectMeta{Name: "example-proxy", Namespace: "different"},
-						Spec: gatewayv1beta1.GatewaySpec{
-							GatewayClassName: "example-proxy",
-							Listeners: []gatewayv1beta1.Listener{{
-								Name:     "example-net-http",
-								Port:     80,
-								Protocol: gatewayv1beta1.HTTPProtocolType,
-								Hostname: ptrTo(gatewayv1beta1.Hostname("example.net")),
-							}},
-						},
-					},
-				},
-				HTTPRoutes: map[types.NamespacedName]gatewayv1beta1.HTTPRoute{
-					{Namespace: "different", Name: "example-net"}: {
-						ObjectMeta: metav1.ObjectMeta{Name: "example-net", Namespace: "different"},
-						Spec: gatewayv1beta1.HTTPRouteSpec{
-							CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
-								ParentRefs: []gatewayv1beta1.ParentReference{{
-									Name: "example-proxy",
-								}},
-							},
-							Hostnames: []gatewayv1beta1.Hostname{"example.net"},
-							Rules: []gatewayv1beta1.HTTPRouteRule{{
-								Matches: []gatewayv1beta1.HTTPRouteMatch{{
-									Path: &gatewayv1beta1.HTTPPathMatch{
-										Type:  &gExact,
-										Value: ptrTo("/bar"),
-									},
-								}},
-								BackendRefs: []gatewayv1beta1.HTTPBackendRef{{
-									BackendRef: gatewayv1beta1.BackendRef{
-										BackendObjectReference: gatewayv1beta1.BackendObjectReference{
-											Name:  "custom",
-											Group: ptrTo(gatewayv1beta1.Group("vendor.example.com")),
-											Kind:  ptrTo(gatewayv1beta1.Kind("StorageBucket")),
-										},
-									},
-								}},
-							}},
-						},
-					},
-					{Namespace: "different", Name: "net-default-backend"}: {
-						ObjectMeta: metav1.ObjectMeta{Name: "net-default-backend", Namespace: "different"},
-						Spec: gatewayv1beta1.HTTPRouteSpec{
-							CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
-								ParentRefs: []gatewayv1beta1.ParentReference{{
-									Name: "example-proxy",
-								}},
-							},
-							Rules: []gatewayv1beta1.HTTPRouteRule{{
-								BackendRefs: []gatewayv1beta1.HTTPBackendRef{{
-									BackendRef: gatewayv1beta1.BackendRef{
-										BackendObjectReference: gatewayv1beta1.BackendObjectReference{
-											Name: "default",
-											Port: ptrTo(gatewayv1beta1.PortNumber(8080)),
-										},
-									}},
-								}},
-							},
-						},
-					},
-				},
-			},
-			expectedErrors: field.ErrorList{},
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-
-			gatewayResources, errs := ToGateway(tc.ingresses)
-
-			if len(gatewayResources.HTTPRoutes) != len(tc.expectedGatewayResources.HTTPRoutes) {
-				t.Errorf("Expected %d HTTPRoutes, got %d: %+v",
-					len(tc.expectedGatewayResources.HTTPRoutes), len(gatewayResources.HTTPRoutes), gatewayResources.HTTPRoutes)
-			} else {
-				for i, got := range gatewayResources.HTTPRoutes {
-					key := types.NamespacedName{Namespace: got.Namespace, Name: got.Name}
-					want := tc.expectedGatewayResources.HTTPRoutes[key]
-					want.SetGroupVersionKind(HTTPRouteGVK)
-					if !apiequality.Semantic.DeepEqual(got, want) {
-						t.Errorf("Expected HTTPRoute %s to be %+v\n Got: %+v\n Diff: %s", i, want, got, cmp.Diff(want, got))
-					}
-				}
-			}
-
-			if len(gatewayResources.Gateways) != len(tc.expectedGatewayResources.Gateways) {
-				t.Errorf("Expected %d Gateways, got %d: %+v",
-					len(tc.expectedGatewayResources.Gateways), len(gatewayResources.Gateways), gatewayResources.Gateways)
-			} else {
-				for i, got := range gatewayResources.Gateways {
-					key := types.NamespacedName{Namespace: got.Namespace, Name: got.Name}
-					want := tc.expectedGatewayResources.Gateways[key]
-					want.SetGroupVersionKind(GatewayGVK)
-					if !apiequality.Semantic.DeepEqual(got, want) {
-						t.Errorf("Expected Gateway %s to be %+v\n Got: %+v\n Diff: %s", i, want, got, cmp.Diff(want, got))
-					}
-				}
-			}
-
-			if len(errs) != len(tc.expectedErrors) {
-				t.Errorf("Expected %d errors, got %d: %+v", len(tc.expectedErrors), len(errs), errs)
-			} else {
-				for i, e := range errs {
-					if errors.Is(e, tc.expectedErrors[i]) {
-						t.Errorf("Unexpected error message at %d index. Got %s, want: %s", i, e, tc.expectedErrors[i])
-					}
-				}
-			}
-		})
-	}
-}
-
-func ptrTo[T any](a T) *T {
-	return &a
-}
diff --git a/pkg/i2gw/providers/ingressnginx/canary_test.go b/pkg/i2gw/providers/ingressnginx/canary_test.go
deleted file mode 100644
index 907ecdc..0000000
--- a/pkg/i2gw/providers/ingressnginx/canary_test.go
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
-Copyright 2023 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package ingressnginx
-
-import (
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-	corev1 "k8s.io/api/core/v1"
-	networkingv1 "k8s.io/api/networking/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/util/validation/field"
-	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
-)
-
-func Test_ingressRuleGroup_calculateBackendRefWeight(t *testing.T) {
-	testCases := []struct {
-		name                string
-		paths               []ingressPath
-		expectedBackendRefs []gatewayv1beta1.HTTPBackendRef
-		expectedErrors      field.ErrorList
-	}{
-		{
-			name: "respect weight boundaries",
-			paths: []ingressPath{
-				{
-					path: networkingv1.HTTPIngressPath{
-						Backend: networkingv1.IngressBackend{
-							Resource: &corev1.TypedLocalObjectReference{
-								Name:     "canary",
-								Kind:     "StorageBucket",
-								APIGroup: ptrTo("vendor.example.com"),
-							},
-						},
-					},
-					extra: &extra{canary: &canaryAnnotations{
-						weight: 101,
-					}},
-				},
-				{
-					path: networkingv1.HTTPIngressPath{
-						Backend: networkingv1.IngressBackend{
-							Resource: &corev1.TypedLocalObjectReference{
-								Name:     "prod",
-								Kind:     "StorageBucket",
-								APIGroup: ptrTo("vendor.example.com"),
-							},
-						},
-					},
-				},
-			},
-			expectedBackendRefs: []gatewayv1beta1.HTTPBackendRef{
-				{BackendRef: gatewayv1beta1.BackendRef{Weight: ptrTo(int32(100))}},
-				{BackendRef: gatewayv1beta1.BackendRef{Weight: ptrTo(int32(0))}},
-			},
-		},
-		{
-			name: "default total weight",
-			paths: []ingressPath{
-				{
-					path: networkingv1.HTTPIngressPath{
-						Backend: networkingv1.IngressBackend{
-							Resource: &corev1.TypedLocalObjectReference{
-								Name:     "canary",
-								Kind:     "StorageBucket",
-								APIGroup: ptrTo("vendor.example.com"),
-							},
-						},
-					},
-					extra: &extra{canary: &canaryAnnotations{
-						weight: 30,
-					}},
-				},
-				{
-					path: networkingv1.HTTPIngressPath{
-						Backend: networkingv1.IngressBackend{
-							Resource: &corev1.TypedLocalObjectReference{
-								Name:     "prod",
-								Kind:     "StorageBucket",
-								APIGroup: ptrTo("vendor.example.com"),
-							},
-						},
-					},
-				},
-			},
-			expectedBackendRefs: []gatewayv1beta1.HTTPBackendRef{
-				{BackendRef: gatewayv1beta1.BackendRef{Weight: ptrTo(int32(30))}},
-				{BackendRef: gatewayv1beta1.BackendRef{Weight: ptrTo(int32(70))}},
-			},
-		},
-		{
-			name: "weight total assigned",
-			paths: []ingressPath{
-				{
-					path: networkingv1.HTTPIngressPath{
-						Backend: networkingv1.IngressBackend{
-							Resource: &corev1.TypedLocalObjectReference{
-								Name:     "canary",
-								Kind:     "StorageBucket",
-								APIGroup: ptrTo("vendor.example.com"),
-							},
-						},
-					},
-					extra: &extra{canary: &canaryAnnotations{
-						weight:      50,
-						weightTotal: 200,
-					}},
-				},
-				{
-					path: networkingv1.HTTPIngressPath{
-						Backend: networkingv1.IngressBackend{
-							Resource: &corev1.TypedLocalObjectReference{
-								Name:     "prod",
-								Kind:     "StorageBucket",
-								APIGroup: ptrTo("vendor.example.com"),
-							},
-						},
-					},
-				},
-			},
-			expectedBackendRefs: []gatewayv1beta1.HTTPBackendRef{
-				{BackendRef: gatewayv1beta1.BackendRef{Weight: ptrTo(int32(50))}},
-				{BackendRef: gatewayv1beta1.BackendRef{Weight: ptrTo(int32(150))}},
-			},
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-
-			actualBackendRefs, errs := calculateBackendRefWeight(tc.paths)
-			if len(errs) != len(tc.expectedErrors) {
-				t.Fatalf("expected %d errors, got %d", len(tc.expectedErrors), len(errs))
-			}
-
-			if len(actualBackendRefs) != len(tc.expectedBackendRefs) {
-				t.Fatalf("expected %d backend refs, got %d", len(tc.expectedBackendRefs), len(actualBackendRefs))
-			}
-			for i := 0; i < len(tc.expectedBackendRefs); i++ {
-				if *tc.expectedBackendRefs[i].Weight != *actualBackendRefs[i].Weight {
-					t.Fatalf("%s backendRef expected weight is %d, actual %d",
-						actualBackendRefs[i].Name, *tc.expectedBackendRefs[i].Weight, *actualBackendRefs[i].Weight)
-				}
-			}
-		})
-	}
-}
-
-func Test_parseCanaryAnnotations(t *testing.T) {
-	testCases := []struct {
-		name          string
-		ingress       networkingv1.Ingress
-		expectedExtra *extra
-		expectedError field.ErrorList
-	}{
-		{
-			name: "actually get weights",
-			ingress: networkingv1.Ingress{
-				ObjectMeta: metav1.ObjectMeta{
-					Annotations: map[string]string{
-						"nginx.ingress.kubernetes.io/canary":              "true",
-						"nginx.ingress.kubernetes.io/canary-weight":       "50",
-						"nginx.ingress.kubernetes.io/canary-weight-total": "100",
-					},
-				},
-			},
-			expectedExtra: &extra{
-				canary: &canaryAnnotations{
-					enable:      true,
-					weight:      50,
-					weightTotal: 100,
-				},
-			},
-		},
-		{
-			name: "assigns default weight total",
-			ingress: networkingv1.Ingress{
-				ObjectMeta: metav1.ObjectMeta{
-					Annotations: map[string]string{
-						"nginx.ingress.kubernetes.io/canary":        "true",
-						"nginx.ingress.kubernetes.io/canary-weight": "50",
-					},
-				},
-			},
-			expectedExtra: &extra{
-				canary: &canaryAnnotations{
-					enable:      true,
-					weight:      50,
-					weightTotal: 100,
-				},
-			},
-		},
-		{
-			name: "errors on non integer weight",
-			ingress: networkingv1.Ingress{
-				ObjectMeta: metav1.ObjectMeta{
-					Annotations: map[string]string{
-						"nginx.ingress.kubernetes.io/canary":        "true",
-						"nginx.ingress.kubernetes.io/canary-weight": "50.5",
-					},
-				},
-			},
-			expectedExtra: &extra{},
-			expectedError: field.ErrorList{field.TypeInvalid(field.NewPath(""), "", "")},
-		},
-		{
-			name: "errors on non integer weight total",
-			ingress: networkingv1.Ingress{
-				ObjectMeta: metav1.ObjectMeta{
-					Annotations: map[string]string{
-						"nginx.ingress.kubernetes.io/canary":              "true",
-						"nginx.ingress.kubernetes.io/canary-weight-total": "50.5",
-					},
-				},
-			},
-			expectedExtra: &extra{},
-			expectedError: field.ErrorList{field.TypeInvalid(field.NewPath(""), "", "")},
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-
-			actualCanary, errs := parseCanaryAnnotations(tc.ingress)
-			if len(errs) != len(tc.expectedError) {
-				t.Fatalf("expected %d errors, got %d", len(tc.expectedError), len(errs))
-			}
-
-			if len(tc.expectedError) > 0 {
-				return
-			}
-
-			expectedCanary := tc.expectedExtra.canary
-
-			if diff := cmp.Diff(actualCanary, *expectedCanary, cmp.AllowUnexported(canaryAnnotations{})); diff != "" {
-				t.Fatalf("getExtra() mismatch (-want +got):\n%s", diff)
-			}
-		})
-	}
-}
diff --git a/pkg/i2gw/providers/ingressnginx/converter_test.go b/pkg/i2gw/providers/ingressnginx/converter_test.go
deleted file mode 100644
index c5eac64..0000000
--- a/pkg/i2gw/providers/ingressnginx/converter_test.go
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
-Copyright 2023 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package ingressnginx
-
-import (
-	"errors"
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-	"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw"
-	"github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common"
-	corev1 "k8s.io/api/core/v1"
-	networkingv1 "k8s.io/api/networking/v1"
-	apiequality "k8s.io/apimachinery/pkg/api/equality"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/validation/field"
-	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
-)
-
-func Test_ToGateway(t *testing.T) {
-	iPrefix := networkingv1.PathTypePrefix
-	//iExact := networkingv1.PathTypeExact
-	gPathPrefix := gatewayv1beta1.PathMatchPathPrefix
-	//gExact := gatewayv1beta1.PathMatchExact
-
-	testCases := []struct {
-		name                     string
-		ingresses                []networkingv1.Ingress
-		expectedGatewayResources i2gw.GatewayResources
-		expectedErrors           field.ErrorList
-	}{
-		{
-			name: "canary deployment",
-			ingresses: []networkingv1.Ingress{
-				{
-					ObjectMeta: metav1.ObjectMeta{Name: "production", Namespace: "default"},
-					Spec: networkingv1.IngressSpec{
-						IngressClassName: ptrTo("ingress-nginx"),
-						Rules: []networkingv1.IngressRule{{
-							Host: "echo.prod.mydomain.com",
-							IngressRuleValue: networkingv1.IngressRuleValue{
-								HTTP: &networkingv1.HTTPIngressRuleValue{
-									Paths: []networkingv1.HTTPIngressPath{{
-										Path:     "/",
-										PathType: &iPrefix,
-										Backend: networkingv1.IngressBackend{
-											Resource: &corev1.TypedLocalObjectReference{
-												Name:     "production",
-												Kind:     "StorageBucket",
-												APIGroup: ptrTo("vendor.example.com"),
-											},
-										},
-									}},
-								},
-							},
-						}},
-					},
-				},
-				{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "canary",
-						Namespace: "default",
-						Annotations: map[string]string{
-							"nginx.ingress.kubernetes.io/canary":        "true",
-							"nginx.ingress.kubernetes.io/canary-weight": "20",
-						},
-					},
-					Spec: networkingv1.IngressSpec{
-						IngressClassName: ptrTo("ingress-nginx"),
-						Rules: []networkingv1.IngressRule{{
-							Host: "echo.prod.mydomain.com",
-							IngressRuleValue: networkingv1.IngressRuleValue{
-								HTTP: &networkingv1.HTTPIngressRuleValue{
-									Paths: []networkingv1.HTTPIngressPath{{
-										Path:     "/",
-										PathType: &iPrefix,
-										Backend: networkingv1.IngressBackend{
-											Resource: &corev1.TypedLocalObjectReference{
-												Name:     "canary",
-												Kind:     "StorageBucket",
-												APIGroup: ptrTo("vendor.example.com"),
-											},
-										},
-									}},
-								},
-							},
-						}},
-					},
-				},
-			},
-			expectedGatewayResources: i2gw.GatewayResources{
-				Gateways: map[types.NamespacedName]gatewayv1beta1.Gateway{
-					{Namespace: "default", Name: "ingress-nginx"}: {
-						ObjectMeta: metav1.ObjectMeta{Name: "ingress-nginx", Namespace: "default"},
-						Spec: gatewayv1beta1.GatewaySpec{
-							GatewayClassName: "ingress-nginx",
-							Listeners: []gatewayv1beta1.Listener{{
-								Name:     "echo-prod-mydomain-com-http",
-								Port:     80,
-								Protocol: gatewayv1beta1.HTTPProtocolType,
-								Hostname: ptrTo(gatewayv1beta1.Hostname("echo.prod.mydomain.com")),
-							}},
-						},
-					},
-				},
-				HTTPRoutes: map[types.NamespacedName]gatewayv1beta1.HTTPRoute{
-					{Namespace: "default", Name: "echo-prod-mydomain-com"}: {
-						ObjectMeta: metav1.ObjectMeta{Name: "echo-prod-mydomain-com", Namespace: "default"},
-						Spec: gatewayv1beta1.HTTPRouteSpec{
-							CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
-								ParentRefs: []gatewayv1beta1.ParentReference{{
-									Name: "ingress-nginx",
-								}},
-							},
-							Hostnames: []gatewayv1beta1.Hostname{"echo.prod.mydomain.com"},
-							Rules: []gatewayv1beta1.HTTPRouteRule{{
-								Matches: []gatewayv1beta1.HTTPRouteMatch{{
-									Path: &gatewayv1beta1.HTTPPathMatch{
-										Type:  &gPathPrefix,
-										Value: ptrTo("/"),
-									},
-								}},
-								BackendRefs: []gatewayv1beta1.HTTPBackendRef{
-									{
-										BackendRef: gatewayv1beta1.BackendRef{
-											BackendObjectReference: gatewayv1beta1.BackendObjectReference{
-												Name:  "production",
-												Group: ptrTo(gatewayv1beta1.Group("vendor.example.com")),
-												Kind:  ptrTo(gatewayv1beta1.Kind("StorageBucket")),
-											},
-											Weight: ptrTo(int32(80)),
-										},
-									},
-									{
-										BackendRef: gatewayv1beta1.BackendRef{
-											BackendObjectReference: gatewayv1beta1.BackendObjectReference{
-												Name:  "canary",
-												Group: ptrTo(gatewayv1beta1.Group("vendor.example.com")),
-												Kind:  ptrTo(gatewayv1beta1.Kind("StorageBucket")),
-											},
-											Weight: ptrTo(int32(20)),
-										},
-									},
-								},
-							}},
-						},
-					},
-				},
-			},
-			expectedErrors: field.ErrorList{},
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-
-			provider := NewProvider(&i2gw.ProviderConf{})
-
-			resources := i2gw.InputResources{
-				Ingresses:       tc.ingresses,
-				CustomResources: nil,
-			}
-
-			gatewayResources, errs := provider.ToGatewayAPI(resources)
-
-			if len(gatewayResources.HTTPRoutes) != len(tc.expectedGatewayResources.HTTPRoutes) {
-				t.Errorf("Expected %d HTTPRoutes, got %d: %+v",
-					len(tc.expectedGatewayResources.HTTPRoutes), len(gatewayResources.HTTPRoutes), gatewayResources.HTTPRoutes)
-			} else {
-				for i, got := range gatewayResources.HTTPRoutes {
-					key := types.NamespacedName{Namespace: got.Namespace, Name: got.Name}
-					want := tc.expectedGatewayResources.HTTPRoutes[key]
-					want.SetGroupVersionKind(common.HTTPRouteGVK)
-					if !apiequality.Semantic.DeepEqual(got, want) {
-						t.Errorf("Expected HTTPRoute %s to be %+v\n Got: %+v\n Diff: %s", i, want, got, cmp.Diff(want, got))
-					}
-				}
-			}
-
-			if len(gatewayResources.Gateways) != len(tc.expectedGatewayResources.Gateways) {
-				t.Errorf("Expected %d Gateways, got %d: %+v",
-					len(tc.expectedGatewayResources.Gateways), len(gatewayResources.Gateways), gatewayResources.Gateways)
-			} else {
-				for i, got := range gatewayResources.Gateways {
-					key := types.NamespacedName{Namespace: got.Namespace, Name: got.Name}
-					want := tc.expectedGatewayResources.Gateways[key]
-					want.SetGroupVersionKind(common.GatewayGVK)
-					if !apiequality.Semantic.DeepEqual(got, want) {
-						t.Errorf("Expected Gateway %s to be %+v\n Got: %+v\n Diff: %s", i, want, got, cmp.Diff(want, got))
-					}
-				}
-			}
-
-			if len(errs) != len(tc.expectedErrors) {
-				t.Errorf("Expected %d errors, got %d: %+v", len(tc.expectedErrors), len(errs), errs)
-			} else {
-				for i, e := range errs {
-					if errors.Is(e, tc.expectedErrors[i]) {
-						t.Errorf("Unexpected error message at %d index. Got %s, want: %s", i, e, tc.expectedErrors[i])
-					}
-				}
-			}
-		})
-	}
-}
-
-func ptrTo[T any](a T) *T {
-	return &a
-}