portainer/portainer

View on GitHub
api/http/handler/kubernetes/ingresses.go

Summary

Maintainability
D
2 days
Test Coverage
package kubernetes

import (
    "net/http"

    portainer "github.com/portainer/portainer/api"
    "github.com/portainer/portainer/api/http/middlewares"
    models "github.com/portainer/portainer/api/http/models/kubernetes"
    "github.com/portainer/portainer/api/http/security"
    httperror "github.com/portainer/portainer/pkg/libhttp/error"
    "github.com/portainer/portainer/pkg/libhttp/request"
    "github.com/portainer/portainer/pkg/libhttp/response"
)

// @id getKubernetesIngressControllers
// @summary Get a list of ingress controllers
// @description Get a list of ingress controllers for the given environment
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param allowedOnly query boolean false "Only return allowed ingress controllers"
// @success 200 {object} models.K8sIngressControllers "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/ingresscontrollers [get]
func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
    endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
    if err != nil {
        return httperror.BadRequest(
            "Invalid environment identifier route variable",
            err,
        )
    }

    endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
    if handler.DataStore.IsErrObjectNotFound(err) {
        return httperror.NotFound(
            "Unable to find an environment with the specified identifier inside the database",
            err,
        )
    } else if err != nil {
        return httperror.InternalServerError(
            "Unable to find an environment with the specified identifier inside the database",
            err,
        )
    }

    allowedOnly, err := request.RetrieveBooleanQueryParameter(r, "allowedOnly", true)
    if err != nil {
        return httperror.BadRequest(
            "Invalid allowedOnly boolean query parameter",
            err,
        )
    }

    cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
    if err != nil {
        return httperror.InternalServerError(
            "Unable to create Kubernetes client",
            err,
        )
    }

    controllers, err := cli.GetIngressControllers()
    if err != nil {
        return httperror.InternalServerError(
            "Failed to fetch ingressclasses",
            err,
        )
    }

    // Add none controller if "AllowNone" is set for endpoint.
    if endpoint.Kubernetes.Configuration.AllowNoneIngressClass {
        controllers = append(controllers, models.K8sIngressController{
            Name:      "none",
            ClassName: "none",
            Type:      "custom",
        })
    }
    existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
    var updatedClasses []portainer.KubernetesIngressClassConfig
    for i := range controllers {
        controllers[i].Availability = true
        if controllers[i].ClassName != "none" {
            controllers[i].New = true
        }

        var updatedClass portainer.KubernetesIngressClassConfig
        updatedClass.Name = controllers[i].ClassName
        updatedClass.Type = controllers[i].Type

        // Check if the controller is already known.
        for _, existingClass := range existingClasses {
            if controllers[i].ClassName != existingClass.Name {
                continue
            }
            controllers[i].New = false
            controllers[i].Availability = !existingClass.GloballyBlocked
            updatedClass.GloballyBlocked = existingClass.GloballyBlocked
            updatedClass.BlockedNamespaces = existingClass.BlockedNamespaces
        }
        updatedClasses = append(updatedClasses, updatedClass)
    }

    endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses
    err = handler.DataStore.Endpoint().UpdateEndpoint(
        portainer.EndpointID(endpointID),
        endpoint,
    )
    if err != nil {
        return httperror.InternalServerError(
            "Unable to store found IngressClasses inside the database",
            err,
        )
    }

    // If the allowedOnly query parameter was set. We need to prune out
    // disallowed controllers from the response.
    if allowedOnly {
        var allowedControllers models.K8sIngressControllers
        for _, controller := range controllers {
            if controller.Availability {
                allowedControllers = append(allowedControllers, controller)
            }
        }
        controllers = allowedControllers
    }
    return response.JSON(w, controllers)
}

