Added operation_type to host_certificate_templates (#36926)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #36684 

This is a DB migration change as the first part of this story #36684

# Checklist for submitter

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

## Database migrations

- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added operation type tracking to host certificate templates, enabling
the system to better manage certificate operations during mobile device
management workflows.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky
2025-12-09 10:57:44 -06:00
committed by GitHub
parent fcec4b8159
commit ec61f2cd81
10 changed files with 106 additions and 33 deletions
@@ -214,12 +214,13 @@ func (ds *Datastore) GetHostCertificateTemplates(ctx context.Context, hostUUID s
}
stmt := `
SELECT
ct.name,
SELECT
ct.name,
hct.status,
hct.detail
hct.detail,
hct.operation_type
FROM host_certificate_templates hct
INNER JOIN certificate_templates ct ON ct.id = hct.certificate_template_id
INNER JOIN certificate_templates ct ON ct.id = hct.certificate_template_id
WHERE hct.host_uuid = ?`
var hTemplates []fleet.HostCertificateTemplate
@@ -889,14 +889,14 @@ func testGetHostCertificateTemplates(t *testing.T, ds *Datastore) {
// Set the installation status on the certificate templates
_, err = ds.writer(ctx).ExecContext(ctx,
"INSERT INTO host_certificate_templates (host_uuid, certificate_template_id, fleet_challenge, status) VALUES (?, ?, ?, ?)",
h2.UUID, ct1.ID, "test-challenge", fleet.OSSettingsVerified,
"INSERT INTO host_certificate_templates (host_uuid, certificate_template_id, fleet_challenge, status, operation_type) VALUES (?, ?, ?, ?, ?)",
h2.UUID, ct1.ID, "test-challenge", fleet.OSSettingsVerified, fleet.MDMOperationTypeInstall,
)
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx,
"INSERT INTO host_certificate_templates (host_uuid, certificate_template_id, fleet_challenge, status, detail) VALUES (?, ?, ?, ?, ?)",
h2.UUID, ct2.ID, "test-challenge", fleet.OSSettingsFailed, "some error yooo",
"INSERT INTO host_certificate_templates (host_uuid, certificate_template_id, fleet_challenge, status, detail, operation_type) VALUES (?, ?, ?, ?, ?, ?)",
h2.UUID, ct2.ID, "test-challenge", fleet.OSSettingsFailed, "some error yooo", fleet.MDMOperationTypeInstall,
)
require.NoError(t, err)
@@ -932,10 +932,12 @@ func testGetHostCertificateTemplates(t *testing.T, ds *Datastore) {
require.Equal(t, ct1.Name, templates[0].Name)
require.Equal(t, fleet.MDMDeliveryVerified, templates[0].Status)
require.Equal(t, fleet.MDMOperationTypeInstall, templates[0].OperationType)
require.Equal(t, ct2.Name, templates[1].Name)
require.Equal(t, fleet.MDMDeliveryFailed, templates[1].Status)
require.Equal(t, "some error yooo", *templates[1].Detail)
require.Equal(t, fleet.MDMOperationTypeInstall, templates[1].OperationType)
},
},
}
@@ -1152,6 +1154,7 @@ func testGetCertificateTemplateForHost(t *testing.T, ds *Datastore) {
CertificateTemplateID: ct1.ID,
FleetChallenge: "challenge-123",
Status: fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeInstall,
},
})
require.NoError(t, err)
@@ -106,14 +106,15 @@ func (ds *Datastore) BulkInsertHostCertificateTemplates(ctx context.Context, hos
return nil
}
const argsCount = 4
const argsCount = 5
const sqlInsert = `
INSERT INTO host_certificate_templates (
host_uuid,
certificate_template_id,
fleet_challenge,
status
status,
operation_type
) VALUES %s
`
@@ -121,8 +122,8 @@ func (ds *Datastore) BulkInsertHostCertificateTemplates(ctx context.Context, hos
args := make([]interface{}, 0, len(hostCertTemplates)*argsCount)
for _, hct := range hostCertTemplates {
args = append(args, hct.HostUUID, hct.CertificateTemplateID, hct.FleetChallenge, hct.Status)
placeholders.WriteString("(?,?,?,?),")
args = append(args, hct.HostUUID, hct.CertificateTemplateID, hct.FleetChallenge, hct.Status, hct.OperationType)
placeholders.WriteString("(?,?,?,?,?),")
}
stmt := fmt.Sprintf(sqlInsert, strings.TrimSuffix(placeholders.String(), ","))
@@ -178,8 +179,8 @@ func (ds *Datastore) UpsertCertificateStatus(
WHERE host_uuid = ? AND certificate_template_id = ?`
insertStmt := `
INSERT INTO host_certificate_templates (host_uuid, certificate_template_id, status, detail, fleet_challenge)
VALUES (?, ?, ?, ?, ?)`
INSERT INTO host_certificate_templates (host_uuid, certificate_template_id, status, detail, fleet_challenge, operation_type)
VALUES (?, ?, ?, ?, ?, ?)`
// Validate the status.
if !status.IsValid() {
@@ -211,7 +212,8 @@ func (ds *Datastore) UpsertCertificateStatus(
return ctxerr.Wrap(ctx, err, "could not read certificate template for inserting new record")
}
params := []any{hostUUID, certificateTemplateID, status, detail, ""}
// Default to install operation type for new records
params := []any{hostUUID, certificateTemplateID, status, detail, "", fleet.MDMOperationTypeInstall}
if _, err := ds.writer(ctx).ExecContext(ctx, insertStmt, params...); err != nil {
return ctxerr.Wrap(ctx, err, "could not insert new host certificate template")
}
@@ -480,9 +480,9 @@ func testBulkInsertAndDeleteHostCertificateTemplates(t *testing.T, ds *Datastore
func(t *testing.T, ds *Datastore) {
// Insert host certificate templates
hostCerts := []fleet.HostCertificateTemplate{
{HostUUID: "host-1", CertificateTemplateID: certificateTemplateID, FleetChallenge: "challenge-1", Status: fleet.MDMDeliveryPending},
{HostUUID: "host-1", CertificateTemplateID: certificateTemplateIDTwo, FleetChallenge: "challenge-2", Status: fleet.MDMDeliveryPending},
{HostUUID: "host-2", CertificateTemplateID: certificateTemplateID, FleetChallenge: "challenge-3", Status: fleet.MDMDeliveryVerified},
{HostUUID: "host-1", CertificateTemplateID: certificateTemplateID, FleetChallenge: "challenge-1", Status: fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall},
{HostUUID: "host-1", CertificateTemplateID: certificateTemplateIDTwo, FleetChallenge: "challenge-2", Status: fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall},
{HostUUID: "host-2", CertificateTemplateID: certificateTemplateID, FleetChallenge: "challenge-3", Status: fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
}
err := ds.BulkInsertHostCertificateTemplates(ctx, hostCerts)
require.NoError(t, err)
@@ -557,8 +557,8 @@ func testBulkInsertAndDeleteHostCertificateTemplates(t *testing.T, ds *Datastore
// Insert host certificate templates
hostCerts := []fleet.HostCertificateTemplate{
{HostUUID: "host-1", CertificateTemplateID: certificateTemplateID, FleetChallenge: "challenge-1", Status: fleet.MDMDeliveryPending},
{HostUUID: "host-1", CertificateTemplateID: certificateTemplateIDTwo, FleetChallenge: "challenge-2", Status: fleet.MDMDeliveryPending},
{HostUUID: "host-1", CertificateTemplateID: certificateTemplateID, FleetChallenge: "challenge-1", Status: fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall},
{HostUUID: "host-1", CertificateTemplateID: certificateTemplateIDTwo, FleetChallenge: "challenge-2", Status: fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall},
}
err = ds.BulkInsertHostCertificateTemplates(ctx, hostCerts)
require.NoError(t, err)
@@ -0,0 +1,33 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20251208215800, Down_20251208215800)
}
func Up_20251208215800(tx *sql.Tx) error {
// Add operation_type column to host_certificate_templates table.
// This column tracks whether the certificate is being installed or removed,
// consistent with other MDM profile tables.
// Note: VARCHAR(20) with FK constraint is not efficient, but consistent with the other similar tables.
// A more efficient approach would be: ENUM('install', 'remove')
addColumnStmt := `
ALTER TABLE host_certificate_templates
ADD COLUMN operation_type VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'install',
ADD CONSTRAINT fk_host_certificate_templates_operation_type
FOREIGN KEY (operation_type) REFERENCES mdm_operation_types (operation_type) ON UPDATE CASCADE
`
if _, err := tx.Exec(addColumnStmt); err != nil {
return fmt.Errorf("add operation_type column to host_certificate_templates: %w", err)
}
return nil
}
func Down_20251208215800(tx *sql.Tx) error {
return nil
}
File diff suppressed because one or more lines are too long
+5 -3
View File
@@ -27,15 +27,17 @@ func TestHostCertificateTemplate(t *testing.T) {
{
name: "maps fields correctly",
template: &HostCertificateTemplate{
HostUUID: "1234",
Name: "HostCertificate",
Status: MDMDeliveryVerified,
HostUUID: "1234",
Name: "HostCertificate",
Status: MDMDeliveryVerified,
OperationType: MDMOperationTypeInstall,
},
expectation: func(t *testing.T, profile HostMDMProfile) {
require.Equal(t, "1234", profile.HostUUID)
require.Equal(t, "HostCertificate", profile.Name)
require.Equal(t, "android", profile.Platform)
require.Equal(t, MDMDeliveryVerified, *profile.Status)
require.Equal(t, MDMOperationTypeInstall, profile.OperationType)
require.Empty(t, profile.Detail)
},
},
+6 -4
View File
@@ -7,6 +7,7 @@ type HostCertificateTemplate struct {
CertificateTemplateID uint `db:"certificate_template_id"`
FleetChallenge string `db:"fleet_challenge"`
Status MDMDeliveryStatus `db:"status"`
OperationType MDMOperationType `db:"operation_type"`
Detail *string `db:"detail" json:"-"`
CreatedAt string `db:"created_at"`
UpdatedAt string `db:"updated_at"`
@@ -19,10 +20,11 @@ func (p *HostCertificateTemplate) ToHostMDMProfile() HostMDMProfile {
}
profile := HostMDMProfile{
HostUUID: p.HostUUID,
Name: p.Name,
Platform: "android",
Status: &p.Status,
HostUUID: p.HostUUID,
Name: p.Name,
Platform: "android",
Status: &p.Status,
OperationType: p.OperationType,
}
if p.Detail != nil {
profile.Detail = *p.Detail
+1
View File
@@ -1194,6 +1194,7 @@ func (svc *Service) BuildAndSendFleetAgentConfig(ctx context.Context, enterprise
CertificateTemplateID: template.CertificateTemplateID,
FleetChallenge: challenge,
Status: fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeInstall,
})
}
+29 -3
View File
@@ -14832,15 +14832,41 @@ INSERT INTO host_certificate_templates (
host_uuid,
certificate_template_id,
status,
fleet_challenge
) VALUES (?, ?, ?, ?);
fleet_challenge,
operation_type
) VALUES (?, ?, ?, ?, ?);
`
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
_, err = q.ExecContext(ctx, sql, host.UUID, certificateTemplateID, "pending", "some_challenge_value")
_, err = q.ExecContext(ctx, sql, host.UUID, certificateTemplateID, "pending", "some_challenge_value", "install")
require.NoError(t, err)
return nil
})
// Enable Android MDM and verify GetHost returns operation_type for certificate templates
appCfg, err := s.ds.AppConfig(ctx)
require.NoError(t, err)
origAndroidEnabled := appCfg.MDM.AndroidEnabledAndConfigured
appCfg.MDM.AndroidEnabledAndConfigured = true
err = s.ds.SaveAppConfig(ctx, appCfg)
require.NoError(t, err)
err = s.ds.SetAndroidEnabledAndConfigured(ctx, true)
require.NoError(t, err)
defer func() {
appCfg.MDM.AndroidEnabledAndConfigured = origAndroidEnabled
_ = s.ds.SaveAppConfig(ctx, appCfg)
_ = s.ds.SetAndroidEnabledAndConfigured(ctx, origAndroidEnabled)
}()
// Verify GetHost returns operation_type for certificate templates
var getHostResp getHostResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
require.NotNil(t, getHostResp.Host)
require.NotNil(t, getHostResp.Host.MDM.Profiles)
require.Len(t, *getHostResp.Host.MDM.Profiles, 1)
profile := (*getHostResp.Host.MDM.Profiles)[0]
require.Equal(t, savedTemplate.Name, profile.Name)
require.Equal(t, fleet.MDMOperationTypeInstall, profile.OperationType, "operation_type should be populated for certificate templates")
// Test cases
cases := []struct {
name string