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 }