Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply refactoring to istio-jwt branch #114

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 44 additions & 133 deletions controllers/apirule_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ import (
"fmt"
"time"

gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1"

"github.com/kyma-incubator/api-gateway/internal/helpers"
"github.com/kyma-incubator/api-gateway/internal/processing"
"github.com/kyma-incubator/api-gateway/internal/processing/istio"
"github.com/kyma-incubator/api-gateway/internal/processing/ory"
"github.com/kyma-incubator/api-gateway/internal/validation"

"github.com/go-logr/logr"
networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1"
"github.com/kyma-incubator/api-gateway/internal/processing"
corev1 "k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -134,76 +134,61 @@ func (r *APIRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
failuresJson, _ := json.Marshal(configValidationFailures)
r.Log.Error(err, fmt.Sprintf(`Config validation failure {"controller": "Api", "failures": %s}`, string(failuresJson)))
}
return r.doneReconcile()
return doneReconcile()
}
}

api := &gatewayv1beta1.APIRule{}
err := r.Client.Get(ctx, req.NamespacedName, api)
apiRule := &gatewayv1beta1.APIRule{}
err := r.Client.Get(ctx, req.NamespacedName, apiRule)
if err != nil {
if apierrs.IsNotFound(err) {
//There is no APIRule. Nothing to process, dependent objects will be garbage-collected.
return r.doneReconcile()
return doneReconcile()
}

//Nothing is yet processed: StatusSkipped
return r.setStatusForError(ctx, api, err, gatewayv1beta1.StatusSkipped)
status := processing.GetStatusForError(&r.Log, err, gatewayv1beta1.StatusSkipped)
return r.updateStatusOrRetry(ctx, apiRule, status)
}

configValidationFailures := validator.ValidateConfig(r.Config)
if len(configValidationFailures) > 0 {
failuresJson, _ := json.Marshal(configValidationFailures)
r.Log.Error(err, fmt.Sprintf(`Config validation failure {"controller": "Api", "request": "%s/%s", "failures": %s}`, api.Namespace, api.Name, string(failuresJson)))
return r.setStatus(ctx, api, generateValidationStatus(configValidationFailures), gatewayv1beta1.StatusError)
r.Log.Error(err, fmt.Sprintf(`Config validation failure {"controller": "Api", "request": "%s/%s", "failures": %s}`, apiRule.Namespace, apiRule.Name, string(failuresJson)))
return r.updateStatusOrRetry(ctx, apiRule, processing.GetFailedValidationStatus(configValidationFailures))
}

