mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add prebuild invalidation via last_invalidated_at timestamp (#20582)
Updates #17917
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user