Skip to content

Commit 52f9b36

Browse files
Add installation deletion scheduling (#1110)
This change adds optional installation deletion scheduling logic to the provisioner. Installations can now be created with a configurable TTL which can be useful for quick testing and other use-cases. Installations with scheduled deletion will only be deleted if they are not deletion-locked and the current time is past their scheduled deletion time. Deletion-locked installations will be deleted once their lock is removed if the scheduled deletion time has passed. This is completely separate from the existing deleting pending logic which allows for specifying how long an installation remains in a deletion-pending state after it was scheduled for deletion.
1 parent f946052 commit 52f9b36

File tree

12 files changed

+600
-15
lines changed

12 files changed

+600
-15
lines changed

cmd/cloud/installation.go

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func newCmdInstallation() *cobra.Command {
3737
cmd.AddCommand(newCmdInstallationVolumeDelete())
3838
cmd.AddCommand(newCmdInstallationUpdateDeletion())
3939
cmd.AddCommand(newCmdInstallationCancelDeletion())
40+
cmd.AddCommand(newCmdInstallationScheduleDeletion())
4041
cmd.AddCommand(newCmdInstallationHibernate())
4142
cmd.AddCommand(newCmdInstallationWakeup())
4243
cmd.AddCommand(newCmdInstallationGet())
@@ -78,11 +79,16 @@ func executeInstallationCreateCmd(ctx context.Context, flags installationCreateF
7879

7980
envVarMap, err := parseEnvVarInput(flags.mattermostEnv, false)
8081
if err != nil {
81-
return err
82+
return errors.Wrap(err, "failed to parse env var input")
8283
}
8384
priorityEnvVarMap, err := parseEnvVarInput(flags.priorityEnv, false)
8485
if err != nil {
85-
return err
86+
return errors.Wrap(err, "failed to parse priority env var input")
87+
}
88+
89+
var scheduledDeletionTime int64
90+
if flags.scheduledDeletionTime > 0 {
91+
scheduledDeletionTime = model.GetMillisAtTime(time.Now().Add(flags.scheduledDeletionTime))
8692
}
8793

8894
request := &model.CreateInstallationRequest{
@@ -100,6 +106,7 @@ func executeInstallationCreateCmd(ctx context.Context, flags installationCreateF
100106
PriorityEnv: priorityEnvVarMap,
101107
Annotations: flags.annotations,
102108
GroupSelectionAnnotations: flags.groupSelectionAnnotations,
109+
ScheduledDeletionTime: scheduledDeletionTime,
103110
}
104111

105112
// For CLI to be backward compatible, if only one DNS is passed we use
@@ -359,6 +366,46 @@ func newCmdInstallationCancelDeletion() *cobra.Command {
359366
return cmd
360367
}
361368

369+
func newCmdInstallationScheduleDeletion() *cobra.Command {
370+
var flags installationScheduledDeletionFlags
371+
372+
cmd := &cobra.Command{
373+
Use: "schedule-deletion",
374+
Short: "Schedule an installation for future deletion.",
375+
RunE: func(command *cobra.Command, args []string) error {
376+
command.SilenceUsage = true
377+
client := createClient(command.Context(), flags.clusterFlags)
378+
379+
request := &model.PatchInstallationScheduledDeletionRequest{}
380+
if flags.scheduledDeletionTimeChanged {
381+
var scheduledTimeMillis int64
382+
if flags.scheduledDeletionTime > 0 {
383+
scheduledTimeMillis = model.GetMillisAtTime(time.Now().Add(flags.scheduledDeletionTime))
384+
}
385+
request.ScheduledDeletionTime = &scheduledTimeMillis
386+
}
387+
388+
if flags.dryRun {
389+
return runDryRun(request)
390+
}
391+
392+
installation, err := client.UpdateInstallationScheduledDeletion(flags.installationID, request)
393+
if err != nil {
394+
return errors.Wrap(err, "failed to update installation scheduled deletion")
395+
}
396+
397+
return printJSON(installation)
398+
},
399+
PreRun: func(cmd *cobra.Command, args []string) {
400+
flags.clusterFlags.addFlags(cmd)
401+
flags.installationScheduledDeletionRequestOptionsChanged.addFlags(cmd)
402+
},
403+
}
404+
flags.addFlags(cmd)
405+
406+
return cmd
407+
}
408+
362409
func newCmdInstallationHibernate() *cobra.Command {
363410
var flags installationHibernateFlags
364411

@@ -802,7 +849,12 @@ func executeInstallationDeploymentReportCmd(ctx context.Context, flags installat
802849
case model.InstallationStateDeleted:
803850
output += fmt.Sprintf(" │ └ Deleted: %s\n", installation.DeletionDateString())
804851
case model.InstallationStateDeletionPending:
805-
output += fmt.Sprintf(" │ └ Scheduled Deletion: %s\n", installation.DeletionPendingExpiryCompleteTimeString())
852+
output += fmt.Sprintf(" │ └ Deletion Pending Expiry: %s\n", installation.DeletionPendingExpiryCompleteTimeString())
853+
default:
854+
scheduledDeletion := installation.ScheculedDeletionCompleteTimeString()
855+
if scheduledDeletion != "n/a" {
856+
output += fmt.Sprintf(" │ └ Scheduled Deletion: %s\n", scheduledDeletion)
857+
}
806858
}
807859
output += fmt.Sprintf(" ├ DNS: %s (primary)\n", installation.DNS) //nolint
808860
if len(installation.DNSRecords) > 1 {

cmd/cloud/installation_flag.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type installationCreateRequestOptions struct {
2727
priorityEnv []string
2828
annotations []string
2929
groupSelectionAnnotations []string
30+
scheduledDeletionTime time.Duration
3031
}
3132

3233
func (flags *installationCreateRequestOptions) addFlags(command *cobra.Command) {
@@ -44,6 +45,7 @@ func (flags *installationCreateRequestOptions) addFlags(command *cobra.Command)
4445
command.Flags().StringArrayVar(&flags.priorityEnv, "priority-env", []string{}, "Env vars to add to the Mattermost App that take priority over group config. Accepts format: KEY_NAME=VALUE. Use the flag multiple times to set multiple env vars.")
4546
command.Flags().StringArrayVar(&flags.annotations, "annotation", []string{}, "Additional annotations for the installation. Accepts multiple values, for example: '... --annotation abc --annotation def'")
4647
command.Flags().StringArrayVar(&flags.groupSelectionAnnotations, "group-selection-annotation", []string{}, "Annotations for automatic group selection. Accepts multiple values, for example: '... --group-selection-annotation abc --group-selection-annotation def'")
48+
command.Flags().DurationVar(&flags.scheduledDeletionTime, "scheduled-deletion-time", 0, "The time from now when the installation should be deleted. Use 0 for no scheduled deletion.")
4749

4850
_ = command.MarkFlagRequired("owner")
4951
}
@@ -524,3 +526,32 @@ type installationDeletionReportFlags struct {
524526
func (flags *installationDeletionReportFlags) addFlags(command *cobra.Command) {
525527
command.Flags().IntVar(&flags.days, "days", 7, "The number of days include in the deletion report.")
526528
}
529+
530+
type installationScheduledDeletionRequestOptions struct {
531+
scheduledDeletionTime time.Duration
532+
}
533+
534+
func (flags *installationScheduledDeletionRequestOptions) addFlags(command *cobra.Command) {
535+
command.Flags().DurationVar(&flags.scheduledDeletionTime, "scheduled-deletion-time", 0, "The time from now when the installation should be deleted. Use 0 to cancel scheduled deletion.")
536+
}
537+
538+
type installationScheduledDeletionRequestOptionsChanged struct {
539+
scheduledDeletionTimeChanged bool
540+
}
541+
542+
func (flags *installationScheduledDeletionRequestOptionsChanged) addFlags(command *cobra.Command) {
543+
flags.scheduledDeletionTimeChanged = command.Flags().Changed("scheduled-deletion-time")
544+
}
545+
546+
type installationScheduledDeletionFlags struct {
547+
clusterFlags
548+
installationScheduledDeletionRequestOptions
549+
installationScheduledDeletionRequestOptionsChanged
550+
installationID string
551+
}
552+
553+
func (flags *installationScheduledDeletionFlags) addFlags(command *cobra.Command) {
554+
flags.installationScheduledDeletionRequestOptions.addFlags(command)
555+
command.Flags().StringVar(&flags.installationID, "installation", "", "The id of the installation to schedule deletion for.")
556+
_ = command.MarkFlagRequired("installation")
557+
}

internal/api/installation.go

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/json"
99
"fmt"
1010
"net/http"
11+
"slices"
1112
"time"
1213

1314
"github.com/mattermost/mattermost-cloud/internal/common"
@@ -52,6 +53,7 @@ func initInstallation(apiRouter *mux.Router, context *Context) {
5253
installationRouter.Handle("", addContext(handleDeleteInstallation)).Methods("DELETE")
5354
installationRouter.Handle("/deletion", addContext(handleUpdateInstallationDeletion)).Methods("PUT")
5455
installationRouter.Handle("/deletion/cancel", addContext(handleCancelInstallationDeletion)).Methods("POST")
56+
installationRouter.Handle("/deletion/schedule", addContext(handleUpdateInstallationScheduledDeletion)).Methods("PUT")
5557
installationRouter.Handle("/annotations", addContext(handleAddInstallationAnnotations)).Methods("POST")
5658
installationRouter.Handle("/annotation/{annotation-name}", addContext(handleDeleteInstallationAnnotation)).Methods("DELETE")
5759

@@ -238,6 +240,7 @@ func handleCreateInstallation(c *Context, w http.ResponseWriter, r *http.Request
238240
APISecurityLock: createInstallationRequest.APISecurityLock,
239241
MattermostEnv: createInstallationRequest.MattermostEnv,
240242
PriorityEnv: createInstallationRequest.PriorityEnv,
243+
ScheduledDeletionTime: createInstallationRequest.ScheduledDeletionTime,
241244
SingleTenantDatabaseConfig: createInstallationRequest.SingleTenantDatabaseConfig.ToDBConfig(createInstallationRequest.Database),
242245
ExternalDatabaseConfig: createInstallationRequest.ExternalDatabaseConfig.ToDBConfig(createInstallationRequest.Database),
243246
CRVersion: model.DefaultCRVersion,
@@ -957,9 +960,17 @@ func handleCancelInstallationDeletion(c *Context, w http.ResponseWriter, r *http
957960
}
958961
defer unlockOnce()
959962

960-
err := updateInstallationState(c, installationDTO, newState)
963+
// If the installation is scheduled for deletion, cancel the scheduled
964+
// deletion so that it isn't immediately deleted again.
965+
if installationDTO.ScheduledDeletionTime != 0 {
966+
c.Logger.Debug("Removing scheduled deletion time as part of deletion cancellation")
967+
installationDTO.ScheduledDeletionTime = 0
968+
}
969+
installationDTO.State = newState
970+
971+
err := c.Store.UpdateInstallation(installationDTO.Installation)
961972
if err != nil {
962-
c.Logger.WithError(err).Errorf("failed to update installation state to %q", newState)
973+
c.Logger.WithError(err).Error("failed to update installation")
963974
w.WriteHeader(http.StatusInternalServerError)
964975
return
965976
}
@@ -970,6 +981,53 @@ func handleCancelInstallationDeletion(c *Context, w http.ResponseWriter, r *http
970981
w.WriteHeader(http.StatusAccepted)
971982
}
972983

984+
// handleUpdateInstallationScheduledDeletion responds to PUT /api/installation/{installation}/deletion/schedule,
985+
// updating the scheduled deletion time of an installation.
986+
func handleUpdateInstallationScheduledDeletion(c *Context, w http.ResponseWriter, r *http.Request) {
987+
vars := mux.Vars(r)
988+
installationID := vars["installation"]
989+
c.Logger = c.Logger.WithField("installation", installationID)
990+
991+
patchRequest, err := model.NewPatchInstallationScheduledDeletionRequestFromReader(r.Body)
992+
if err != nil {
993+
c.Logger.WithError(err).Error("failed to decode request")
994+
w.WriteHeader(http.StatusBadRequest)
995+
return
996+
}
997+
998+
installationDTO, status, unlockOnce := lockInstallation(c, installationID)
999+
if status != 0 {
1000+
w.WriteHeader(status)
1001+
return
1002+
}
1003+
defer unlockOnce()
1004+
1005+
if installationDTO.APISecurityLock {
1006+
logSecurityLockConflict("installation", c.Logger)
1007+
w.WriteHeader(http.StatusForbidden)
1008+
return
1009+
}
1010+
1011+
if slices.Contains(model.AllInstallationDeletionStates, installationDTO.State) {
1012+
c.Logger.Warnf("installation is in a deletion state, cannot update scheduled deletion time")
1013+
w.WriteHeader(http.StatusBadRequest)
1014+
return
1015+
}
1016+
1017+
if patchRequest.Apply(installationDTO.Installation) {
1018+
err := c.Store.UpdateInstallation(installationDTO.Installation)
1019+
if err != nil {
1020+
c.Logger.WithError(err).Error("failed to update installation")
1021+
w.WriteHeader(http.StatusInternalServerError)
1022+
return
1023+
}
1024+
}
1025+
1026+
w.Header().Set("Content-Type", "application/json")
1027+
w.WriteHeader(http.StatusOK)
1028+
outputJSON(c, w, installationDTO)
1029+
}
1030+
9731031
// handleAddInstallationAnnotations responds to POST /api/installation/{installation}/annotations,
9741032
// adds the set of annotations to the Installation.
9751033
func handleAddInstallationAnnotations(c *Context, w http.ResponseWriter, r *http.Request) {

internal/store/installation.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func init() {
3030
"Installation.CreateAt", "Installation.DeleteAt",
3131
"Installation.DeletionPendingExpiry", "APISecurityLock", "LockAcquiredBy",
3232
"LockAcquiredAt", "CRVersion", "Installation.DeletionLocked",
33-
"AllowedIPRanges", "Volumes",
33+
"AllowedIPRanges", "Volumes", "ScheduledDeletionTime",
3434
).From(installationTable)
3535
}
3636

@@ -346,6 +346,44 @@ func (sqlStore *SQLStore) GetUnlockedInstallationsPendingDeletion() ([]*model.In
346346
return installations, nil
347347
}
348348

349+
// GetUnlockedInstallationsWithScheduledDeletion returns unlocked installations
350+
// that are scheduled for deletion.
351+
func (sqlStore *SQLStore) GetUnlockedInstallationsWithScheduledDeletion() ([]*model.Installation, error) {
352+
builder := installationSelect.
353+
Where(sq.Eq{
354+
"State": []string{model.InstallationStateStable, model.InstallationStateHibernating},
355+
}).
356+
Where("DeletionLocked = false").
357+
Where("ScheduledDeletionTime > 0").
358+
Where("LockAcquiredAt = 0").
359+
OrderBy("CreateAt ASC")
360+
361+
var rawInstallationsOutput rawInstallations
362+
err := sqlStore.selectBuilder(sqlStore.db, &rawInstallationsOutput, builder)
363+
if err != nil {
364+
return nil, errors.Wrap(err, "failed to get installations pending deletion")
365+
}
366+
367+
installations, err := rawInstallationsOutput.toInstallations()
368+
if err != nil {
369+
return nil, err
370+
}
371+
372+
for _, installation := range installations {
373+
if !installation.IsInGroup() {
374+
continue
375+
}
376+
377+
group, err := sqlStore.GetGroup(*installation.GroupID)
378+
if err != nil {
379+
return nil, err
380+
}
381+
installation.MergeWithGroup(group, false)
382+
}
383+
384+
return installations, nil
385+
}
386+
349387
// GetSingleTenantDatabaseConfigForInstallation fetches single tenant database configuration
350388
// for specified installation.
351389
func (sqlStore *SQLStore) GetSingleTenantDatabaseConfigForInstallation(installationID string) (*model.SingleTenantDatabaseConfig, error) {
@@ -449,6 +487,7 @@ func (sqlStore *SQLStore) createInstallation(db execer, installation *model.Inst
449487
"CreateAt": installation.CreateAt,
450488
"DeleteAt": 0,
451489
"DeletionPendingExpiry": 0,
490+
"ScheduledDeletionTime": installation.ScheduledDeletionTime,
452491
"APISecurityLock": installation.APISecurityLock,
453492
"LockAcquiredBy": nil,
454493
"LockAcquiredAt": 0,
@@ -530,6 +569,7 @@ func (sqlStore *SQLStore) updateInstallation(db execer, installation *model.Inst
530569
"State": installation.State,
531570
"CRVersion": installation.CRVersion,
532571
"DeletionPendingExpiry": installation.DeletionPendingExpiry,
572+
"ScheduledDeletionTime": installation.ScheduledDeletionTime,
533573
"AllowedIPRanges": installation.AllowedIPRanges,
534574
"Volumes": installation.Volumes,
535575
}).

internal/store/migrations.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2253,6 +2253,22 @@ var migrations = []migration{
22532253
return err
22542254
}
22552255

2256+
return nil
2257+
}},
2258+
{semver.MustParse("0.50.0"), semver.MustParse("0.51.0"), func(e execer) error {
2259+
_, err := e.Exec(`
2260+
ALTER TABLE Installation
2261+
ADD COLUMN ScheduledDeletionTime BIGINT NOT NULL DEFAULT '0';
2262+
`)
2263+
if err != nil {
2264+
return errors.Wrap(err, "failed to create ScheduledDeletionTime column")
2265+
}
2266+
2267+
_, err = e.Exec("ALTER TABLE Installation ALTER COLUMN ScheduledDeletionTime SET NOT NULL;")
2268+
if err != nil {
2269+
return errors.Wrap(err, "failed to remove not null constraint")
2270+
}
2271+
22562272
return nil
22572273
}},
22582274
}

0 commit comments

Comments
 (0)