open-cluster-management.io/governance-policy-propagator@v0.13.0/controllers/complianceeventsapi/types.go (about)

     1  package complianceeventsapi
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"database/sql/driver"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"reflect"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/lib/pq"
    15  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    16  
    17  	policiesv1 "open-cluster-management.io/governance-policy-propagator/api/v1"
    18  )
    19  
    20  var (
    21  	errRequiredFieldNotProvided = errors.New("required field not provided")
    22  	errInvalidInput             = errors.New("invalid input")
    23  	errDuplicateComplianceEvent = errors.New("the compliance event already exists")
    24  )
    25  
    26  type dbRow interface {
    27  	InsertQuery() (string, []any)
    28  	SelectQuery(returnedColumns ...string) (string, []any)
    29  }
    30  
    31  type metadata struct {
    32  	Page    uint64 `json:"page"`
    33  	Pages   uint64 `json:"pages"`
    34  	PerPage uint64 `json:"per_page"` //nolint:tagliatelle
    35  	Total   uint64 `json:"total"`
    36  }
    37  
    38  type ListResponse struct {
    39  	Data     []ComplianceEvent `json:"data"`
    40  	Metadata metadata          `json:"metadata"`
    41  }
    42  
    43  type queryOptions struct {
    44  	ArrayFilters    map[string][]string
    45  	Direction       string
    46  	Filters         map[string][]string
    47  	IncludeSpec     bool
    48  	MessageIncludes string
    49  	MessageLike     string
    50  	NullFilters     []string
    51  	Page            uint64
    52  	PerPage         uint64
    53  	Sort            []string
    54  	TimestampAfter  time.Time
    55  	TimestampBefore time.Time
    56  }
    57  
    58  type ComplianceEvent struct {
    59  	EventID      int32         `json:"id"`
    60  	Cluster      Cluster       `json:"cluster"`
    61  	Event        EventDetails  `json:"event"`
    62  	ParentPolicy *ParentPolicy `json:"parent_policy"` //nolint:tagliatelle
    63  	Policy       Policy        `json:"policy"`
    64  }
    65  
    66  // Validate ensures that a valid POST request for a compliance event is set. This means that if the shorthand approach
    67  // of providing parent_policy.id and/or policy.id is used, the other fields for ParentPolicy and Policy will not be
    68  // present.
    69  func (ce ComplianceEvent) Validate(ctx context.Context, serverContext *ComplianceServerCtx) error {
    70  	errs := make([]error, 0)
    71  
    72  	if err := ce.Cluster.Validate(); err != nil {
    73  		errs = append(errs, err)
    74  	}
    75  
    76  	if ce.ParentPolicy != nil {
    77  		if ce.ParentPolicy.KeyID != 0 {
    78  			row := serverContext.DB.QueryRowContext(
    79  				ctx, `SELECT EXISTS(SELECT * FROM parent_policies WHERE id=$1);`, ce.ParentPolicy.KeyID,
    80  			)
    81  
    82  			var exists bool
    83  
    84  			err := row.Scan(&exists)
    85  			if err != nil {
    86  				log.Error(err, "Failed to query for the existence of the parent policy ID", getPqErrKeyVals(err)...)
    87  
    88  				return errors.New("failed to determine if parent_policy.id is valid")
    89  			}
    90  
    91  			if exists {
    92  				// If the user provided extra data, ignore it since it won't be validated that it matches the database
    93  				ce.ParentPolicy = &ParentPolicy{KeyID: ce.ParentPolicy.KeyID}
    94  			} else {
    95  				errs = append(errs, fmt.Errorf("%w: parent_policy.id not found", errInvalidInput))
    96  			}
    97  		} else if err := ce.ParentPolicy.Validate(); err != nil {
    98  			errs = append(errs, err)
    99  		}
   100  	}
   101  
   102  	if err := ce.Event.Validate(); err != nil {
   103  		errs = append(errs, err)
   104  	}
   105  
   106  	if ce.Policy.KeyID != 0 {
   107  		row := serverContext.DB.QueryRowContext(
   108  			ctx, `SELECT EXISTS(SELECT * FROM policies WHERE id=$1);`, ce.Policy.KeyID,
   109  		)
   110  
   111  		var exists bool
   112  
   113  		err := row.Scan(&exists)
   114  		if err != nil {
   115  			log.Error(err, "Failed to query for the existence of the policy ID", getPqErrKeyVals(err)...)
   116  
   117  			return errors.New("failed to determine if policy.id is valid")
   118  		}
   119  
   120  		if exists {
   121  			// If the user provided extra data, ignore it since it won't be validated that it matches the database
   122  			ce.Policy = Policy{KeyID: ce.Policy.KeyID}
   123  		} else {
   124  			errs = append(errs, fmt.Errorf("%w: policy.id not found", errInvalidInput))
   125  		}
   126  	} else if err := ce.Policy.Validate(); err != nil {
   127  		errs = append(errs, err)
   128  	}
   129  
   130  	return errors.Join(errs...)
   131  }
   132  
   133  func (ce *ComplianceEvent) Create(ctx context.Context, db *sql.DB) error {
   134  	if ce.Event.ClusterID == 0 {
   135  		ce.Event.ClusterID = ce.Cluster.KeyID
   136  	}
   137  
   138  	if ce.Event.PolicyID == 0 {
   139  		ce.Event.PolicyID = ce.Policy.KeyID
   140  	}
   141  
   142  	if ce.Event.ParentPolicyID == nil && ce.ParentPolicy != nil {
   143  		ce.Event.ParentPolicyID = &ce.ParentPolicy.KeyID
   144  	}
   145  
   146  	insertQuery, insertArgs := ce.Event.InsertQuery()
   147  
   148  	row := db.QueryRowContext( //nolint:execinquery
   149  		ctx, insertQuery+" ON CONFLICT DO NOTHING RETURNING id", insertArgs...,
   150  	)
   151  
   152  	err := row.Scan(&ce.Event.KeyID)
   153  	if err != nil {
   154  		// If this is true, then we know we encountered a conflict. This is simpler than parsing the unique constraint
   155  		// error.
   156  		if errors.Is(err, sql.ErrNoRows) {
   157  			return errDuplicateComplianceEvent
   158  		}
   159  
   160  		return err
   161  	}
   162  
   163  	return nil
   164  }
   165  
   166  type Cluster struct {
   167  	KeyID     int32  `db:"id" json:"-"`
   168  	Name      string `db:"name" json:"name"`
   169  	ClusterID string `db:"cluster_id" json:"cluster_id"` //nolint:tagliatelle
   170  }
   171  
   172  func (c Cluster) Validate() error {
   173  	errs := make([]error, 0)
   174  
   175  	if c.Name == "" {
   176  		errs = append(errs, fmt.Errorf("%w: cluster.name", errRequiredFieldNotProvided))
   177  	}
   178  
   179  	if c.ClusterID == "" {
   180  		errs = append(errs, fmt.Errorf("%w: cluster.cluster_id", errRequiredFieldNotProvided))
   181  	}
   182  
   183  	return errors.Join(errs...)
   184  }
   185  
   186  func (c *Cluster) InsertQuery() (string, []any) {
   187  	sql := `INSERT INTO clusters (cluster_id, name) VALUES ($1, $2)`
   188  	values := []any{c.ClusterID, c.Name}
   189  
   190  	return sql, values
   191  }
   192  
   193  func (c *Cluster) SelectQuery(returnedColumns ...string) (string, []any) {
   194  	if len(returnedColumns) == 0 {
   195  		returnedColumns = []string{"*"}
   196  	}
   197  
   198  	sql := fmt.Sprintf(
   199  		`SELECT %s FROM clusters WHERE cluster_id=$1 AND name=$2`,
   200  		strings.Join(returnedColumns, ", "),
   201  	)
   202  	values := []any{c.ClusterID, c.Name}
   203  
   204  	return sql, values
   205  }
   206  
   207  func (c *Cluster) GetOrCreate(ctx context.Context, db *sql.DB) error {
   208  	return getOrCreate(ctx, db, c)
   209  }
   210  
   211  // EventDetailsQueued is a slimmed down EventDetails that supports being put in a client-go work queue.
   212  // The client-go work queue rejects an EventDetails object because it is not hashable due to the Metadata field
   213  // using the JSONMap type.
   214  type EventDetailsQueued struct {
   215  	ClusterID      int32
   216  	PolicyID       int32
   217  	ParentPolicyID int32
   218  	Compliance     string
   219  	Message        string
   220  	Timestamp      time.Time
   221  	ReportedBy     string
   222  }
   223  
   224  func (e *EventDetailsQueued) EventDetails() *EventDetails {
   225  	return &EventDetails{
   226  		ClusterID:      e.ClusterID,
   227  		PolicyID:       e.PolicyID,
   228  		ParentPolicyID: &e.ParentPolicyID,
   229  		Compliance:     e.Compliance,
   230  		Message:        e.Message,
   231  		Timestamp:      e.Timestamp,
   232  		ReportedBy:     &e.ReportedBy,
   233  	}
   234  }
   235  
   236  func (e *EventDetailsQueued) InsertQuery() (string, []any) {
   237  	return e.EventDetails().InsertQuery()
   238  }
   239  
   240  type EventDetails struct {
   241  	KeyID          int32     `db:"id" json:"-"`
   242  	ClusterID      int32     `db:"cluster_id" json:"-"`
   243  	PolicyID       int32     `db:"policy_id" json:"-"`
   244  	ParentPolicyID *int32    `db:"parent_policy_id" json:"-"`
   245  	Compliance     string    `db:"compliance" json:"compliance"`
   246  	Message        string    `db:"message" json:"message"`
   247  	Timestamp      time.Time `db:"timestamp" json:"timestamp"`
   248  	Metadata       JSONMap   `db:"metadata" json:"metadata"`
   249  	ReportedBy     *string   `db:"reported_by" json:"reported_by"` //nolint:tagliatelle
   250  }
   251  
   252  func (e EventDetails) Validate() error {
   253  	errs := make([]error, 0)
   254  
   255  	if e.Compliance == "" {
   256  		errs = append(errs, fmt.Errorf("%w: event.compliance", errRequiredFieldNotProvided))
   257  	} else {
   258  		switch e.Compliance {
   259  		case "Compliant", "NonCompliant", "Disabled", "Pending":
   260  		default:
   261  			errs = append(
   262  				errs,
   263  				fmt.Errorf(
   264  					"%w: event.compliance should be Compliant, NonCompliant, Disabled, or Pending got %v",
   265  					errInvalidInput, e.Compliance,
   266  				),
   267  			)
   268  		}
   269  	}
   270  
   271  	if e.Message == "" {
   272  		errs = append(errs, fmt.Errorf("%w: event.message", errRequiredFieldNotProvided))
   273  	}
   274  
   275  	if e.Timestamp.IsZero() {
   276  		errs = append(errs, fmt.Errorf("%w: event.timestamp", errRequiredFieldNotProvided))
   277  	}
   278  
   279  	return errors.Join(errs...)
   280  }
   281  
   282  func (e *EventDetails) InsertQuery() (string, []any) {
   283  	sql := `INSERT INTO compliance_events` +
   284  		`(cluster_id, compliance, message, metadata, parent_policy_id, policy_id, reported_by, timestamp) ` +
   285  		`VALUES($1, $2, $3, $4, $5, $6, $7, $8)`
   286  	values := []any{
   287  		e.ClusterID, e.Compliance, e.Message, e.Metadata, e.ParentPolicyID, e.PolicyID, e.ReportedBy, e.Timestamp,
   288  	}
   289  
   290  	return sql, values
   291  }
   292  
   293  type ParentPolicy struct {
   294  	KeyID      int32          `db:"id" json:"id"`
   295  	Name       string         `db:"name" json:"name"`
   296  	Namespace  string         `db:"namespace" json:"namespace"`
   297  	Categories pq.StringArray `db:"categories" json:"categories"`
   298  	Controls   pq.StringArray `db:"controls" json:"controls"`
   299  	Standards  pq.StringArray `db:"standards" json:"standards"`
   300  }
   301  
   302  func (p ParentPolicy) Validate() error {
   303  	errs := []error{}
   304  
   305  	if p.Name == "" {
   306  		errs = append(errs, fmt.Errorf("%w: parent_policy.name", errRequiredFieldNotProvided))
   307  	}
   308  
   309  	if p.Namespace == "" {
   310  		errs = append(errs, fmt.Errorf("%w: parent_policy.namespace", errRequiredFieldNotProvided))
   311  	}
   312  
   313  	return errors.Join(errs...)
   314  }
   315  
   316  func (p *ParentPolicy) InsertQuery() (string, []any) {
   317  	sql := `INSERT INTO parent_policies` +
   318  		`(categories, controls, name, namespace, standards) ` +
   319  		`VALUES($1, $2, $3, $4, $5)`
   320  	values := []any{p.Categories, p.Controls, p.Name, p.Namespace, p.Standards}
   321  
   322  	return sql, values
   323  }
   324  
   325  func (p *ParentPolicy) SelectQuery(returnedColumns ...string) (string, []any) {
   326  	if len(returnedColumns) == 0 {
   327  		returnedColumns = []string{"*"}
   328  	}
   329  
   330  	sql := fmt.Sprintf(
   331  		`SELECT %s FROM parent_policies WHERE name=$1 AND namespace=$2`,
   332  		strings.Join(returnedColumns, ", "),
   333  	)
   334  	values := []any{p.Name, p.Namespace}
   335  
   336  	columnCount := 2
   337  
   338  	if p.Categories == nil {
   339  		sql += " AND categories IS NULL"
   340  	} else {
   341  		columnCount++
   342  		sql += fmt.Sprintf(" AND categories=$%d", columnCount)
   343  		values = append(values, p.Categories)
   344  	}
   345  
   346  	if p.Controls == nil {
   347  		sql += " AND controls IS NULL"
   348  	} else {
   349  		columnCount++
   350  		sql += fmt.Sprintf(" AND controls=$%d", columnCount)
   351  		values = append(values, p.Controls)
   352  	}
   353  
   354  	if p.Standards == nil {
   355  		sql += " AND standards IS NULL"
   356  	} else {
   357  		columnCount++
   358  		sql += fmt.Sprintf(" AND standards=$%d", columnCount)
   359  		values = append(values, p.Standards)
   360  	}
   361  
   362  	return sql, values
   363  }
   364  
   365  func (p *ParentPolicy) GetOrCreate(ctx context.Context, db *sql.DB) error {
   366  	return getOrCreate(ctx, db, p)
   367  }
   368  
   369  func (p ParentPolicy) Key() string {
   370  	return fmt.Sprintf("%s;%s;%v;%v;%v", p.Namespace, p.Name, p.Categories, p.Controls, p.Standards)
   371  }
   372  
   373  func ParentPolicyFromPolicyObj(plc *policiesv1.Policy) ParentPolicy {
   374  	annotations := plc.GetAnnotations()
   375  	categories := []string{}
   376  
   377  	for _, category := range strings.Split(annotations["policy.open-cluster-management.io/categories"], ",") {
   378  		category = strings.TrimSpace(category)
   379  		if category != "" {
   380  			categories = append(categories, category)
   381  		}
   382  	}
   383  
   384  	controls := []string{}
   385  
   386  	for _, control := range strings.Split(annotations["policy.open-cluster-management.io/controls"], ",") {
   387  		control = strings.TrimSpace(control)
   388  		if control != "" {
   389  			controls = append(controls, control)
   390  		}
   391  	}
   392  
   393  	standards := []string{}
   394  
   395  	for _, standard := range strings.Split(annotations["policy.open-cluster-management.io/standards"], ",") {
   396  		standard = strings.TrimSpace(standard)
   397  		if standard != "" {
   398  			standards = append(standards, standard)
   399  		}
   400  	}
   401  
   402  	return ParentPolicy{
   403  		Name:       plc.Name,
   404  		Namespace:  plc.Namespace,
   405  		Categories: categories,
   406  		Controls:   controls,
   407  		Standards:  standards,
   408  	}
   409  }
   410  
   411  func PolicyFromUnstructured(obj unstructured.Unstructured) *Policy {
   412  	policy := &Policy{}
   413  
   414  	policy.APIGroup = obj.GetAPIVersion()
   415  	policy.Kind = obj.GetKind()
   416  	ns := obj.GetNamespace()
   417  
   418  	if ns != "" {
   419  		policy.Namespace = &ns
   420  	}
   421  
   422  	policy.Name = obj.GetName()
   423  
   424  	spec, ok, _ := unstructured.NestedMap(obj.Object, "spec")
   425  	if ok {
   426  		typedSpec := JSONMap{}
   427  
   428  		for key, val := range spec {
   429  			typedSpec[key] = val
   430  		}
   431  
   432  		policy.Spec = typedSpec
   433  	}
   434  
   435  	if severity, ok := spec["severity"]; ok {
   436  		if sevString, ok := severity.(string); ok {
   437  			policy.Severity = &sevString
   438  		}
   439  	}
   440  
   441  	return policy
   442  }
   443  
   444  type Policy struct {
   445  	KeyID     int32   `db:"id" json:"id"`
   446  	Kind      string  `db:"kind" json:"kind"`
   447  	APIGroup  string  `db:"api_group" json:"apiGroup"`
   448  	Name      string  `db:"name" json:"name"`
   449  	Namespace *string `db:"namespace" json:"namespace"`
   450  	Spec      JSONMap `db:"spec" json:"spec,omitempty"`
   451  	Severity  *string `db:"severity" json:"severity"`
   452  }
   453  
   454  func (p *Policy) Validate() error {
   455  	errs := make([]error, 0)
   456  
   457  	if p.APIGroup == "" {
   458  		errs = append(errs, fmt.Errorf("%w: policy.apiGroup", errRequiredFieldNotProvided))
   459  	}
   460  
   461  	if p.Kind == "" {
   462  		errs = append(errs, fmt.Errorf("%w: policy.kind", errRequiredFieldNotProvided))
   463  	}
   464  
   465  	if p.Name == "" {
   466  		errs = append(errs, fmt.Errorf("%w: policy.name", errRequiredFieldNotProvided))
   467  	}
   468  
   469  	if p.Spec == nil {
   470  		errs = append(errs, fmt.Errorf("%w: policy.spec", errRequiredFieldNotProvided))
   471  	}
   472  
   473  	return errors.Join(errs...)
   474  }
   475  
   476  func (p *Policy) InsertQuery() (string, []any) {
   477  	sql := `INSERT INTO policies` +
   478  		`(api_group, kind, name, namespace, severity, spec)` +
   479  		`VALUES($1, $2, $3, $4, $5, $6)`
   480  	values := []any{p.APIGroup, p.Kind, p.Name, p.Namespace, p.Severity, p.Spec}
   481  
   482  	return sql, values
   483  }
   484  
   485  func (p *Policy) SelectQuery(returnedColumns ...string) (string, []any) {
   486  	if len(returnedColumns) == 0 {
   487  		returnedColumns = []string{"*"}
   488  	}
   489  
   490  	sql := fmt.Sprintf(
   491  		`SELECT %s FROM policies `+
   492  			`WHERE api_group=$1 AND kind=$2 AND name=$3 AND spec=$4`,
   493  		strings.Join(returnedColumns, ", "),
   494  	)
   495  
   496  	values := []any{p.APIGroup, p.Kind, p.Name, p.Spec}
   497  
   498  	columnCount := 4
   499  
   500  	if p.Namespace == nil {
   501  		sql += " AND namespace is NULL"
   502  	} else {
   503  		columnCount++
   504  		sql += fmt.Sprintf(" AND namespace=$%d", columnCount)
   505  		values = append(values, p.Namespace)
   506  	}
   507  
   508  	if p.Severity == nil {
   509  		sql += " AND severity is NULL"
   510  	} else {
   511  		columnCount++
   512  		sql += fmt.Sprintf(" AND severity=$%d", columnCount)
   513  		values = append(values, p.Severity)
   514  	}
   515  
   516  	return sql, values
   517  }
   518  
   519  func (p *Policy) GetOrCreate(ctx context.Context, db *sql.DB) error {
   520  	return getOrCreate(ctx, db, p)
   521  }
   522  
   523  func (p *Policy) Key() string {
   524  	var namespace string
   525  
   526  	if p.Namespace != nil {
   527  		namespace = *p.Namespace
   528  	}
   529  
   530  	var severity string
   531  
   532  	if p.Severity != nil {
   533  		severity = *p.Severity
   534  	}
   535  
   536  	// Note that as of Go 1.20, it sorts the keys in the underlying map of p.Spec, which is why this is deterministic.
   537  	// https://github.com/golang/go/blob/97c8ff8d53759e7a82b1862403df1694f2b6e073/src/fmt/print.go#L816-L828
   538  	return fmt.Sprintf("%s;%s;%s;%v;%v;%v", p.APIGroup, p.Kind, p.Name, namespace, severity, p.Spec)
   539  }
   540  
   541  type JSONMap map[string]interface{}
   542  
   543  // Value returns a value that the database driver can use, or an error.
   544  func (j JSONMap) Value() (driver.Value, error) {
   545  	return json.Marshal(j)
   546  }
   547  
   548  // Scan allows for reading a JSONMap from the database.
   549  func (j *JSONMap) Scan(src interface{}) error {
   550  	var source []byte
   551  
   552  	switch s := src.(type) {
   553  	case string:
   554  		source = []byte(s)
   555  	case []byte:
   556  		source = s
   557  	case nil:
   558  		source = nil
   559  	default:
   560  		return errors.New("incompatible type for JSONMap")
   561  	}
   562  
   563  	return json.Unmarshal(source, j)
   564  }
   565  
   566  // getOrCreate will translate the input object to an INSERT SQL query. When the input object already exists in the
   567  // database, a SELECT query is performed. The primary key is set on the input object when it is inserted or gotten
   568  // from the database. The INSERT first then SELECT approach is a clean way to account for race conditions of multiple
   569  // goroutines creating the same row.
   570  func getOrCreate(ctx context.Context, db *sql.DB, obj dbRow) error {
   571  	insertQuery, insertArgs := obj.InsertQuery()
   572  
   573  	// On inserts, it returns the primary key value (e.g. id). If it already exists, nothing is returned.
   574  	row := db.QueryRowContext(ctx, fmt.Sprintf(`%s ON CONFLICT DO NOTHING RETURNING "id"`, insertQuery), insertArgs...)
   575  
   576  	var primaryKey int32
   577  
   578  	err := row.Scan(&primaryKey)
   579  	if errors.Is(err, sql.ErrNoRows) {
   580  		// The insertion did not return anything, so that means the value already exists, so perform a SELECT query
   581  		// just using the unique columns. No more information is needed to get the right row and it allows the unique
   582  		// indexes to be used to make the query more efficient.
   583  		selectQuery, selectArgs := obj.SelectQuery("id")
   584  
   585  		row = db.QueryRowContext(ctx, selectQuery, selectArgs...)
   586  
   587  		err = row.Scan(&primaryKey)
   588  		if err != nil {
   589  			return err
   590  		}
   591  	} else if err != nil {
   592  		return err
   593  	}
   594  
   595  	// Set the primary key value on the object
   596  	values := reflect.Indirect(reflect.ValueOf(obj))
   597  	values.FieldByName("KeyID").Set(reflect.ValueOf(primaryKey))
   598  
   599  	return nil
   600  }