-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[ws-manager-mk2] Support workspace snapshots #16471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
eae17ef
1a88ec1
d45ff92
d3e8aa2
c2a20e7
f7e231b
e21000c
cbc8c59
0d10ba0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
// Copyright (c) 2022 Gitpod GmbH. All rights reserved. | ||
// Licensed under the GNU Affero General Public License (AGPL). | ||
// See License-AGPL.txt in the project root for license information. | ||
|
||
package controller | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"k8s.io/client-go/util/retry" | ||
ctrl "sigs.k8s.io/controller-runtime" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
"sigs.k8s.io/controller-runtime/pkg/event" | ||
"sigs.k8s.io/controller-runtime/pkg/log" | ||
"sigs.k8s.io/controller-runtime/pkg/predicate" | ||
|
||
workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1" | ||
) | ||
|
||
// SnapshotReconciler reconciles a Snapshot object | ||
type SnapshotReconciler struct { | ||
client.Client | ||
nodeName string | ||
operations *WorkspaceOperations | ||
} | ||
|
||
func NewSnapshotController(c client.Client, nodeName string, wso *WorkspaceOperations) *SnapshotReconciler { | ||
return &SnapshotReconciler{ | ||
Client: c, | ||
nodeName: nodeName, | ||
operations: wso, | ||
} | ||
} | ||
|
||
// SetupWithManager sets up the controller with the Manager. | ||
func (r *SnapshotReconciler) SetupWithManager(mgr ctrl.Manager) error { | ||
return ctrl.NewControllerManagedBy(mgr). | ||
Named("snapshot"). | ||
For(&workspacev1.Snapshot{}). | ||
WithEventFilter(snapshotEventFilter(r.nodeName)). | ||
kylos101 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Complete(r) | ||
} | ||
|
||
func snapshotEventFilter(nodeName string) predicate.Predicate { | ||
return predicate.Funcs{ | ||
CreateFunc: func(e event.CreateEvent) bool { | ||
if ss, ok := e.Object.(*workspacev1.Snapshot); ok { | ||
return ss.Spec.NodeName == nodeName | ||
} | ||
return false | ||
}, | ||
UpdateFunc: func(ue event.UpdateEvent) bool { | ||
return false | ||
}, | ||
DeleteFunc: func(de event.DeleteEvent) bool { | ||
return false | ||
}, | ||
} | ||
} | ||
|
||
//+kubebuilder:rbac:groups=workspace.gitpod.io,resources=snapshots,verbs=get;list;watch;create;update;patch;delete | ||
//+kubebuilder:rbac:groups=workspace.gitpod.io,resources=snapshots/status,verbs=get;update;patch | ||
//+kubebuilder:rbac:groups=workspace.gitpod.io,resources=snapshots/finalizers,verbs=update | ||
|
||
// Reconcile is part of the main kubernetes reconciliation loop which aims to | ||
// move the current state of the cluster closer to the desired state. | ||
// For more details, check Reconcile and its Result here: | ||
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile | ||
func (ssc *SnapshotReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { | ||
log := log.FromContext(ctx) | ||
|
||
var snapshot workspacev1.Snapshot | ||
if err := ssc.Client.Get(ctx, req.NamespacedName, &snapshot); err != nil { | ||
return ctrl.Result{}, client.IgnoreNotFound(err) | ||
} | ||
|
||
if snapshot.Status.Completed { | ||
return ctrl.Result{}, nil | ||
} | ||
|
||
snapshotURL, snapshotName, snapshotErr := ssc.operations.SnapshotIDs(snapshot.Spec.WorkspaceID) | ||
if snapshotErr != nil { | ||
return ctrl.Result{}, snapshotErr | ||
} | ||
|
||
err := retry.RetryOnConflict(retryParams, func() error { | ||
err := ssc.Client.Get(ctx, req.NamespacedName, &snapshot) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
snapshot.Status.URL = snapshotURL | ||
return ssc.Client.Status().Update(ctx, &snapshot) | ||
}) | ||
|
||
if err != nil { | ||
log.Error(err, "could not set snapshot url", "workspace", snapshot.Spec.WorkspaceID) | ||
return ctrl.Result{}, err | ||
} | ||
|
||
snapshotErr = ssc.operations.TakeSnapshot(ctx, snapshot.Spec.WorkspaceID, snapshotName) | ||
err = retry.RetryOnConflict(retryParams, func() error { | ||
err := ssc.Client.Get(ctx, req.NamespacedName, &snapshot) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
snapshot.Status.Completed = true | ||
if snapshotErr != nil { | ||
snapshot.Status.Error = fmt.Errorf("could not take snapshot: %w", snapshotErr).Error() | ||
} | ||
|
||
return ssc.Status().Update(ctx, &snapshot) | ||
}) | ||
|
||
if err != nil { | ||
log.Error(err, "could not set completion status for snapshot", "workspace", snapshot.Spec.WorkspaceID) | ||
} | ||
|
||
return ctrl.Result{}, err | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we delete the Snapshot resource somewhere after some time? Might make sense to set a completion timestamp, and then clean up any snapshots completed e.g. > 30 mins ago? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The owner of the snapshot is the workspace so the snapshot gets deleted when the workspace gets deleted (cascading delete) which I think is pretty nice. We can inspect the snapshots taken by a workspace while it is running and we ensure that the snapshot CR does not outlive the workspace so that we do not try to take a snapshot from a stopped workspace. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// Copyright (c) 2022 Gitpod GmbH. All rights reserved. | ||
// Licensed under the GNU Affero General Public License (AGPL). | ||
// See License-AGPL.txt in the project root for license information. | ||
|
||
package v1 | ||
|
||
import ( | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
) | ||
|
||
// SnapshotSpec defines the desired state of the snapshot | ||
type SnapshotSpec struct { | ||
// +kubebuilder:validation:Required | ||
NodeName string `json:"nodeName"` | ||
|
||
// +kubebuilder:validation:Required | ||
WorkspaceID string `json:"workspaceID"` | ||
} | ||
|
||
// SnapshotStatus defines the observed state of the snapshot | ||
type SnapshotStatus struct { | ||
// // +kubebuilder:validation:Optional | ||
// Conditions []metav1.Condition `json:"conditions"` | ||
|
||
// Erorr is the error observed during snapshot creation if any | ||
// +kubebuilder:validation:Optional | ||
Error string `json:"error,omitempty"` | ||
|
||
// URL contains the url of the snapshot | ||
// +kubebuilder:validation:Optional | ||
URL string `json:"url,omitempty"` | ||
|
||
// Completed indicates if the snapshot operation has completed either by taking the snapshot or through failure | ||
// +kubebuilder:validation:Required | ||
Completed bool `json:"completed"` | ||
} | ||
|
||
//+kubebuilder:object:root=true | ||
//+kubebuilder:subresource:status | ||
//+kubebuilder:resource:shortName=snapshot | ||
// Custom print columns on the Custom Resource Definition. These are the columns | ||
// showing up when doing e.g. `kubectl get snapshots`. | ||
// Columns with priority > 0 will only show up with `-o wide`. | ||
//+kubebuilder:printcolumn:name="Workspace",type="string",JSONPath=".spec.workspaceID" | ||
//+kubebuilder:printcolumn:name="URL",type="string",JSONPath=".status.url",priority=10 | ||
//+kubebuilder:printcolumn:name="Completed",type="boolean",JSONPath=".status.completed" | ||
|
||
// Snapshot is the Schema for the snapshot API | ||
type Snapshot struct { | ||
metav1.TypeMeta `json:",inline"` | ||
metav1.ObjectMeta `json:"metadata,omitempty"` | ||
|
||
Spec SnapshotSpec `json:"spec,omitempty"` | ||
Status SnapshotStatus `json:"status,omitempty"` | ||
} | ||
|
||
//+kubebuilder:object:root=true | ||
|
||
// SnapshotList contains a list of Snapshots | ||
type SnapshotList struct { | ||
metav1.TypeMeta `json:",inline"` | ||
metav1.ListMeta `json:"metadata,omitempty"` | ||
Items []Snapshot `json:"items"` | ||
} | ||
|
||
func init() { | ||
SchemeBuilder.Register(&Snapshot{}, &SnapshotList{}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed this recently for the other controllers, lets explicitly name each controller, as the name shows up in the metrics and allows us to differentiate different controllers for the same resource. (By default the controller name is the name of the resource it owns)