// @id getKubernetesIngressControllersByNamespace
// @summary Get a list ingress controllers by namespace
// @description Get a list of ingress controllers for the given environment in the provided namespace
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param namespace path string true "Namespace"
// @success 200 {object} models.K8sIngressControllers "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces/{namespace}/ingresscontrollers [get]
func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
    endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
    if err != nil {
        return httperror.BadRequest(
            "Invalid environment identifier route variable",
            err,
        )
    }

    endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
    if handler.DataStore.IsErrObjectNotFound(err) {
        return httperror.NotFound(
            "Unable to find an environment with the specified identifier inside the database",
            err,
        )
    } else if err != nil {
        return httperror.InternalServerError(
            "Unable to find an environment with the specified identifier inside the database",
            err,
        )
    }

    namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
    if err != nil {
        return httperror.BadRequest(
            "Invalid namespace identifier route variable",
            err,
        )
    }

    cli, handlerErr := handler.getProxyKubeClient(r)
    if handlerErr != nil {
        return handlerErr
    }

    currentControllers, err := cli.GetIngressControllers()
    if err != nil {
        return httperror.InternalServerError(
            "Failed to fetch ingressclasses",
            err,
        )
    }
    // Add none controller if "AllowNone" is set for endpoint.
    if endpoint.Kubernetes.Configuration.AllowNoneIngressClass {
        currentControllers = append(currentControllers, models.K8sIngressController{
            Name:      "none",
            ClassName: "none",
            Type:      "custom",
        })
    }
    kubernetesConfig := endpoint.Kubernetes.Configuration
    existingClasses := kubernetesConfig.IngressClasses
    ingressAvailabilityPerNamespace := kubernetesConfig.IngressAvailabilityPerNamespace
    var updatedClasses []portainer.KubernetesIngressClassConfig
    var controllers models.K8sIngressControllers
    for i := range currentControllers {
        var globallyblocked bool
        currentControllers[i].Availability = true
        if currentControllers[i].ClassName != "none" {
            currentControllers[i].New = true
        }

        var updatedClass portainer.KubernetesIngressClassConfig
        updatedClass.Name = currentControllers[i].ClassName
        updatedClass.Type = currentControllers[i].Type

        // Check if the controller is blocked globally or in the current
        // namespace.
        for _, existingClass := range existingClasses {
            if currentControllers[i].ClassName != existingClass.Name {
                continue
            }
            currentControllers[i].New = false
            updatedClass.GloballyBlocked = existingClass.GloballyBlocked
            updatedClass.BlockedNamespaces = existingClass.BlockedNamespaces

            globallyblocked = existingClass.GloballyBlocked

            // Check if the current namespace is blocked if ingressAvailabilityPerNamespace is set to true
            if ingressAvailabilityPerNamespace {
                for _, ns := range existingClass.BlockedNamespaces {
                    if namespace == ns {
                        currentControllers[i].Availability = false
                    }
                }
            }
        }
        if !globallyblocked {
            controllers = append(controllers, currentControllers[i])
        }
        updatedClasses = append(updatedClasses, updatedClass)
    }

    // Update the database to match the list of found controllers.
    // This includes pruning out controllers which no longer exist.
    endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses
    err = handler.DataStore.Endpoint().UpdateEndpoint(
        portainer.EndpointID(endpointID),
        endpoint,
    )
    if err != nil {
        return httperror.InternalServerError(
            "Unable to store found IngressClasses inside the database",
            err,
        )
    }
    return response.JSON(w, controllers)
}

