diff --git a/changes/42675-conditional-download-cache b/changes/42675-conditional-download-cache new file mode 100644 index 0000000000..ddd9d125d5 --- /dev/null +++ b/changes/42675-conditional-download-cache @@ -0,0 +1,2 @@ +- Added conditional HTTP downloads using ETag headers for software in GitOps, skipping re-download when content hasn't changed. +- Added `always_download` option for software in GitOps to bypass the new conditional download feature. diff --git a/cmd/fleetctl/fleetctl/gitops_test.go b/cmd/fleetctl/fleetctl/gitops_test.go index eef80ad64b..24f92bcf0a 100644 --- a/cmd/fleetctl/fleetctl/gitops_test.go +++ b/cmd/fleetctl/fleetctl/gitops_test.go @@ -2849,6 +2849,9 @@ func TestGitOpsFullGlobalAndTeam(t *testing.T) { ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) { return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil } + ds.GetInstallerByTeamAndURLFunc = func(ctx context.Context, teamID uint, url string) (*fleet.ExistingSoftwareInstaller, error) { + return nil, nil + } ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) { return []uint{}, nil } diff --git a/cmd/fleetctl/fleetctl/testing_utils.go b/cmd/fleetctl/fleetctl/testing_utils.go index 3d67040aaa..ff0dfa5dd0 100644 --- a/cmd/fleetctl/fleetctl/testing_utils.go +++ b/cmd/fleetctl/fleetctl/testing_utils.go @@ -212,6 +212,9 @@ func setupEmptyGitOpsMocks(ds *mock.Store) { ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) { return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil } + ds.GetInstallerByTeamAndURLFunc = func(ctx context.Context, teamID uint, url string) (*fleet.ExistingSoftwareInstaller, error) { + return nil, nil + } ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error { return nil } diff --git a/cmd/fleetctl/integrationtest/gitops/software_test.go b/cmd/fleetctl/integrationtest/gitops/software_test.go index 314482068c..fc82ca1d5c 100644 --- a/cmd/fleetctl/integrationtest/gitops/software_test.go +++ b/cmd/fleetctl/integrationtest/gitops/software_test.go @@ -142,6 +142,9 @@ func TestGitOpsTeamSoftwareInstallers(t *testing.T) { ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) { return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil } + ds.GetInstallerByTeamAndURLFunc = func(ctx context.Context, teamID uint, url string) (*fleet.ExistingSoftwareInstaller, error) { + return nil, nil + } ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) { return []uint{}, nil } @@ -191,6 +194,9 @@ func TestGitOpsTeamSoftwareInstallersQueryEnv(t *testing.T) { ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) { return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil } + ds.GetInstallerByTeamAndURLFunc = func(ctx context.Context, teamID uint, url string) (*fleet.ExistingSoftwareInstaller, error) { + return nil, nil + } ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) { return []uint{}, nil } @@ -443,6 +449,9 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) { ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) { return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil } + ds.GetInstallerByTeamAndURLFunc = func(ctx context.Context, teamID uint, url string) (*fleet.ExistingSoftwareInstaller, error) { + return nil, nil + } ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) { return []uint{}, nil } diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index b6088f891b..ea8b31c97e 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -2105,6 +2105,12 @@ func (svc *Service) BatchSetSoftwareInstallers( "Couldn't edit software. One or more software packages is missing url or hash_sha256 fields.", ) } + if payload.AlwaysDownload && payload.SHA256 != "" { + return "", fleet.NewInvalidArgumentError( + "software", + "Couldn't edit software. The 'always_download' option cannot be used with 'hash_sha256'.", + ) + } if len(payload.URL) > fleet.SoftwareInstallerURLMaxLength { return "", fleet.NewInvalidArgumentError( "software.url", @@ -2269,6 +2275,78 @@ const ( batchSetFailedPrefix = "failed:" ) +// downloadInstallerURL downloads an installer from a URL. If ifNoneMatch is +// non-empty, the request includes an If-None-Match header for conditional GET. +// +// On 304 Not Modified, returns (resp, nil, nil): resp has StatusCode 304 and a +// closed body, tfr is nil. Callers MUST check resp.StatusCode before using tfr. +func downloadInstallerURL(ctx context.Context, downloadURL string, ifNoneMatch string, maxInstallerSize int64) (*http.Response, *fleet.TempFileReader, error) { + client := fleethttp.NewClient() + client.Transport = fleethttp.NewSizeLimitTransport(maxInstallerSize) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil) + if err != nil { + return nil, nil, fmt.Errorf("creating request for URL %q: %w", downloadURL, err) + } + if ifNoneMatch != "" { + req.Header.Set("If-None-Match", ifNoneMatch) + } + + resp, err := client.Do(req) + if err != nil { + var maxBytesErr *http.MaxBytesError + if errors.Is(err, fleethttp.ErrMaxSizeExceeded) || errors.As(err, &maxBytesErr) { + return nil, nil, fleet.NewInvalidArgumentError( + "software.url", + fmt.Sprintf("Couldn't edit software. URL (%q). The maximum file size is %s", downloadURL, installersize.Human(maxInstallerSize)), + ) + } + + return nil, nil, fmt.Errorf("performing request for URL %q: %w", downloadURL, err) + } + + // 304 Not Modified: content unchanged, return response with no body. + // Set Body to http.NoBody after closing so downstream Close() calls are safe. + if resp.StatusCode == http.StatusNotModified { + resp.Body.Close() + resp.Body = http.NoBody + return resp, nil, nil + } + + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil, nil, fleet.NewInvalidArgumentError( + "software.url", + fmt.Sprintf("Couldn't edit software. URL (%q) returned \"Not Found\". Please make sure that URLs are reachable from your Fleet server.", downloadURL), + ) + } + + // Allow all 2xx and 3xx status codes in this pass. + if resp.StatusCode >= 400 { + return nil, nil, fleet.NewInvalidArgumentError( + "software.url", + fmt.Sprintf("Couldn't edit software. URL (%q) received response status code %d.", downloadURL, resp.StatusCode), + ) + } + + tfr, err := fleet.NewTempFileReader(resp.Body, nil) + if err != nil { + // the max size error can be received either at client.Do or here when + // reading the body if it's caught via a limited body reader. + var maxBytesErr *http.MaxBytesError + if errors.Is(err, fleethttp.ErrMaxSizeExceeded) || errors.As(err, &maxBytesErr) { + return nil, nil, fleet.NewInvalidArgumentError( + "software.url", + fmt.Sprintf("Couldn't edit software. URL (%q). The maximum file size is %s", downloadURL, installersize.Human(maxInstallerSize)), + ) + } + return nil, nil, fmt.Errorf("reading installer %q contents: %w", downloadURL, err) + } + + return resp, tfr, nil +} + func (svc *Service) softwareBatchUpload( requestUUID string, teamID *uint, @@ -2326,59 +2404,31 @@ func (svc *Service) softwareBatchUpload( defer close(done) maxInstallerSize := svc.config.Server.MaxInstallerSizeBytes - downloadURLFn := func(ctx context.Context, url string) (*http.Response, *fleet.TempFileReader, error) { - client := fleethttp.NewClient() - client.Transport = fleethttp.NewSizeLimitTransport(maxInstallerSize) + downloadURLFn := func(ctx context.Context, downloadURL string, ifNoneMatch string) (*http.Response, *fleet.TempFileReader, error) { + return downloadInstallerURL(ctx, downloadURL, ifNoneMatch, maxInstallerSize) + } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, nil, fmt.Errorf("creating request for URL %q: %w", url, err) - } - - resp, err := client.Do(req) - if err != nil { - var maxBytesErr *http.MaxBytesError - if errors.Is(err, fleethttp.ErrMaxSizeExceeded) || errors.As(err, &maxBytesErr) { - return nil, nil, fleet.NewInvalidArgumentError( - "software.url", - fmt.Sprintf("Couldn't edit software. URL (%q). The maximum file size is %s", url, installersize.Human(maxInstallerSize)), - ) + // retryDownload wraps downloadURLFn with the standard retry policy. + // Note: a 304 response returns nil error and is treated as success (not retried). + retryDownload := func(ctx context.Context, downloadURL, ifNoneMatch string) (*http.Response, *fleet.TempFileReader, error) { + var resp *http.Response + var tfr *fleet.TempFileReader + err := retry.Do(func() error { + // Close resources from a previous attempt to avoid leaking + // file descriptors, temp files, and HTTP connections. + if tfr != nil { + tfr.Close() + tfr = nil } - - return nil, nil, fmt.Errorf("performing request for URL %q: %w", url, err) - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusNotFound { - return nil, nil, fleet.NewInvalidArgumentError( - "software.url", - fmt.Sprintf("Couldn't edit software. URL (%q) returned \"Not Found\". Please make sure that URLs are reachable from your Fleet server.", url), - ) - } - - // Allow all 2xx and 3xx status codes in this pass. - if resp.StatusCode >= 400 { - return nil, nil, fleet.NewInvalidArgumentError( - "software.url", - fmt.Sprintf("Couldn't edit software. URL (%q) received response status code %d.", url, resp.StatusCode), - ) - } - - tfr, err := fleet.NewTempFileReader(resp.Body, nil) - if err != nil { - // the max size error can be received either at client.Do or here when - // reading the body if it's caught via a limited body reader. - var maxBytesErr *http.MaxBytesError - if errors.Is(err, fleethttp.ErrMaxSizeExceeded) || errors.As(err, &maxBytesErr) { - return nil, nil, fleet.NewInvalidArgumentError( - "software.url", - fmt.Sprintf("Couldn't edit software. URL (%q). The maximum file size is %s", url, installersize.Human(maxInstallerSize)), - ) + if resp != nil && resp.Body != nil { + resp.Body.Close() + resp = nil } - return nil, nil, fmt.Errorf("reading installer %q contents: %w", url, err) - } - - return resp, tfr, nil + var retryErr error + resp, tfr, retryErr = downloadURLFn(ctx, downloadURL, ifNoneMatch) + return retryErr + }, retry.WithMaxAttempts(fleet.BatchDownloadMaxRetries), retry.WithInterval(fleet.BatchSoftwareInstallerRetryInterval())) + return resp, tfr, err } var manualAgentInstall bool @@ -2444,6 +2494,7 @@ func (svc *Service) softwareBatchUpload( Categories: p.Categories, DisplayName: p.DisplayName, RollbackVersion: p.RollbackVersion, + AlwaysDownload: p.AlwaysDownload, } var extraInstallers []*fleet.UploadSoftwareInstallerPayload @@ -2597,40 +2648,101 @@ func (svc *Service) softwareBatchUpload( installer.UninstallScript = "" installer.PreInstallQuery = "" } else { - var resp *http.Response - err = retry.Do(func() error { - var retryErr error - resp, tfr, retryErr = downloadURLFn(ctx, p.URL) - if retryErr != nil { - return retryErr + // Conditional GET (default behavior, disabled by always_download: true). + // Look up existing installer by URL for its ETag, only when + // we're about to download (avoids wasted DB queries). + var existingForCache *fleet.ExistingSoftwareInstaller + var ifNoneMatch string + if !p.AlwaysDownload && p.SHA256 == "" && p.URL != "" { + existing, lookupErr := svc.ds.GetInstallerByTeamAndURL(ctx, tmID, p.URL) + if lookupErr != nil { + svc.logger.WarnContext(ctx, "conditional download lookup failed, will download normally", "url", p.URL, "err", lookupErr) + } else if existing != nil && existing.StorageID != "" && + existing.HTTPETag != nil && *existing.HTTPETag != "" && + existing.Extension != "ipa" && // skip conditional download for .ipa (multi-platform extraInstallers) + validETag(*existing.HTTPETag) { // re-validate before use as defense-in-depth + existingForCache = existing + ifNoneMatch = *existing.HTTPETag } + } - return nil - }, retry.WithMaxAttempts(fleet.BatchDownloadMaxRetries), retry.WithInterval(fleet.BatchSoftwareInstallerRetryInterval())) + resp, tfr, err := retryDownload(ctx, p.URL, ifNoneMatch) if err != nil { return err } - installer.InstallerFile = tfr - toBeClosedTFRs[i] = tfr + // Handle 304 Not Modified (conditional download with matching ETag). + // TRUST ASSUMPTION: conditional download trusts the origin server's + // ETag as a content fingerprint, so we reuse the cached installer + // bytes and metadata (filename, version, extension, etc.) without + // re-extraction. Flow continues past the download-specific code so + // that script fields from the user's GitOps config still pass + // through the shared normalization/validation below. + var cacheHit bool + if resp != nil && resp.StatusCode == http.StatusNotModified && existingForCache != nil { + bytesExist, existErr := svc.softwareInstallStore.Exists(ctx, existingForCache.StorageID) + if existErr == nil && bytesExist { + fillSoftwareInstallerPayloadFromExisting(installer, existingForCache, existingForCache.StorageID) + installer.HTTPETag = existingForCache.HTTPETag + // Propagate the existing hash so FMA hydration below + // doesn't try to recompute it from the (nil) file + // reader when the manifest uses noCheckHash. + if p.MaintainedApp != nil { + p.MaintainedApp.SHA256 = existingForCache.StorageID + } + cacheHit = true + } else { + svc.logger.WarnContext(ctx, "304 received but installer bytes missing, re-downloading", "url", p.URL) + resp, tfr, err = retryDownload(ctx, p.URL, "") + if err != nil { + return err + } + if resp != nil && resp.StatusCode == http.StatusNotModified { + return fmt.Errorf("server returned 304 on unconditional re-download of %q", p.URL) + } + } + } - filename := maintained_apps.FilenameFromResponse(resp) - installer.Filename = filename + if !cacheHit { + // Protocol violation guards: downloadURLFn never returns nil resp + // on success, but guard defensively for server misbehavior. + if resp == nil || tfr == nil { + statusCode := 0 + if resp != nil { + statusCode = resp.StatusCode + } + return fmt.Errorf("download of %q returned no body (status %d)", p.URL, statusCode) + } - // For script packages (.sh and .ps1) and in-house apps (.ipa), clear - // unsupported fields early. Determine extension from filename to - // validate before metadata extraction. - ext := strings.ToLower(filepath.Ext(filename)) - ext = strings.TrimPrefix(ext, ".") - if fleet.IsScriptPackage(ext) { - installer.PostInstallScript = "" - installer.UninstallScript = "" - installer.PreInstallQuery = "" - } else if ext == "ipa" { - installer.InstallScript = "" - installer.PostInstallScript = "" - installer.UninstallScript = "" - installer.PreInstallQuery = "" + installer.InstallerFile = tfr + toBeClosedTFRs[i] = tfr + + filename := maintained_apps.FilenameFromResponse(resp) + installer.Filename = filename + + // Always capture ETag from download response so it's available + // immediately if always_download is later disabled. + if etag := resp.Header.Get("ETag"); etag != "" && validETag(etag) { + installer.HTTPETag = &etag + } else { + svc.logger.DebugContext(ctx, "no usable ETag from server for conditional download", "url", p.URL, "etag", resp.Header.Get("ETag")) + } + + // For script packages (.sh and .ps1) and in-house apps (.ipa), + // clear unsupported fields early. Determine extension from + // filename to validate before metadata extraction. + ext := strings.ToLower(filepath.Ext(filename)) + ext = strings.TrimPrefix(ext, ".") + if fleet.IsScriptPackage(ext) { + installer.PostInstallScript = "" + installer.UninstallScript = "" + installer.PreInstallQuery = "" + } else if ext == "ipa" { + installer.InstallScript = "" + installer.PostInstallScript = "" + installer.UninstallScript = "" + installer.PreInstallQuery = "" + } } } } @@ -2869,6 +2981,38 @@ func fillSoftwareInstallerPayloadFromExisting(payload *fleet.UploadSoftwareInsta payload.PackageIDs = existing.PackageIDs } +// validETag checks if an ETag value is a strong ETag per RFC 7232 +// section 2.3: a quoted opaque-tag without the weak validator prefix. +// Weak ETags (W/"...") are rejected because they indicate semantic +// equivalence rather than byte-for-byte identity, which is insufficient +// for validating cached binary installers. +// The opaque-tag body must consist of RFC 7232 etagc characters +// (%x21 / %x23-7E), which excludes control chars, spaces, inner DQUOTEs, +// and DEL. We reject obs-text (>0x7F) for defense-in-depth. Values over +// 512 bytes are rejected. +func validETag(etag string) bool { + if len(etag) > 512 { + return false + } + // Reject weak ETags — they don't guarantee byte-identical content. + if strings.HasPrefix(etag, "W/") { + return false + } + e := etag + if len(e) < 2 || e[0] != '"' || e[len(e)-1] != '"' { + return false + } + for i := 1; i < len(e)-1; i++ { + c := e[i] + // RFC 7232 etagc = %x21 / %x23-7E / obs-text. Reject obs-text + // (>0x7F) for defense-in-depth. + if c != 0x21 && (c < 0x23 || c > 0x7E) { + return false + } + } + return true +} + func (svc *Service) GetBatchSetSoftwareInstallersResult(ctx context.Context, tmName string, requestUUID string, dryRun bool) (string, string, []fleet.SoftwarePackageResponse, error) { // We've already authorized in the POST /api/latest/fleet/software/batch, // but adding it here so we don't need to worry about a special case endpoint. diff --git a/ee/server/service/software_installers_test.go b/ee/server/service/software_installers_test.go index 4401f2e686..545b8bf070 100644 --- a/ee/server/service/software_installers_test.go +++ b/ee/server/service/software_installers_test.go @@ -1019,6 +1019,156 @@ func TestSelfServiceInstallSoftwareTitleFailsOnPersonallyEnrolledDevices(t *test } } +func TestConditionalGETBehavior(t *testing.T) { + t.Parallel() + + content := []byte("#!/bin/bash\necho 'test'\n") + etag := fmt.Sprintf(`"%x"`, sha256.Sum256(content)) + + tests := []struct { + name string + ifNoneMatch string + handler http.HandlerFunc + expectStatus int + expectBodyNil bool + expectErr bool + }{ + { + name: "no If-None-Match, normal 200 response", + ifNoneMatch: "", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Empty(t, r.Header.Get("If-None-Match")) + w.Header().Set("ETag", etag) + w.Header().Set("Content-Disposition", `attachment; filename="app.sh"`) + _, _ = w.Write(content) + }, + expectStatus: 200, + expectBodyNil: false, + }, + { + name: "If-None-Match sent, server returns 304", + ifNoneMatch: etag, + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, etag, r.Header.Get("If-None-Match")) + w.WriteHeader(http.StatusNotModified) + }, + expectStatus: 304, + expectBodyNil: true, + }, + { + name: "If-None-Match sent, server returns 200 (ETag changed)", + ifNoneMatch: `"old-etag"`, + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, `"old-etag"`, r.Header.Get("If-None-Match")) + w.Header().Set("ETag", etag) + w.Header().Set("Content-Disposition", `attachment; filename="app.sh"`) + _, _ = w.Write(content) + }, + expectStatus: 200, + expectBodyNil: false, + }, + { + name: "If-None-Match sent, server returns 403", + ifNoneMatch: etag, + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + }, + expectStatus: 0, + expectErr: true, + }, + { + name: "If-None-Match sent, server returns 500", + ifNoneMatch: etag, + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + expectStatus: 0, + expectErr: true, + }, + { + name: "If-None-Match with S3 multipart ETag", + ifNoneMatch: `"8fabd6dcf50afffcafbd5c1dbc5f49a4-20"`, + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, `"8fabd6dcf50afffcafbd5c1dbc5f49a4-20"`, r.Header.Get("If-None-Match")) + w.WriteHeader(http.StatusNotModified) + }, + expectStatus: 304, + expectBodyNil: true, + }, + { + name: "server returns no ETag, normal download", + ifNoneMatch: "", + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Disposition", `attachment; filename="app.sh"`) + _, _ = w.Write(content) + }, + expectStatus: 200, + expectBodyNil: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(tt.handler) + t.Cleanup(srv.Close) + + const maxSize = 512 * 1024 * 1024 // 512 MiB, generous for test payloads + resp, tfr, err := downloadInstallerURL(t.Context(), srv.URL+"/test.sh", tt.ifNoneMatch, maxSize) + if tt.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assert.Equal(t, tt.expectStatus, resp.StatusCode) + if tt.expectBodyNil { + assert.Nil(t, tfr) + } else { + require.NotNil(t, tfr) + t.Cleanup(func() { tfr.Close() }) + } + }) + } +} + +func TestValidETag(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + valid bool + }{ + {"strong ETag", `"abc123"`, true}, + {"weak ETag rejected", `W/"abc123"`, false}, + {"empty quotes", `""`, true}, + {"S3 multipart", `"8fabd6dcf50afffcafbd5c1dbc5f49a4-20"`, true}, + {"unquoted", `abc123`, false}, + {"single quote", `"`, false}, + {"empty string", ``, false}, + {"missing closing quote", `"abc`, false}, + {"control char (newline)", "\"abc\n\"", false}, + {"control char (carriage return)", "\"abc\r\"", false}, + {"control char (null)", "\"abc\x00\"", false}, + {"DEL character", "\"abc\x7f\"", false}, + {"tab rejected per RFC 7232", "\"abc\t123\"", false}, + {"inner double-quote rejected", `"abc"def"`, false}, + {"inner space rejected per RFC 7232", `"abc def"`, false}, + {"weak prefix unquoted inner", `W/abc123`, false}, + {"oversized (>512)", `"` + strings.Repeat("a", 512) + `"`, false}, + {"exactly 511 bytes", `"` + strings.Repeat("a", 509) + `"`, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.valid, validETag(tt.input)) + }) + } +} + func TestGetInstallScript(t *testing.T) { t.Parallel() diff --git a/server/datastore/mysql/migrations/tables/20260410173222_AddHTTPETagToSoftwareInstallers.go b/server/datastore/mysql/migrations/tables/20260410173222_AddHTTPETagToSoftwareInstallers.go new file mode 100644 index 0000000000..4b46546f6a --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20260410173222_AddHTTPETagToSoftwareInstallers.go @@ -0,0 +1,31 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20260410173222, Down_20260410173222) +} + +func Up_20260410173222(tx *sql.Tx) error { + _, err := tx.Exec(`ALTER TABLE software_installers ADD COLUMN http_etag VARCHAR(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL`) + if err != nil { + return fmt.Errorf("failed to add http_etag column to software_installers: %w", err) + } + // Index prefix url(255) is a MySQL limitation for InnoDB key length. + // URLs longer than 255 bytes are still matched correctly (full row comparison) + // but with reduced index selectivity. + _, err = tx.Exec(`CREATE INDEX idx_software_installers_team_url ON software_installers (global_or_team_id, url(255))`) + if err != nil { + return fmt.Errorf("failed to add team+url index to software_installers: %w", err) + } + return nil +} + +// Down_20260410173222 is a no-op. Fleet convention: down migrations return nil +// because forward-only migrations are safer than attempting rollback DDL. +func Down_20260410173222(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 46bbf7760a..2e0c712e1e 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -1934,9 +1934,9 @@ CREATE TABLE `migration_status_tables` ( `is_applied` tinyint(1) NOT NULL, `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=514 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=515 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,1,'2020-01-01 01:01:01'),(352,20250213104005,1,'2020-01-01 01:01:01'),(353,20250214205657,1,'2020-01-01 01:01:01'),(354,20250217093329,1,'2020-01-01 01:01:01'),(355,20250219090511,1,'2020-01-01 01:01:01'),(356,20250219100000,1,'2020-01-01 01:01:01'),(357,20250219142401,1,'2020-01-01 01:01:01'),(358,20250224184002,1,'2020-01-01 01:01:01'),(359,20250225085436,1,'2020-01-01 01:01:01'),(360,20250226000000,1,'2020-01-01 01:01:01'),(361,20250226153445,1,'2020-01-01 01:01:01'),(362,20250304162702,1,'2020-01-01 01:01:01'),(363,20250306144233,1,'2020-01-01 01:01:01'),(364,20250313163430,1,'2020-01-01 01:01:01'),(365,20250317130944,1,'2020-01-01 01:01:01'),(366,20250318165922,1,'2020-01-01 01:01:01'),(367,20250320132525,1,'2020-01-01 01:01:01'),(368,20250320200000,1,'2020-01-01 01:01:01'),(369,20250326161930,1,'2020-01-01 01:01:01'),(370,20250326161931,1,'2020-01-01 01:01:01'),(371,20250331042354,1,'2020-01-01 01:01:01'),(372,20250331154206,1,'2020-01-01 01:01:01'),(373,20250401155831,1,'2020-01-01 01:01:01'),(374,20250408133233,1,'2020-01-01 01:01:01'),(375,20250410104321,1,'2020-01-01 01:01:01'),(376,20250421085116,1,'2020-01-01 01:01:01'),(377,20250422095806,1,'2020-01-01 01:01:01'),(378,20250424153059,1,'2020-01-01 01:01:01'),(379,20250430103833,1,'2020-01-01 01:01:01'),(380,20250430112622,1,'2020-01-01 01:01:01'),(381,20250501162727,1,'2020-01-01 01:01:01'),(382,20250502154517,1,'2020-01-01 01:01:01'),(383,20250502222222,1,'2020-01-01 01:01:01'),(384,20250507170845,1,'2020-01-01 01:01:01'),(385,20250513162912,1,'2020-01-01 01:01:01'),(386,20250519161614,1,'2020-01-01 01:01:01'),(387,20250519170000,1,'2020-01-01 01:01:01'),(388,20250520153848,1,'2020-01-01 01:01:01'),(389,20250528115932,1,'2020-01-01 01:01:01'),(390,20250529102706,1,'2020-01-01 01:01:01'),(391,20250603105558,1,'2020-01-01 01:01:01'),(392,20250609102714,1,'2020-01-01 01:01:01'),(393,20250609112613,1,'2020-01-01 01:01:01'),(394,20250613103810,1,'2020-01-01 01:01:01'),(395,20250616193950,1,'2020-01-01 01:01:01'),(396,20250624140757,1,'2020-01-01 01:01:01'),(397,20250626130239,1,'2020-01-01 01:01:01'),(398,20250629131032,1,'2020-01-01 01:01:01'),(399,20250701155654,1,'2020-01-01 01:01:01'),(400,20250707095725,1,'2020-01-01 01:01:01'),(401,20250716152435,1,'2020-01-01 01:01:01'),(402,20250718091828,1,'2020-01-01 01:01:01'),(403,20250728122229,1,'2020-01-01 01:01:01'),(404,20250731122715,1,'2020-01-01 01:01:01'),(405,20250731151000,1,'2020-01-01 01:01:01'),(406,20250803000000,1,'2020-01-01 01:01:01'),(407,20250805083116,1,'2020-01-01 01:01:01'),(408,20250807140441,1,'2020-01-01 01:01:01'),(409,20250808000000,1,'2020-01-01 01:01:01'),(410,20250811155036,1,'2020-01-01 01:01:01'),(411,20250813205039,1,'2020-01-01 01:01:01'),(412,20250814123333,1,'2020-01-01 01:01:01'),(413,20250815130115,1,'2020-01-01 01:01:01'),(414,20250816115553,1,'2020-01-01 01:01:01'),(415,20250817154557,1,'2020-01-01 01:01:01'),(416,20250825113751,1,'2020-01-01 01:01:01'),(417,20250827113140,1,'2020-01-01 01:01:01'),(418,20250828120836,1,'2020-01-01 01:01:01'),(419,20250902112642,1,'2020-01-01 01:01:01'),(420,20250904091745,1,'2020-01-01 01:01:01'),(421,20250905090000,1,'2020-01-01 01:01:01'),(422,20250922083056,1,'2020-01-01 01:01:01'),(423,20250923120000,1,'2020-01-01 01:01:01'),(424,20250926123048,1,'2020-01-01 01:01:01'),(425,20251015103505,1,'2020-01-01 01:01:01'),(426,20251015103600,1,'2020-01-01 01:01:01'),(427,20251015103700,1,'2020-01-01 01:01:01'),(428,20251015103800,1,'2020-01-01 01:01:01'),(429,20251015103900,1,'2020-01-01 01:01:01'),(430,20251028140000,1,'2020-01-01 01:01:01'),(431,20251028140100,1,'2020-01-01 01:01:01'),(432,20251028140110,1,'2020-01-01 01:01:01'),(433,20251028140200,1,'2020-01-01 01:01:01'),(434,20251028140300,1,'2020-01-01 01:01:01'),(435,20251028140400,1,'2020-01-01 01:01:01'),(436,20251031154558,1,'2020-01-01 01:01:01'),(437,20251103160848,1,'2020-01-01 01:01:01'),(438,20251104112849,1,'2020-01-01 01:01:01'),(439,20251106000000,1,'2020-01-01 01:01:01'),(440,20251107164629,1,'2020-01-01 01:01:01'),(441,20251107170854,1,'2020-01-01 01:01:01'),(442,20251110172137,1,'2020-01-01 01:01:01'),(443,20251111153133,1,'2020-01-01 01:01:01'),(444,20251117020000,1,'2020-01-01 01:01:01'),(445,20251117020100,1,'2020-01-01 01:01:01'),(446,20251117020200,1,'2020-01-01 01:01:01'),(447,20251121100000,1,'2020-01-01 01:01:01'),(448,20251121124239,1,'2020-01-01 01:01:01'),(449,20251124090450,1,'2020-01-01 01:01:01'),(450,20251124135808,1,'2020-01-01 01:01:01'),(451,20251124140138,1,'2020-01-01 01:01:01'),(452,20251124162948,1,'2020-01-01 01:01:01'),(453,20251127113559,1,'2020-01-01 01:01:01'),(454,20251202162232,1,'2020-01-01 01:01:01'),(455,20251203170808,1,'2020-01-01 01:01:01'),(456,20251207050413,1,'2020-01-01 01:01:01'),(457,20251208215800,1,'2020-01-01 01:01:01'),(458,20251209221730,1,'2020-01-01 01:01:01'),(459,20251209221850,1,'2020-01-01 01:01:01'),(460,20251215163721,1,'2020-01-01 01:01:01'),(461,20251217000000,1,'2020-01-01 01:01:01'),(462,20251217120000,1,'2020-01-01 01:01:01'),(463,20251229000000,1,'2020-01-01 01:01:01'),(464,20251229000010,1,'2020-01-01 01:01:01'),(465,20251229000020,1,'2020-01-01 01:01:01'),(466,20260106000000,1,'2020-01-01 01:01:01'),(467,20260108200708,1,'2020-01-01 01:01:01'),(468,20260108214732,1,'2020-01-01 01:01:01'),(469,20260109231821,1,'2020-01-01 01:01:01'),(470,20260113012054,1,'2020-01-01 01:01:01'),(471,20260124200020,1,'2020-01-01 01:01:01'),(472,20260126150840,1,'2020-01-01 01:01:01'),(473,20260126210724,1,'2020-01-01 01:01:01'),(474,20260202151756,1,'2020-01-01 01:01:01'),(475,20260205184907,1,'2020-01-01 01:01:01'),(476,20260210151544,1,'2020-01-01 01:01:01'),(477,20260210155109,1,'2020-01-01 01:01:01'),(478,20260210181120,1,'2020-01-01 01:01:01'),(479,20260211200153,1,'2020-01-01 01:01:01'),(480,20260217141240,1,'2020-01-01 01:01:01'),(481,20260217200906,1,'2020-01-01 01:01:01'),(482,20260218175704,1,'2020-01-01 01:01:01'),(483,20260314120000,1,'2020-01-01 01:01:01'),(484,20260316120000,1,'2020-01-01 01:01:01'),(485,20260316120001,1,'2020-01-01 01:01:01'),(486,20260316120002,1,'2020-01-01 01:01:01'),(487,20260316120003,1,'2020-01-01 01:01:01'),(488,20260316120004,1,'2020-01-01 01:01:01'),(489,20260316120005,1,'2020-01-01 01:01:01'),(490,20260316120006,1,'2020-01-01 01:01:01'),(491,20260316120007,1,'2020-01-01 01:01:01'),(492,20260316120008,1,'2020-01-01 01:01:01'),(493,20260316120009,1,'2020-01-01 01:01:01'),(494,20260316120010,1,'2020-01-01 01:01:01'),(495,20260317120000,1,'2020-01-01 01:01:01'),(496,20260318184559,1,'2020-01-01 01:01:01'),(497,20260319120000,1,'2020-01-01 01:01:01'),(498,20260323144117,1,'2020-01-01 01:01:01'),(499,20260324161944,1,'2020-01-01 01:01:01'),(500,20260324223334,1,'2020-01-01 01:01:01'),(501,20260326131501,1,'2020-01-01 01:01:01'),(502,20260326210603,1,'2020-01-01 01:01:01'),(503,20260331000000,1,'2020-01-01 01:01:01'),(504,20260401153000,1,'2020-01-01 01:01:01'),(505,20260401153001,1,'2020-01-01 01:01:01'),(506,20260401153503,1,'2020-01-01 01:01:01'),(507,20260403120000,1,'2020-01-01 01:01:01'),(508,20260409153713,1,'2020-01-01 01:01:01'),(509,20260409153714,1,'2020-01-01 01:01:01'),(510,20260409153715,1,'2020-01-01 01:01:01'),(511,20260409153716,1,'2020-01-01 01:01:01'),(512,20260409153717,1,'2020-01-01 01:01:01'),(513,20260409183610,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,1,'2020-01-01 01:01:01'),(352,20250213104005,1,'2020-01-01 01:01:01'),(353,20250214205657,1,'2020-01-01 01:01:01'),(354,20250217093329,1,'2020-01-01 01:01:01'),(355,20250219090511,1,'2020-01-01 01:01:01'),(356,20250219100000,1,'2020-01-01 01:01:01'),(357,20250219142401,1,'2020-01-01 01:01:01'),(358,20250224184002,1,'2020-01-01 01:01:01'),(359,20250225085436,1,'2020-01-01 01:01:01'),(360,20250226000000,1,'2020-01-01 01:01:01'),(361,20250226153445,1,'2020-01-01 01:01:01'),(362,20250304162702,1,'2020-01-01 01:01:01'),(363,20250306144233,1,'2020-01-01 01:01:01'),(364,20250313163430,1,'2020-01-01 01:01:01'),(365,20250317130944,1,'2020-01-01 01:01:01'),(366,20250318165922,1,'2020-01-01 01:01:01'),(367,20250320132525,1,'2020-01-01 01:01:01'),(368,20250320200000,1,'2020-01-01 01:01:01'),(369,20250326161930,1,'2020-01-01 01:01:01'),(370,20250326161931,1,'2020-01-01 01:01:01'),(371,20250331042354,1,'2020-01-01 01:01:01'),(372,20250331154206,1,'2020-01-01 01:01:01'),(373,20250401155831,1,'2020-01-01 01:01:01'),(374,20250408133233,1,'2020-01-01 01:01:01'),(375,20250410104321,1,'2020-01-01 01:01:01'),(376,20250421085116,1,'2020-01-01 01:01:01'),(377,20250422095806,1,'2020-01-01 01:01:01'),(378,20250424153059,1,'2020-01-01 01:01:01'),(379,20250430103833,1,'2020-01-01 01:01:01'),(380,20250430112622,1,'2020-01-01 01:01:01'),(381,20250501162727,1,'2020-01-01 01:01:01'),(382,20250502154517,1,'2020-01-01 01:01:01'),(383,20250502222222,1,'2020-01-01 01:01:01'),(384,20250507170845,1,'2020-01-01 01:01:01'),(385,20250513162912,1,'2020-01-01 01:01:01'),(386,20250519161614,1,'2020-01-01 01:01:01'),(387,20250519170000,1,'2020-01-01 01:01:01'),(388,20250520153848,1,'2020-01-01 01:01:01'),(389,20250528115932,1,'2020-01-01 01:01:01'),(390,20250529102706,1,'2020-01-01 01:01:01'),(391,20250603105558,1,'2020-01-01 01:01:01'),(392,20250609102714,1,'2020-01-01 01:01:01'),(393,20250609112613,1,'2020-01-01 01:01:01'),(394,20250613103810,1,'2020-01-01 01:01:01'),(395,20250616193950,1,'2020-01-01 01:01:01'),(396,20250624140757,1,'2020-01-01 01:01:01'),(397,20250626130239,1,'2020-01-01 01:01:01'),(398,20250629131032,1,'2020-01-01 01:01:01'),(399,20250701155654,1,'2020-01-01 01:01:01'),(400,20250707095725,1,'2020-01-01 01:01:01'),(401,20250716152435,1,'2020-01-01 01:01:01'),(402,20250718091828,1,'2020-01-01 01:01:01'),(403,20250728122229,1,'2020-01-01 01:01:01'),(404,20250731122715,1,'2020-01-01 01:01:01'),(405,20250731151000,1,'2020-01-01 01:01:01'),(406,20250803000000,1,'2020-01-01 01:01:01'),(407,20250805083116,1,'2020-01-01 01:01:01'),(408,20250807140441,1,'2020-01-01 01:01:01'),(409,20250808000000,1,'2020-01-01 01:01:01'),(410,20250811155036,1,'2020-01-01 01:01:01'),(411,20250813205039,1,'2020-01-01 01:01:01'),(412,20250814123333,1,'2020-01-01 01:01:01'),(413,20250815130115,1,'2020-01-01 01:01:01'),(414,20250816115553,1,'2020-01-01 01:01:01'),(415,20250817154557,1,'2020-01-01 01:01:01'),(416,20250825113751,1,'2020-01-01 01:01:01'),(417,20250827113140,1,'2020-01-01 01:01:01'),(418,20250828120836,1,'2020-01-01 01:01:01'),(419,20250902112642,1,'2020-01-01 01:01:01'),(420,20250904091745,1,'2020-01-01 01:01:01'),(421,20250905090000,1,'2020-01-01 01:01:01'),(422,20250922083056,1,'2020-01-01 01:01:01'),(423,20250923120000,1,'2020-01-01 01:01:01'),(424,20250926123048,1,'2020-01-01 01:01:01'),(425,20251015103505,1,'2020-01-01 01:01:01'),(426,20251015103600,1,'2020-01-01 01:01:01'),(427,20251015103700,1,'2020-01-01 01:01:01'),(428,20251015103800,1,'2020-01-01 01:01:01'),(429,20251015103900,1,'2020-01-01 01:01:01'),(430,20251028140000,1,'2020-01-01 01:01:01'),(431,20251028140100,1,'2020-01-01 01:01:01'),(432,20251028140110,1,'2020-01-01 01:01:01'),(433,20251028140200,1,'2020-01-01 01:01:01'),(434,20251028140300,1,'2020-01-01 01:01:01'),(435,20251028140400,1,'2020-01-01 01:01:01'),(436,20251031154558,1,'2020-01-01 01:01:01'),(437,20251103160848,1,'2020-01-01 01:01:01'),(438,20251104112849,1,'2020-01-01 01:01:01'),(439,20251106000000,1,'2020-01-01 01:01:01'),(440,20251107164629,1,'2020-01-01 01:01:01'),(441,20251107170854,1,'2020-01-01 01:01:01'),(442,20251110172137,1,'2020-01-01 01:01:01'),(443,20251111153133,1,'2020-01-01 01:01:01'),(444,20251117020000,1,'2020-01-01 01:01:01'),(445,20251117020100,1,'2020-01-01 01:01:01'),(446,20251117020200,1,'2020-01-01 01:01:01'),(447,20251121100000,1,'2020-01-01 01:01:01'),(448,20251121124239,1,'2020-01-01 01:01:01'),(449,20251124090450,1,'2020-01-01 01:01:01'),(450,20251124135808,1,'2020-01-01 01:01:01'),(451,20251124140138,1,'2020-01-01 01:01:01'),(452,20251124162948,1,'2020-01-01 01:01:01'),(453,20251127113559,1,'2020-01-01 01:01:01'),(454,20251202162232,1,'2020-01-01 01:01:01'),(455,20251203170808,1,'2020-01-01 01:01:01'),(456,20251207050413,1,'2020-01-01 01:01:01'),(457,20251208215800,1,'2020-01-01 01:01:01'),(458,20251209221730,1,'2020-01-01 01:01:01'),(459,20251209221850,1,'2020-01-01 01:01:01'),(460,20251215163721,1,'2020-01-01 01:01:01'),(461,20251217000000,1,'2020-01-01 01:01:01'),(462,20251217120000,1,'2020-01-01 01:01:01'),(463,20251229000000,1,'2020-01-01 01:01:01'),(464,20251229000010,1,'2020-01-01 01:01:01'),(465,20251229000020,1,'2020-01-01 01:01:01'),(466,20260106000000,1,'2020-01-01 01:01:01'),(467,20260108200708,1,'2020-01-01 01:01:01'),(468,20260108214732,1,'2020-01-01 01:01:01'),(469,20260109231821,1,'2020-01-01 01:01:01'),(470,20260113012054,1,'2020-01-01 01:01:01'),(471,20260124200020,1,'2020-01-01 01:01:01'),(472,20260126150840,1,'2020-01-01 01:01:01'),(473,20260126210724,1,'2020-01-01 01:01:01'),(474,20260202151756,1,'2020-01-01 01:01:01'),(475,20260205184907,1,'2020-01-01 01:01:01'),(476,20260210151544,1,'2020-01-01 01:01:01'),(477,20260210155109,1,'2020-01-01 01:01:01'),(478,20260210181120,1,'2020-01-01 01:01:01'),(479,20260211200153,1,'2020-01-01 01:01:01'),(480,20260217141240,1,'2020-01-01 01:01:01'),(481,20260217200906,1,'2020-01-01 01:01:01'),(482,20260218175704,1,'2020-01-01 01:01:01'),(483,20260314120000,1,'2020-01-01 01:01:01'),(484,20260316120000,1,'2020-01-01 01:01:01'),(485,20260316120001,1,'2020-01-01 01:01:01'),(486,20260316120002,1,'2020-01-01 01:01:01'),(487,20260316120003,1,'2020-01-01 01:01:01'),(488,20260316120004,1,'2020-01-01 01:01:01'),(489,20260316120005,1,'2020-01-01 01:01:01'),(490,20260316120006,1,'2020-01-01 01:01:01'),(491,20260316120007,1,'2020-01-01 01:01:01'),(492,20260316120008,1,'2020-01-01 01:01:01'),(493,20260316120009,1,'2020-01-01 01:01:01'),(494,20260316120010,1,'2020-01-01 01:01:01'),(495,20260317120000,1,'2020-01-01 01:01:01'),(496,20260318184559,1,'2020-01-01 01:01:01'),(497,20260319120000,1,'2020-01-01 01:01:01'),(498,20260323144117,1,'2020-01-01 01:01:01'),(499,20260324161944,1,'2020-01-01 01:01:01'),(500,20260324223334,1,'2020-01-01 01:01:01'),(501,20260326131501,1,'2020-01-01 01:01:01'),(502,20260326210603,1,'2020-01-01 01:01:01'),(503,20260331000000,1,'2020-01-01 01:01:01'),(504,20260401153000,1,'2020-01-01 01:01:01'),(505,20260401153001,1,'2020-01-01 01:01:01'),(506,20260401153503,1,'2020-01-01 01:01:01'),(507,20260403120000,1,'2020-01-01 01:01:01'),(508,20260409153713,1,'2020-01-01 01:01:01'),(509,20260409153714,1,'2020-01-01 01:01:01'),(510,20260409153715,1,'2020-01-01 01:01:01'),(511,20260409153716,1,'2020-01-01 01:01:01'),(512,20260409153717,1,'2020-01-01 01:01:01'),(513,20260409183610,1,'2020-01-01 01:01:01'),(514,20260410173222,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( @@ -2849,6 +2849,7 @@ CREATE TABLE `software_installers` ( `upgrade_code` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `is_active` tinyint(1) NOT NULL DEFAULT '0', `patch_query` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `http_etag` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `idx_software_installers_team_title_version` (`global_or_team_id`,`title_id`,`version`), KEY `fk_software_installers_title` (`title_id`), @@ -2858,6 +2859,7 @@ CREATE TABLE `software_installers` ( KEY `fk_software_installers_user_id` (`user_id`), KEY `fk_uninstall_script_content_id` (`uninstall_script_content_id`), KEY `fk_software_installers_fleet_library_app_id` (`fleet_maintained_app_id`), + KEY `idx_software_installers_team_url` (`global_or_team_id`,`url`(255)), CONSTRAINT `fk_software_installers_install_script_content_id` FOREIGN KEY (`install_script_content_id`) REFERENCES `script_contents` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT `fk_software_installers_post_install_script_content_id` FOREIGN KEY (`post_install_script_content_id`) REFERENCES `script_contents` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT `fk_software_installers_team_id` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go index a4911d2bc3..7fbe7bddbc 100644 --- a/server/datastore/mysql/software_installers.go +++ b/server/datastore/mysql/software_installers.go @@ -2331,10 +2331,12 @@ INSERT INTO software_installers ( install_during_setup, fleet_maintained_app_id, is_active, + http_etag, patch_query ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, - (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?), ?, ?, COALESCE(?, false), ?, ?, ? + (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?), ?, ?, COALESCE(?, false), ?, ?, + ?, ? ) ON DUPLICATE KEY UPDATE install_script_content_id = VALUES(install_script_content_id), @@ -2354,6 +2356,7 @@ ON DUPLICATE KEY UPDATE url = VALUES(url), install_during_setup = COALESCE(?, install_during_setup), is_active = VALUES(is_active), + http_etag = VALUES(http_etag), patch_query = VALUES(patch_query) ` @@ -2778,6 +2781,7 @@ WHERE isActive = 1 } + // Args match insertNewOrEditedInstaller column order. args := []interface{}{ tmID, globalOrTeamID, @@ -2794,15 +2798,16 @@ WHERE installer.UpgradeCode, titleID, installer.UserID, - installer.UserID, - installer.UserID, + installer.UserID, // user_name subselect + installer.UserID, // user_email subselect installer.URL, strings.Join(installer.PackageIDs, ","), installer.InstallDuringSetup, installer.FleetMaintainedAppID, isActive, + installer.HTTPETag, installer.PatchQuery, - installer.InstallDuringSetup, + installer.InstallDuringSetup, // ON DUPLICATE KEY } // For FMA installers, skip the insert if this exact version is already cached // for this team+title. This prevents duplicate rows from repeated batch sets @@ -3547,15 +3552,17 @@ func (ds *Datastore) GetTeamsWithInstallerByHash(ctx context.Context, sha256, ur stmt := ` SELECT si.id AS installer_id, - si.team_id AS team_id, - si.filename AS filename, - si.extension AS extension, - si.version AS version, - si.platform AS platform, - st.source AS source, - st.bundle_identifier AS bundle_identifier, + si.team_id, + si.storage_id, + si.http_etag, + si.filename, + si.extension, + si.version, + si.platform, + st.source, + st.bundle_identifier, st.name AS title, - si.package_ids AS package_ids + si.package_ids FROM software_installers si JOIN software_titles st ON si.title_id = st.id @@ -3566,13 +3573,15 @@ UNION ALL SELECT iha.id AS installer_id, - iha.team_id AS team_id, - iha.filename AS filename, + iha.team_id, + iha.storage_id, + NULL AS http_etag, + iha.filename, 'ipa' AS extension, - iha.version AS version, - iha.platform AS platform, - st.source AS source, - st.bundle_identifier AS bundle_identifier, + iha.version, + iha.platform, + st.source, + st.bundle_identifier, st.name AS title, '' AS package_ids FROM @@ -3615,6 +3624,45 @@ WHERE return byTeam, nil } +func (ds *Datastore) GetInstallerByTeamAndURL(ctx context.Context, teamID uint, url string) (*fleet.ExistingSoftwareInstaller, error) { + stmt := ` +SELECT + si.id AS installer_id, + si.team_id AS team_id, + si.storage_id AS storage_id, + si.filename AS filename, + si.extension AS extension, + si.version AS version, + si.platform AS platform, + st.source AS source, + st.bundle_identifier AS bundle_identifier, + st.name AS title, + si.package_ids AS package_ids, + si.http_etag AS http_etag +FROM + software_installers si + JOIN software_titles st ON si.title_id = st.id +WHERE + si.global_or_team_id = ? AND si.url = ? AND si.is_active = 1 +ORDER BY si.id DESC +LIMIT 1 +` + var installer fleet.ExistingSoftwareInstaller + // Use reader: the installer was written in a previous GitOps run. On rapid sequential + // runs, the replica may be slightly stale, which results in a cache miss (full download) + // rather than incorrect behavior. This is an acceptable trade-off vs loading the writer. + if err := sqlx.GetContext(ctx, ds.reader(ctx), &installer, stmt, teamID, url); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, ctxerr.Wrap(ctx, err, "get installer by team and URL") + } + if installer.PackageIDList != "" { + installer.PackageIDs = strings.Split(installer.PackageIDList, ",") + } + return &installer, nil +} + func (ds *Datastore) checkSoftwareConflictsByIdentifier(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) error { // if this is an in-house app, check if an installer exists if payload.Extension == "ipa" { diff --git a/server/datastore/mysql/software_installers_test.go b/server/datastore/mysql/software_installers_test.go index af67054fc7..afd7818d89 100644 --- a/server/datastore/mysql/software_installers_test.go +++ b/server/datastore/mysql/software_installers_test.go @@ -58,6 +58,7 @@ func TestSoftwareInstallers(t *testing.T) { {"AddSoftwareTitleToMatchingSoftware", testAddSoftwareTitleToMatchingSoftware}, {"FleetMaintainedAppInstallerUpdates", testFleetMaintainedAppInstallerUpdates}, {"RepointCustomPackagePolicyToNewInstaller", testRepointPolicyToNewInstaller}, + {"GetInstallerByTeamAndURL", testGetInstallerByTeamAndURL}, } for _, c := range cases { @@ -3450,6 +3451,7 @@ func testGetTeamsWithInstallerByHash(t *testing.T, ds *Datastore) { require.Equal(t, "pkg", i.Extension) require.Equal(t, "1.0", i.Version) require.Equal(t, "darwin", i.Platform) + require.Equal(t, hash1, i.StorageID) } installers, err = ds.GetTeamsWithInstallerByHash(ctx, hash2, "https://example.com/2") @@ -3473,6 +3475,8 @@ func testGetTeamsWithInstallerByHash(t *testing.T, ds *Datastore) { var foundPlatforms []string for _, inst := range installers[team1.ID] { foundPlatforms = append(foundPlatforms, inst.Platform) + require.Equal(t, "inhouse", inst.StorageID) + require.Nil(t, inst.HTTPETag) // in-house apps don't have ETags } require.ElementsMatch(t, []string{"ios", "ipados"}, foundPlatforms) @@ -4606,3 +4610,127 @@ func testRepointPolicyToNewInstaller(t *testing.T, ds *Datastore) { require.Equal(t, metadata.InstallerID, *policyAfterUpdate.SoftwareInstallerID) }) } + +func testGetInstallerByTeamAndURL(t *testing.T, ds *Datastore) { + ctx := context.Background() + user := test.NewUser(t, ds, "Alice", "alice@example.com", true) + team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"}) + require.NoError(t, err) + team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"}) + require.NoError(t, err) + + tfr, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir) + require.NoError(t, err) + + etag := `"abc123"` + + err = ds.BatchSetSoftwareInstallers(ctx, &team1.ID, []*fleet.UploadSoftwareInstallerPayload{ + { + InstallerFile: tfr, + BundleIdentifier: "com.example.app", + Extension: "pkg", + StorageID: "hash1", + Filename: "app.pkg", + Title: "App", + Version: "1.0", + Source: "apps", + UserID: user.ID, + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + TeamID: &team1.ID, + Platform: "darwin", + URL: "https://example.com/app/latest", + HTTPETag: &etag, + }, + }) + require.NoError(t, err) + + // Correct team and URL returns the installer with ETag + existing, err := ds.GetInstallerByTeamAndURL(ctx, team1.ID, "https://example.com/app/latest") + require.NoError(t, err) + require.NotNil(t, existing) + assert.Equal(t, "hash1", existing.StorageID) + assert.Equal(t, "app.pkg", existing.Filename) + require.NotNil(t, existing.HTTPETag) + assert.Equal(t, etag, *existing.HTTPETag) + + // Wrong team returns nil + existing, err = ds.GetInstallerByTeamAndURL(ctx, team2.ID, "https://example.com/app/latest") + require.NoError(t, err) + assert.Nil(t, existing) + + // Wrong URL returns nil + existing, err = ds.GetInstallerByTeamAndURL(ctx, team1.ID, "https://example.com/other") + require.NoError(t, err) + assert.Nil(t, existing) + + // URL with query params (GlobalProtect pattern) + err = ds.BatchSetSoftwareInstallers(ctx, &team1.ID, []*fleet.UploadSoftwareInstallerPayload{ + { + InstallerFile: tfr, + BundleIdentifier: "com.example.gp", + Extension: "msi", + StorageID: "hash2", + Filename: "gp.msi", + Title: "GlobalProtect", + Version: "1.0", + Source: "programs", + UserID: user.ID, + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + TeamID: &team1.ID, + Platform: "windows", + URL: "https://example.com/gp?version=64&platform=windows", + HTTPETag: &etag, + }, + }) + require.NoError(t, err) + + existing, err = ds.GetInstallerByTeamAndURL(ctx, team1.ID, "https://example.com/gp?version=64&platform=windows") + require.NoError(t, err) + require.NotNil(t, existing) + assert.Equal(t, "hash2", existing.StorageID) + + // Simulate an FMA rollback: two rows for the same (team, URL), where the + // inactive row has a higher id than the active row. The lookup must return + // the active row even though ORDER BY id DESC would otherwise pick the + // inactive one. + rollbackURL := "https://example.com/rollback" + err = ds.BatchSetSoftwareInstallers(ctx, &team2.ID, []*fleet.UploadSoftwareInstallerPayload{ + { + InstallerFile: tfr, + BundleIdentifier: "com.example.rb", + Extension: "pkg", + StorageID: "active_hash", + Filename: "rb.pkg", + Title: "Rollback", + Version: "1.0", + Source: "apps", + UserID: user.ID, + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + TeamID: &team2.ID, + Platform: "darwin", + URL: rollbackURL, + HTTPETag: &etag, + }, + }) + require.NoError(t, err) + + inactiveETag := `"inactive-etag"` + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, ` + INSERT INTO software_installers + (team_id, global_or_team_id, storage_id, filename, extension, version, platform, title_id, + install_script_content_id, uninstall_script_content_id, is_active, url, package_ids, patch_query, http_etag) + SELECT team_id, global_or_team_id, 'inactive_hash', filename, extension, 'old_version', platform, title_id, + install_script_content_id, uninstall_script_content_id, 0, url, package_ids, patch_query, ? + FROM software_installers WHERE team_id = ? AND url = ? + `, inactiveETag, team2.ID, rollbackURL) + return err + }) + + existing, err = ds.GetInstallerByTeamAndURL(ctx, team2.ID, rollbackURL) + require.NoError(t, err) + require.NotNil(t, existing) + assert.Equal(t, "active_hash", existing.StorageID, "must return the active installer, not the inactive duplicate") + require.NotNil(t, existing.HTTPETag) + assert.Equal(t, etag, *existing.HTTPETag) +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index a1ea05b1b7..36dc722bee 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -2402,6 +2402,11 @@ type Datastore interface { // metadata by the installer's hash. GetTeamsWithInstallerByHash(ctx context.Context, sha256, url string) (map[uint][]*ExistingSoftwareInstaller, error) + // GetInstallerByTeamAndURL looks up an existing software installer by team + // and URL. Returns the most recently inserted installer matching the team and + // URL, including its storage_id (SHA256) and http_etag for conditional downloads. + GetInstallerByTeamAndURL(ctx context.Context, teamID uint, url string) (*ExistingSoftwareInstaller, error) + // TeamIDsWithSetupExperienceIdPEnabled returns the list of team IDs that // have the setup experience IdP (End user authentication) enabled. It uses // id 0 to represent "No team", should IdP be enabled for that team. diff --git a/server/fleet/scripts.go b/server/fleet/scripts.go index 0d9f40e8e4..4ae230e4aa 100644 --- a/server/fleet/scripts.go +++ b/server/fleet/scripts.go @@ -626,6 +626,8 @@ type SoftwareInstallerPayload struct { IconPath string `json:"-"` IconHash string `json:"-"` + // AlwaysDownload disables conditional HTTP downloads using ETag headers. + AlwaysDownload bool `json:"always_download"` } type HostLockWipeStatus struct { diff --git a/server/fleet/software_installer.go b/server/fleet/software_installer.go index 4a56d61a78..7453fbaf6a 100644 --- a/server/fleet/software_installer.go +++ b/server/fleet/software_installer.go @@ -542,7 +542,13 @@ type UploadSoftwareInstallerPayload struct { // automatically created when a software installer is added to Fleet. This field should be set // after software installer creation if AutomaticInstall is true. AddedAutomaticInstallPolicy *Policy - PatchQuery string + // AlwaysDownload disables conditional HTTP downloads using ETag. When false + // (the default), the download request includes If-None-Match with the stored ETag. + AlwaysDownload bool + // HTTPETag stores the ETag from the last download response, used for + // conditional GET requests when AlwaysDownload is false. + HTTPETag *string + PatchQuery string } func (p UploadSoftwareInstallerPayload) UniqueIdentifier() string { @@ -587,6 +593,8 @@ type ExistingSoftwareInstaller struct { Title string `db:"title"` PackageIDList string `db:"package_ids"` PackageIDs []string + StorageID string `db:"storage_id"` + HTTPETag *string `db:"http_etag"` } type UpdateSoftwareInstallerPayload struct { @@ -817,6 +825,11 @@ type SoftwarePackageSpec struct { SHA256 string `json:"hash_sha256"` Categories []string `json:"categories"` DisplayName string `json:"display_name,omitempty"` + // AlwaysDownload disables conditional HTTP downloads using ETag headers. + // When false (the default), Fleet sends If-None-Match with the stored ETag + // on subsequent downloads. If the server returns 304 Not Modified, the + // download is skipped entirely. + AlwaysDownload bool `json:"always_download"` } func (spec SoftwarePackageSpec) ResolveSoftwarePackagePaths(baseDir string) SoftwarePackageSpec { diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 842ab2cb47..d4f701e018 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -1531,6 +1531,8 @@ type GetHostAwaitingConfigurationFunc func(ctx context.Context, hostUUID string) type GetTeamsWithInstallerByHashFunc func(ctx context.Context, sha256 string, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) +type GetInstallerByTeamAndURLFunc func(ctx context.Context, teamID uint, url string) (*fleet.ExistingSoftwareInstaller, error) + type TeamIDsWithSetupExperienceIdPEnabledFunc func(ctx context.Context) ([]uint, error) type ListSetupExperienceResultsByHostUUIDFunc func(ctx context.Context, hostUUID string, teamID uint) ([]*fleet.SetupExperienceStatusResult, error) @@ -4122,6 +4124,9 @@ type DataStore struct { GetTeamsWithInstallerByHashFunc GetTeamsWithInstallerByHashFunc GetTeamsWithInstallerByHashFuncInvoked bool + GetInstallerByTeamAndURLFunc GetInstallerByTeamAndURLFunc + GetInstallerByTeamAndURLFuncInvoked bool + TeamIDsWithSetupExperienceIdPEnabledFunc TeamIDsWithSetupExperienceIdPEnabledFunc TeamIDsWithSetupExperienceIdPEnabledFuncInvoked bool @@ -9895,6 +9900,13 @@ func (s *DataStore) GetTeamsWithInstallerByHash(ctx context.Context, sha256 stri return s.GetTeamsWithInstallerByHashFunc(ctx, sha256, url) } +func (s *DataStore) GetInstallerByTeamAndURL(ctx context.Context, teamID uint, url string) (*fleet.ExistingSoftwareInstaller, error) { + s.mu.Lock() + s.GetInstallerByTeamAndURLFuncInvoked = true + s.mu.Unlock() + return s.GetInstallerByTeamAndURLFunc(ctx, teamID, url) +} + func (s *DataStore) TeamIDsWithSetupExperienceIdPEnabled(ctx context.Context) ([]uint, error) { s.mu.Lock() s.TeamIDsWithSetupExperienceIdPEnabledFuncInvoked = true diff --git a/server/service/client.go b/server/service/client.go index d71a7f25d2..ba1bbbff74 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -1174,6 +1174,10 @@ func validateTeamOrNoTeamMacOSSetupSoftware(teamName string, macOSSetupSoftware func buildSoftwarePackagesPayload(specs []fleet.SoftwarePackageSpec, installDuringSetupKeys map[fleet.MacOSSetupSoftware]struct{}) ([]fleet.SoftwareInstallerPayload, error) { softwarePayloads := make([]fleet.SoftwareInstallerPayload, len(specs)) for i, si := range specs { + if si.AlwaysDownload && si.SHA256 != "" { + return nil, errors.New("Couldn't edit software. The 'always_download' option cannot be used with 'hash_sha256'.") + } + var qc string var err error @@ -1304,6 +1308,7 @@ func buildSoftwarePackagesPayload(specs []fleet.SoftwarePackageSpec, installDuri DisplayName: si.DisplayName, IconPath: si.Icon.Path, IconHash: iconHash, + AlwaysDownload: si.AlwaysDownload, } if si.Slug != nil { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index de0cfe42f8..2953e914d1 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -14459,6 +14459,212 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersSideEffec s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/%s/results", installUUID), nil, http.StatusNotFound, &installDetailsResp) } +func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersConditionalDownload() { + t := s.T() + + tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{ + Name: t.Name(), + Description: "desc", + }) + require.NoError(t, err) + + var ( + mu sync.Mutex + requestCount int + ifNoneMatchSet = []string{} + etag = `"ruby-deb-v1"` + trailer = "" // appended to the body to force a different content hash + ) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + requestCount++ + ifNoneMatchSet = append(ifNoneMatchSet, r.Header.Get("If-None-Match")) + currentETag := etag + currentTrailer := trailer + mu.Unlock() + + // Honor the conditional request: if the client sends our current + // ETag, return 304 with no body. + if r.Header.Get("If-None-Match") == currentETag { + w.Header().Set("ETag", currentETag) + w.WriteHeader(http.StatusNotModified) + return + } + + file, err := os.Open(filepath.Join("testdata", "software-installers", "ruby.deb")) + if !assert.NoError(t, err) { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + w.Header().Set("Content-Type", "application/vnd.debian.binary-package") + w.Header().Set("ETag", currentETag) + _, err = io.Copy(w, file) + assert.NoError(t, err) + _, err = w.Write([]byte(currentTrailer)) + assert.NoError(t, err) + }) + srv := httptest.NewServer(handler) + t.Cleanup(srv.Close) + + // First run: full download, Fleet should capture the ETag and storage_id. + softwareToInstall := []*fleet.SoftwareInstallerPayload{{URL: srv.URL}} + var batchResponse batchSetSoftwareInstallersResponse + s.DoJSON("POST", "/api/latest/fleet/software/batch", + batchSetSoftwareInstallersRequest{Software: softwareToInstall}, + http.StatusAccepted, &batchResponse, "team_name", tm.Name) + packages := waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, tm.Name, batchResponse.RequestUUID) + require.Len(t, packages, 1) + firstHash := packages[0].HashSHA256 + require.NotEmpty(t, firstHash) + + titlesResp := listSoftwareTitlesResponse{} + s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &titlesResp, + "available_for_install", "true", "team_id", fmt.Sprint(tm.ID)) + require.Len(t, titlesResp.SoftwareTitles, 1) + titleID := titlesResp.SoftwareTitles[0].ID + + titleResp := getSoftwareTitleResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, + "team_id", fmt.Sprint(tm.ID)) + firstUploadedAt := titleResp.SoftwareTitle.SoftwarePackage.UploadedAt + + mu.Lock() + require.Equal(t, 1, requestCount, "expected exactly one HTTP request on first run") + require.Empty(t, ifNoneMatchSet[0], "first run must not send If-None-Match") + mu.Unlock() + + // Second run: same URL, no changes. The server will return 304 and Fleet + // should reuse the cached installer (uploaded_at must not change). + s.DoJSON("POST", "/api/latest/fleet/software/batch", + batchSetSoftwareInstallersRequest{Software: softwareToInstall}, + http.StatusAccepted, &batchResponse, "team_name", tm.Name) + packages = waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, tm.Name, batchResponse.RequestUUID) + require.Len(t, packages, 1) + require.Equal(t, firstHash, packages[0].HashSHA256, "hash must match on cache hit") + + titleResp = getSoftwareTitleResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, + "team_id", fmt.Sprint(tm.ID)) + require.Equal(t, firstUploadedAt, titleResp.SoftwareTitle.SoftwarePackage.UploadedAt, + "uploaded_at must not change on 304 cache hit") + + mu.Lock() + require.Equal(t, 2, requestCount, "expected one additional HTTP request on second run") + require.Equal(t, etag, ifNoneMatchSet[1], "second run must send If-None-Match with stored ETag") + mu.Unlock() + + // Third run: server will again respond 304, but the user has supplied an + // install_script that fails validation (exceeds SavedScriptMaxRuneLen). + // The 304 fast-path must still run script validation and reject the batch. + oversizeScript := strings.Repeat("a", fleet.SavedScriptMaxRuneLen+1) + withBadScript := []*fleet.SoftwareInstallerPayload{{URL: srv.URL, InstallScript: oversizeScript}} + s.DoJSON("POST", "/api/latest/fleet/software/batch", + batchSetSoftwareInstallersRequest{Software: withBadScript}, + http.StatusAccepted, &batchResponse, "team_name", tm.Name) + msg := waitBatchSetSoftwareInstallersFailed(t, &s.withServer, tm.Name, batchResponse.RequestUUID) + require.Contains(t, msg, "install script") + + // Fourth run: always_download bypasses conditional download, so the server + // should receive a request with no If-None-Match header and return the full + // body. + withAlwaysDownload := []*fleet.SoftwareInstallerPayload{{URL: srv.URL, AlwaysDownload: true}} + s.DoJSON("POST", "/api/latest/fleet/software/batch", + batchSetSoftwareInstallersRequest{Software: withAlwaysDownload}, + http.StatusAccepted, &batchResponse, "team_name", tm.Name) + _ = waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, tm.Name, batchResponse.RequestUUID) + + mu.Lock() + require.GreaterOrEqual(t, requestCount, 4, "always_download must issue an HTTP request") + require.Empty(t, ifNoneMatchSet[len(ifNoneMatchSet)-1], "always_download must not send If-None-Match") + mu.Unlock() + + // Fifth run: flip always_download back off. The ETag captured during the + // previous always_download run must still be usable, so this run should + // immediately go back to sending If-None-Match and receiving 304 — no + // "warm-up" download required. + titleResp = getSoftwareTitleResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, + "team_id", fmt.Sprint(tm.ID)) + uploadedAtBeforeConditional := titleResp.SoftwareTitle.SoftwarePackage.UploadedAt + + mu.Lock() + countBeforeConditional := requestCount + mu.Unlock() + + s.DoJSON("POST", "/api/latest/fleet/software/batch", + batchSetSoftwareInstallersRequest{Software: softwareToInstall}, + http.StatusAccepted, &batchResponse, "team_name", tm.Name) + packages = waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, tm.Name, batchResponse.RequestUUID) + require.Len(t, packages, 1) + + titleResp = getSoftwareTitleResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, + "team_id", fmt.Sprint(tm.ID)) + require.Equal(t, uploadedAtBeforeConditional, titleResp.SoftwareTitle.SoftwarePackage.UploadedAt, + "uploaded_at must not change when ETag from prior always_download run is reused") + + mu.Lock() + require.Equal(t, countBeforeConditional+1, requestCount, "expected one HTTP request after re-enabling conditional download") + require.Equal(t, `"ruby-deb-v1"`, ifNoneMatchSet[countBeforeConditional], + "must send the ETag captured during the previous always_download run") + mu.Unlock() + + // Sixth run: upstream content has changed — new ETag and modified body. + // Fleet still sends the stored ETag (v1) as If-None-Match; the server + // returns 200 with the new bytes and new ETag. Fleet must re-upload, bump + // uploaded_at, and persist the new ETag. + const newETag = `"ruby-deb-v2"` + mu.Lock() + etag = newETag + trailer = " " // changes the body hash + countBeforeContentChange := requestCount + mu.Unlock() + + titleResp = getSoftwareTitleResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, + "team_id", fmt.Sprint(tm.ID)) + uploadedAtBeforeContentChange := titleResp.SoftwareTitle.SoftwarePackage.UploadedAt + hashBeforeContentChange := titleResp.SoftwareTitle.SoftwarePackage.StorageID + + s.DoJSON("POST", "/api/latest/fleet/software/batch", + batchSetSoftwareInstallersRequest{Software: softwareToInstall}, + http.StatusAccepted, &batchResponse, "team_name", tm.Name) + packages = waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, tm.Name, batchResponse.RequestUUID) + require.Len(t, packages, 1) + require.NotEqual(t, hashBeforeContentChange, packages[0].HashSHA256, "hash must change when upstream content changes") + + titleResp = getSoftwareTitleResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, + "team_id", fmt.Sprint(tm.ID)) + require.NotEqual(t, uploadedAtBeforeContentChange, titleResp.SoftwareTitle.SoftwarePackage.UploadedAt, + "uploaded_at must advance when bytes are re-downloaded") + + mu.Lock() + require.Equal(t, countBeforeContentChange+1, requestCount, "expected one HTTP request on sixth run") + require.Equal(t, `"ruby-deb-v1"`, ifNoneMatchSet[countBeforeContentChange], + "sixth run must send the previously stored ETag (v1) so the server can detect the mismatch") + mu.Unlock() + + // Seventh run: no further content change. Fleet should now be sending the + // new ETag (v2) it captured from run 6, and the server should reply 304. + mu.Lock() + countBeforeFinalRun := requestCount + mu.Unlock() + + s.DoJSON("POST", "/api/latest/fleet/software/batch", + batchSetSoftwareInstallersRequest{Software: softwareToInstall}, + http.StatusAccepted, &batchResponse, "team_name", tm.Name) + packages = waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, tm.Name, batchResponse.RequestUUID) + require.Len(t, packages, 1) + + mu.Lock() + require.Equal(t, countBeforeFinalRun+1, requestCount, "expected one HTTP request on seventh run") + require.Equal(t, newETag, ifNoneMatchSet[countBeforeFinalRun], + "seventh run must send the new ETag captured from the content-change run") + mu.Unlock() +} + func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersWithPoliciesAssociated() { ctx := context.Background() t := s.T() diff --git a/server/service/software_installers_test.go b/server/service/software_installers_test.go index 625569fd07..5eaf5d25ec 100644 --- a/server/service/software_installers_test.go +++ b/server/service/software_installers_test.go @@ -627,6 +627,10 @@ func TestSoftwareInstallerUploadRetries(t *testing.T) { return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil } + ds.GetInstallerByTeamAndURLFunc = func(ctx context.Context, teamID uint, url string) (*fleet.ExistingSoftwareInstaller, error) { + return nil, nil + } + ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) { return []fleet.SoftwarePackageResponse{}, nil } diff --git a/tools/cloner-check/generated_files/teamconfig.txt b/tools/cloner-check/generated_files/teamconfig.txt index c773a2a395..8924b8d0d8 100644 --- a/tools/cloner-check/generated_files/teamconfig.txt +++ b/tools/cloner-check/generated_files/teamconfig.txt @@ -119,6 +119,7 @@ github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec ReferencedYamlPath github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec SHA256 string github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec Categories []string github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec DisplayName string +github.com/fleetdm/fleet/v4/server/fleet/SoftwarePackageSpec AlwaysDownload bool github.com/fleetdm/fleet/v4/server/fleet/SoftwareSpec FleetMaintainedApps optjson.Slice[github.com/fleetdm/fleet/v4/server/fleet.MaintainedAppSpec] github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MaintainedAppSpec] Set bool github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MaintainedAppSpec] Valid bool