diff --git a/controllers/errors.go b/controllers/errors.go new file mode 100644 index 00000000..f448594c --- /dev/null +++ b/controllers/errors.go @@ -0,0 +1,10 @@ +package controllers + +import "errors" + +var ( + GroupNameError = errors.New("wrong group name") + KindError = errors.New("target kind error") + TargetRefNotFound = errors.New("targetRef not found") + TargetRefConflict = errors.New("targetRef has conflict") +) diff --git a/controllers/iamauthpolicy_controller.go b/controllers/iamauthpolicy_controller.go index c6937fae..4e8c0bbd 100644 --- a/controllers/iamauthpolicy_controller.go +++ b/controllers/iamauthpolicy_controller.go @@ -2,19 +2,21 @@ package controllers import ( "context" + "errors" "fmt" - "time" anv1alpha1 "github.com/aws/aws-application-networking-k8s/pkg/apis/applicationnetworking/v1alpha1" pkg_aws "github.com/aws/aws-application-networking-k8s/pkg/aws" "github.com/aws/aws-application-networking-k8s/pkg/aws/services" deploy "github.com/aws/aws-application-networking-k8s/pkg/deploy/lattice" + "github.com/aws/aws-application-networking-k8s/pkg/k8s" model "github.com/aws/aws-application-networking-k8s/pkg/model/lattice" - "github.com/aws/aws-application-networking-k8s/pkg/utils" "github.com/aws/aws-application-networking-k8s/pkg/utils/gwlog" + "golang.org/x/exp/slices" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -27,17 +29,26 @@ import ( gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) +const ( + IAMAuthPolicyAnnotation = "iam-auth-policy" + IAMAuthPolicyAnnotationResId = k8s.AnnotationPrefix + IAMAuthPolicyAnnotation + "-resource-id" + IAMAuthPolicyAnnotationType = k8s.AnnotationPrefix + IAMAuthPolicyAnnotation + "-resource-type" + IAMAuthPolicyFinalizer = k8s.AnnotationPrefix + IAMAuthPolicyAnnotation +) + type IAMAuthPolicyController struct { log gwlog.Logger client client.Client - policyMgr deploy.IAMAuthPolicyManager + policyMgr *deploy.IAMAuthPolicyManager + cloud pkg_aws.Cloud } func RegisterIAMAuthPolicyController(log gwlog.Logger, mgr ctrl.Manager, cloud pkg_aws.Cloud) error { controller := &IAMAuthPolicyController{ log: log, client: mgr.GetClient(), - policyMgr: deploy.IAMAuthPolicyManager{Cloud: cloud}, + policyMgr: deploy.NewIAMAuthPolicyManager(cloud), + cloud: cloud, } mapfn := iamAuthPolicyMapFunc(mgr.GetClient(), log) err := ctrl.NewControllerManagedBy(mgr). @@ -52,6 +63,23 @@ func RegisterIAMAuthPolicyController(log gwlog.Logger, mgr ctrl.Manager, cloud p return err } +// Reconciles IAMAuthPolicy CRD. +// +// IAMAuthPolicy has a plain text policy field and targetRef.Content of policy is not validated by +// controller, but Lattice API. +// +// TargetRef Kind can be Gatbeway, HTTPRoute, or GRPCRoute. Other Kinds will result in Invalid +// status. Policy can be attached to single targetRef only. Attempt to attach more than 1 policy +// will result in Policy Conflict. If policies created in sequence, the first one will be in +// Accepted status, and second in Conflict. Any following updates to accepted policy will put it +// into conflicting status, and requires manual resolution - delete conflicting policy. +// +// Lattice side. Gateway attaches to Lattice ServiceNetwork, and HTTP/GRPCRoute to Service. Policy +// attachment changes ServiceNetowrk and Service auth-type to IAM, and detachment to +// NONE. Successful creation of lattice policy updates k8s policy annotation with ARN/Id of Lattice +// Resouce +// +// Policy Attachment Spec is defined in [GEP-713]: https://gateway-api.sigs.k8s.io/geps/gep-713/. func (c *IAMAuthPolicyController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { k8sPolicy := &anv1alpha1.IAMAuthPolicy{} err := c.client.Get(ctx, req.NamespacedName, k8sPolicy) @@ -59,194 +87,150 @@ func (c *IAMAuthPolicyController) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, client.IgnoreNotFound(err) } c.log.Infow("reconcile", "req", req, "targetRef", k8sPolicy.Spec.TargetRef) - isDelete := !k8sPolicy.DeletionTimestamp.IsZero() - kind := k8sPolicy.Spec.TargetRef.Kind - var reconcileFunc func(context.Context, *anv1alpha1.IAMAuthPolicy) error - switch kind { - case "Gateway": - if isDelete { - reconcileFunc = c.deleteGatewayPolicy - } else { - reconcileFunc = c.upsertGatewayPolicy - } - case "HTTPRoute", "GRPCRoute": - if isDelete { - reconcileFunc = c.deleteRoutePolicy - } else { - reconcileFunc = c.upsertRoutePolicy - } - default: - c.log.Errorw("unsupported targetRef", "kind", kind, "req", req) - return ctrl.Result{RequeueAfter: time.Hour}, nil + var res ctrl.Result + if isDelete { + res, err = c.reconcileDelete(ctx, k8sPolicy) + } else { + res, err = c.reconcileUpsert(ctx, k8sPolicy) } - - err = reconcileFunc(ctx, k8sPolicy) if err != nil { - c.log.Infof("reconcile error, retry in 30 sec: %s", err) - return ctrl.Result{RequeueAfter: time.Second * 30}, nil + return ctrl.Result{}, err } - - err = c.handleFinalizer(ctx, k8sPolicy) + err = c.client.Update(ctx, k8sPolicy) if err != nil { return reconcile.Result{}, err } - c.log.Infow("reconciled IAM policy", "req", req, "targetRef", k8sPolicy.Spec.TargetRef, "isDeleted", isDelete, ) - return ctrl.Result{}, nil + return res, nil } -func (c *IAMAuthPolicyController) handleFinalizer(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) error { - authPolicyFinalizer := "iamauthpolicy.k8s.aws/resources" - if k8sPolicy.DeletionTimestamp.IsZero() { - if !controllerutil.ContainsFinalizer(k8sPolicy, authPolicyFinalizer) { - controllerutil.AddFinalizer(k8sPolicy, authPolicyFinalizer) - } - } else { - if controllerutil.ContainsFinalizer(k8sPolicy, authPolicyFinalizer) { - controllerutil.RemoveFinalizer(k8sPolicy, authPolicyFinalizer) +func (c *IAMAuthPolicyController) reconcileDelete(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) (ctrl.Result, error) { + err := c.validateSpec(ctx, k8sPolicy) + if err == nil { + modelPolicy := model.NewIAMAuthPolicy(k8sPolicy) + _, err := c.policyMgr.Delete(ctx, modelPolicy) + if err != nil { + return ctrl.Result{}, services.IgnoreNotFound(err) } } - return c.client.Update(ctx, k8sPolicy) + err = c.handleLatticeResourceChange(ctx, k8sPolicy, model.IAMAuthPolicyStatus{}) + if err != nil { + return ctrl.Result{}, err + } + c.removeFinalizer(k8sPolicy) + return ctrl.Result{}, nil } -func (c *IAMAuthPolicyController) deleteGatewayPolicy(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) error { - snId, err := c.findSnId(ctx, k8sPolicy) +func (c *IAMAuthPolicyController) reconcileUpsert(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) (ctrl.Result, error) { + validationErr := c.validateSpec(ctx, k8sPolicy) + err := c.updateStatus(ctx, k8sPolicy, validationErr) if err != nil { - return ignoreTargetRefNotFound(err) + return ctrl.Result{}, err } - err = c.policyMgr.Delete(ctx, snId) - if err != nil { - return err + var statusPolicy model.IAMAuthPolicyStatus + if validationErr == nil { + modelPolicy := model.NewIAMAuthPolicy(k8sPolicy) + statusPolicy, err = c.policyMgr.Put(ctx, modelPolicy) + if err != nil { + return reconcile.Result{}, services.IgnoreNotFound(err) + } + c.updateLatticeAnnotaion(k8sPolicy, statusPolicy.ResourceId, modelPolicy.Type) } - err = c.policyMgr.DisableSnIAMAuth(ctx, snId) + err = c.handleLatticeResourceChange(ctx, k8sPolicy, statusPolicy) if err != nil { - return err + return reconcile.Result{}, err } - return nil + c.addFinalizer(k8sPolicy) + return ctrl.Result{}, nil } -func (c *IAMAuthPolicyController) findSnId(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) (string, error) { +func (c *IAMAuthPolicyController) validateSpec(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) error { tr := k8sPolicy.Spec.TargetRef - snInfo, err := c.policyMgr.Cloud.Lattice().FindServiceNetwork(ctx, string(tr.Name), "") - if err != nil { - return "", err + if tr.Group != gwv1beta1.GroupName { + return fmt.Errorf("%w: %s", GroupNameError, tr.Group) } - return *snInfo.SvcNetwork.Id, nil -} - -func (c *IAMAuthPolicyController) upsertGatewayPolicy(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) error { - err := c.handleConflicts(ctx, k8sPolicy) - if err != nil { - return err - } - snId, err := c.findSnId(ctx, k8sPolicy) - if err != nil { - return c.handleTargetRefNotFound(ctx, k8sPolicy, err) + if !slices.Contains([]string{"Gateway", "HTTPRoute", "GRPCRoute"}, string(tr.Kind)) { + return fmt.Errorf("%w: %s", KindError, tr.Kind) } - err = c.policyMgr.EnableSnIAMAuth(ctx, snId) + refExists, err := c.targetRefExists(ctx, k8sPolicy) if err != nil { return err } - err = c.putPolicy(ctx, snId, k8sPolicy.Spec.Policy) - if err != nil { - return err + if !refExists { + return fmt.Errorf("%w: %s", TargetRefNotFound, tr.Name) } - err = c.updatePolicyCondition(ctx, k8sPolicy, gwv1alpha2.PolicyReasonAccepted) + conflictingPolicies, err := c.findConflictingPolicies(ctx, k8sPolicy) if err != nil { return err } - err = c.updateLatticeAnnotaion(ctx, k8sPolicy, snId) - if err != nil { - return err + if len(conflictingPolicies) > 0 { + return fmt.Errorf("%w, policies: %v", TargetRefConflict, conflictingPolicies) } return nil } -func (c *IAMAuthPolicyController) findSvcId(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) (string, error) { - tr := k8sPolicy.Spec.TargetRef - svcName := utils.LatticeServiceName(string(tr.Name), k8sPolicy.Namespace) - svcInfo, err := c.policyMgr.Cloud.Lattice().FindService(ctx, svcName) - if err != nil { - return "", err +func (c *IAMAuthPolicyController) updateStatus(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy, validationErr error) error { + reason := validationErrToStatusReason(validationErr) + msg := "" + if validationErr != nil { + msg = validationErr.Error() } - return *svcInfo.Id, nil + c.updatePolicyCondition(k8sPolicy, reason, msg) + err := c.client.Status().Update(ctx, k8sPolicy) + return err } -func (c *IAMAuthPolicyController) deleteRoutePolicy(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) error { - svcId, err := c.findSvcId(ctx, k8sPolicy) - if err != nil { - return ignoreTargetRefNotFound(err) - } - err = c.policyMgr.Delete(ctx, svcId) - if err != nil { - return err - } - err = c.policyMgr.DisableSvcIAMAuth(ctx, svcId) - if err != nil { - return err +func validationErrToStatusReason(validationErr error) gwv1alpha2.PolicyConditionReason { + var reason gwv1alpha2.PolicyConditionReason + switch { + case validationErr == nil: + reason = gwv1alpha2.PolicyReasonAccepted + case errors.Is(validationErr, GroupNameError) || errors.Is(validationErr, KindError): + reason = gwv1alpha2.PolicyReasonInvalid + case errors.Is(validationErr, TargetRefNotFound): + reason = gwv1alpha2.PolicyReasonTargetNotFound + case errors.Is(validationErr, TargetRefConflict): + reason = gwv1alpha2.PolicyReasonConflicted + default: + panic("unexpected validation error: " + validationErr.Error()) } - return nil + return reason } -func (c *IAMAuthPolicyController) upsertRoutePolicy(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) error { - err := c.handleConflicts(ctx, k8sPolicy) - if err != nil { - return err - } - svcId, err := c.findSvcId(ctx, k8sPolicy) - if err != nil { - return c.handleTargetRefNotFound(ctx, k8sPolicy, err) - } - err = c.policyMgr.EnableSvcIAMAuth(ctx, svcId) - if err != nil { - return err - } - err = c.putPolicy(ctx, svcId, k8sPolicy.Spec.Policy) - if err != nil { - return err - } - err = c.updatePolicyCondition(ctx, k8sPolicy, gwv1alpha2.PolicyReasonAccepted) - if err != nil { - return err - } - err = c.updateLatticeAnnotaion(ctx, k8sPolicy, svcId) - if err != nil { - return err +func (c *IAMAuthPolicyController) targetRefExists(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) (bool, error) { + tr := k8sPolicy.Spec.TargetRef + var obj client.Object + switch tr.Kind { + case "Gateway": + obj = &gwv1beta1.Gateway{} + case "HTTPRoute": + obj = &gwv1beta1.HTTPRoute{} + case "GRPCRoute": + obj = &gwv1alpha2.GRPCRoute{} + default: + panic("unexpected targetRef Kind=" + tr.Kind) } - return nil + return k8s.ObjExists(ctx, c.client, types.NamespacedName{ + Namespace: k8sPolicy.Namespace, + Name: string(tr.Name), + }, obj) } -func (c *IAMAuthPolicyController) putPolicy(ctx context.Context, resId, policy string) error { - modelPolicy := model.IAMAuthPolicy{ - ResourceId: resId, - Policy: policy, +func (c *IAMAuthPolicyController) removeFinalizer(k8sPolicy *anv1alpha1.IAMAuthPolicy) { + if controllerutil.ContainsFinalizer(k8sPolicy, IAMAuthPolicyFinalizer) { + controllerutil.RemoveFinalizer(k8sPolicy, IAMAuthPolicyFinalizer) } - _, err := c.policyMgr.Put(ctx, modelPolicy) - return err } -func (c *IAMAuthPolicyController) handleConflicts(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) error { - if !k8sPolicy.DeletionTimestamp.IsZero() { - return nil +func (c *IAMAuthPolicyController) addFinalizer(k8sPolicy *anv1alpha1.IAMAuthPolicy) { + if !controllerutil.ContainsFinalizer(k8sPolicy, IAMAuthPolicyFinalizer) { + controllerutil.AddFinalizer(k8sPolicy, IAMAuthPolicyFinalizer) } - conflictingPolicies, err := c.findConflictingPolicies(ctx, k8sPolicy) - if err != nil { - return err - } - if len(conflictingPolicies) > 0 { - err = c.updatePolicyCondition(ctx, k8sPolicy, gwv1alpha2.PolicyReasonConflicted) - if err != nil { - return err - } - return fmt.Errorf("conflict with other policies for same TargetRef, policy: %s, conflicted with: %v", - k8sPolicy.Name, conflictingPolicies) - } - return nil } func (c *IAMAuthPolicyController) findConflictingPolicies(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) ([]string, error) { @@ -269,19 +253,33 @@ func (c *IAMAuthPolicyController) findConflictingPolicies(ctx context.Context, k return out, nil } -func (c IAMAuthPolicyController) updatePolicyCondition(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy, reason gwv1alpha2.PolicyConditionReason) error { +// cleanup lattice resources after targetRef changes +func (c *IAMAuthPolicyController) handleLatticeResourceChange(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy, statusPolicy model.IAMAuthPolicyStatus) error { + prevModel, ok := c.getLatticeAnnotation(k8sPolicy) + if !ok { + return nil + } + if prevModel.ResourceId != statusPolicy.ResourceId { + _, err := c.policyMgr.Delete(ctx, prevModel) + if err != nil { + return services.IgnoreNotFound(err) + } + } + return nil +} + +func (c *IAMAuthPolicyController) updatePolicyCondition(k8sPolicy *anv1alpha1.IAMAuthPolicy, reason gwv1alpha2.PolicyConditionReason, msg string) { status := metav1.ConditionTrue if reason != gwv1alpha2.PolicyReasonAccepted { status = metav1.ConditionFalse } cnd := metav1.Condition{ - Type: string(gwv1alpha2.PolicyConditionAccepted), - Status: status, - Reason: string(reason), + Type: string(gwv1alpha2.PolicyConditionAccepted), + Status: status, + Reason: string(reason), + Message: msg, } meta.SetStatusCondition(&k8sPolicy.Status.Conditions, cnd) - err := c.client.Status().Update(ctx, k8sPolicy) - return err } func iamAuthPolicyMapFunc(c client.Client, log gwlog.Logger) handler.MapFunc { @@ -302,23 +300,19 @@ func iamAuthPolicyMapFunc(c client.Client, log gwlog.Logger) handler.MapFunc { } } -// TODO: move into services package after Erik's target group renaming -func ignoreTargetRefNotFound(err error) error { - if services.IsNotFoundError(err) { - return nil - } - return err -} - -func (c *IAMAuthPolicyController) handleTargetRefNotFound(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy, err error) error { - if services.IsNotFoundError(err) { - err = c.updatePolicyCondition(ctx, k8sPolicy, gwv1alpha2.PolicyReasonTargetNotFound) - } - return err +func (c *IAMAuthPolicyController) updateLatticeAnnotaion(k8sPolicy *anv1alpha1.IAMAuthPolicy, resId, resType string) { + k8sPolicy.Annotations[IAMAuthPolicyAnnotationResId] = resId + k8sPolicy.Annotations[IAMAuthPolicyAnnotationType] = resType } -func (c *IAMAuthPolicyController) updateLatticeAnnotaion(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy, resId string) error { - k8sPolicy.Annotations["application-networking.k8s.aws/resourceId"] = resId - err := c.client.Update(ctx, k8sPolicy) - return err +func (c *IAMAuthPolicyController) getLatticeAnnotation(k8sPolicy *anv1alpha1.IAMAuthPolicy) (model.IAMAuthPolicy, bool) { + resourceId := k8sPolicy.Annotations[IAMAuthPolicyAnnotationResId] + resourceType := k8sPolicy.Annotations[IAMAuthPolicyAnnotationType] + if resourceId == "" || resourceType == "" { + return model.IAMAuthPolicy{}, false + } + return model.IAMAuthPolicy{ + Type: resourceType, + ResourceId: resourceId, + }, true } diff --git a/controllers/iamauthpolicy_controller_test.go b/controllers/iamauthpolicy_controller_test.go new file mode 100644 index 00000000..7572731d --- /dev/null +++ b/controllers/iamauthpolicy_controller_test.go @@ -0,0 +1,130 @@ +package controllers + +import ( + "context" + "fmt" + "testing" + + mock_client "github.com/aws/aws-application-networking-k8s/mocks/controller-runtime/client" + anv1alpha1 "github.com/aws/aws-application-networking-k8s/pkg/apis/applicationnetworking/v1alpha1" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +func TestIAMAuthControllerValidate(t *testing.T) { + c := gomock.NewController(t) + defer c.Finish() + + ctx := context.Background() + mockClient := mock_client.NewMockClient(c) + iamCtrl := &IAMAuthPolicyController{ + client: mockClient, + } + k8sPolicy := &anv1alpha1.IAMAuthPolicy{} + k8sPolicy.Name = "iam-policy" + + t.Run("wrong group name", func(t *testing.T) { + k8sPolicy.Spec.TargetRef = &gwv1alpha2.PolicyTargetReference{ + Group: "Wrong", + } + + err := iamCtrl.validateSpec(ctx, k8sPolicy) + assert.ErrorIs(t, err, GroupNameError) + }) + + t.Run("wrong kind", func(t *testing.T) { + k8sPolicy.Spec.TargetRef = &gwv1alpha2.PolicyTargetReference{ + Group: gwv1alpha2.GroupName, + Kind: "Wrong", + } + + err := iamCtrl.validateSpec(ctx, k8sPolicy) + assert.ErrorIs(t, err, KindError) + }) + + t.Run("targetRef not found", func(t *testing.T) { + k8sPolicy.Spec.TargetRef = &gwv1alpha2.PolicyTargetReference{ + Group: gwv1beta1.GroupName, + Kind: "Gateway", + Name: "GwName", + } + + // mock for not found + notFoundErr := apierrors.NewNotFound(schema.GroupResource{}, "") + mockClient.EXPECT().Get(ctx, types.NamespacedName{ + Name: "GwName", + }, gomock.Any()).Return(notFoundErr) + + err := iamCtrl.validateSpec(ctx, k8sPolicy) + assert.ErrorIs(t, err, TargetRefNotFound) + }) + + t.Run("targetRef conflict", func(t *testing.T) { + k8sPolicy.Spec.TargetRef = &gwv1alpha2.PolicyTargetReference{ + Group: gwv1beta1.GroupName, + Kind: "Gateway", + Name: "GwName", + } + + // mock for not found + mockClient.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).Return(nil) + // mock for conflicts + conflictPolicy := anv1alpha1.IAMAuthPolicy{} + conflictPolicy.Name = "another-policy" + conflictPolicy.Spec.TargetRef = k8sPolicy.Spec.TargetRef + mockClient.EXPECT().List(ctx, gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, l *anv1alpha1.IAMAuthPolicyList, _ ...client.ListOption) error { + l.Items = append(l.Items, conflictPolicy) + return nil + }) + + err := iamCtrl.validateSpec(ctx, k8sPolicy) + assert.ErrorIs(t, err, TargetRefConflict) + }) + + t.Run("valid policy", func(t *testing.T) { + k8sPolicy.Spec.TargetRef = &gwv1alpha2.PolicyTargetReference{ + Group: gwv1beta1.GroupName, + Kind: "Gateway", + Name: "GwName", + } + + // mock for not found + mockClient.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).Return(nil) + // mock for conflicts + mockClient.EXPECT().List(ctx, gomock.Any(), gomock.Any()).Return(nil) + + err := iamCtrl.validateSpec(ctx, k8sPolicy) + assert.Nil(t, err) + }) +} + +func TestIAMAuthPolicyValidationErrToStatus(t *testing.T) { + + type test struct { + validationErr error + wantReason gwv1alpha2.PolicyConditionReason + } + + tests := []test{ + {fmt.Errorf("%w", GroupNameError), gwv1alpha2.PolicyReasonInvalid}, + {fmt.Errorf("%w", KindError), gwv1alpha2.PolicyReasonInvalid}, + {fmt.Errorf("%w", TargetRefNotFound), gwv1alpha2.PolicyReasonTargetNotFound}, + {fmt.Errorf("%w", TargetRefConflict), gwv1alpha2.PolicyReasonConflicted}, + {nil, gwv1alpha2.PolicyReasonAccepted}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("%T", tt.validationErr), func(t *testing.T) { + reason := validationErrToStatusReason(tt.validationErr) + assert.Equal(t, tt.wantReason, reason) + }) + } +} diff --git a/controllers/route_controller.go b/controllers/route_controller.go index b38bb7f0..d7c3b3be 100644 --- a/controllers/route_controller.go +++ b/controllers/route_controller.go @@ -19,6 +19,7 @@ package controllers import ( "context" "fmt" + "github.com/aws/aws-application-networking-k8s/pkg/aws/services" "github.com/aws/aws-application-networking-k8s/pkg/utils" "github.com/aws/aws-application-networking-k8s/pkg/utils/gwlog" diff --git a/examples/access-log-policy-example.yaml b/examples/access-log-policy-example.yaml index 3e193042..6f7da0cd 100644 --- a/examples/access-log-policy-example.yaml +++ b/examples/access-log-policy-example.yaml @@ -7,4 +7,4 @@ spec: group: gateway.networking.k8s.io kind: Gateway name: my-hotel - destinationArn: "arn:aws:s3:::my-bucket" \ No newline at end of file + destinationArn: "arn:aws:s3:::my-bucket" diff --git a/examples/iam-auth-policy-example.yaml b/examples/iam-auth-policy-example.yaml index f48c41c8..461c979f 100644 --- a/examples/iam-auth-policy-example.yaml +++ b/examples/iam-auth-policy-example.yaml @@ -23,4 +23,4 @@ spec: } } ] - } \ No newline at end of file + } diff --git a/pkg/aws/services/vpclattice.go b/pkg/aws/services/vpclattice.go index 34ece7c7..9e291515 100644 --- a/pkg/aws/services/vpclattice.go +++ b/pkg/aws/services/vpclattice.go @@ -4,9 +4,10 @@ import ( "context" "errors" "fmt" - "github.com/aws/aws-sdk-go/aws/awserr" "os" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/session" @@ -37,10 +38,22 @@ func NewNotFoundError(resourceType string, name string) error { } func IsNotFoundError(err error) bool { + if aerr, ok := err.(awserr.Error); ok { + if aerr.Code() == vpclattice.ErrCodeResourceNotFoundException { + return true + } + } nfErr := &NotFoundError{} return errors.As(err, &nfErr) } +func IgnoreNotFound(err error) error { + if IsNotFoundError(err) { + return nil + } + return err +} + type ConflictError struct { ResourceType string Name string diff --git a/pkg/aws/services/vpclattice_test.go b/pkg/aws/services/vpclattice_test.go index 16425ca9..ecc32063 100644 --- a/pkg/aws/services/vpclattice_test.go +++ b/pkg/aws/services/vpclattice_test.go @@ -3,10 +3,11 @@ package services import ( "context" "fmt" + "testing" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/request" - "testing" "github.com/aws/aws-sdk-go/service/vpclattice" "github.com/golang/mock/gomock" @@ -482,7 +483,7 @@ func Test_defaultLattice_FindServiceNetwork_pagedResults(t *testing.T) { tokens := []*string{&one, &two, nil} results := [][]*vpclattice.ServiceNetworkSummary{{}, {}, {}} - for i, _ := range results { + for i := range results { for j := 1; j <= 5; j++ { // ids will be 11 - 15, 21 - 21, 31 - 35 id := fmt.Sprintf("%d%d", i+1, j) diff --git a/pkg/deploy/lattice/iamauthpolicy_manager.go b/pkg/deploy/lattice/iamauthpolicy_manager.go index 0a146b03..98c3e44d 100644 --- a/pkg/deploy/lattice/iamauthpolicy_manager.go +++ b/pkg/deploy/lattice/iamauthpolicy_manager.go @@ -10,39 +10,127 @@ import ( ) type IAMAuthPolicyManager struct { - Cloud pkg_aws.Cloud + cloud pkg_aws.Cloud +} + +func NewIAMAuthPolicyManager(cloud pkg_aws.Cloud) *IAMAuthPolicyManager { + return &IAMAuthPolicyManager{cloud: cloud} } func (m *IAMAuthPolicyManager) Put(ctx context.Context, policy model.IAMAuthPolicy) (model.IAMAuthPolicyStatus, error) { + switch policy.Type { + case model.ServiceNetworkType: + return m.putSn(ctx, policy) + case model.ServiceType: + return m.putSvc(ctx, policy) + default: + panic("unknown policy resource type: " + policy.Type) + } +} + +func (m *IAMAuthPolicyManager) putSn(ctx context.Context, policy model.IAMAuthPolicy) (model.IAMAuthPolicyStatus, error) { + sn, err := m.cloud.Lattice().FindServiceNetwork(ctx, policy.Name, "") + if err != nil { + return model.IAMAuthPolicyStatus{}, err + } + resourceId := *sn.SvcNetwork.Id + err = m.enableSnIAMAuth(ctx, resourceId) + if err != nil { + return model.IAMAuthPolicyStatus{}, err + } + err = m.putPolicy(ctx, resourceId, policy.Policy) + if err != nil { + return model.IAMAuthPolicyStatus{}, err + } + return model.IAMAuthPolicyStatus{ResourceId: resourceId}, nil +} + +func (m *IAMAuthPolicyManager) putSvc(ctx context.Context, policy model.IAMAuthPolicy) (model.IAMAuthPolicyStatus, error) { + svc, err := m.cloud.Lattice().FindService(ctx, policy.Name) + if err != nil { + return model.IAMAuthPolicyStatus{}, err + } + resourceId := *svc.Id + err = m.enableSvcIAMAuth(ctx, resourceId) + if err != nil { + return model.IAMAuthPolicyStatus{}, err + } + err = m.putPolicy(ctx, resourceId, policy.Policy) + if err != nil { + return model.IAMAuthPolicyStatus{}, err + } + return model.IAMAuthPolicyStatus{ResourceId: resourceId}, nil +} + +func (m *IAMAuthPolicyManager) putPolicy(ctx context.Context, id, policy string) error { req := &vpclattice.PutAuthPolicyInput{ - Policy: &policy.Policy, - ResourceIdentifier: &policy.ResourceId, + Policy: &policy, + ResourceIdentifier: &id, + } + _, err := m.cloud.Lattice().PutAuthPolicyWithContext(ctx, req) + return err +} + +func (m *IAMAuthPolicyManager) Delete(ctx context.Context, policy model.IAMAuthPolicy) (model.IAMAuthPolicyStatus, error) { + switch policy.Type { + case model.ServiceNetworkType: + return m.deleteSn(ctx, policy) + case model.ServiceType: + return m.deleteSvc(ctx, policy) + default: + panic("unknown policy resource type: " + policy.Type) + } +} + +func (m *IAMAuthPolicyManager) deleteSn(ctx context.Context, policy model.IAMAuthPolicy) (model.IAMAuthPolicyStatus, error) { + if policy.ResourceId == "" { + sn, err := m.cloud.Lattice().FindServiceNetwork(ctx, policy.Name, "") + if err != nil { + return model.IAMAuthPolicyStatus{}, err + } + policy.ResourceId = *sn.SvcNetwork.Id } - resp, err := m.Cloud.Lattice().PutAuthPolicyWithContext(ctx, req) + err := m.disableSnIAMAuth(ctx, policy.ResourceId) if err != nil { return model.IAMAuthPolicyStatus{}, err } - return model.IAMAuthPolicyStatus{ - ResourceId: policy.ResourceId, - State: *resp.State, - }, nil + err = m.deletePolicy(ctx, policy.ResourceId) + if err != nil { + return model.IAMAuthPolicyStatus{}, err + } + return model.IAMAuthPolicyStatus{ResourceId: policy.ResourceId}, nil } -func (m *IAMAuthPolicyManager) Delete(ctx context.Context, resourceId string) error { - req := &vpclattice.DeleteAuthPolicyInput{ - ResourceIdentifier: &resourceId, +func (m *IAMAuthPolicyManager) deleteSvc(ctx context.Context, policy model.IAMAuthPolicy) (model.IAMAuthPolicyStatus, error) { + if policy.ResourceId == "" { + svc, err := m.cloud.Lattice().FindService(ctx, policy.Name) + if err != nil { + return model.IAMAuthPolicyStatus{}, err + } + policy.ResourceId = *svc.Id } - _, err := m.Cloud.Lattice().DeleteAuthPolicyWithContext(ctx, req) + err := m.disableSvcIAMAuth(ctx, policy.ResourceId) if err != nil { - return err + return model.IAMAuthPolicyStatus{}, err } - return nil + err = m.deletePolicy(ctx, policy.ResourceId) + if err != nil { + return model.IAMAuthPolicyStatus{}, err + } + return model.IAMAuthPolicyStatus{ResourceId: policy.ResourceId}, nil } -func (m *IAMAuthPolicyManager) EnableSnIAMAuth(ctx context.Context, snId string) error { +func (m *IAMAuthPolicyManager) deletePolicy(ctx context.Context, resId string) error { + req := &vpclattice.DeleteAuthPolicyInput{ResourceIdentifier: &resId} + _, err := m.cloud.Lattice().DeleteAuthPolicy(req) + return err +} + +func (m *IAMAuthPolicyManager) enableSnIAMAuth(ctx context.Context, snId string) error { return m.setSnAuthType(ctx, snId, vpclattice.AuthTypeAwsIam) } -func (m *IAMAuthPolicyManager) DisableSnIAMAuth(ctx context.Context, snId string) error { + +func (m *IAMAuthPolicyManager) disableSnIAMAuth(ctx context.Context, snId string) error { return m.setSnAuthType(ctx, snId, vpclattice.AuthTypeNone) } @@ -51,15 +139,15 @@ func (m *IAMAuthPolicyManager) setSnAuthType(ctx context.Context, snId, authType AuthType: &authType, ServiceNetworkIdentifier: &snId, } - _, err := m.Cloud.Lattice().UpdateServiceNetworkWithContext(ctx, req) + _, err := m.cloud.Lattice().UpdateServiceNetworkWithContext(ctx, req) return err } -func (m *IAMAuthPolicyManager) EnableSvcIAMAuth(ctx context.Context, svcId string) error { +func (m *IAMAuthPolicyManager) enableSvcIAMAuth(ctx context.Context, svcId string) error { return m.setSvcAuthType(ctx, svcId, vpclattice.AuthTypeAwsIam) } -func (m *IAMAuthPolicyManager) DisableSvcIAMAuth(ctx context.Context, svcId string) error { +func (m *IAMAuthPolicyManager) disableSvcIAMAuth(ctx context.Context, svcId string) error { return m.setSvcAuthType(ctx, svcId, vpclattice.AuthTypeNone) } @@ -68,6 +156,6 @@ func (m *IAMAuthPolicyManager) setSvcAuthType(ctx context.Context, svcId, authTy AuthType: &authType, ServiceIdentifier: &svcId, } - _, err := m.Cloud.Lattice().UpdateServiceWithContext(ctx, req) + _, err := m.cloud.Lattice().UpdateServiceWithContext(ctx, req) return err } diff --git a/pkg/deploy/lattice/iamauthpolicy_manager_test.go b/pkg/deploy/lattice/iamauthpolicy_manager_test.go index 20549b0c..d7636f8a 100644 --- a/pkg/deploy/lattice/iamauthpolicy_manager_test.go +++ b/pkg/deploy/lattice/iamauthpolicy_manager_test.go @@ -1,105 +1 @@ package lattice - -import ( - "context" - "testing" - - pkg_aws "github.com/aws/aws-application-networking-k8s/pkg/aws" - mocks "github.com/aws/aws-application-networking-k8s/pkg/aws/services" - model "github.com/aws/aws-application-networking-k8s/pkg/model/lattice" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/vpclattice" - gomock "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" -) - -func TestIAMAuthPolicyManager(t *testing.T) { - c := gomock.NewController(t) - defer c.Finish() - - mockLattice := mocks.NewMockLattice(c) - cfg := pkg_aws.CloudConfig{VpcId: "vpc-id", AccountId: "account-id"} - cl := pkg_aws.NewDefaultCloud(mockLattice, cfg) - ctx := context.Background() - - m := IAMAuthPolicyManager{ - Cloud: cl, - } - - t.Run("existing sn", func(t *testing.T) { - snId := "sn-abc" - - policy := model.IAMAuthPolicy{ - ResourceId: snId, - Policy: "{}", - } - - mockLattice.EXPECT(). - PutAuthPolicyWithContext(gomock.Any(), &vpclattice.PutAuthPolicyInput{ - Policy: &policy.Policy, - ResourceIdentifier: &snId, - }). - Return(&vpclattice.PutAuthPolicyOutput{ - Policy: &policy.Policy, - State: aws.String(vpclattice.AuthPolicyStateActive), - }, nil).Times(1) - - statusGot, _ := m.Put(ctx, policy) - statusWant := model.IAMAuthPolicyStatus{ - ResourceId: snId, - State: vpclattice.AuthPolicyStateActive, - } - assert.Equal(t, statusWant, statusGot) - }) - - t.Run("existing svc", func(t *testing.T) { - svcId := "svc-abc" - - policy := model.IAMAuthPolicy{ - ResourceId: svcId, - Policy: "{}", - } - - mockLattice.EXPECT(). - PutAuthPolicyWithContext(gomock.Any(), &vpclattice.PutAuthPolicyInput{ - Policy: &policy.Policy, - ResourceIdentifier: &svcId, - }). - Return(&vpclattice.PutAuthPolicyOutput{ - Policy: &policy.Policy, - State: aws.String(vpclattice.AuthPolicyStateActive), - }, nil).Times(1) - - statusGot, _ := m.Put(ctx, policy) - statusWant := model.IAMAuthPolicyStatus{ - ResourceId: svcId, - State: vpclattice.AuthPolicyStateActive, - } - assert.Equal(t, statusWant, statusGot) - }) - - t.Run("enable SN IAM Auth", func(t *testing.T) { - snId := "snId" - - mockLattice.EXPECT(). - UpdateServiceNetworkWithContext(ctx, &vpclattice.UpdateServiceNetworkInput{ - AuthType: aws.String(vpclattice.AuthTypeAwsIam), - ServiceNetworkIdentifier: &snId, - }).Return(&vpclattice.UpdateServiceNetworkOutput{}, nil).Times(1) - - m.EnableSnIAMAuth(ctx, snId) - }) - - t.Run("enable Svc IAM Auth", func(t *testing.T) { - svcId := "svcId" - - mockLattice.EXPECT(). - UpdateServiceWithContext(ctx, &vpclattice.UpdateServiceInput{ - AuthType: aws.String(vpclattice.AuthTypeAwsIam), - ServiceIdentifier: &svcId, - }).Return(&vpclattice.UpdateServiceOutput{}, nil).Times(1) - - m.EnableSvcIAMAuth(ctx, svcId) - }) -} diff --git a/pkg/k8s/utils.go b/pkg/k8s/utils.go index dc2cc0c5..0a1a39cf 100644 --- a/pkg/k8s/utils.go +++ b/pkg/k8s/utils.go @@ -1,11 +1,14 @@ package k8s import ( + "context" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/discovery" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -55,3 +58,14 @@ func IsGVKSupported(mgr ctrl.Manager, groupVersion string, kind string) (bool, e } return false, nil } + +func ObjExists(ctx context.Context, c client.Client, key types.NamespacedName, obj client.Object) (bool, error) { + err := c.Get(ctx, key, obj) + if err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/pkg/model/lattice/iamauthpolicy.go b/pkg/model/lattice/iamauthpolicy.go index f57aedea..845e9e2a 100644 --- a/pkg/model/lattice/iamauthpolicy.go +++ b/pkg/model/lattice/iamauthpolicy.go @@ -1,11 +1,40 @@ package lattice +import ( + "fmt" + + anv1alpha1 "github.com/aws/aws-application-networking-k8s/pkg/apis/applicationnetworking/v1alpha1" + "github.com/aws/aws-application-networking-k8s/pkg/utils" +) + type IAMAuthPolicy struct { + Type string + Name string ResourceId string Policy string } type IAMAuthPolicyStatus struct { ResourceId string - State string +} + +func NewIAMAuthPolicy(k8sPolicy *anv1alpha1.IAMAuthPolicy) IAMAuthPolicy { + kind := k8sPolicy.Spec.TargetRef.Kind + policy := k8sPolicy.Spec.Policy + switch kind { + case "Gateway": + return IAMAuthPolicy{ + Type: ServiceNetworkType, + Name: string(k8sPolicy.Spec.TargetRef.Name), + Policy: policy, + } + case "HTTPRoute", "GRPCRoute": + return IAMAuthPolicy{ + Type: ServiceType, + Name: utils.LatticeServiceName(string(k8sPolicy.Spec.TargetRef.Name), k8sPolicy.Namespace), + Policy: policy, + } + default: + panic(fmt.Sprintf("unexpected targetRef, Kind=%s", kind)) + } } diff --git a/pkg/model/lattice/types.go b/pkg/model/lattice/types.go new file mode 100644 index 00000000..1fc7e2d6 --- /dev/null +++ b/pkg/model/lattice/types.go @@ -0,0 +1,6 @@ +package lattice + +const ( + ServiceNetworkType = "ServiceNetwork" + ServiceType = "Service" +)