// @id updateKubernetesIngressControllers
// @summary Update (block/unblock) ingress controllers
// @description Update (block/unblock) ingress controllers
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param body body []models.K8sIngressControllers true "Ingress controllers"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/ingresscontrollers [put]
func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

    endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
    if err != nil {
        return httperror.BadRequest(
            "Invalid environment identifier route variable",
            err,
        )
    }

    endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
    if handler.DataStore.IsErrObjectNotFound(err) {
        return httperror.NotFound(
            "Unable to find an environment with the specified identifier inside the database",
            err,
        )
    } else if err != nil {
        return httperror.InternalServerError(
            "Unable to find an environment with the specified identifier inside the database",
            err,
        )
    }

    var payload models.K8sIngressControllers
    err = request.DecodeAndValidateJSONPayload(r, &payload)
    if err != nil {
        return httperror.BadRequest(
            "Invalid request payload",
            err,
        )
    }

    cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
    if err != nil {
        return httperror.InternalServerError(
            "Unable to create Kubernetes client",
            err,
        )
    }

    existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
    controllers, err := cli.GetIngressControllers()
    if err != nil {
        return httperror.InternalServerError(
            "Unable to get ingress controllers",
            err,
        )
    }

    // Add none controller if "AllowNone" is set for endpoint.
    if endpoint.Kubernetes.Configuration.AllowNoneIngressClass {
        controllers = append(controllers, models.K8sIngressController{
            Name:      "none",
            ClassName: "none",
            Type:      "custom",
        })
    }

    var updatedClasses []portainer.KubernetesIngressClassConfig
    for i := range controllers {
        controllers[i].Availability = true
        controllers[i].New = true

        var updatedClass portainer.KubernetesIngressClassConfig
        updatedClass.Name = controllers[i].ClassName
        updatedClass.Type = controllers[i].Type

        // Check if the controller is already known.
        for _, existingClass := range existingClasses {
            if controllers[i].ClassName != existingClass.Name {
                continue
            }
            controllers[i].New = false
            controllers[i].Availability = !existingClass.GloballyBlocked
            updatedClass.GloballyBlocked = existingClass.GloballyBlocked
            updatedClass.BlockedNamespaces = existingClass.BlockedNamespaces
        }
        updatedClasses = append(updatedClasses, updatedClass)
    }

    for _, p := range payload {
        for i := range controllers {
            // Now set new payload data
            if updatedClasses[i].Name == p.ClassName {
                updatedClasses[i].GloballyBlocked = !p.Availability
            }
        }
    }

    endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses
    err = handler.DataStore.Endpoint().UpdateEndpoint(
        portainer.EndpointID(endpointID),
        endpoint,
    )
    if err != nil {
        return httperror.InternalServerError(
            "Unable to update the BlockedIngressClasses inside the database",
            err,
        )
    }
    return response.Empty(w)
}

// @id updateKubernetesIngressControllersByNamespace
// @summary Update (block/unblock) ingress controllers by namespace
// @description Update (block/unblock) ingress controllers by namespace for the provided environment
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param namespace path string true "Namespace name"
// @param body body []models.K8sIngressControllers true "Ingress controllers"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces/{namespace}/ingresscontrollers [put]
func (handler *Handler) updateKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
    endpoint, err := middlewares.FetchEndpoint(r)
    if err != nil {
        return httperror.NotFound("Unable to find an environment on request context", err)
    }

    namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
    if err != nil {
        return httperror.BadRequest("Invalid namespace identifier route variable", err)
    }

    var payload models.K8sIngressControllers
    err = request.DecodeAndValidateJSONPayload(r, &payload)
    if err != nil {
        return httperror.BadRequest("Invalid request payload", err)
    }

    existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
    var updatedClasses []portainer.KubernetesIngressClassConfig
PayloadLoop:
    for _, p := range payload {
        for _, existingClass := range existingClasses {
            if p.ClassName != existingClass.Name {
                continue
            }
            var updatedClass portainer.KubernetesIngressClassConfig
            updatedClass.Name = existingClass.Name
            updatedClass.Type = existingClass.Type
            updatedClass.GloballyBlocked = existingClass.GloballyBlocked

            // Handle "allow"
            if p.Availability {
                // remove the namespace from the list of blocked namespaces
                // in the existingClass.
                for _, blockedNS := range existingClass.BlockedNamespaces {
                    if blockedNS != namespace {
                        updatedClass.BlockedNamespaces = append(updatedClass.BlockedNamespaces, blockedNS)
                    }
                }

                updatedClasses = append(updatedClasses, updatedClass)
                continue PayloadLoop
            }

            // Handle "disallow"
            // If it's meant to be blocked we need to add the current
            // namespace. First, check if it's already in the
            // BlockedNamespaces and if not we append it.
            updatedClass.BlockedNamespaces = existingClass.BlockedNamespaces
            for _, ns := range updatedClass.BlockedNamespaces {
                if namespace == ns {
                    updatedClasses = append(updatedClasses, updatedClass)
                    continue PayloadLoop
                }
            }
            updatedClass.BlockedNamespaces = append(
                updatedClass.BlockedNamespaces,
                namespace,
            )
            updatedClasses = append(updatedClasses, updatedClass)
        }
    }

    // At this point it's possible we had an existing class which was globally
    // blocked and thus not included in the payload. As a result it is not yet
    // part of updatedClasses, but we MUST include it or we would remove the
    // global block.
    for _, existingClass := range existingClasses {
        var found bool

        for _, updatedClass := range updatedClasses {
            if existingClass.Name == updatedClass.Name {
                found = true
            }
        }

        if !found {
            updatedClasses = append(updatedClasses, existingClass)
        }
    }
    endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses

    err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
    if err != nil {
        return httperror.InternalServerError("Unable to update the BlockedIngressClasses inside the database", err)
    }

    return response.Empty(w)
}

