diff --git a/changes/40841-gitops-sw-upload-error b/changes/40841-gitops-sw-upload-error new file mode 100644 index 0000000000..6d81324a0c --- /dev/null +++ b/changes/40841-gitops-sw-upload-error @@ -0,0 +1 @@ +- Fixed GitOps policy software resolution failing when URL lookup doesn't match, by falling back to hash-based lookup. diff --git a/server/service/client.go b/server/service/client.go index bf534d910a..3f2d683814 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -2812,6 +2812,40 @@ func (c *Client) doGitOpsLabels( return c.ApplyLabels(config.Labels, config.TeamID, namesToMove) } +// resolvePolicySoftwareTitleID attempts to resolve the software title ID for a +// policy by trying each available identifier in order: URL, App Store ID, hash, +// then FMA slug. Returns the resolved title ID and true if found, or 0 and +// false if no identifier matched. +func resolvePolicySoftwareTitleID( + policy *spec.GitOpsPolicySpec, + byURL, byAppStoreID, byHash, bySlug map[string]uint, +) (titleID uint, resolved bool) { + if policy.InstallSoftwareURL != "" { + if id, ok := byURL[policy.InstallSoftwareURL]; ok { + return id, true + } + } + if policy.InstallSoftware.Other == nil { + return 0, false + } + if policy.InstallSoftware.Other.AppStoreID != "" { + if id, ok := byAppStoreID[policy.InstallSoftware.Other.AppStoreID]; ok { + return id, true + } + } + if policy.InstallSoftware.Other.HashSHA256 != "" { + if id, ok := byHash[policy.InstallSoftware.Other.HashSHA256]; ok { + return id, true + } + } + if policy.InstallSoftware.Other.FleetMaintainedAppSlug != "" { + if id, ok := bySlug[policy.InstallSoftware.Other.FleetMaintainedAppSlug]; ok { + return id, true + } + } + return 0, false +} + func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers []fleet.SoftwarePackageResponse, teamVPPApps []fleet.VPPAppResponse, teamScripts []fleet.ScriptResponse, logFn func(format string, args ...interface{}), dryRun bool) error { // Collect policy names that have webhooks_and_tickets_enabled set. var policyNamesWithWebhooks []string @@ -2900,49 +2934,34 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, teamSoftwareInstallers [] if config.Policies[i].InstallSoftware.Other == nil { continue } - if config.Policies[i].InstallSoftwareURL != "" { - softwareTitleID, ok := softwareTitleIDsByInstallerURL[config.Policies[i].InstallSoftwareURL] - if !ok { - // Should not happen because software packages are uploaded first. - if !dryRun { - logFn("[!] software URL without software title ID: %s\n", config.Policies[i].InstallSoftwareURL) - } - continue - } + + // Try each identifier type in order of specificity. For package policies, + // both URL and hash are set (from the referenced YAML file). If the primary + // identifier (URL) fails, fall back to secondary identifiers rather than + // skipping the policy entirely. + softwareTitleID, resolved := resolvePolicySoftwareTitleID( + config.Policies[i], + softwareTitleIDsByInstallerURL, + softwareTitleIDsByAppStoreAppID, + softwareTitleIDsByHash, + softwareTitleIDsBySlug, + ) + if resolved { config.Policies[i].SoftwareTitleID = &softwareTitleID - } - if config.Policies[i].InstallSoftware.Other.AppStoreID != "" { - softwareTitleID, ok := softwareTitleIDsByAppStoreAppID[config.Policies[i].InstallSoftware.Other.AppStoreID] - if !ok { - // Should not happen because app store apps are uploaded first. - if !dryRun { - logFn("[!] software app store app ID without software title ID: %s\n", config.Policies[i].InstallSoftware.Other.AppStoreID) + // Log a warning if URL was set but didn't match (resolved via fallback). + if !dryRun && config.Policies[i].InstallSoftwareURL != "" { + if _, urlOK := softwareTitleIDsByInstallerURL[config.Policies[i].InstallSoftwareURL]; !urlOK { + logFn("[!] policy %q: software URL lookup failed, resolved via fallback (url=%q, hash=%q)\n", + config.Policies[i].Name, config.Policies[i].InstallSoftwareURL, config.Policies[i].InstallSoftware.Other.HashSHA256) } - continue } - config.Policies[i].SoftwareTitleID = &softwareTitleID - } - if config.Policies[i].InstallSoftware.Other.HashSHA256 != "" { - softwareTitleID, ok := softwareTitleIDsByHash[config.Policies[i].InstallSoftware.Other.HashSHA256] - if !ok { - // Should not happen because software packages are uploaded first. - if !dryRun { - logFn("[!] software hash without software title ID: %s\n", config.Policies[i].InstallSoftware.Other.HashSHA256) - } - continue + } else { + if !dryRun { + logFn("[!] policy %q: could not resolve software title ID (url=%q, hash=%q)\n", + config.Policies[i].Name, config.Policies[i].InstallSoftwareURL, + config.Policies[i].InstallSoftware.Other.HashSHA256) } - config.Policies[i].SoftwareTitleID = &softwareTitleID - } - if config.Policies[i].InstallSoftware.Other.FleetMaintainedAppSlug != "" { - softwareTitleID, ok := softwareTitleIDsBySlug[config.Policies[i].InstallSoftware.Other.FleetMaintainedAppSlug] - if !ok { - // Should not happen because FMAs are uploaded first. - if !dryRun { - logFn("[!] fleet-maintained app slug without software title ID: %s\n", config.Policies[i].InstallSoftware.Other.FleetMaintainedAppSlug) - } - continue - } - config.Policies[i].SoftwareTitleID = &softwareTitleID + continue } } diff --git a/server/service/client_test.go b/server/service/client_test.go index 4d519786d7..508b4c274c 100644 --- a/server/service/client_test.go +++ b/server/service/client_test.go @@ -1047,3 +1047,130 @@ func TestGitOpsErrors(t *testing.T) { }) } } + +func TestResolvePolicySoftwareTitleID(t *testing.T) { + byURL := map[string]uint{ + "https://example.com/pkg.pkg": 100, + } + byAppStoreID := map[string]uint{ + "com.example.app": 200, + } + byHash := map[string]uint{ + "abc123hash": 100, // same title as the URL entry + "different-hash": 999, // different title — used to test URL-over-hash precedence + } + bySlug := map[string]uint{ + "some-fma-slug": 300, + } + + tests := []struct { + name string + policy *spec.GitOpsPolicySpec + wantTitleID uint + wantResolved bool + }{ + { + name: "URL lookup succeeds", + policy: &spec.GitOpsPolicySpec{ + InstallSoftwareURL: "https://example.com/pkg.pkg", + InstallSoftware: optjson.BoolOr[*spec.PolicyInstallSoftware]{ + IsOther: true, + Other: &spec.PolicyInstallSoftware{ + HashSHA256: "abc123hash", + }, + }, + }, + wantTitleID: 100, + wantResolved: true, + }, + { + name: "URL takes precedence over hash when both match different titles", + policy: &spec.GitOpsPolicySpec{ + InstallSoftwareURL: "https://example.com/pkg.pkg", + InstallSoftware: optjson.BoolOr[*spec.PolicyInstallSoftware]{ + IsOther: true, + Other: &spec.PolicyInstallSoftware{ + HashSHA256: "different-hash", + }, + }, + }, + wantTitleID: 100, // URL's title (100), not hash's title (999) + wantResolved: true, + }, + { + name: "URL lookup fails, hash fallback succeeds", + policy: &spec.GitOpsPolicySpec{ + InstallSoftwareURL: "https://example.com/DIFFERENT-url.pkg", + InstallSoftware: optjson.BoolOr[*spec.PolicyInstallSoftware]{ + IsOther: true, + Other: &spec.PolicyInstallSoftware{ + HashSHA256: "abc123hash", + }, + }, + }, + wantTitleID: 100, + wantResolved: true, + }, + { + name: "App Store ID lookup succeeds", + policy: &spec.GitOpsPolicySpec{ + InstallSoftware: optjson.BoolOr[*spec.PolicyInstallSoftware]{ + IsOther: true, + Other: &spec.PolicyInstallSoftware{ + AppStoreID: "com.example.app", + }, + }, + }, + wantTitleID: 200, + wantResolved: true, + }, + { + name: "FMA slug lookup succeeds", + policy: &spec.GitOpsPolicySpec{ + InstallSoftware: optjson.BoolOr[*spec.PolicyInstallSoftware]{ + IsOther: true, + Other: &spec.PolicyInstallSoftware{ + FleetMaintainedAppSlug: "some-fma-slug", + }, + }, + }, + wantTitleID: 300, + wantResolved: true, + }, + { + name: "all lookups fail", + policy: &spec.GitOpsPolicySpec{ + InstallSoftwareURL: "https://example.com/nonexistent.pkg", + InstallSoftware: optjson.BoolOr[*spec.PolicyInstallSoftware]{ + IsOther: true, + Other: &spec.PolicyInstallSoftware{ + HashSHA256: "nonexistent-hash", + }, + }, + }, + wantTitleID: 0, + wantResolved: false, + }, + { + name: "hash-only policy (no URL)", + policy: &spec.GitOpsPolicySpec{ + InstallSoftware: optjson.BoolOr[*spec.PolicyInstallSoftware]{ + IsOther: true, + Other: &spec.PolicyInstallSoftware{ + HashSHA256: "abc123hash", + }, + }, + }, + wantTitleID: 100, + wantResolved: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + titleID, resolved := resolvePolicySoftwareTitleID(tt.policy, byURL, byAppStoreID, byHash, bySlug) + require.Equal(t, tt.wantResolved, resolved) + require.Equal(t, tt.wantTitleID, titleID) + }) + } +}