From 62b33c440279e4a571b1d2ec188aa08c61bda868 Mon Sep 17 00:00:00 2001 From: Nick Nellis Date: Tue, 22 Oct 2024 08:25:07 -0500 Subject: [PATCH 1/5] prints gateway api resources --- go.mod | 1 + go.sum | 2 + .../gloo/cli/pkg/cmd/gatewayapi/command.go | 16 + .../cli/pkg/cmd/gatewayapi/convert/command.go | 1011 +++++++++++++++++ .../cli/pkg/cmd/gatewayapi/convert/domain.go | 184 +++ .../cli/pkg/cmd/gatewayapi/convert/helpers.go | 50 + .../cli/pkg/cmd/gatewayapi/convert/metrics.go | 77 ++ projects/gloo/cli/pkg/cmd/root.go | 2 + 8 files changed, 1343 insertions(+) create mode 100644 projects/gloo/cli/pkg/cmd/gatewayapi/command.go create mode 100644 projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go create mode 100644 projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go create mode 100644 projects/gloo/cli/pkg/cmd/gatewayapi/convert/helpers.go create mode 100644 projects/gloo/cli/pkg/cmd/gatewayapi/convert/metrics.go diff --git a/go.mod b/go.mod index 5ba55925d07..e31f63ffcd1 100644 --- a/go.mod +++ b/go.mod @@ -215,6 +215,7 @@ require ( github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.3.1 // indirect + github.com/itchyny/json2yaml v0.1.4 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jmespath/go-jmespath v0.3.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect diff --git a/go.sum b/go.sum index f25d81b1bfb..172d983a8f7 100644 --- a/go.sum +++ b/go.sum @@ -2189,6 +2189,8 @@ github.com/influxdata/tdigest v0.0.0-20180711151920-a7d76c6f093a/go.mod h1:9Gkys github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= +github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8= +github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/command.go b/projects/gloo/cli/pkg/cmd/gatewayapi/command.go new file mode 100644 index 00000000000..5fdcf3318c3 --- /dev/null +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/command.go @@ -0,0 +1,16 @@ +package gatewayapi + +import ( + "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/gatewayapi/convert" + "github.com/spf13/cobra" +) + +func RootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "gateway-api", + Short: "Gateway API specific commands", + } + cmd.SilenceUsage = true + cmd.AddCommand(convert.RootCmd()) + return cmd +} diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go new file mode 100644 index 00000000000..eb2b9eee8d2 --- /dev/null +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go @@ -0,0 +1,1011 @@ +package convert + +import ( + "errors" + "fmt" + "log" + "math/rand" + "os" + "path/filepath" + "regexp" + "strings" + + gloogwv1 "github.com/solo-io/gloo/projects/gateway/pkg/api/v1" + gatewaykube "github.com/solo-io/gloo/projects/gateway/pkg/api/v1/kube/apis/gateway.solo.io/v1" + gloov1 "github.com/solo-io/gloo/projects/gloo/pkg/api/v1" + "github.com/solo-io/gloo/projects/gloo/pkg/api/v1/core/matchers" + "github.com/solo-io/gloo/projects/gloo/pkg/api/v1/enterprise/options/gcp" + glookube "github.com/solo-io/gloo/projects/gloo/pkg/api/v1/kube/apis/gloo.solo.io/v1" + v3 "github.com/solo-io/skv2/pkg/api/core.skv2.solo.io/v1" + v2 "github.com/solo-io/solo-apis/pkg/api/enterprise.gloo.solo.io/v1" + "github.com/spf13/cobra" + "google.golang.org/protobuf/types/known/wrapperspb" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/utils/ptr" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +const ( + RandomSuffix = 4 +) + +var runtimeScheme *runtime.Scheme +var codecs serializer.CodecFactory +var decoder runtime.Decoder + +func RootCmd() *cobra.Command { + rand.Seed(1) + opts := &Options{} + cmd := &cobra.Command{ + Use: "convert", + Short: "Convert Gloo Edge APIs to Gateway API", + RunE: func(cmd *cobra.Command, args []string) error { + return run(opts) + }, + } + opts.addToFlags(cmd.PersistentFlags()) + cmd.SilenceUsage = true + return cmd +} + +func run(opts *Options) error { + + foundFiles, err := findFiles(opts) + if err != nil { + return err + } + + var inputs []*GlooEdgeInput + + for _, file := range foundFiles { + input, err := translateFileToEdgeInput(file) + if err != nil { + return err + } + inputs = append(inputs, input) + } + // All the objects have been found in all the files + + // now we need to convert the easy stuff like route tables + var outputs []*GatewayAPIOutput + for _, input := range inputs { + output, err := translateEdgeAPIToGatewayAPI(input, opts) + if err != nil { + return err + } + outputs = append(outputs, output) + } + + // we need to parse through the known delegated routes and translate them + for _, output := range outputs { + doDelegation(output) + } + + // write all the outputs to their files + for _, output := range outputs { + fmt.Fprintf(os.Stdout, "\n\n---\n# --------------------------------\n# %s\n# --------------------------------", output.FileName) + txt, err := output.ToString() + if err != nil { + return err + } + fmt.Fprintf(os.Stdout, "%s\n", txt) + } + + return nil +} + +// find all the HTTPRoutes that match and add a parent ref to the correct parent +func doDelegation(output *GatewayAPIOutput) { + for _, delegateRoute := range output.DelegationReferences { + addParentRefsToHTTPRoutes(output, delegateRoute) + } +} + +func addParentRefsToHTTPRoutes(output *GatewayAPIOutput, delegateRoute *DelegateParentReference) { + + // for each delegate route we need to go find the RouteTable that matches and assign a parent + for _, httpRoute := range output.HTTPRoutes { + if len(httpRoute.Labels) > 0 && doHttpRouteLabelsMatch(delegateRoute.Labels, httpRoute.Labels) { + + parentRef := gwv1.ParentReference{ + Name: gwv1.ObjectName(delegateRoute.ParentName), + Namespace: (*gwv1.Namespace)(&delegateRoute.ParentNamespace), + Kind: (*gwv1.Kind)(ptr.To("HTTPRoute")), + Group: (*gwv1.Group)(ptr.To("gateway.networking.k8s.io")), + } + + httpRoute.Spec.ParentRefs = append(httpRoute.Spec.ParentRefs, parentRef) + } + } +} + +func doHttpRouteLabelsMatch(matches map[string]string, labels map[string]string) bool { + for k, v := range matches { + if labels[k] != v { + return false + } + } + return true +} + +func translateEdgeAPIToGatewayAPI(input *GlooEdgeInput, opts *Options) (*GatewayAPIOutput, error) { + + output := &GatewayAPIOutput{ + FileName: input.FileName, + YamlObjects: input.YamlObjects, + VirtualHostOptions: input.VirtualHostOptions, + RouteOptions: input.RouteOptions, + AuthConfigs: input.AuthConfigs, + // Gateways: input.Gateways, + } + + for _, upstream := range input.Upstreams { + if opts.GCPRegex != "" { + newUpstream, err := convertGCPUpstream(upstream, opts.GCPRegex) + if err != nil { + return nil, err + } + // only update it if we get something back + if newUpstream != nil { + upstream = newUpstream + } + } + output.Upstreams = append(output.Upstreams, upstream) + } + + for _, authConfig := range input.AuthConfigs { + // In the past users had to place gcp auth in authConfig instead of Upstream, if true then we need to remove the AuthConfig + if opts.RemoveGCPAUthConfig && authConfigContainsGCPAuth(authConfig) { + newAuthConfig := removeGCPAuthFromAuthConfig(authConfig) + + // if gcp_auth was the only config then we dont need to render it + if newAuthConfig != nil && len(newAuthConfig.Spec.Configs) > 0 { + output.AuthConfigs = append(output.AuthConfigs, newAuthConfig) + } + } + } + + for _, routeTable := range input.RouteTables { + httpRoute, routeOptions, delegates, err := convertRouteTableToHTTPRoute(routeTable) + if err != nil { + return nil, err + } + output.HTTPRoutes = append(output.HTTPRoutes, httpRoute) + if len(delegates) > 0 { + output.DelegationReferences = append(output.DelegationReferences, delegates...) + } + output.RouteOptions = append(output.RouteOptions, routeOptions...) + } + + for _, virtualService := range input.VirtualServices { + httpRoute, routeOptions, virtualHostOptions, delegates, err := convertVSToHTTPRoute(virtualService) + if err != nil { + return nil, err + } + output.HTTPRoutes = append(output.HTTPRoutes, httpRoute) + output.RouteOptions = append(output.RouteOptions, routeOptions...) + output.VirtualHostOptions = append(output.VirtualHostOptions, virtualHostOptions...) + if len(delegates) > 0 { + output.DelegationReferences = append(output.DelegationReferences, delegates...) + } + } + + return output, nil +} + +func convertVSToHTTPRoute(vs *gatewaykube.VirtualService) ( + *gwv1.HTTPRoute, + []*gatewaykube.RouteOption, + []*gatewaykube.VirtualHostOption, + []*DelegateParentReference, + error, +) { + var options []*gatewaykube.RouteOption + var vhOptions []*gatewaykube.VirtualHostOption + var delegates []*DelegateParentReference + hr := &gwv1.HTTPRoute{ + TypeMeta: vs.TypeMeta, + ObjectMeta: vs.ObjectMeta, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName("http"), + Namespace: (*gwv1.Namespace)(ptr.To("gloo-system")), + Kind: (*gwv1.Kind)(ptr.To("Gateway")), + Group: (*gwv1.Group)(ptr.To("gateway.networking.k8s.io")), + }, + }, + }, + Hostnames: convertDomains(vs.Spec.VirtualHost.Domains), + Rules: []gwv1.HTTPRouteRule{}, + }, + } + // To avoid naming collisions we need to postpend VW HTTPRoutes with -vs + hr.GetObjectMeta().SetName(hr.GetObjectMeta().GetName() + "-vs") + + for _, route := range vs.Spec.VirtualHost.Routes { + rr, option, genDelegates, err := convertRouteToRule(route, vs.Name, vs.Namespace) + if err != nil { + return nil, nil, nil, nil, err + } + if genDelegates != nil { + delegates = append(delegates, genDelegates) + } + + hr.Spec.Rules = append(hr.Spec.Rules, rr) + if option != nil { + options = append(options, option) + } + if route.GetOptions() != nil { + routeOptions := route.GetOptions() + + // prefix rewrite, sets it on HTTPRoute + if routeOptions.GetPrefixRewrite() != nil { + rr.Filters = append(rr.Filters, gwv1.HTTPRouteFilter{ + Type: gwv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gwv1.HTTPURLRewriteFilter{ + Path: &gwv1.HTTPPathModifier{ + Type: gwv1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptr.To(routeOptions.GetPrefixRewrite().Value), + }, + }, + }) + } + } + if vs.Spec.VirtualHost.GetOptions() != nil { + vo := vs.Spec.VirtualHost.GetOptions() + + opt, filter := convertVirtualHostOptions(vo, vs.Namespace, vs.Name) + if filter != nil { + rr.Filters = append(rr.Filters, *filter) + } + vhOptions = append(vhOptions, opt) + } + } + + return hr, options, vhOptions, delegates, nil +} + +func convertVirtualHostOptions( + options *gloov1.VirtualHostOptions, + namespace string, + routeName string, +) (*gatewaykube.VirtualHostOption, *gwv1.HTTPRouteFilter) { + var ro *gatewaykube.VirtualHostOption + var filter *gwv1.HTTPRouteFilter + associationID := RandStringRunes(8) + if routeName == "" { + routeName = "vh-association" + } + associationName := fmt.Sprintf("%s-%s", routeName, associationID) + + // converts options to RouteOptions but we need to this for everything except prefix rewrite + ro = &gatewaykube.VirtualHostOption{ + TypeMeta: v1.TypeMeta{ + Kind: "VirtualHostOption", + APIVersion: "gateway.solo.io/v1", + }, + ObjectMeta: v1.ObjectMeta{ + Name: associationName, + Namespace: namespace, + }, + Spec: gloogwv1.VirtualHostOption{ + Options: options, + // TODO we just referenece a non existant gateway today + TargetRefs: []*v3.PolicyTargetReferenceWithSectionName{ + { + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "http", + Namespace: wrapperspb.String("gloo-system"), + }, + }, + }, + } + + filter = &gwv1.HTTPRouteFilter{ + Type: gwv1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gwv1.LocalObjectReference{ + Group: "gateway.solo.io", + Kind: "RouteOption", + Name: gwv1.ObjectName(associationName), + }, + } + + return ro, filter +} + +func convertRouteTableToHTTPRoute(rt *gatewaykube.RouteTable) ( + *gwv1.HTTPRoute, + []*gatewaykube.RouteOption, + []*DelegateParentReference, + error, +) { + + var options []*gatewaykube.RouteOption + var delegates []*DelegateParentReference + + hr := &gwv1.HTTPRoute{ + TypeMeta: rt.TypeMeta, + ObjectMeta: rt.ObjectMeta, + Spec: gwv1.HTTPRouteSpec{ + // CommonRouteSpec: gwv1.CommonRouteSpec{}, + // Hostnames: [], + Rules: []gwv1.HTTPRouteRule{}, + }, + } + + for _, route := range rt.Spec.Routes { + rule, option, delegate, err := convertRouteToRule(route, rt.Name, rt.Namespace) + if err != nil { + return nil, nil, nil, err + } + hr.Spec.Rules = append(hr.Spec.Rules, rule) + if option != nil { + options = append(options, option) + } + if delegate != nil { + delegates = append(delegates, delegate) + } + } + + return hr, options, delegates, nil +} + +func convertRouteToRule(r *gloogwv1.Route, routeTableName string, routeTableNamespace string) ( + gwv1.HTTPRouteRule, + *gatewaykube.RouteOption, + *DelegateParentReference, + error, +) { + var ro *gatewaykube.RouteOption + + rr := gwv1.HTTPRouteRule{ + Matches: []gwv1.HTTPRouteMatch{}, + Filters: []gwv1.HTTPRouteFilter{}, + BackendRefs: []gwv1.HTTPBackendRef{}, + } + + for _, m := range r.Matchers { + match, err := convertMatch(m) + if err != nil { + return rr, nil, nil, err + } + rr.Matches = append(rr.Matches, match) + } + if r.GetRedirectAction() != nil { + + rf, err := generateFilterForRedirectAction(r) + if err != nil { + return rr, nil, nil, err + } + rr.Filters = append(rr.Filters, rf) + } + if r.GetOptions() != nil { + options := r.GetOptions() + + // prefix rewrite, sets it on HTTPRoute + if options.GetPrefixRewrite() != nil { + rf, err := generateFilterForURLRewrite(r) + if err != nil { + return rr, nil, nil, err + } + rr.Filters = append(rr.Filters, rf) + } + + var filter *gwv1.HTTPRouteFilter + ro, filter = convertRouteOptions(options, r.Name, routeTableNamespace) + if filter != nil { + rr.Filters = append(rr.Filters, *filter) + } + } + var delegate *DelegateParentReference + + if r.GetRouteAction() != nil && r.GetRouteAction().GetSingle() != nil { + // single static upstream + if r.GetRouteAction().GetSingle().GetUpstream() != nil { + backendRef := generateBackendRefForSingleUpstream(r) + + rr.BackendRefs = append(rr.BackendRefs, backendRef) + } + } else if r.GetDelegateAction() != nil { + // intermediate delegation step. This is a placeholder for the next path to do delegation + backendRef, genDelegates := generateBackendRefForDelegateAction(r, routeTableName, routeTableNamespace) + + if len(backendRef) > 0 { + for _, b := range backendRef { + rr.BackendRefs = append(rr.BackendRefs, *b) + } + } + delegate = genDelegates + } + + return rr, ro, delegate, nil +} + +func convertMatch(m *matchers.Matcher) (gwv1.HTTPRouteMatch, error) { + hrm := gwv1.HTTPRouteMatch{ + QueryParams: []gwv1.HTTPQueryParamMatch{}, + } + + // header matching + if len(m.Headers) > 0 { + hrm.Headers = []gwv1.HTTPHeaderMatch{} + for _, h := range m.Headers { + if h.InvertMatch == true { + return hrm, errors.New("invert match not currently supported") + } + if h.Regex { + hrm.Headers = append(hrm.Headers, gwv1.HTTPHeaderMatch{ + Type: ptr.To(gwv1.HeaderMatchRegularExpression), + Value: h.Value, + Name: gwv1.HTTPHeaderName(h.Name), + }) + } else { + hrm.Headers = append(hrm.Headers, gwv1.HTTPHeaderMatch{ + Type: ptr.To(gwv1.HeaderMatchExact), + Value: h.Value, + Name: gwv1.HTTPHeaderName(h.Name), + }) + } + } + // TODO support Invert header match https://github.com/solo-io/gloo/blob/main/projects/gateway2/translator/httproute/gateway_http_route_translator.go#L274 + } + + // method mathching + if len(m.Methods) > 0 { + if len(m.Methods) > 1 { + return hrm, errors.New(fmt.Sprintf("Gateway API only supports 1 method match per rule and %d were detected", len(m.Methods))) + } + hrm.Method = (*gwv1.HTTPMethod)(ptr.To(m.Methods[0])) + } + + // query param matching + if len(m.QueryParameters) > 0 { + for _, m := range m.QueryParameters { + if m.Regex { + hrm.QueryParams = append(hrm.QueryParams, gwv1.HTTPQueryParamMatch{ + Type: ptr.To(gwv1.QueryParamMatchRegularExpression), + Name: (gwv1.HTTPHeaderName)(m.Name), + Value: m.Value, + }) + } else { + hrm.QueryParams = append(hrm.QueryParams, gwv1.HTTPQueryParamMatch{ + Type: ptr.To(gwv1.QueryParamMatchExact), + Name: (gwv1.HTTPHeaderName)(m.Name), + Value: m.Value, + }) + } + } + } + + // Path matching + if m.GetPathSpecifier() != nil { + if m.GetPrefix() != "" { + hrm.Path = &gwv1.HTTPPathMatch{ + Type: ptr.To(gwv1.PathMatchPathPrefix), + Value: ptr.To(m.GetPrefix()), + } + } + if m.GetExact() != "" { + hrm.Path = &gwv1.HTTPPathMatch{ + Type: ptr.To(gwv1.PathMatchExact), + Value: ptr.To(m.GetExact()), + } + } + if m.GetRegex() != "" { + hrm.Path = &gwv1.HTTPPathMatch{ + Type: ptr.To(gwv1.PathMatchRegularExpression), + Value: ptr.To(m.GetRegex()), + } + } + } + return hrm, nil +} + +// Converts a single upstream to a GatewayAPI backend ref +func generateBackendRefForSingleUpstream(r *gloogwv1.Route) gwv1.HTTPBackendRef { + upstream := r.GetRouteAction().GetSingle().GetUpstream() + + // static upstream reference + backendRef := gwv1.HTTPBackendRef{ + BackendRef: gwv1.BackendRef{ + BackendObjectReference: gwv1.BackendObjectReference{ + Name: gwv1.ObjectName(upstream.GetName()), + Namespace: (*gwv1.Namespace)(ptr.To(upstream.GetNamespace())), + Kind: (*gwv1.Kind)(ptr.To("Upstream")), + Group: (*gwv1.Group)(ptr.To("gloo.solo.io")), + }, + }, + } + + // AWS lambda integration + if r.GetRouteAction().GetSingle().GetDestinationSpec() != nil && r.GetRouteAction().GetSingle().GetDestinationSpec().GetAws() != nil { + // we need to add a parameter for the lambda name reference + backendRef.Filters = append(backendRef.Filters, gwv1.HTTPRouteFilter{ + Type: gwv1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gwv1.LocalObjectReference{ + Kind: "Parameter", + Group: "gloo.solo.io", + Name: (gwv1.ObjectName)(r.GetRouteAction().GetSingle().GetDestinationSpec().GetAws().LogicalName), + }, + }) + } + return backendRef +} + +func generateFilterForURLRewrite(r *gloogwv1.Route) (gwv1.HTTPRouteFilter, error) { + + rf := gwv1.HTTPRouteFilter{ + Type: gwv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gwv1.HTTPURLRewriteFilter{ + Path: &gwv1.HTTPPathModifier{}, + }, + } + match, err := isExactMatch(r.GetMatchers()) + if err != nil { + return rf, errors.New(fmt.Sprintf("RouteTable %s has multiple matchers in same route", r.Name)) + } + if match { + rf.URLRewrite.Path.Type = gwv1.FullPathHTTPPathModifier + rf.URLRewrite.Path.ReplaceFullPath = ptr.To(r.GetOptions().GetPrefixRewrite().Value) + rf.URLRewrite.Path.ReplacePrefixMatch = nil + } + match, err = isPrefixMatch(r.GetMatchers()) + if err != nil { + return rf, errors.New(fmt.Sprintf("RouteTable %s has multiple matchers in same route", r.Name)) + } + + if match { + rf.URLRewrite.Path.Type = gwv1.PrefixMatchHTTPPathModifier + rf.URLRewrite.Path.ReplacePrefixMatch = ptr.To(r.GetOptions().GetPrefixRewrite().Value) + rf.URLRewrite.Path.ReplaceFullPath = nil + } + + // TODO regex rewrite, NOT SUPPORTED IN GATEWAY API + if r.GetOptions().GetRegexRewrite() != nil { + return rf, errors.New(fmt.Sprintf("Reject rewrite not supported, need to convert to another match")) + } + // rr.Filters = append(rr.Filters, gwv1.HTTPRouteFilter{ + // Type: gwv1.HTTPRouteFilterURLRewrite, + // URLRewrite: &gwv1.HTTPURLRewriteFilter{ + // Path: &gwv1.HTTPPathModifier{ + // Type: gwv1.HTTPPathModifierType(gwv1.PathMatchRegularExpression), + // ReplacePrefixMatch: ptr.To(options.GetRegexRewrite().get), + // }, + // }, + // }) + // } + + return rf, nil +} + +func generateBackendRefForDelegateAction( + r *gloogwv1.Route, + routeTableName string, + routeTableNamespace string, +) ([]*gwv1.HTTPBackendRef, *DelegateParentReference) { + var backends []*gwv1.HTTPBackendRef + if r.GetDelegateAction().GetRef() != nil { + delegate := r.GetDelegateAction().GetRef() + backendRef := &gwv1.HTTPBackendRef{ + BackendRef: gwv1.BackendRef{ + BackendObjectReference: gwv1.BackendObjectReference{ + Name: gwv1.ObjectName(delegate.GetName()), + Namespace: (*gwv1.Namespace)(ptr.To(delegate.GetNamespace())), + Kind: (*gwv1.Kind)(ptr.To("HTTPRoute")), + Group: (*gwv1.Group)(ptr.To("gateway.networking.k8s.io")), + }, + }, + } + backends = append(backends, backendRef) + return backends, nil + } else if r.GetDelegateAction().GetSelector() != nil { + + selector := r.GetDelegateAction().GetSelector() + + delegateParentRef := &DelegateParentReference{ + Labels: selector.Labels, + ParentName: routeTableName, + ParentNamespace: routeTableNamespace, + } + if len(r.GetDelegateAction().GetSelector().Namespaces) > 0 { + for _, namespace := range r.GetDelegateAction().GetSelector().Namespaces { + backendRef := &gwv1.HTTPBackendRef{ + BackendRef: gwv1.BackendRef{ + BackendObjectReference: gwv1.BackendObjectReference{ + Name: "*", + Namespace: (*gwv1.Namespace)(&namespace), + Kind: (*gwv1.Kind)(ptr.To("HTTPRoute")), + Group: (*gwv1.Group)(ptr.To("gateway.networking.k8s.io")), + }, + }, + } + backends = append(backends, backendRef) + } + } else { + // default is gloo system for namespace if none are selected + backendRef := &gwv1.HTTPBackendRef{ + BackendRef: gwv1.BackendRef{ + BackendObjectReference: gwv1.BackendObjectReference{ + Name: "*", + Namespace: (*gwv1.Namespace)(ptr.To("gloo-system")), + Kind: (*gwv1.Kind)(ptr.To("HTTPRoute")), + Group: (*gwv1.Group)(ptr.To("gateway.networking.k8s.io")), + }, + }, + } + backends = append(backends, backendRef) + } + + return backends, delegateParentRef + } + return nil, nil +} + +func generateFilterForRedirectAction(r *gloogwv1.Route) (gwv1.HTTPRouteFilter, error) { + var statusCode int + + switch r.GetRedirectAction().ResponseCode { + case gloov1.RedirectAction_MOVED_PERMANENTLY: + statusCode = 301 + case gloov1.RedirectAction_FOUND: + statusCode = 302 + case gloov1.RedirectAction_SEE_OTHER: + statusCode = 303 + case gloov1.RedirectAction_TEMPORARY_REDIRECT: + statusCode = 307 + case gloov1.RedirectAction_PERMANENT_REDIRECT: + statusCode = 308 + default: + statusCode = 301 + } + + rf := gwv1.HTTPRouteFilter{ + Type: gwv1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gwv1.HTTPRequestRedirectFilter{ + StatusCode: ptr.To(statusCode), + Path: &gwv1.HTTPPathModifier{}, + }, + } + if r.GetRedirectAction().HostRedirect != "" { + rf.RequestRedirect.Hostname = ptr.To(gwv1.PreciseHostname(r.GetRedirectAction().HostRedirect)) + } + if r.GetRedirectAction().PortRedirect != nil { + rf.RequestRedirect.Port = ptr.To(gwv1.PortNumber(r.GetRedirectAction().PortRedirect.Value)) + } + if r.GetRedirectAction().HttpsRedirect == true { + rf.RequestRedirect.Scheme = ptr.To("https") + } + // TODO we dont support stripQuery https://github.com/solo-io/gloo/blob/main/projects/gateway2/translator/plugins/redirect/redirect_plugin.go#L43 + // if r.GetRedirectAction().StripQuery == true { + // rf.RequestRedirect. + // } + + if r.GetRedirectAction().GetPathRedirect() != "" { + + match, err := isExactMatch(r.GetMatchers()) + if err != nil { + return rf, errors.New(fmt.Sprintf("RouteTable %s has multiple matchers in same route", r.Name)) + } + if match { + rf.RequestRedirect.Path.Type = gwv1.FullPathHTTPPathModifier + rf.RequestRedirect.Path.ReplaceFullPath = ptr.To(r.GetRedirectAction().GetPathRedirect()) + rf.RequestRedirect.Path.ReplacePrefixMatch = nil + } + match, err = isPrefixMatch(r.GetMatchers()) + if err != nil { + return rf, errors.New(fmt.Sprintf("RouteTable %s has multiple matchers in same route", r.Name)) + } + + if match { + rf.RequestRedirect.Path.Type = gwv1.PrefixMatchHTTPPathModifier + rf.RequestRedirect.Path.ReplacePrefixMatch = ptr.To(r.GetRedirectAction().GetPathRedirect()) + rf.RequestRedirect.Path.ReplaceFullPath = nil + } + } + return rf, nil +} + +func convertRouteOptions( + options *gloov1.RouteOptions, + routeName string, + routeTableNamespace string, +) (*gatewaykube.RouteOption, *gwv1.HTTPRouteFilter) { + var ro *gatewaykube.RouteOption + var filter *gwv1.HTTPRouteFilter + associationID := RandStringRunes(RandomSuffix) + if routeName == "" { + routeName = "route-association" + } + associationName := fmt.Sprintf("%s-%s", routeName, associationID) + + // converts options to RouteOptions but we need to this for everything except prefixrewrite + if isRouteOptionsSet(options) { + ro = &gatewaykube.RouteOption{ + TypeMeta: v1.TypeMeta{ + Kind: "RouteOption", + APIVersion: "gateway.solo.io/v1", + }, + ObjectMeta: v1.ObjectMeta{ + Name: associationName, + Namespace: routeTableNamespace, + }, + Spec: gloogwv1.RouteOption{ + Options: options, + }, + } + + // Because we move rewrites to a filter we need to remove it from RouteOptions + if options.GetPrefixRewrite() != nil { + ro.Spec.GetOptions().PrefixRewrite = nil + } + + filter = &gwv1.HTTPRouteFilter{ + Type: gwv1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gwv1.LocalObjectReference{ + Group: "gateway.solo.io", + Kind: "RouteOption", + Name: gwv1.ObjectName(associationName), + }, + } + } + return ro, filter +} + +// TODO this is a mess +func isRouteOptionsSet(options *gloov1.RouteOptions) bool { + return options.GetExtProc() != nil || options.GetCors() != nil || options.GetRetries() != nil || options.GetTimeout() != nil || + options.GetStagedTransformations() != nil || options.GetAutoHostRewrite() != nil || + options.GetFaults() != nil || options.GetExtensions() != nil || options.GetTracing() != nil || options.GetShadowing() != nil || + options.GetHeaderManipulation() != nil || options.GetAppendXForwardedHost() != nil || options.GetLbHash() != nil || options.GetUpgrades() != nil || + options.GetRatelimit() != nil || options.GetRatelimitBasic() != nil || options.GetWaf() != nil || options.GetJwtConfig() != nil || options.GetRbac() != nil || + options.GetDlp() != nil || options.GetStagedTransformations() != nil || options.GetEnvoyMetadata() != nil || options.GetMaxStreamDuration() != nil || + options.GetIdleTimeout() != nil || options.GetRegexRewrite() != nil || options.GetExtauth() != nil +} + +func removeGCPAuthFromAuthConfig(config *v2.AuthConfig) *v2.AuthConfig { + newAuth := &v2.AuthConfig{ + TypeMeta: config.TypeMeta, + ObjectMeta: config.ObjectMeta, + Spec: v2.AuthConfigSpec{ + BooleanExpr: config.Spec.BooleanExpr, + FailOnRedirect: config.Spec.FailOnRedirect, + }, + } + + for _, config := range config.Spec.Configs { + const gcpAuthPluginName = "gcp_auth" + if config.GetPluginAuth() == nil { + newAuth.Spec.Configs = append(newAuth.Spec.Configs, config) + } else if config.GetPluginAuth().Name != gcpAuthPluginName { + newAuth.Spec.Configs = append(newAuth.Spec.Configs, config) + } + } + return newAuth +} + +func authConfigContainsGCPAuth(config *v2.AuthConfig) bool { + if len(config.Spec.Configs) > 0 { + for _, config := range config.Spec.Configs { + if config.GetPluginAuth() != nil && config.GetPluginAuth().Name == "gcp_auth" { + return true + } + } + } + return false +} + +// this function looks for a matching regex in the upstream host to determine if its a gcp function or not +func convertGCPUpstream(upstream *glookube.Upstream, regex string) (*glookube.Upstream, error) { + if upstream.Spec.GetStatic() != nil && len(upstream.Spec.GetStatic().GetHosts()) > 0 { + if len(upstream.Spec.GetStatic().GetHosts()) > 1 { + return nil, errors.New("unable to convert upstream to gcp, more than one host listed: " + upstream.Name) + } + for _, h := range upstream.Spec.GetStatic().Hosts { + match, _ := regexp.MatchString(regex, h.Addr) + if match { + // log.Printf("Found regex %s: %s File: %s", upstreamRegex, h.Addr, filePath) + // we need to replace the Upstream with a new gcp one + newO := &glookube.Upstream{ + TypeMeta: upstream.TypeMeta, + ObjectMeta: upstream.ObjectMeta, + Spec: gloov1.Upstream{ + UpstreamType: &gloov1.Upstream_Gcp{ + Gcp: &gcp.UpstreamSpec{ + Host: h.Addr, + }, + }, + }, + } + return newO, nil + } + } + } + return nil, nil +} + +func translateFileToEdgeInput(fileName string) (*GlooEdgeInput, error) { + + gei := &GlooEdgeInput{ + FileName: fileName, + } + + // Read the file + data, err := os.ReadFile(fileName) + if err != nil { + return gei, err + } + for _, resourceYAML := range strings.Split(string(data), "---") { + if len(resourceYAML) == 0 { + continue + } + // yaml to object + obj, k, err := decoder.Decode([]byte(resourceYAML), nil, nil) + if err != nil { + if runtime.IsNotRegisteredError(err) { + // we just want to add the yaml and move on + gei.YamlObjects = append(gei.YamlObjects, resourceYAML) + continue + } + + // if we cant decode it, don't do anything and continue + log.Printf("# Skipping object due to error %s", err) + continue + } + switch o := obj.(type) { + case *v2.AuthConfig: + gei.AuthConfigs = append(gei.AuthConfigs, o) + case *glookube.Upstream: + glooConfigMetric.WithLabelValues("Upstream").Inc() + gei.Upstreams = append(gei.Upstreams, o) + case *gatewaykube.RouteTable: + glooConfigMetric.WithLabelValues("RouteTable").Inc() + gei.RouteTables = append(gei.RouteTables, o) + case *gatewaykube.VirtualService: + glooConfigMetric.WithLabelValues("VirtualService").Inc() + gei.VirtualServices = append(gei.VirtualServices, o) + case *gatewaykube.RouteOption: + glooConfigMetric.WithLabelValues("RouteOption").Inc() + gei.YamlObjects = append(gei.YamlObjects, resourceYAML) + case *gatewaykube.VirtualHostOption: + glooConfigMetric.WithLabelValues("VirtualHostOption").Inc() + gei.YamlObjects = append(gei.YamlObjects, resourceYAML) + case *gatewaykube.Gateway: + glooConfigMetric.WithLabelValues("Gateway").Inc() + gei.YamlObjects = append(gei.YamlObjects, resourceYAML) + default: + // if we dont know what type it is we just add it back + // no change so just add it back + glooConfigMetric.WithLabelValues(k.Kind).Inc() + gei.YamlObjects = append(gei.YamlObjects, resourceYAML) + } + } + return gei, nil +} + +func init() { + runtimeScheme = runtime.NewScheme() + + if err := glookube.AddToScheme(runtimeScheme); err != nil { + log.Fatal(err) + } + if err := gatewaykube.AddToScheme(runtimeScheme); err != nil { + log.Fatal(err) + } + if err := v2.AddToScheme(runtimeScheme); err != nil { + log.Fatal(err) + } + if err := gwv1.Install(runtimeScheme); err != nil { + log.Fatal(err) + } + codecs = serializer.NewCodecFactory(runtimeScheme) + decoder = codecs.UniversalDeserializer() +} + +func findFiles(opts *Options) ([]string, error) { + var files []string + if opts.Directory != "" { + fs, err := findYamlFiles(opts.Directory) + if err != nil { + return nil, err + } + files = fs + } else { + files = append(files, opts.InputFile) + } + + return files, nil +} + +func findYamlFiles(directory string) ([]string, error) { + var files []string + libRegEx, e := regexp.Compile("^.+\\.(yaml|yml)$") + if e != nil { + return nil, e + } + + e = filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { + if err == nil && libRegEx.MatchString(info.Name()) { + // println(path) + if !strings.Contains(info.Name(), "kustomization") { + files = append(files, path) + } + } + return nil + }) + if e != nil { + return nil, e + } + return files, nil +} + +// This function validates that the RouteRable matchers are the same match type prefix or exact +// The reason being is that if you are doing a rewrite you can only have one type of filter applied +func validateMatchersAreSame(matches []*matchers.Matcher) error { + + var foundExact, foundPrefix, foundRegex bool + for _, m := range matches { + if m.GetExact() != "" { + if foundPrefix || foundRegex { + return errors.New("multiple matchers found") + } + foundExact = true + } + if m.GetPrefix() != "" { + if foundExact || foundRegex { + return errors.New("multiple matchers found") + } + foundPrefix = true + } + if m.GetRegex() != "" { + if foundExact || foundPrefix { + return errors.New("multiple matchers found") + } + foundRegex = true + } + } + return nil +} + +// tests to see if all matchers are exact +func isExactMatch(matches []*matchers.Matcher) (bool, error) { + if err := validateMatchersAreSame(matches); err != nil { + return false, err + } + for _, m := range matches { + if m.GetExact() != "" { + return true, nil + } + } + return false, nil +} + +// tests to see if all matchers are exact +func isPrefixMatch(matches []*matchers.Matcher) (bool, error) { + if err := validateMatchersAreSame(matches); err != nil { + return false, err + } + for _, m := range matches { + if m.GetPrefix() != "" { + return true, nil + } + } + return false, nil +} + +// tests to see if all matchers are regex +func isRegexMatch(matches []*matchers.Matcher) (bool, error) { + if err := validateMatchersAreSame(matches); err != nil { + return false, err + } + for _, m := range matches { + if m.GetRegex() != "" { + return true, nil + } + } + return false, nil +} diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go new file mode 100644 index 00000000000..eee9a2f7624 --- /dev/null +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go @@ -0,0 +1,184 @@ +package convert + +import ( + "bytes" + "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/options" + "strings" + + "github.com/itchyny/json2yaml" + gatewaykube "github.com/solo-io/gloo/projects/gateway/pkg/api/v1/kube/apis/gateway.solo.io/v1" + glookube "github.com/solo-io/gloo/projects/gloo/pkg/api/v1/kube/apis/gloo.solo.io/v1" + v1 "github.com/solo-io/solo-apis/pkg/api/enterprise.gloo.solo.io/v1" + "github.com/spf13/pflag" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +type Options struct { + *options.Options + + InputFile string + OutputFile string + Directory string + GCPRegex string + RemoveGCPAUthConfig bool + RouteOptionStategicMerge bool +} + +func (o *Options) addToFlags(flags *pflag.FlagSet) { + flags.StringVar(&o.InputFile, "input-file", "", "File to convert") + flags.StringVar(&o.OutputFile, "output-file", "", "Where to write the output") + flags.StringVar(&o.Directory, "dir", "", "Directory to read yaml/yml files") +} + +type GlooEdgeInput struct { + FileName string + YamlObjects []string + RouteTables []*gatewaykube.RouteTable + RouteOptions []*gatewaykube.RouteOption + VirtualHostOptions []*gatewaykube.VirtualHostOption + Upstreams []*glookube.Upstream + VirtualServices []*gatewaykube.VirtualService + // Gateways []*gatewaykube.Gateway // TODO do we need these? + AuthConfigs []*v1.AuthConfig +} + +type DelegateParentReference struct { + Labels map[string]string + ParentName string + ParentNamespace string +} + +type GatewayAPIOutput struct { + FileName string + YamlObjects []string + KubernetesObjects []runtime.Object + DelegationReferences []*DelegateParentReference + HTTPRoutes []*gwv1.HTTPRoute + RouteOptions []*gatewaykube.RouteOption + VirtualHostOptions []*gatewaykube.VirtualHostOption + Upstreams []*glookube.Upstream + AuthConfigs []*v1.AuthConfig + // Gateways []*gwv1.Gateway +} + +func (g *GatewayAPIOutput) HasItems() bool { + + if len(g.KubernetesObjects) > 0 { + return true + } + if len(g.HTTPRoutes) > 0 { + return true + } + if len(g.RouteOptions) > 0 { + return true + } + if len(g.VirtualHostOptions) > 0 { + return true + } + if len(g.Upstreams) > 0 { + return true + } + if len(g.AuthConfigs) > 0 { + return true + } + // if there are only yaml objects then skip because we didnt change anything in the file + + return false +} + +func (g *GatewayAPIOutput) ToString() (string, error) { + output := "" + + for _, y := range g.YamlObjects { + output += "\n---\n" + y + "\n" + } + + for _, obj := range g.KubernetesObjects { + o, err := runtime.Encode(codecs.LegacyCodec(corev1.SchemeGroupVersion, gwv1.SchemeGroupVersion, gatewaykube.SchemeGroupVersion, glookube.SchemeGroupVersion), obj) + if err != nil { + return "", err + } + + var yaml strings.Builder + if err := json2yaml.Convert(&yaml, bytes.NewReader(o)); err != nil { + return "", err + } + + output += "\n---\n" + yaml.String() + } + + for _, obj := range g.Upstreams { + o, err := runtime.Encode(codecs.LegacyCodec(corev1.SchemeGroupVersion, gwv1.SchemeGroupVersion, gatewaykube.SchemeGroupVersion, glookube.SchemeGroupVersion), obj) + if err != nil { + return "", err + } + + var yaml strings.Builder + if err := json2yaml.Convert(&yaml, bytes.NewReader(o)); err != nil { + return "", err + } + + output += "\n---\n" + yaml.String() + } + + for _, obj := range g.HTTPRoutes { + o, err := runtime.Encode(codecs.LegacyCodec(corev1.SchemeGroupVersion, gwv1.SchemeGroupVersion, gatewaykube.SchemeGroupVersion, glookube.SchemeGroupVersion), obj) + if err != nil { + return "", err + } + + var yaml strings.Builder + if err := json2yaml.Convert(&yaml, bytes.NewReader(o)); err != nil { + return "", err + } + + output += "\n---\n" + yaml.String() + } + for _, op := range g.RouteOptions { + marshaller := YamlMarshaller{} + yaml, err := marshaller.ToYaml(&op) + if err != nil { + return "", err + } + + output += "\n---\n" + string(yaml) + } + for _, op := range g.AuthConfigs { + marshaller := YamlMarshaller{} + yaml, err := marshaller.ToYaml(&op) + if err != nil { + return "", err + } + + output += "\n---\n" + string(yaml) + } + + for _, op := range g.VirtualHostOptions { + marshaller := YamlMarshaller{} + yaml, err := marshaller.ToYaml(&op) + if err != nil { + return "", err + } + + output += "\n---\n" + string(yaml) + } + + // need to remove a few values + // creationTimestamp: null + // status: {} + // status: + // parents: null + output = strings.ReplaceAll(output, " creationTimestamp: null\n", "") + output = strings.ReplaceAll(output, "status:\n", "") + output = strings.ReplaceAll(output, "parents: null\n", "") + output = strings.ReplaceAll(output, "status: {}\n", "") + output = strings.ReplaceAll(output, "\n\n\n", "\n") + output = strings.ReplaceAll(output, "\n\n", "\n") + output = strings.ReplaceAll(output, "spec: {}\n", "") + + // TODO remove leading and trailing --- + // log.Printf("%s", output) + return output, nil +} diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/helpers.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/helpers.go new file mode 100644 index 00000000000..14b4a237976 --- /dev/null +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/helpers.go @@ -0,0 +1,50 @@ +package convert + +import ( + "bytes" + + "golang.org/x/exp/rand" + + "github.com/golang/protobuf/jsonpb" + "github.com/golang/protobuf/proto" + + "github.com/ghodss/yaml" + + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz") + +func RandStringRunes(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} + +func convertDomains(domains []string) []gwv1.Hostname { + + var hostnames []gwv1.Hostname + for _, d := range domains { + hostnames = append(hostnames, gwv1.Hostname(d)) + } + return hostnames +} + +type YamlMarshaller struct{} + +func (YamlMarshaller) ToYaml(resource interface{}) ([]byte, error) { + switch typedResource := resource.(type) { + case nil: + return []byte{}, nil + case proto.Message: + buf := &bytes.Buffer{} + if err := (&jsonpb.Marshaler{OrigName: true}).Marshal(buf, typedResource); err != nil { + return nil, err + } + return yaml.JSONToYAML(buf.Bytes()) + default: + return yaml.Marshal(resource) + } +} diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/metrics.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/metrics.go new file mode 100644 index 00000000000..0abae87daec --- /dev/null +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/metrics.go @@ -0,0 +1,77 @@ +package convert + +import ( + "log" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + filesMetrics = prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "files_evaluated", + }, + ) + glooConfigMetric = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "gloo_config_count", + }, []string{"type"}, + ) + gatewayAPIConfigMetrics = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "gatewayapi_config_count", + }, []string{"type"}, + ) + unknownObjects = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "unknown_objects", + }, []string{"kind"}, + ) +) + +func init() { + prometheus.MustRegister(filesMetrics) + prometheus.MustRegister(glooConfigMetric) + prometheus.MustRegister(gatewayAPIConfigMetrics) + prometheus.MustRegister(unknownObjects) + glooConfigMetric.WithLabelValues("none").Inc() + + glooConfigMetric.WithLabelValues("AuthConfig").Inc() + glooConfigMetric.WithLabelValues("RouteTable").Inc() + glooConfigMetric.WithLabelValues("Upstream").Inc() + glooConfigMetric.WithLabelValues("VirtualService").Inc() + glooConfigMetric.WithLabelValues("RouteOption").Inc() + glooConfigMetric.WithLabelValues("VirtualHostOption").Inc() + + gatewayAPIConfigMetrics.WithLabelValues("AuthConfig").Inc() + gatewayAPIConfigMetrics.WithLabelValues("HTTPRoute").Inc() + gatewayAPIConfigMetrics.WithLabelValues("Upstream").Inc() + gatewayAPIConfigMetrics.WithLabelValues("RouteOption").Inc() + gatewayAPIConfigMetrics.WithLabelValues("VirtualHostOption").Inc() +} + +func printMetrics() { + metrics, _ := prometheus.DefaultGatherer.Gather() + + for _, m := range metrics { + if *m.Name == "gloo_config_count" { + var count float64 + for _, t := range m.Metric { + log.Printf("Gloo Config: Number of %s: %v", *t.Label[0].Value, *t.Counter.Value-1) + count += *t.Counter.Value - 1 + } + log.Printf("Total Gloo Config: %v", count) + } + if *m.Name == "gatewayapi_config_count" { + var count float64 + for _, t := range m.Metric { + log.Printf("Gateway API Config: Number of %s: %v", *t.Label[0].Value, *t.Counter.Value-1) + count += *t.Counter.Value - 1 + } + log.Printf("Total Gateway API Config: %v", count) + } + if *m.Name == "files_evaluated" { + log.Printf("Files evaluated: %v", *m.Metric[0].Counter.Value) + } + } +} diff --git a/projects/gloo/cli/pkg/cmd/root.go b/projects/gloo/cli/pkg/cmd/root.go index 4cab5620c89..49016f76c09 100644 --- a/projects/gloo/cli/pkg/cmd/root.go +++ b/projects/gloo/cli/pkg/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/gatewayapi" "os" "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/add" @@ -129,6 +130,7 @@ func CommandWithContext(ctx context.Context) *cobra.Command { federation.RootCmd(opts), plugin.RootCmd(opts), istio.RootCmd(opts), + gatewayapi.RootCmd(), license.RootCmd(opts), initpluginmanager.Command(context.Background()), // TODO: re-enable this when it's working again From cf18e31916d68e48fe5d2b734391a7beabd5ea16 Mon Sep 17 00:00:00 2001 From: Nick Nellis Date: Tue, 22 Oct 2024 10:33:01 -0500 Subject: [PATCH 2/5] added metrics and ability to write to a file --- .../cli/pkg/cmd/gatewayapi/convert/command.go | 60 +++++++++--- .../cli/pkg/cmd/gatewayapi/convert/domain.go | 94 +++++++++++++++++-- .../cli/pkg/cmd/gatewayapi/convert/helpers.go | 3 +- .../cli/pkg/cmd/gatewayapi/convert/metrics.go | 54 ++++++++--- 4 files changed, 172 insertions(+), 39 deletions(-) diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go index eb2b9eee8d2..0f17db46dcf 100644 --- a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "log" - "math/rand" "os" "path/filepath" "regexp" @@ -29,6 +28,7 @@ import ( const ( RandomSuffix = 4 + RandomSeed = 1 ) var runtimeScheme *runtime.Scheme @@ -36,7 +36,6 @@ var codecs serializer.CodecFactory var decoder runtime.Decoder func RootCmd() *cobra.Command { - rand.Seed(1) opts := &Options{} cmd := &cobra.Command{ Use: "convert", @@ -57,6 +56,7 @@ func run(opts *Options) error { return err } + filesMetrics.Add(float64(len(foundFiles))) var inputs []*GlooEdgeInput for _, file := range foundFiles { @@ -85,12 +85,39 @@ func run(opts *Options) error { // write all the outputs to their files for _, output := range outputs { - fmt.Fprintf(os.Stdout, "\n\n---\n# --------------------------------\n# %s\n# --------------------------------", output.FileName) + //only write or txt, err := output.ToString() if err != nil { return err } - fmt.Fprintf(os.Stdout, "%s\n", txt) + + if opts.Overwrite { + if output.HasItems() { + _, _ = fmt.Fprintf(os.Stdout, "Updated File: %s\n", output.FileName) + file, err := os.OpenFile(output.FileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + log.Fatal(err) + } + defer file.Close() + fmt.Fprintf(file, "%s", txt) + } else { + _, _ = fmt.Fprintf(os.Stdout, "Skipping File because no Edge APIs Detected: %s\n", output.FileName) + } + } else { + _, _ = fmt.Fprintf(os.Stdout, "\n\n---\n# --------------------------------\n# %s\n# --------------------------------", output.FileName) + _, _ = fmt.Fprintf(os.Stdout, "%s\n", txt) + } + if opts.Stats { + totalLines.WithLabelValues("Gateway API").Add(float64(len(strings.Split(txt, "\n")))) + } + } + if opts.Stats { + //count total lines of generated yaml (probably expensive) + for _, input := range inputs { + txt, _ := input.ToString() + totalLines.WithLabelValues("Gloo").Add(float64(len(strings.Split(txt, "\n")))) + } + printMetrics(outputs) } return nil @@ -294,7 +321,7 @@ func convertVirtualHostOptions( }, Spec: gloogwv1.VirtualHostOption{ Options: options, - // TODO we just referenece a non existant gateway today + // TODO we just reference a non existent gateway today TargetRefs: []*v3.PolicyTargetReferenceWithSectionName{ { Group: "gateway.networking.k8s.io", @@ -435,6 +462,7 @@ func convertMatch(m *matchers.Matcher) (gwv1.HTTPRouteMatch, error) { if len(m.Headers) > 0 { hrm.Headers = []gwv1.HTTPHeaderMatch{} for _, h := range m.Headers { + // support invert header match https://github.com/solo-io/gloo/blob/main/projects/gateway2/translator/httproute/gateway_http_route_translator.go#L274 if h.InvertMatch == true { return hrm, errors.New("invert match not currently supported") } @@ -452,10 +480,10 @@ func convertMatch(m *matchers.Matcher) (gwv1.HTTPRouteMatch, error) { }) } } - // TODO support Invert header match https://github.com/solo-io/gloo/blob/main/projects/gateway2/translator/httproute/gateway_http_route_translator.go#L274 + } - // method mathching + // method matching if len(m.Methods) > 0 { if len(m.Methods) > 1 { return hrm, errors.New(fmt.Sprintf("Gateway API only supports 1 method match per rule and %d were detected", len(m.Methods))) @@ -565,9 +593,9 @@ func generateFilterForURLRewrite(r *gloogwv1.Route) (gwv1.HTTPRouteFilter, error rf.URLRewrite.Path.ReplaceFullPath = nil } - // TODO regex rewrite, NOT SUPPORTED IN GATEWAY API + // regex rewrite, NOT SUPPORTED IN GATEWAY API if r.GetOptions().GetRegexRewrite() != nil { - return rf, errors.New(fmt.Sprintf("Reject rewrite not supported, need to convert to another match")) + return rf, errors.New(fmt.Sprintf("regex rewrite not supported, need to convert to another match")) } // rr.Filters = append(rr.Filters, gwv1.HTTPRouteFilter{ // Type: gwv1.HTTPRouteFilterURLRewrite, @@ -680,10 +708,11 @@ func generateFilterForRedirectAction(r *gloogwv1.Route) (gwv1.HTTPRouteFilter, e if r.GetRedirectAction().HttpsRedirect == true { rf.RequestRedirect.Scheme = ptr.To("https") } - // TODO we dont support stripQuery https://github.com/solo-io/gloo/blob/main/projects/gateway2/translator/plugins/redirect/redirect_plugin.go#L43 - // if r.GetRedirectAction().StripQuery == true { - // rf.RequestRedirect. - // } + + // we dont support stripQuery https://github.com/solo-io/gloo/blob/main/projects/gateway2/translator/plugins/redirect/redirect_plugin.go#L43 + if r.GetRedirectAction().StripQuery == true { + return rf, errors.New("strip query not supported by Gateway API") + } if r.GetRedirectAction().GetPathRedirect() != "" { @@ -852,12 +881,13 @@ func translateFileToEdgeInput(fileName string) (*GlooEdgeInput, error) { continue } - // if we cant decode it, don't do anything and continue - log.Printf("# Skipping object due to error %s", err) + // TODO if we cant decode it, don't do anything and continue + //log.Printf("# Skipping object due to error file parsing error %s", err) continue } switch o := obj.(type) { case *v2.AuthConfig: + glooConfigMetric.WithLabelValues("AuthConfig").Inc() gei.AuthConfigs = append(gei.AuthConfigs, o) case *glookube.Upstream: glooConfigMetric.WithLabelValues("Upstream").Inc() diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go index eee9a2f7624..8bf70a09e67 100644 --- a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go @@ -19,8 +19,9 @@ type Options struct { *options.Options InputFile string - OutputFile string Directory string + Overwrite bool + Stats bool GCPRegex string RemoveGCPAUthConfig bool RouteOptionStategicMerge bool @@ -28,7 +29,8 @@ type Options struct { func (o *Options) addToFlags(flags *pflag.FlagSet) { flags.StringVar(&o.InputFile, "input-file", "", "File to convert") - flags.StringVar(&o.OutputFile, "output-file", "", "Where to write the output") + flags.BoolVar(&o.Overwrite, "overwrite", false, "Overwrite the existing files with the changes") + flags.BoolVar(&o.Stats, "stats", false, "Print stats about the conversion") flags.StringVar(&o.Directory, "dir", "", "Directory to read yaml/yml files") } @@ -53,7 +55,6 @@ type DelegateParentReference struct { type GatewayAPIOutput struct { FileName string YamlObjects []string - KubernetesObjects []runtime.Object DelegationReferences []*DelegateParentReference HTTPRoutes []*gwv1.HTTPRoute RouteOptions []*gatewaykube.RouteOption @@ -65,9 +66,6 @@ type GatewayAPIOutput struct { func (g *GatewayAPIOutput) HasItems() bool { - if len(g.KubernetesObjects) > 0 { - return true - } if len(g.HTTPRoutes) > 0 { return true } @@ -95,7 +93,7 @@ func (g *GatewayAPIOutput) ToString() (string, error) { output += "\n---\n" + y + "\n" } - for _, obj := range g.KubernetesObjects { + for _, obj := range g.Upstreams { o, err := runtime.Encode(codecs.LegacyCodec(corev1.SchemeGroupVersion, gwv1.SchemeGroupVersion, gatewaykube.SchemeGroupVersion, glookube.SchemeGroupVersion), obj) if err != nil { return "", err @@ -109,6 +107,73 @@ func (g *GatewayAPIOutput) ToString() (string, error) { output += "\n---\n" + yaml.String() } + for _, obj := range g.HTTPRoutes { + o, err := runtime.Encode(codecs.LegacyCodec(corev1.SchemeGroupVersion, gwv1.SchemeGroupVersion, gatewaykube.SchemeGroupVersion, glookube.SchemeGroupVersion), obj) + if err != nil { + return "", err + } + + var yaml strings.Builder + if err := json2yaml.Convert(&yaml, bytes.NewReader(o)); err != nil { + return "", err + } + + output += "\n---\n" + yaml.String() + } + for _, op := range g.RouteOptions { + marshaller := YamlMarshaller{} + yaml, err := marshaller.ToYaml(&op) + if err != nil { + return "", err + } + + output += "\n---\n" + string(yaml) + } + for _, op := range g.AuthConfigs { + marshaller := YamlMarshaller{} + yaml, err := marshaller.ToYaml(&op) + if err != nil { + return "", err + } + + output += "\n---\n" + string(yaml) + } + + for _, op := range g.VirtualHostOptions { + marshaller := YamlMarshaller{} + yaml, err := marshaller.ToYaml(&op) + if err != nil { + return "", err + } + + output += "\n---\n" + string(yaml) + } + + // need to remove a few values + // creationTimestamp: null + // status: {} + // status: + // parents: null + output = strings.ReplaceAll(output, " creationTimestamp: null\n", "") + output = strings.ReplaceAll(output, "status:\n", "") + output = strings.ReplaceAll(output, "parents: null\n", "") + output = strings.ReplaceAll(output, "status: {}\n", "") + output = strings.ReplaceAll(output, "\n\n\n", "\n") + output = strings.ReplaceAll(output, "\n\n", "\n") + output = strings.ReplaceAll(output, "spec: {}\n", "") + + // TODO remove leading and trailing --- + // log.Printf("%s", output) + return output, nil +} + +func (g *GlooEdgeInput) ToString() (string, error) { + output := "" + + for _, y := range g.YamlObjects { + output += "\n---\n" + y + "\n" + } + for _, obj := range g.Upstreams { o, err := runtime.Encode(codecs.LegacyCodec(corev1.SchemeGroupVersion, gwv1.SchemeGroupVersion, gatewaykube.SchemeGroupVersion, glookube.SchemeGroupVersion), obj) if err != nil { @@ -123,7 +188,20 @@ func (g *GatewayAPIOutput) ToString() (string, error) { output += "\n---\n" + yaml.String() } - for _, obj := range g.HTTPRoutes { + for _, obj := range g.RouteTables { + o, err := runtime.Encode(codecs.LegacyCodec(corev1.SchemeGroupVersion, gwv1.SchemeGroupVersion, gatewaykube.SchemeGroupVersion, glookube.SchemeGroupVersion), obj) + if err != nil { + return "", err + } + + var yaml strings.Builder + if err := json2yaml.Convert(&yaml, bytes.NewReader(o)); err != nil { + return "", err + } + + output += "\n---\n" + yaml.String() + } + for _, obj := range g.VirtualServices { o, err := runtime.Encode(codecs.LegacyCodec(corev1.SchemeGroupVersion, gwv1.SchemeGroupVersion, gatewaykube.SchemeGroupVersion, glookube.SchemeGroupVersion), obj) if err != nil { return "", err diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/helpers.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/helpers.go index 14b4a237976..3b784fd0177 100644 --- a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/helpers.go +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/helpers.go @@ -16,9 +16,10 @@ import ( var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz") func RandStringRunes(n int) string { + r := rand.New(rand.NewSource(RandomSeed)) b := make([]rune, n) for i := range b { - b[i] = letterRunes[rand.Intn(len(letterRunes))] + b[i] = letterRunes[r.Intn(len(letterRunes))] } return string(b) } diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/metrics.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/metrics.go index 0abae87daec..c9ee9bc23b0 100644 --- a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/metrics.go +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/metrics.go @@ -1,7 +1,8 @@ package convert import ( - "log" + "fmt" + "os" "github.com/prometheus/client_golang/prometheus" ) @@ -12,6 +13,11 @@ var ( Name: "files_evaluated", }, ) + totalLines = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "total_lines_of_yaml", + }, []string{"api"}, + ) glooConfigMetric = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "gloo_config_count", @@ -22,19 +28,13 @@ var ( Name: "gatewayapi_config_count", }, []string{"type"}, ) - unknownObjects = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "unknown_objects", - }, []string{"kind"}, - ) ) func init() { prometheus.MustRegister(filesMetrics) + prometheus.MustRegister(totalLines) prometheus.MustRegister(glooConfigMetric) prometheus.MustRegister(gatewayAPIConfigMetrics) - prometheus.MustRegister(unknownObjects) - glooConfigMetric.WithLabelValues("none").Inc() glooConfigMetric.WithLabelValues("AuthConfig").Inc() glooConfigMetric.WithLabelValues("RouteTable").Inc() @@ -50,28 +50,52 @@ func init() { gatewayAPIConfigMetrics.WithLabelValues("VirtualHostOption").Inc() } -func printMetrics() { - metrics, _ := prometheus.DefaultGatherer.Gather() +func printMetrics(outputs []*GatewayAPIOutput) { + for _, output := range outputs { + //we need to save the output to metrics + gatewayAPIConfigMetrics.WithLabelValues("AuthConfig").Add(float64(len(output.AuthConfigs))) + gatewayAPIConfigMetrics.WithLabelValues("HTTPRoute").Add(float64(len(output.HTTPRoutes))) + gatewayAPIConfigMetrics.WithLabelValues("Upstream").Add(float64(len(output.Upstreams))) + gatewayAPIConfigMetrics.WithLabelValues("RouteOption").Add(float64(len(output.RouteOptions))) + gatewayAPIConfigMetrics.WithLabelValues("VirtualHostOption").Add(float64(len(output.VirtualHostOptions))) + } + + metrics, _ := prometheus.DefaultGatherer.Gather() + fmt.Fprintf(os.Stdout, "-------------------------------------\n") for _, m := range metrics { if *m.Name == "gloo_config_count" { var count float64 for _, t := range m.Metric { - log.Printf("Gloo Config: Number of %s: %v", *t.Label[0].Value, *t.Counter.Value-1) + _, _ = fmt.Fprintf(os.Stdout, "Gloo Config: Number of %s: %v\n", *t.Label[0].Value, *t.Counter.Value-1) count += *t.Counter.Value - 1 } - log.Printf("Total Gloo Config: %v", count) + _, _ = fmt.Fprintf(os.Stdout, "Total Gloo Config: %v\n", count) } + } + fmt.Fprintf(os.Stdout, "-------------------------------------\n") + for _, m := range metrics { if *m.Name == "gatewayapi_config_count" { var count float64 for _, t := range m.Metric { - log.Printf("Gateway API Config: Number of %s: %v", *t.Label[0].Value, *t.Counter.Value-1) + _, _ = fmt.Fprintf(os.Stdout, "Gateway API Config: Number of %s: %v\n", *t.Label[0].Value, *t.Counter.Value-1) count += *t.Counter.Value - 1 } - log.Printf("Total Gateway API Config: %v", count) + _, _ = fmt.Fprintf(os.Stdout, "Total Gateway API Config: %v\n", count) + } + } + fmt.Fprintf(os.Stdout, "-------------------------------------\n") + for _, m := range metrics { + if *m.Name == "total_lines_of_yaml" { + for _, t := range m.Metric { + _, _ = fmt.Fprintf(os.Stdout, "Lines of Yaml %s: %v\n", *t.Label[0].Value, *t.Counter.Value-1) + } } + } + fmt.Fprintf(os.Stdout, "-------------------------------------\n") + for _, m := range metrics { if *m.Name == "files_evaluated" { - log.Printf("Files evaluated: %v", *m.Metric[0].Counter.Value) + _, _ = fmt.Fprintf(os.Stdout, "Files evaluated: %v\n", *m.Metric[0].Counter.Value) } } } From 6ef22e50ebc62539abd4c6113fbd9452fb022ace Mon Sep 17 00:00:00 2001 From: Nick Nellis Date: Tue, 22 Oct 2024 13:35:18 -0500 Subject: [PATCH 3/5] added support for lists --- .../cli/pkg/cmd/gatewayapi/convert/command.go | 139 ++++++++++++++++-- 1 file changed, 124 insertions(+), 15 deletions(-) diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go index 0f17db46dcf..d1807948761 100644 --- a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go @@ -3,10 +3,14 @@ package convert import ( "errors" "fmt" + "github.com/solo-io/gloo/pkg/schemes" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "log" "os" "path/filepath" "regexp" + "sigs.k8s.io/yaml" "strings" gloogwv1 "github.com/solo-io/gloo/projects/gateway/pkg/api/v1" @@ -397,7 +401,7 @@ func convertRouteToRule(r *gloogwv1.Route, routeTableName string, routeTableName } for _, m := range r.Matchers { - match, err := convertMatch(m) + match, err := convertMatch(m, routeTableName) if err != nil { return rr, nil, nil, err } @@ -453,7 +457,7 @@ func convertRouteToRule(r *gloogwv1.Route, routeTableName string, routeTableName return rr, ro, delegate, nil } -func convertMatch(m *matchers.Matcher) (gwv1.HTTPRouteMatch, error) { +func convertMatch(m *matchers.Matcher, routeTableName string) (gwv1.HTTPRouteMatch, error) { hrm := gwv1.HTTPRouteMatch{ QueryParams: []gwv1.HTTPQueryParamMatch{}, } @@ -486,7 +490,7 @@ func convertMatch(m *matchers.Matcher) (gwv1.HTTPRouteMatch, error) { // method matching if len(m.Methods) > 0 { if len(m.Methods) > 1 { - return hrm, errors.New(fmt.Sprintf("Gateway API only supports 1 method match per rule and %d were detected", len(m.Methods))) + return hrm, errors.New(fmt.Sprintf("Gateway API only supports 1 method match per rule and %d were detected for RouteTable %s", len(m.Methods), routeTableName)) } hrm.Method = (*gwv1.HTTPMethod)(ptr.To(m.Methods[0])) } @@ -885,6 +889,31 @@ func translateFileToEdgeInput(fileName string) (*GlooEdgeInput, error) { //log.Printf("# Skipping object due to error file parsing error %s", err) continue } + + // a lot of times lists are missing the group so this object doesnt match + if k.Kind == "List" { + var list unstructured.UnstructuredList + if err := yaml.Unmarshal([]byte(resourceYAML), &list); err != nil { + return nil, err + } + + for _, item := range list.Items { + + tmpGei, err := parseObjects(item, k) + if err != nil { + return nil, err + } + gei.RouteOptions = append(gei.RouteOptions, tmpGei.RouteOptions...) + gei.VirtualServices = append(gei.VirtualServices, tmpGei.VirtualServices...) + gei.YamlObjects = append(gei.YamlObjects, tmpGei.YamlObjects...) + gei.RouteTables = append(gei.RouteTables, tmpGei.RouteTables...) + gei.VirtualHostOptions = append(gei.VirtualHostOptions, tmpGei.VirtualHostOptions...) + gei.Upstreams = append(gei.Upstreams, tmpGei.Upstreams...) + gei.AuthConfigs = append(gei.AuthConfigs, tmpGei.AuthConfigs...) + } + continue + } + switch o := obj.(type) { case *v2.AuthConfig: glooConfigMetric.WithLabelValues("AuthConfig").Inc() @@ -892,18 +921,38 @@ func translateFileToEdgeInput(fileName string) (*GlooEdgeInput, error) { case *glookube.Upstream: glooConfigMetric.WithLabelValues("Upstream").Inc() gei.Upstreams = append(gei.Upstreams, o) + case *glookube.UpstreamList: + for _, upstream := range o.Items { + glooConfigMetric.WithLabelValues("Upstream").Inc() + gei.Upstreams = append(gei.Upstreams, &upstream) + } case *gatewaykube.RouteTable: glooConfigMetric.WithLabelValues("RouteTable").Inc() gei.RouteTables = append(gei.RouteTables, o) + case *gatewaykube.RouteTableList: + for _, routeTable := range o.Items { + glooConfigMetric.WithLabelValues("RouteTable").Inc() + gei.RouteTables = append(gei.RouteTables, &routeTable) + } case *gatewaykube.VirtualService: glooConfigMetric.WithLabelValues("VirtualService").Inc() gei.VirtualServices = append(gei.VirtualServices, o) + case *gatewaykube.VirtualServiceList: + for _, vs := range o.Items { + glooConfigMetric.WithLabelValues("VirtualService").Inc() + gei.VirtualServices = append(gei.VirtualServices, &vs) + } case *gatewaykube.RouteOption: glooConfigMetric.WithLabelValues("RouteOption").Inc() - gei.YamlObjects = append(gei.YamlObjects, resourceYAML) + gei.RouteOptions = append(gei.RouteOptions, o) + case *gatewaykube.RouteOptionList: + for _, ro := range o.Items { + glooConfigMetric.WithLabelValues("RouteOption").Inc() + gei.RouteOptions = append(gei.RouteOptions, &ro) + } case *gatewaykube.VirtualHostOption: glooConfigMetric.WithLabelValues("VirtualHostOption").Inc() - gei.YamlObjects = append(gei.YamlObjects, resourceYAML) + gei.VirtualHostOptions = append(gei.VirtualHostOptions, o) case *gatewaykube.Gateway: glooConfigMetric.WithLabelValues("Gateway").Inc() gei.YamlObjects = append(gei.YamlObjects, resourceYAML) @@ -917,21 +966,81 @@ func translateFileToEdgeInput(fileName string) (*GlooEdgeInput, error) { return gei, nil } +func parseObjects(item unstructured.Unstructured, k *schema.GroupVersionKind) (*GlooEdgeInput, error) { + gei := &GlooEdgeInput{} + + resourceYaml, err := yaml.Marshal(item) + if err != nil { + return nil, err + } + gvk := item.GroupVersionKind() + obj, err := runtimeScheme.New(gvk) + if runtime.IsNotRegisteredError(err) { + // we just want to add the yaml and move on + gei.YamlObjects = append(gei.YamlObjects, string(resourceYaml)) + return gei, nil + } else if err != nil { + return nil, err + } + + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, obj); err != nil { + return nil, errors.New(fmt.Sprintf("Error converting unstructured to typed: %v", err)) + } + + switch o := obj.(type) { + case *v2.AuthConfig: + glooConfigMetric.WithLabelValues("AuthConfig").Inc() + gei.AuthConfigs = append(gei.AuthConfigs, o) + case *glookube.Upstream: + glooConfigMetric.WithLabelValues("Upstream").Inc() + gei.Upstreams = append(gei.Upstreams, o) + case *gatewaykube.RouteTable: + glooConfigMetric.WithLabelValues("RouteTable").Inc() + gei.RouteTables = append(gei.RouteTables, o) + case *gatewaykube.VirtualService: + glooConfigMetric.WithLabelValues("VirtualService").Inc() + gei.VirtualServices = append(gei.VirtualServices, o) + case *gatewaykube.RouteOption: + glooConfigMetric.WithLabelValues("RouteOption").Inc() + gei.RouteOptions = append(gei.RouteOptions, o) + case *gatewaykube.VirtualHostOption: + glooConfigMetric.WithLabelValues("VirtualHostOption").Inc() + gei.VirtualHostOptions = append(gei.VirtualHostOptions, o) + case *gatewaykube.Gateway: + glooConfigMetric.WithLabelValues("Gateway").Inc() + gei.YamlObjects = append(gei.YamlObjects, string(resourceYaml)) + default: + // if we dont know what type it is we just add it back + // no change so just add it back + glooConfigMetric.WithLabelValues(k.Kind).Inc() + gei.YamlObjects = append(gei.YamlObjects, string(resourceYaml)) + } + return gei, nil +} + func init() { runtimeScheme = runtime.NewScheme() - if err := glookube.AddToScheme(runtimeScheme); err != nil { - log.Fatal(err) - } - if err := gatewaykube.AddToScheme(runtimeScheme); err != nil { - log.Fatal(err) - } - if err := v2.AddToScheme(runtimeScheme); err != nil { - log.Fatal(err) - } - if err := gwv1.Install(runtimeScheme); err != nil { + //if err := metav1.AddToSche; err != nil { + // log.Fatal(err) + //} + //if err := glookube.AddToScheme(runtimeScheme); err != nil { + // log.Fatal(err) + //} + //if err := gatewaykube.AddToScheme(runtimeScheme); err != nil { + // log.Fatal(err) + //} + //if err := v2.AddToScheme(runtimeScheme); err != nil { + // log.Fatal(err) + //} + //if err := gwv1.Install(runtimeScheme); err != nil { + // log.Fatal(err) + //} + + if err := schemes.SchemeBuilder.AddToScheme(runtimeScheme); err != nil { log.Fatal(err) } + codecs = serializer.NewCodecFactory(runtimeScheme) decoder = codecs.UniversalDeserializer() } From 519d37052c2badf4b94bfe33ea4bd0c28b01d943 Mon Sep 17 00:00:00 2001 From: Nick Nellis Date: Tue, 22 Oct 2024 14:49:19 -0500 Subject: [PATCH 4/5] added the ability to write the file next to the existing one --- projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go | 8 ++++++-- projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go index d1807948761..362470e6281 100644 --- a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go @@ -96,9 +96,13 @@ func run(opts *Options) error { } if opts.Overwrite { + fileNameSplit := strings.Split(output.FileName, ".") + // assuming anything before the first . is the file name + + filename := fmt.Sprintf("%s-%s.%s", fileNameSplit[0], opts.OverwriteSuffix, strings.Join(fileNameSplit[1:], ".")) if output.HasItems() { - _, _ = fmt.Fprintf(os.Stdout, "Updated File: %s\n", output.FileName) - file, err := os.OpenFile(output.FileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + _, _ = fmt.Fprintf(os.Stdout, "Writing File: %s\n", filename) + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { log.Fatal(err) } diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go index 8bf70a09e67..39cbb802121 100644 --- a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/domain.go @@ -21,6 +21,7 @@ type Options struct { InputFile string Directory string Overwrite bool + OverwriteSuffix string Stats bool GCPRegex string RemoveGCPAUthConfig bool @@ -30,6 +31,7 @@ type Options struct { func (o *Options) addToFlags(flags *pflag.FlagSet) { flags.StringVar(&o.InputFile, "input-file", "", "File to convert") flags.BoolVar(&o.Overwrite, "overwrite", false, "Overwrite the existing files with the changes") + flags.StringVar(&o.OverwriteSuffix, "suffix", "", "When writing to files add a suffix (to do side by side)") flags.BoolVar(&o.Stats, "stats", false, "Print stats about the conversion") flags.StringVar(&o.Directory, "dir", "", "Directory to read yaml/yml files") } From e15c8404c37aec44f795fca47719264858cdf6f9 Mon Sep 17 00:00:00 2001 From: Nick Nellis Date: Fri, 1 Nov 2024 14:59:01 -0500 Subject: [PATCH 5/5] fixed files --- .../cli/pkg/cmd/gatewayapi/convert/command.go | 12 +++-- .../cmd/gatewayapi/convert/preprocessor.go | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 projects/gloo/cli/pkg/cmd/gatewayapi/convert/preprocessor.go diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go index 362470e6281..bf8b13dc523 100644 --- a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/command.go @@ -70,7 +70,11 @@ func run(opts *Options) error { } inputs = append(inputs, input) } - // All the objects have been found in all the files + + // preprocessing + for _, input := range inputs { + NewPreprocessor().Preprocess(input) + } // now we need to convert the easy stuff like route tables var outputs []*GatewayAPIOutput @@ -98,8 +102,10 @@ func run(opts *Options) error { if opts.Overwrite { fileNameSplit := strings.Split(output.FileName, ".") // assuming anything before the first . is the file name - - filename := fmt.Sprintf("%s-%s.%s", fileNameSplit[0], opts.OverwriteSuffix, strings.Join(fileNameSplit[1:], ".")) + filename := fmt.Sprintf("%s.%s", fileNameSplit[0], strings.Join(fileNameSplit[1:], ".")) + if opts.OverwriteSuffix != "" { + fmt.Sprintf("%s-%s.%s", fileNameSplit[0], opts.OverwriteSuffix, strings.Join(fileNameSplit[1:], ".")) + } if output.HasItems() { _, _ = fmt.Fprintf(os.Stdout, "Writing File: %s\n", filename) file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) diff --git a/projects/gloo/cli/pkg/cmd/gatewayapi/convert/preprocessor.go b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/preprocessor.go new file mode 100644 index 00000000000..489e1b1a983 --- /dev/null +++ b/projects/gloo/cli/pkg/cmd/gatewayapi/convert/preprocessor.go @@ -0,0 +1,49 @@ +package convert + +import ( + "github.com/solo-io/gloo/projects/gateway/pkg/api/v1" + "github.com/solo-io/gloo/projects/gloo/pkg/api/v1/core/matchers" +) + +type Preprocessor struct{} + +func NewPreprocessor() *Preprocessor { + return &Preprocessor{} +} + +func (p *Preprocessor) Preprocess(input *GlooEdgeInput) { + for _, rt := range input.RouteTables { + var newRoutes []*v1.Route + for _, route := range rt.Spec.Routes { + editedRoute := generateRoutesForMethodMatchers(route) + newRoutes = append(newRoutes, editedRoute) + } + rt.Spec.Routes = newRoutes + } +} + +func generateRoutesForMethodMatchers(route *v1.Route) *v1.Route { + + var newMatchers []*matchers.Matcher + for _, m := range route.Matchers { + if len(m.Methods) > 1 { + // for each method we need to split out the matchers + for _, method := range m.Methods { + newMatcher := &matchers.Matcher{ + PathSpecifier: m.PathSpecifier, + CaseSensitive: m.CaseSensitive, + Headers: m.Headers, + QueryParameters: m.QueryParameters, + Methods: []string{method}, + } + newMatchers = append(newMatchers, newMatcher) + } + } else { + //it only has one so we just add it + newMatchers = append(newMatchers, m) + } + } + route.Matchers = newMatchers + + return route +}