//Prevent reconciliation after status update. It should be solved by controller-runtime implementation but still isn't.
if api.Generation != api.Status.ObservedGeneration {
//1.1) Get the list of existing Virtual Services to validate host
var vsList networkingv1beta1.VirtualServiceList
if err := r.Client.List(ctx, &vsList); err != nil {
//Nothing is yet processed: StatusSkipped
return r.setStatusForError(ctx, api, err, gatewayv1beta1.StatusSkipped)
if apiRule.Generation != apiRule.Status.ObservedGeneration {

c := processing.ReconciliationConfig{
OathkeeperSvc: r.OathkeeperSvc,
OathkeeperSvcPort: r.OathkeeperSvcPort,
CorsConfig: r.CorsConfig,
AdditionalLabels: r.GeneratedObjectsLabels,
DefaultDomainName: r.DefaultDomainName,
ServiceBlockList: r.ServiceBlockList,
DomainAllowList: r.DomainAllowList,
HostBlockList: r.HostBlockList,
}

//1.2) Validate input including host
validationFailures := validator.Validate(api, vsList, r.Config)
if len(validationFailures) > 0 {
failuresJson, _ := json.Marshal(validationFailures)
r.Log.Info(fmt.Sprintf(`Validation failure {"controller": "Api", "request": "%s/%s", "failures": %s}`, api.Namespace, api.Name, string(failuresJson)))
return r.setStatus(ctx, api, generateValidationStatus(validationFailures), gatewayv1beta1.StatusSkipped)
}
cmd := r.getReconciliation(c)

//2) Compute list of required objects (the set of objects required to satisfy our contract on apiRule.Spec, not yet applied)
factory := processing.NewFactory(r.Client, r.Log, r.OathkeeperSvc, r.OathkeeperSvcPort, r.CorsConfig, r.GeneratedObjectsLabels, r.DefaultDomainName)
requiredObjects := factory.CalculateRequiredState(api, r.Config)
status := processing.Reconcile(ctx, r.Client, &r.Log, cmd, apiRule)

//3.1 Fetch all existing objects related to _this_ apiRule from the cluster (VS, Rules)
actualObjects, err := factory.GetActualState(ctx, api, r.Config)
if err != nil {
return r.setStatusForError(ctx, api, err, gatewayv1beta1.StatusSkipped)
}

//3.2 Compute patch object
patch := factory.CalculateDiff(requiredObjects, actualObjects, r.Config)

//3.3 Apply changes to the cluster
err = factory.ApplyDiff(ctx, patch, r.Config)
if err != nil {
//We don't know exactly which object(s) are not updated properly.
//The safest approach is to assume nothing is correct and just use `StatusError`.
return r.setStatusForError(ctx, api, err, gatewayv1beta1.StatusError)
}
return r.updateStatusOrRetry(ctx, apiRule, status)
}

//4) Update status of CR
APIStatus := &gatewayv1beta1.APIRuleResourceStatus{
Code: gatewayv1beta1.StatusOK,
}
return doneReconcile()
}

return r.setStatus(ctx, api, APIStatus, gatewayv1beta1.StatusOK)
func (r *APIRuleReconciler) getReconciliation(config processing.ReconciliationConfig) processing.ReconciliationCommand {
if r.Config.JWTHandler == helpers.JWT_HANDLER_ISTIO {
return istio.NewIstioReconciliation(config)
}

return r.doneReconcile()
return ory.NewOryReconciliation(config)

}

// SetupWithManager sets up the controller with the Manager.
Expand All @@ -215,110 +200,36 @@ func (r *APIRuleReconciler) SetupWithManager(mgr ctrl.Manager) error {
Complete(r)
}

// Sets status of APIRule. Accepts an auxilary status code that is used to report VirtualService, AccessRule, RequestAuthentication and AuthorizationPolicy statuses.
func (r *APIRuleReconciler) setStatus(ctx context.Context, api *gatewayv1beta1.APIRule, apiStatus *gatewayv1beta1.APIRuleResourceStatus, auxStatusCode gatewayv1beta1.StatusCode) (ctrl.Result, error) {
virtualServiceStatus := &gatewayv1beta1.APIRuleResourceStatus{
Code: auxStatusCode,
}
accessRuleStatus := &gatewayv1beta1.APIRuleResourceStatus{
Code: auxStatusCode,
}
reqAuthStatus := &gatewayv1beta1.APIRuleResourceStatus{
Code: auxStatusCode,
}
authPolicyStatus := &gatewayv1beta1.APIRuleResourceStatus{
Code: auxStatusCode,
}

return r.updateStatusOrRetry(ctx, api, apiStatus, virtualServiceStatus, accessRuleStatus, reqAuthStatus, authPolicyStatus)
}

// Sets status of APIRule in error condition. Accepts an auxilary status code that is used to report VirtualService, AccessRule, RequestAuthentication and AuthorizationPolicy statuses.
func (r *APIRuleReconciler) setStatusForError(ctx context.Context, api *gatewayv1beta1.APIRule, err error, auxStatusCode gatewayv1beta1.StatusCode) (ctrl.Result, error) {
r.Log.Error(err, "Error during reconciliation")

virtualServiceStatus := &gatewayv1beta1.APIRuleResourceStatus{
Code: auxStatusCode,
}
accessRuleStatus := &gatewayv1beta1.APIRuleResourceStatus{
Code: auxStatusCode,
}
reqAuthStatus := &gatewayv1beta1.APIRuleResourceStatus{
Code: auxStatusCode,
}
authPolicyStatus := &gatewayv1beta1.APIRuleResourceStatus{
Code: auxStatusCode,
}

return r.updateStatusOrRetry(ctx, api, generateErrorStatus(err), virtualServiceStatus, accessRuleStatus, reqAuthStatus, authPolicyStatus)
}

// Updates api status. If there was an error during update, returns the error so that entire reconcile loop is retried. If there is no error, returns a "reconcile success" value.
func (r *APIRuleReconciler) updateStatusOrRetry(ctx context.Context, api *gatewayv1beta1.APIRule, apiStatus, virtualServiceStatus, accessRuleStatus, reqAuthStatus, authPolicyStatus *gatewayv1beta1.APIRuleResourceStatus) (ctrl.Result, error) {
_, updateStatusErr := r.updateStatus(ctx, api, apiStatus, virtualServiceStatus, accessRuleStatus, reqAuthStatus, authPolicyStatus)
func (r *APIRuleReconciler) updateStatusOrRetry(ctx context.Context, api *gatewayv1beta1.APIRule, status processing.ReconciliationStatus) (ctrl.Result, error) {
_, updateStatusErr := r.updateStatus(ctx, api, status)
if updateStatusErr != nil {
return retryReconcile(updateStatusErr) //controller retries to set the correct status eventually.
}
//Fail fast: If status is updated, users are informed about the problem. We don't need to reconcile again.
return r.doneReconcile()
return doneReconcile()
}

func (r *APIRuleReconciler) doneReconcile() (ctrl.Result, error) {
r.Log.Info("Ending reconcilation")
func doneReconcile() (ctrl.Result, error) {
return ctrl.Result{}, nil
}

func retryReconcile(err error) (ctrl.Result, error) {
return reconcile.Result{Requeue: true}, err
}

func (r *APIRuleReconciler) updateStatus(ctx context.Context, api *gatewayv1beta1.APIRule, APIStatus, virtualServiceStatus, accessRuleStatus, reqAuthStatus, authPolicyStatus *gatewayv1beta1.APIRuleResourceStatus) (*gatewayv1beta1.APIRule, error) {
func (r *APIRuleReconciler) updateStatus(ctx context.Context, api *gatewayv1beta1.APIRule, status processing.ReconciliationStatus) (*gatewayv1beta1.APIRule, error) {
api.Status.ObservedGeneration = api.Generation
api.Status.LastProcessedTime = &v1.Time{Time: time.Now()}
api.Status.APIRuleStatus = APIStatus
api.Status.VirtualServiceStatus = virtualServiceStatus
api.Status.AccessRuleStatus = accessRuleStatus
api.Status.RequestAuthenticationStatus = reqAuthStatus
api.Status.AuthorizationPolicyStatus = authPolicyStatus
api.Status.APIRuleStatus = status.ApiRuleStatus
api.Status.VirtualServiceStatus = status.VirtualServiceStatus
api.Status.AccessRuleStatus = status.AccessRuleStatus
api.Status.RequestAuthenticationStatus = status.RequestAuthenticationStatus
api.Status.AuthorizationPolicyStatus = status.AuthorizationPolicyStatus

err := r.Client.Status().Update(ctx, api)
if err != nil {
return nil, err
}
return api, nil
}

func generateErrorStatus(err error) *gatewayv1beta1.APIRuleResourceStatus {
return toStatus(gatewayv1beta1.StatusError, err.Error())
}

func generateValidationStatus(failures []validation.Failure) *gatewayv1beta1.APIRuleResourceStatus {
return toStatus(gatewayv1beta1.StatusError, generateValidationDescription(failures))
}

func toStatus(c gatewayv1beta1.StatusCode, desc string) *gatewayv1beta1.APIRuleResourceStatus {
return &gatewayv1beta1.APIRuleResourceStatus{
Code: c,
Description: desc,
}
}

func generateValidationDescription(failures []validation.Failure) string {
var description string

if len(failures) == 1 {
description = "Validation error: "
description += fmt.Sprintf("Attribute \"%s\": %s", failures[0].AttributePath, failures[0].Message)
} else {
const maxEntries = 3
description = "Multiple validation errors: "
for i := 0; i < len(failures) && i < maxEntries; i++ {
description += fmt.Sprintf("\nAttribute \"%s\": %s", failures[i].AttributePath, failures[i].Message)
}
if len(failures) > maxEntries {
description += fmt.Sprintf("\n%d more error(s)...", len(failures)-maxEntries)
}
}

return description
}
105 changes: 105 additions & 0 deletions internal/processing/access_rule_processor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package processing

import (
"context"
"fmt"
gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1"
rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
)

// AccessRuleProcessor is the generic processor that handles the Ory Rules in the reconciliation of API Rule.
type AccessRuleProcessor struct {
Creator AccessRuleCreator
}

// AccessRuleCreator provides the creation of Rules using the configuration in the given APIRule.
// The key of the map is expected to be unique and comparable with the
type AccessRuleCreator interface {
Create(api *gatewayv1beta1.APIRule) map[string]*rulev1alpha1.Rule
}

func (r AccessRuleProcessor) EvaluateReconciliation(ctx context.Context, client ctrlclient.Client, apiRule *gatewayv1beta1.APIRule) ([]*ObjectChange, error) {
desired := r.getDesiredState(apiRule)
actual, err := r.getActualState(ctx, client, apiRule)
if err != nil {
return make([]*ObjectChange, 0), err
}

c := r.getObjectChanges(desired, actual)

return c, nil
}

func (r AccessRuleProcessor) getObjectChanges(desiredRules map[string]*rulev1alpha1.Rule, actualRules map[string]*rulev1alpha1.Rule) []*ObjectChange {
arChanges := make(map[string]*ObjectChange)

for path, rule := range desiredRules {

if actualRules[path] != nil {
actualRules[path].Spec = rule.Spec
arChanges[path] = NewObjectUpdateAction(actualRules[path])
} else {
arChanges[path] = NewObjectCreateAction(rule)
}

}

for path, rule := range actualRules {
if desiredRules[path] == nil {
arChanges[path] = NewObjectDeleteAction(rule)
}
}

arChangesToApply := make([]*ObjectChange, 0, len(arChanges))

for _, applyCommand := range arChanges {
arChangesToApply = append(arChangesToApply, applyCommand)
}

return arChangesToApply
}

func (r AccessRuleProcessor) getDesiredState(api *gatewayv1beta1.APIRule) map[string]*rulev1alpha1.Rule {
return r.Creator.Create(api)
}

func (r AccessRuleProcessor) getActualState(ctx context.Context, client ctrlclient.Client, api *gatewayv1beta1.APIRule) (map[string]*rulev1alpha1.Rule, error) {
labels := GetOwnerLabels(api)

var arList rulev1alpha1.RuleList
if err := client.List(ctx, &arList, ctrlclient.MatchingLabels(labels)); err != nil {
return nil, err
}

accessRules := make(map[string]*rulev1alpha1.Rule)
pathDuplicates := HasPathDuplicates(api.Spec.Rules)

for i := range arList.Items {
obj := arList.Items[i]
accessRules[SetAccessRuleKey(pathDuplicates, obj)] = &obj
}

return accessRules, nil
}

func SetAccessRuleKey(hasPathDuplicates bool, rule rulev1alpha1.Rule) string {

if hasPathDuplicates {
return fmt.Sprintf("%s:%s", rule.Spec.Match.URL, rule.Spec.Match.Methods)
}

return rule.Spec.Match.URL
}

func HasPathDuplicates(rules []gatewayv1beta1.Rule) bool {
duplicates := map[string]bool{}
for _, rule := range rules {
if duplicates[rule.Path] {
return true
}
duplicates[rule.Path] = true
}

return false
}
Loading