// @id getKubernetesIngresses
// @summary Get kubernetes ingresses by namespace
// @description Get kubernetes ingresses by namespace for the provided environment
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param namespace path string true "Namespace name"
// @param body body []models.K8sIngressInfo true "Ingress details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [get]
func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
    namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
    if err != nil {
        return httperror.BadRequest("Invalid namespace identifier route variable", err)
    }

    cli, handlerErr := handler.getProxyKubeClient(r)
    if handlerErr != nil {
        return handlerErr
    }

    ingresses, err := cli.GetIngresses(namespace)
    if err != nil {
        return httperror.InternalServerError("Unable to retrieve ingresses", err)
    }

    return response.JSON(w, ingresses)
}

// @id createKubernetesIngress
// @summary Create a kubernetes ingress by namespace
// @description Create a kubernetes ingress by namespace for the provided environment
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param namespace path string true "Namespace name"
// @param body body models.K8sIngressInfo true "Ingress details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [post]
func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
    namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
    if err != nil {
        return httperror.BadRequest("Invalid namespace identifier route variable", err)
    }

    var payload models.K8sIngressInfo
    err = request.DecodeAndValidateJSONPayload(r, &payload)
    if err != nil {
        return httperror.BadRequest("Invalid request payload", err)
    }

    owner := "admin"
    tokenData, err := security.RetrieveTokenData(r)
    if err == nil && tokenData != nil {
        owner = tokenData.Username
    }

    cli, handlerErr := handler.getProxyKubeClient(r)
    if handlerErr != nil {
        return handlerErr
    }

    err = cli.CreateIngress(namespace, payload, owner)
    if err != nil {
        return httperror.InternalServerError("Unable to retrieve the ingress", err)
    }

    return response.Empty(w)
}

// @id deleteKubernetesIngresses
// @summary Delete kubernetes ingresses
// @description Delete kubernetes ingresses for the provided environment
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param body body models.K8sIngressDeleteRequests true "Ingress details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/ingresses/delete [post]
func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
    cli, handlerErr := handler.getProxyKubeClient(r)
    if handlerErr != nil {
        return handlerErr
    }

    var payload models.K8sIngressDeleteRequests
    err := request.DecodeAndValidateJSONPayload(r, &payload)
    if err != nil {
        return httperror.BadRequest("Invalid request payload", err)
    }

    err = cli.DeleteIngresses(payload)
    if err != nil {
        return httperror.InternalServerError("Unable to delete ingresses", err)
    }

    return response.Empty(w)
}

// @id updateKubernetesIngress
// @summary Update kubernetes ingress rule
// @description Update kubernetes ingress rule for the provided environment
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param namespace path string true "Namespace name"
// @param body body models.K8sIngressInfo true "Ingress details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [put]
func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
    namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
    if err != nil {
        return httperror.BadRequest("Invalid namespace identifier route variable", err)
    }

    var payload models.K8sIngressInfo
    err = request.DecodeAndValidateJSONPayload(r, &payload)
    if err != nil {
        return httperror.BadRequest("Invalid request payload", err)
    }

    cli, handlerErr := handler.getProxyKubeClient(r)
    if handlerErr != nil {
        return handlerErr
    }

    err = cli.UpdateIngress(namespace, payload)
    if err != nil {
        return httperror.InternalServerError("Unable to update the ingress", err)
    }

    return response.Empty(w)
}