pkg/gateway/model_build_rule.go (221 lines of code) (raw):

package gateway import ( "context" "errors" "fmt" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" anv1alpha1 "github.com/aws/aws-application-networking-k8s/pkg/apis/applicationnetworking/v1alpha1" "github.com/aws/aws-application-networking-k8s/pkg/model/core" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/vpclattice" gwv1 "sigs.k8s.io/gateway-api/apis/v1" model "github.com/aws/aws-application-networking-k8s/pkg/model/lattice" ) const ( LATTICE_NO_SUPPORT_FOR_MULTIPLE_MATCHES = "LATTICE_NO_SUPPORT_FOR_MULTIPLE_MATCHES" LATTICE_EXCEED_MAX_HEADER_MATCHES = "LATTICE_EXCEED_MAX_HEADER_MATCHES" LATTICE_UNSUPPORTED_MATCH_TYPE = "LATTICE_UNSUPPORTED_MATCH_TYPE" LATTICE_UNSUPPORTED_HEADER_MATCH_TYPE = "LATTICE_UNSUPPORTED_HEADER_MATCH_TYPE" LATTICE_UNSUPPORTED_PATH_MATCH_TYPE = "LATTICE_UNSUPPORTED_PATH_MATCH_TYPE" LATTICE_MAX_HEADER_MATCHES = 5 ) func (t *latticeServiceModelBuildTask) buildRules(ctx context.Context, stackListenerId string) error { // note we only build rules for non-deleted routes t.log.Debugf(ctx, "Processing %d rules", len(t.route.Spec().Rules())) for i, rule := range t.route.Spec().Rules() { ruleSpec := model.RuleSpec{ StackListenerId: stackListenerId, Priority: int64(i + 1), } if len(rule.Matches()) > 1 { // only support 1 match today return errors.New(LATTICE_NO_SUPPORT_FOR_MULTIPLE_MATCHES) } else if len(rule.Matches()) > 0 { t.log.Debugf(ctx, "Processing rule match") match := rule.Matches()[0] switch m := match.(type) { case *core.HTTPRouteMatch: if err := t.updateRuleSpecForHttpRoute(m, &ruleSpec); err != nil { return err } case *core.GRPCRouteMatch: if err := t.updateRuleSpecForGrpcRoute(m, &ruleSpec); err != nil { return err } default: return fmt.Errorf("unsupported rule match: %T", m) } if err := t.updateRuleSpecWithHeaderMatches(match, &ruleSpec); err != nil { return err } } else { // Match every traffic on no matches ruleSpec.PathMatchValue = "/" ruleSpec.PathMatchPrefix = true if _, ok := rule.(*core.GRPCRouteRule); ok { ruleSpec.Method = string(gwv1.HTTPMethodPost) } } ruleTgList, err := t.getTargetGroupsForRuleAction(ctx, rule) if err != nil { return err } ruleSpec.Action = model.RuleAction{ TargetGroups: ruleTgList, } // don't bother adding rules on delete, these will be removed automatically with the owning route/lattice service // target groups will still be present and removed as needed if t.route.DeletionTimestamp().IsZero() { stackRule, err := model.NewRule(t.stack, ruleSpec) if err != nil { return err } t.log.Debugf(ctx, "Added rule %d to the stack (ID %s)", stackRule.Spec.Priority, stackRule.ID()) } else { t.log.Debugf(ctx, "Skipping adding rule %d to the stack since the route is deleted", ruleSpec.Priority) } } return nil } func (t *latticeServiceModelBuildTask) updateRuleSpecForHttpRoute(m *core.HTTPRouteMatch, ruleSpec *model.RuleSpec) error { hasPath := m.Path() != nil hasType := hasPath && m.Path().Type != nil if hasPath && !hasType { return errors.New("type is required on path match") } if hasPath { t.log.Debugf(context.TODO(), "Examining pathmatch type %s value %s for for httproute %s-%s ", *m.Path().Type, *m.Path().Value, t.route.Name(), t.route.Namespace()) switch *m.Path().Type { case gwv1.PathMatchExact: t.log.Debugf(context.TODO(), "Using PathMatchExact for httproute %s-%s ", t.route.Name(), t.route.Namespace()) ruleSpec.PathMatchExact = true case gwv1.PathMatchPathPrefix: t.log.Debugf(context.TODO(), "Using PathMatchPathPrefix for httproute %s-%s ", t.route.Name(), t.route.Namespace()) ruleSpec.PathMatchPrefix = true default: t.log.Debugf(context.TODO(), "Unsupported path match type %s for httproute %s-%s", *m.Path().Type, t.route.Name(), t.route.Namespace()) return errors.New(LATTICE_UNSUPPORTED_PATH_MATCH_TYPE) } ruleSpec.PathMatchValue = *m.Path().Value } // method based match if m.Method() != nil { t.log.Infof(context.TODO(), "Examining http method %s for httproute %s-%s", *m.Method(), t.route.Name(), t.route.Namespace()) ruleSpec.Method = string(*m.Method()) } // controller does not support query matcher type today if m.QueryParams() != nil { t.log.Infof(context.TODO(), "Unsupported match type for httproute %s, namespace %s", t.route.Name(), t.route.Namespace()) return errors.New(LATTICE_UNSUPPORTED_MATCH_TYPE) } return nil } func (t *latticeServiceModelBuildTask) updateRuleSpecForGrpcRoute(m *core.GRPCRouteMatch, ruleSpec *model.RuleSpec) error { t.log.Debugf(context.TODO(), "Building rule with GRPCRouteMatch, %+v", *m) ruleSpec.Method = string(gwv1.HTTPMethodPost) // GRPC is always POST method := m.Method() // VPC Lattice doesn't support suffix/regex matching, so we can't support method match without service if method.Service == nil && method.Method != nil { return fmt.Errorf("cannot create GRPCRouteMatch for nil service and non-nil method") } switch *method.Type { case gwv1.GRPCMethodMatchExact: if method.Service == nil { t.log.Debugf(context.TODO(), "Match all paths due to nil service and nil method") ruleSpec.PathMatchPrefix = true ruleSpec.PathMatchValue = "/" } else if method.Method == nil { t.log.Debugf(context.TODO(), "Match by specific gRPC service %s, regardless of method", *method.Service) ruleSpec.PathMatchPrefix = true ruleSpec.PathMatchValue = fmt.Sprintf("/%s/", *method.Service) } else { t.log.Debugf(context.TODO(), "Match by specific gRPC service %s and method %s", *method.Service, *method.Method) ruleSpec.PathMatchExact = true ruleSpec.PathMatchValue = fmt.Sprintf("/%s/%s", *method.Service, *method.Method) } default: return fmt.Errorf("unsupported gRPC method match type %s", *method.Type) } return nil } func (t *latticeServiceModelBuildTask) updateRuleSpecWithHeaderMatches(match core.RouteMatch, ruleSpec *model.RuleSpec) error { if match.Headers() == nil { return nil } if len(match.Headers()) > LATTICE_MAX_HEADER_MATCHES { return errors.New(LATTICE_EXCEED_MAX_HEADER_MATCHES) } t.log.Debugf(context.TODO(), "Examining match headers for route %s-%s", t.route.Name(), t.route.Namespace()) for _, header := range match.Headers() { t.log.Debugf(context.TODO(), "Examining match.Header: header.Type %s", *header.Type()) if header.Type() != nil && *header.Type() != gwv1.HeaderMatchExact { t.log.Debugf(context.TODO(), "Unsupported header matchtype %s for httproute %s-%s", *header.Type(), t.route.Name(), t.route.Namespace()) return errors.New(LATTICE_UNSUPPORTED_HEADER_MATCH_TYPE) } matchType := vpclattice.HeaderMatchType{ Exact: aws.String(header.Value()), } headerName := header.Name() headerMatch := vpclattice.HeaderMatch{} headerMatch.Match = &matchType headerMatch.Name = &headerName ruleSpec.MatchedHeaders = append(ruleSpec.MatchedHeaders, headerMatch) } return nil } func (t *latticeServiceModelBuildTask) getTargetGroupsForRuleAction(ctx context.Context, rule core.RouteRule) ([]*model.RuleTargetGroup, error) { var tgList []*model.RuleTargetGroup for _, backendRef := range rule.BackendRefs() { ruleTG := model.RuleTargetGroup{ Weight: 1, // default value according to spec } if backendRef.Weight() != nil { ruleTG.Weight = int64(*backendRef.Weight()) } namespace := t.route.Namespace() if backendRef.Namespace() != nil { namespace = string(*backendRef.Namespace()) } t.log.Debugf(ctx, "Processing %s backendRef %s-%s", string(*backendRef.Kind()), backendRef.Name(), namespace) if string(*backendRef.Kind()) == "ServiceImport" { // there needs to be a pre-existing target group, we fetch all the fields // needed to identify it svcImportTg := model.SvcImportTargetGroup{ K8SServiceNamespace: namespace, K8SServiceName: string(backendRef.Name()), } // if there's a matching top-level service import, we can get additional fields svcImportName := types.NamespacedName{ Namespace: namespace, Name: string(backendRef.Name()), } svcImport := &anv1alpha1.ServiceImport{} if err := t.client.Get(ctx, svcImportName, svcImport); err != nil { if !apierrors.IsNotFound(err) { return nil, err } } vpc, ok := svcImport.Annotations["application-networking.k8s.aws/aws-vpc"] if ok { svcImportTg.VpcId = vpc } eksCluster, ok := svcImport.Annotations["application-networking.k8s.aws/aws-eks-cluster-name"] if ok { svcImportTg.K8SClusterName = eksCluster } ruleTG.SvcImportTG = &svcImportTg } if string(*backendRef.Kind()) == "Service" { // generate the actual target group model for the backendRef _, tg, err := t.brTgBuilder.Build(ctx, t.route, backendRef, t.stack) if err != nil { ibre := &InvalidBackendRefError{} if !errors.As(err, &ibre) { return nil, err } t.log.Infof(ctx, "Invalid backendRef found on route %s", t.route.Name()) ruleTG.StackTargetGroupId = model.InvalidBackendRefTgId } else { ruleTG.StackTargetGroupId = tg.ID() } } tgList = append(tgList, &ruleTG) } return tgList, nil }