feat: add prebuild invalidation via last_invalidated_at timestamp (#20582)

Updates #17917
This commit is contained in:
Marcin Tojek
2025-11-20 17:12:25 +01:00
committed by GitHub
parent c2319e5b4e
commit d004710a74
27 changed files with 637 additions and 23 deletions
+9
View File
@@ -458,6 +458,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Get("/", api.templateACL)
r.Patch("/", api.patchTemplateACL)
})
r.Route("/templates/{template}/prebuilds", func(r chi.Router) {
r.Use(
api.templateRBACEnabledMW,
apiKeyMiddleware,
httpmw.ExtractTemplateParam(api.Database),
)
r.Post("/invalidate", api.postInvalidateTemplatePresets)
})
r.Route("/groups", func(r chi.Router) {
r.Use(
api.templateRBACEnabledMW,
+44
View File
@@ -8,6 +8,8 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
@@ -338,3 +340,45 @@ func (api *API) RequireFeatureMW(feat codersdk.FeatureName) func(http.Handler) h
})
}
}
// @Summary Invalidate presets for template
// @ID invalidate-presets-for-template
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Param template path string true "Template ID" format(uuid)
// @Success 200 {object} codersdk.InvalidatePresetsResponse
// @Router /templates/{template}/prebuilds/invalidate [post]
func (api *API) postInvalidateTemplatePresets(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
template := httpmw.TemplateParam(r)
// Authorization: user must be able to update the template
if !api.Authorize(r, policy.ActionUpdate, template) {
httpapi.ResourceNotFound(rw)
return
}
// Update last_invalidated_at for all presets of the active template version
invalidatedPresets, err := api.Database.UpdatePresetsLastInvalidatedAt(ctx, database.UpdatePresetsLastInvalidatedAtParams{
TemplateID: template.ID,
LastInvalidatedAt: sql.NullTime{Time: api.Clock.Now(), Valid: true},
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to invalidate presets.",
Detail: err.Error(),
})
return
}
api.Logger.Info(ctx, "invalidated presets",
slog.F("template_id", template.ID),
slog.F("template_name", template.Name),
slog.F("preset_count", len(invalidatedPresets)),
)
httpapi.Write(ctx, rw, http.StatusOK, codersdk.InvalidatePresetsResponse{
Invalidated: db2sdk.InvalidatedPresets(invalidatedPresets),
})
}
+97
View File
@@ -2111,3 +2111,100 @@ func TestMultipleOrganizationTemplates(t *testing.T) {
t.FailNow()
}
}
func TestInvalidateTemplatePrebuilds(t *testing.T) {
t.Parallel()
// Given the following parameters and presets...
templateVersionParameters := []*proto.RichParameter{
{Name: "param1", Type: "string", Required: false, DefaultValue: "default1"},
{Name: "param2", Type: "string", Required: false, DefaultValue: "default2"},
{Name: "param3", Type: "string", Required: false, DefaultValue: "default3"},
}
presetWithParameters1 := &proto.Preset{
Name: "Preset With Parameters 1",
Parameters: []*proto.PresetParameter{
{Name: "param1", Value: "value1"},
{Name: "param2", Value: "value2"},
{Name: "param3", Value: "value3"},
},
}
presetWithParameters2 := &proto.Preset{
Name: "Preset With Parameters 2",
Parameters: []*proto.PresetParameter{
{Name: "param1", Value: "value4"},
{Name: "param2", Value: "value5"},
{Name: "param3", Value: "value6"},
},
}
presetWithParameters3 := &proto.Preset{
Name: "Preset With Parameters 3",
Parameters: []*proto.PresetParameter{
{Name: "param1", Value: "value7"},
{Name: "param2", Value: "value8"},
{Name: "param3", Value: "value9"},
},
}
// Given the template versions and template...
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
IncludeProvisionerDaemon: true,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureTemplateRBAC: 1,
},
},
})
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
buildPlanResponse := func(presets ...*proto.Preset) *proto.Response {
return &proto.Response{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Presets: presets,
Parameters: templateVersionParameters,
},
},
}
}
version1 := coderdtest.CreateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{buildPlanResponse(presetWithParameters1, presetWithParameters2)},
ProvisionApply: echo.ApplyComplete,
})
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version1.ID)
template := coderdtest.CreateTemplate(t, templateAdminClient, owner.OrganizationID, version1.ID)
// When
ctx := testutil.Context(t, testutil.WaitLong)
invalidated, err := templateAdminClient.InvalidateTemplatePresets(ctx, template.ID)
require.NoError(t, err)
// Then
require.Len(t, invalidated.Invalidated, 2)
require.Equal(t, codersdk.InvalidatedPreset{TemplateName: template.Name, TemplateVersionName: version1.Name, PresetName: presetWithParameters1.Name}, invalidated.Invalidated[0])
require.Equal(t, codersdk.InvalidatedPreset{TemplateName: template.Name, TemplateVersionName: version1.Name, PresetName: presetWithParameters2.Name}, invalidated.Invalidated[1])
// Given the template is updated...
version2 := coderdtest.UpdateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{buildPlanResponse(presetWithParameters2, presetWithParameters3)},
ProvisionApply: echo.ApplyComplete,
}, template.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version2.ID)
err = templateAdminClient.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{ID: version2.ID})
require.NoError(t, err)
// When
invalidated, err = templateAdminClient.InvalidateTemplatePresets(ctx, template.ID)
require.NoError(t, err)
// Then: it should only invalidate the presets from the currently active version (preset2 and preset3)
require.Len(t, invalidated.Invalidated, 2)
require.Equal(t, codersdk.InvalidatedPreset{TemplateName: template.Name, TemplateVersionName: version2.Name, PresetName: presetWithParameters2.Name}, invalidated.Invalidated[0])
require.Equal(t, codersdk.InvalidatedPreset{TemplateName: template.Name, TemplateVersionName: version2.Name, PresetName: presetWithParameters3.Name}, invalidated.Invalidated[1])
}