| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- package opa
- import (
- "context"
- "fmt"
- "os"
- "strings"
- "github.com/mitchellh/mapstructure"
- "github.com/open-policy-agent/opa/rego"
- "github.com/porter-dev/porter/api/types"
- "github.com/porter-dev/porter/internal/helm"
- "github.com/porter-dev/porter/internal/kubernetes"
- "github.com/porter-dev/porter/internal/models"
- "github.com/porter-dev/porter/pkg/logger"
- "github.com/stefanmcshane/helm/pkg/release"
- v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "k8s.io/client-go/dynamic"
- )
- type KubernetesPolicies struct {
- Policies map[string]KubernetesOPAQueryCollection
- }
- type KubernetesOPARunner struct {
- *KubernetesPolicies
- cluster *models.Cluster
- k8sAgent *kubernetes.Agent
- dynamicClient dynamic.Interface
- }
- type KubernetesBuiltInKind string
- const (
- HelmRelease KubernetesBuiltInKind = "helm_release"
- Pod KubernetesBuiltInKind = "pod"
- CRDList KubernetesBuiltInKind = "crd_list"
- Daemonset KubernetesBuiltInKind = "daemonset"
- )
- type KubernetesOPAQueryCollection struct {
- Kind KubernetesBuiltInKind
- Match MatchParameters
- MustExist bool
- OverrideSeverity string
- Queries []rego.PreparedEvalQuery
- }
- type MatchParameters struct {
- // global cluster match parameters
- // KubernetesService is a matching service kind, like `eks`
- KubernetesService string `json:"kubernetes_service"`
- // parameters for Helm releases
- Name string `json:"name"`
- Namespace string `json:"namespace"`
- ChartName string `json:"chart_name"`
- // generic labels parameter
- Labels map[string]string `json:"labels"`
- // parameters for CRDs
- Group string `json:"group"`
- Version string `json:"version"`
- Resource string `json:"resource"`
- }
- type OPARecommenderQueryResult struct {
- Allow bool
- CategoryName string
- ObjectID string
- PolicyVersion string
- PolicySeverity string
- PolicyTitle string
- PolicyMessage string
- }
- type rawQueryResult struct {
- Allow bool `mapstructure:"ALLOW"`
- PolicyID string `mapstructure:"POLICY_ID"`
- PolicyVersion string `mapstructure:"POLICY_VERSION"`
- PolicySeverity string `mapstructure:"POLICY_SEVERITY"`
- PolicyTitle string `mapstructure:"POLICY_TITLE"`
- SuccessMessage string `mapstructure:"POLICY_SUCCESS_MESSAGE"`
- FailureMessage []string `mapstructure:"FAILURE_MESSAGE"`
- }
- func NewRunner(policies *KubernetesPolicies, cluster *models.Cluster, k8sAgent *kubernetes.Agent, dynamicClient dynamic.Interface) *KubernetesOPARunner {
- return &KubernetesOPARunner{policies, cluster, k8sAgent, dynamicClient}
- }
- func (runner *KubernetesOPARunner) GetRecommendations(categories []string) ([]*OPARecommenderQueryResult, error) {
- collectionNames := categories
- if len(categories) == 0 {
- for catName := range runner.Policies {
- collectionNames = append(collectionNames, catName)
- }
- }
- res := make([]*OPARecommenderQueryResult, 0)
- // ping the cluster with a version check to make sure it's reachable - if not, return an error
- _, err := runner.k8sAgent.Clientset.Discovery().ServerVersion()
- if err != nil {
- fmt.Printf("discovery check failed: %v\n", err.Error())
- } else {
- for _, name := range collectionNames {
- // look up to determine if the name is registered
- queryCollection, exists := runner.Policies[name]
- if !exists {
- return nil, fmt.Errorf("No policies for %s found", name)
- }
- var currResults []*OPARecommenderQueryResult
- var err error
- // look at global match parameters
- if s := queryCollection.Match.KubernetesService; s != "" && strings.ToLower(string(runner.cluster.ToClusterType().Service)) != s {
- fmt.Printf("skipping %s as it does not match the cluster service", name)
- continue
- }
- switch queryCollection.Kind {
- case HelmRelease:
- currResults, err = runner.runHelmReleaseQueries(name, queryCollection)
- case Pod:
- currResults, err = runner.runPodQueries(name, queryCollection)
- case CRDList:
- currResults, err = runner.runCRDListQueries(name, queryCollection)
- case Daemonset:
- currResults, err = runner.runDaemonsetQueries(name, queryCollection)
- default:
- fmt.Printf("%s is not a supported query kind", queryCollection.Kind)
- continue
- }
- if err != nil {
- fmt.Printf("%s", err.Error())
- continue
- }
- res = append(res, currResults...)
- }
- }
- return res, nil
- }
- func (runner *KubernetesOPARunner) SetK8sAgent(k8sAgent *kubernetes.Agent) {
- runner.k8sAgent = k8sAgent
- }
- func (runner *KubernetesOPARunner) runHelmReleaseQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
- res := make([]*OPARecommenderQueryResult, 0)
- helmAgent, err := helm.GetAgentFromK8sAgent("secret", collection.Match.Namespace, logger.New(false, os.Stdout), runner.k8sAgent)
- if err != nil {
- return nil, err
- }
- // get the matching helm release(s) based on the match
- var helmReleases []*release.Release
- if collection.Match.Name != "" {
- helmRelease, err := helmAgent.GetRelease(collection.Match.Name, 0, false)
- if err != nil {
- if collection.MustExist && strings.Contains(err.Error(), "not found") {
- return []*OPARecommenderQueryResult{
- {
- Allow: false,
- ObjectID: fmt.Sprintf("helm_release/%s/%s/%s", collection.Match.Namespace, collection.Match.Name, "exists"),
- CategoryName: name,
- PolicyVersion: "v0.0.1",
- PolicySeverity: getSeverity("high", collection),
- PolicyTitle: fmt.Sprintf("The helm release %s must exist", collection.Match.Name),
- PolicyMessage: "The helm release was not found on the cluster",
- },
- }, nil
- } else {
- return nil, err
- }
- } else if collection.MustExist {
- res = append(res, &OPARecommenderQueryResult{
- Allow: true,
- ObjectID: fmt.Sprintf("helm_release/%s/%s/%s", collection.Match.Namespace, collection.Match.Name, "exists"),
- CategoryName: name,
- PolicyVersion: "v0.0.1",
- PolicySeverity: getSeverity("high", collection),
- PolicyTitle: fmt.Sprintf("The helm release %s must exist", collection.Match.Name),
- PolicyMessage: "The helm release was found",
- })
- }
- helmReleases = append(helmReleases, helmRelease)
- } else if collection.Match.ChartName != "" {
- prefilterReleases, err := helmAgent.ListReleases(collection.Match.Namespace, &types.ReleaseListFilter{
- ByDate: true,
- StatusFilter: []string{
- "deployed",
- "pending",
- "pending-install",
- "pending-upgrade",
- "pending-rollback",
- "failed",
- },
- })
- if err != nil {
- return nil, err
- }
- for _, prefilterRelease := range prefilterReleases {
- if prefilterRelease.Chart.Name() == collection.Match.ChartName {
- helmReleases = append(helmReleases, prefilterRelease)
- }
- }
- } else {
- return nil, fmt.Errorf("invalid match parameters")
- }
- for _, helmRelease := range helmReleases {
- for _, query := range collection.Queries {
- results, err := query.Eval(
- context.Background(),
- rego.EvalInput(map[string]interface{}{
- "version": helmRelease.Chart.Metadata.Version,
- "values": helmRelease.Config,
- "name": helmRelease.Name,
- "namespace": helmRelease.Namespace,
- }),
- )
- if err != nil {
- return nil, err
- }
- if len(results) == 1 {
- rawQueryRes := &rawQueryResult{}
- err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
- if err != nil {
- return nil, err
- }
- res = append(res, rawQueryResToRecommenderQueryResult(
- rawQueryRes,
- fmt.Sprintf("helm_release/%s/%s/%s", helmRelease.Namespace, helmRelease.Name, rawQueryRes.PolicyID),
- name,
- collection,
- ))
- }
- }
- }
- return res, nil
- }
- func getSeverity(defaultSeverity string, collection KubernetesOPAQueryCollection) string {
- if collection.OverrideSeverity != "" {
- return collection.OverrideSeverity
- }
- return defaultSeverity
- }
- func (runner *KubernetesOPARunner) runPodQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
- res := make([]*OPARecommenderQueryResult, 0)
- lselArr := make([]string, 0)
- for k, v := range collection.Match.Labels {
- lselArr = append(lselArr, fmt.Sprintf("%s=%s", k, v))
- }
- lsel := strings.Join(lselArr, ",")
- pods, err := runner.k8sAgent.GetPodsByLabel(lsel, collection.Match.Namespace)
- if err != nil {
- return nil, err
- }
- for _, pod := range pods.Items {
- unstructuredPod, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&pod)
- if err != nil {
- return nil, err
- }
- for _, query := range collection.Queries {
- results, err := query.Eval(
- context.Background(),
- rego.EvalInput(unstructuredPod),
- )
- if err != nil {
- return nil, err
- }
- if len(results) == 1 {
- rawQueryRes := &rawQueryResult{}
- err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
- if err != nil {
- return nil, err
- }
- res = append(res, rawQueryResToRecommenderQueryResult(
- rawQueryRes,
- fmt.Sprintf("pod/%s/%s", pod.Namespace, pod.Name),
- name,
- collection,
- ))
- }
- }
- }
- return res, nil
- }
- func (runner *KubernetesOPARunner) runDaemonsetQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
- res := make([]*OPARecommenderQueryResult, 0)
- lselArr := make([]string, 0)
- for k, v := range collection.Match.Labels {
- lselArr = append(lselArr, fmt.Sprintf("%s=%s", k, v))
- }
- lsel := strings.Join(lselArr, ",")
- daemonsets, err := runner.k8sAgent.Clientset.AppsV1().DaemonSets(collection.Match.Namespace).List(context.Background(), v1.ListOptions{
- LabelSelector: lsel,
- })
- if err != nil {
- return nil, err
- }
- for _, ds := range daemonsets.Items {
- unstructuredDS, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&ds)
- if err != nil {
- return nil, err
- }
- for _, query := range collection.Queries {
- results, err := query.Eval(
- context.Background(),
- rego.EvalInput(unstructuredDS),
- )
- if err != nil {
- return nil, err
- }
- if len(results) == 1 {
- rawQueryRes := &rawQueryResult{}
- err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
- if err != nil {
- return nil, err
- }
- res = append(res, rawQueryResToRecommenderQueryResult(
- rawQueryRes,
- fmt.Sprintf("daemonset/%s/%s", ds.Namespace, ds.Name),
- name,
- collection,
- ))
- }
- }
- }
- return res, nil
- }
- func (runner *KubernetesOPARunner) runCRDListQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
- res := make([]*OPARecommenderQueryResult, 0)
- objRes := schema.GroupVersionResource{
- Group: collection.Match.Group,
- Version: collection.Match.Version,
- Resource: collection.Match.Resource,
- }
- // just case on the "core" group and unset it
- if collection.Match.Group == "core" {
- objRes.Group = ""
- }
- crdList, err := runner.dynamicClient.Resource(objRes).Namespace(collection.Match.Namespace).List(context.Background(), v1.ListOptions{})
- if err != nil {
- return nil, err
- }
- for _, crd := range crdList.Items {
- for _, query := range collection.Queries {
- results, err := query.Eval(
- context.Background(),
- rego.EvalInput(crd.Object),
- )
- if err != nil {
- return nil, err
- }
- if len(results) == 1 {
- rawQueryRes := &rawQueryResult{}
- err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
- if err != nil {
- return nil, err
- }
- res = append(res, rawQueryResToRecommenderQueryResult(
- rawQueryRes,
- fmt.Sprintf("%s/%s/%s/%s", collection.Match.Group, collection.Match.Version, collection.Match.Resource, rawQueryRes.PolicyID),
- name,
- collection,
- ))
- }
- }
- }
- return res, nil
- }
- func rawQueryResToRecommenderQueryResult(rawQueryRes *rawQueryResult, objectID, categoryName string, collection KubernetesOPAQueryCollection) *OPARecommenderQueryResult {
- queryRes := &OPARecommenderQueryResult{
- ObjectID: objectID,
- CategoryName: categoryName,
- }
- message := rawQueryRes.SuccessMessage
- // if failure, compose failure messages into single string
- if !rawQueryRes.Allow {
- message = strings.Join(rawQueryRes.FailureMessage, ". ")
- }
- queryRes.PolicyMessage = message
- queryRes.Allow = rawQueryRes.Allow
- queryRes.PolicySeverity = getSeverity(rawQueryRes.PolicySeverity, collection)
- queryRes.PolicyTitle = rawQueryRes.PolicyTitle
- queryRes.PolicyVersion = rawQueryRes.PolicyVersion
- return queryRes
- }
|