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:
@@ -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)
|
||||
|
||||
+33
@@ -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
@@ -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)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1194,6 +1194,7 @@ func (svc *Service) BuildAndSendFleetAgentConfig(ctx context.Context, enterprise
|
||||
CertificateTemplateID: template.CertificateTemplateID,
|
||||
FleetChallenge: challenge,
|
||||
Status: fleet.MDMDeliveryPending,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user