Skip to content
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

KEP-20 part 2: trigger plans manually #1352

Merged
merged 13 commits into from
Mar 9, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 23 additions & 21 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,60 +60,57 @@ func main() {
log.Printf("KUDO Version: %#v", version.Get())

// create new controller-runtime manager

syncPeriod, err := parseSyncPeriod()
if err != nil {
log.Printf("unable to parse manager sync period variable: %v", err)
log.Printf("unable to parse manager sync period variable: %v", err)
zen-dog marked this conversation as resolved.
Show resolved Hide resolved
os.Exit(1)
}

if syncPeriod != nil {
log.Print(fmt.Sprintf("setting up manager, sync-period is %v", syncPeriod))
log.Print(fmt.Sprintf("⌛ Setting up manager, sync-period is %v:", syncPeriod))
zen-dog marked this conversation as resolved.
Show resolved Hide resolved
} else {
log.Print("setting up manager")
log.Print("🏝 Setting up manager")
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
CertDir: "/tmp/cert",
SyncPeriod: syncPeriod,
})
if err != nil {
log.Printf("unable to start manager: %v", err)
log.Printf("unable to start manager: %v", err)
os.Exit(1)
}

log.Print("Registering Components")
log.Print("Registering Components")

log.Print("setting up scheme")
if err := apis.AddToScheme(mgr.GetScheme()); err != nil {
log.Printf("unable to add APIs to scheme: %v", err)
log.Printf("unable to add APIs to scheme: %v", err)
}
log.Print("💎 Scheme initialization")

if err := apiextenstionsv1beta1.AddToScheme(mgr.GetScheme()); err != nil {
log.Printf("unable to add extension APIs to scheme: %v", err)
log.Printf("unable to add extension APIs to scheme: %v", err)
}

// Setup all Controllers

log.Print("Setting up operator controller")
err = (&operator.Reconciler{
Client: mgr.GetClient(),
}).SetupWithManager(mgr)
if err != nil {
log.Printf("unable to register operator controller to the manager: %v", err)
log.Printf("unable to register operator controller to the manager: %v", err)
os.Exit(1)
}
log.Print("📗 Operator controller")

log.Print("Setting up operator version controller")
err = (&operatorversion.Reconciler{
Client: mgr.GetClient(),
}).SetupWithManager(mgr)
if err != nil {
log.Printf("unable to register operator controller to the manager: %v", err)
log.Printf("unable to register operator controller to the manager: %v", err)
os.Exit(1)
}
log.Print("📘 OperatorVersion controller")

log.Print("Setting up instance controller")
discoveryClient, err := utils.GetDiscoveryClient(mgr)
if err != nil {
log.Println(err)
Expand All @@ -128,32 +125,37 @@ func main() {
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr)
if err != nil {
log.Printf("unable to register instance controller to the manager: %v", err)
log.Printf("unable to register instance controller to the manager: %v", err)
os.Exit(1)
}
log.Print("📙 Instance controller")

if strings.ToLower(os.Getenv("ENABLE_WEBHOOKS")) == "true" {
log.Printf("Setting up webhooks")
log.Printf("🔸 Setting up webhooks")

if err := registerWebhook("/validate", &v1beta1.Instance{}, &webhook.Admission{Handler: &kudohook.InstanceAdmission{}}, mgr); err != nil {
log.Printf("unable to create instance validation webhook: %v", err)
if err := registerWebhook("/admit", &v1beta1.Instance{}, &webhook.Admission{Handler: &kudohook.InstanceAdmission{}}, mgr); err != nil {
log.Printf("unable to create instance validation webhook: %v", err)
zen-dog marked this conversation as resolved.
Show resolved Hide resolved
os.Exit(1)
}
log.Printf("🧲 Instance admission webhook")
zen-dog marked this conversation as resolved.
Show resolved Hide resolved

// Add more webhooks below using the above registerWebhook method
}

// Start the KUDO manager
log.Print("Starting KUDO manager")
log.Print("🏄 Done! Everything is setup, starting KUDO manager now")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
log.Printf("unable to run the manager: %v", err)
log.Printf("💀 unable to run the manager: %v", err)
zen-dog marked this conversation as resolved.
Show resolved Hide resolved
os.Exit(1)
}
}

// registerWebhook method registers passed webhook using a give prefix (e.g. "/validate") and runtime object
// (e.g. v1beta1.Instance) to generate a webhook path e.g. "/validate-kudo-dev-v1beta1-instances". Webhook
// has to implement http.Handler interface (see v1beta1.InstanceAdmission for an example)
//
// NOTE: generated webhook path HAS to match the one, used in the webhook configuration. See for example how
zen-dog marked this conversation as resolved.
Show resolved Hide resolved
// MutatingWebhookConfiguration.Webhooks[0].ClientConfig.Service.Path is set in pkg/kudoctl/kudoinit/prereq/webhook.go:101
zen-dog marked this conversation as resolved.
Show resolved Hide resolved
func registerWebhook(prefix string, obj runtime.Object, hook http.Handler, mgr manager.Manager) error {
path, err := webhookPath(prefix, obj, mgr)
if err != nil {
Expand Down
14 changes: 10 additions & 4 deletions config/crds/kudo.dev_instances.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,26 @@ spec:
planExecution:
description: 'There are two ways a plan execution can be triggered: 1)
indirectly through update of a corresponding parameter in the InstanceSpec.Parameters
map 2) directly through setting of the InstanceSpec.PlanExecution
map 2) directly through setting of the InstanceSpec.PlanExecution.PlanName
field While indirect (1) triggers happens every time a user changes
a parameter, a directly (2) triggered plan is reserved for the situations
when parameters doesn''t change e.g. a periodic backup is triggered
overriding the existing backup file. Additionally, this opens room
for canceling and overriding currently running plans in the future.
Note: PlanExecution field defines plan name and corresponding parameters
that IS CURRENTLY executed. Once the instance controller (IC) is done
with the execution, this field will be cleared.'
with the execution, this field will be cleared. Each plan execution
has a unique UID so should the same plan be re-triggered it will have
a new UID'
properties:
planName:
type: string
required:
- planName
uid:
description: UID is a type that holds unique ID values, including
UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being
a type captures intent and helps make sure that UIDs and names
do not get conflated.
type: string
type: object
type: object
status:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/go-bindata/go-bindata v3.1.2+incompatible
github.com/go-openapi/jsonreference v0.19.3 // indirect
github.com/go-openapi/spec v0.19.3 // indirect
github.com/go-openapi/validate v0.19.2
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect
github.com/google/btree v1.0.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions pkg/apis/kudo/v1beta1/instance_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@ type InstanceSpec struct {

// There are two ways a plan execution can be triggered:
// 1) indirectly through update of a corresponding parameter in the InstanceSpec.Parameters map
// 2) directly through setting of the InstanceSpec.PlanExecution field
// 2) directly through setting of the InstanceSpec.PlanExecution.PlanName field
// While indirect (1) triggers happens every time a user changes a parameter, a directly (2) triggered
// plan is reserved for the situations when parameters doesn't change e.g. a periodic backup is triggered
// overriding the existing backup file. Additionally, this opens room for canceling and overriding
// currently running plans in the future.
// Note: PlanExecution field defines plan name and corresponding parameters that IS CURRENTLY executed.
// Once the instance controller (IC) is done with the execution, this field will be cleared.
// Each plan execution has a unique UID so should the same plan be re-triggered it will have a new UID
type PlanExecution struct {
PlanName string `json:"planName" validate:"required"`
PlanName string `json:"planName,omitempty"`
UID apimachinerytypes.UID `json:"uid,omitempty"`

// Future PE options like Force: bool. Not needed for now
}
Expand Down
102 changes: 100 additions & 2 deletions pkg/apis/kudo/v1beta1/instance_types_helpers.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package v1beta1

import (
"encoding/json"
"fmt"

"github.com/thoas/go-funk"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/uuid"

"github.com/kudobuilder/kudo/pkg/util/kudo"
)

const (
instanceCleanupFinalizerName = "kudo.dev.instance.cleanup"
snapshotAnnotation = "kudo.dev/last-applied-instance-state"
)

// GetPlanInProgress returns plan status of currently active plan or nil if no plan is running
func (i *Instance) GetPlanInProgress() *PlanStatus {
for _, p := range i.Status.PlanStatus {
Expand Down Expand Up @@ -78,7 +85,12 @@ func (i *Instance) ResetPlanStatus(plan string, updatedTimestamp *metav1.Time) e

// reset plan's phases and steps by setting them to ExecutionPending
planStatus.Set(ExecutionPending)
planStatus.UID = uuid.NewUUID()
// when using webhooks, instance admission webhook already generate an UID for current plan, otherwise, we generate a new one.
zen-dog marked this conversation as resolved.
Show resolved Hide resolved
if i.Spec.PlanExecution.UID != "" {
planStatus.UID = i.Spec.PlanExecution.UID
} else {
planStatus.UID = uuid.NewUUID()
zen-dog marked this conversation as resolved.
Show resolved Hide resolved
}

for i, ph := range planStatus.Phases {
planStatus.Phases[i].Set(ExecutionPending)
Expand All @@ -88,7 +100,7 @@ func (i *Instance) ResetPlanStatus(plan string, updatedTimestamp *metav1.Time) e
}
}

// update instance aggregated status
// update plan status and instance aggregated status
i.UpdateInstanceStatus(planStatus, updatedTimestamp)
return nil
}
Expand Down Expand Up @@ -118,6 +130,85 @@ func (i *Instance) PlanStatus(plan string) *PlanStatus {
return nil
}

// annotateSnapshot stores the current spec of Instance into the snapshot annotation
Copy link
Contributor Author

@zen-dog zen-dog Feb 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved here from the instance_controller as they're needed in the webhook too.

// this information is used when executing update/upgrade plans, this overrides any snapshot that existed before
func (i *Instance) AnnotateSnapshot() error {
jsonBytes, err := json.Marshal(i.Spec)
if err != nil {
return err
}
if i.Annotations == nil {
i.Annotations = make(map[string]string)
}
i.Annotations[snapshotAnnotation] = string(jsonBytes)
return nil
}

func (i *Instance) SnapshotSpec() (*InstanceSpec, error) {
if i.Annotations != nil {
snapshot, ok := i.Annotations[snapshotAnnotation]
if ok {
var spec *InstanceSpec
err := json.Unmarshal([]byte(snapshot), &spec)
if err != nil {
return nil, err
}
return spec, nil
}
}
return nil, nil
}

func (i *Instance) HasCleanupFinalizer() bool {
return funk.ContainsString(i.ObjectMeta.Finalizers, instanceCleanupFinalizerName)
}

// TryAddFinalizer adds the cleanup finalizer to an instance if the finalizer
// hasn't been added yet, the instance has a cleanup plan and the cleanup plan
// didn't run yet. Returns true if the cleanup finalizer has been added.
func (i *Instance) TryAddFinalizer() bool {
if !i.HasCleanupFinalizer() {
planStatus := i.PlanStatus(CleanupPlanName)
// avoid adding a finalizer multiple times: we only add it if the corresponding
// plan.Status is nil (meaning the plan never ran) or if it exists but equals ExecutionNeverRun
if planStatus == nil || planStatus.Status == ExecutionNeverRun {
i.ObjectMeta.Finalizers = append(i.ObjectMeta.Finalizers, instanceCleanupFinalizerName)
return true
}
}

return false
}

// TryRemoveFinalizer removes the cleanup finalizer of an instance if it has
// been added, the instance has a cleanup plan and the cleanup plan *successfully* finished.
// Returns true if the cleanup finalizer has been removed.
func (i *Instance) TryRemoveFinalizer() bool {
if i.HasCleanupFinalizer() {
if planStatus := i.PlanStatus(CleanupPlanName); planStatus != nil {
// we check IsFinished and *not* IsTerminal here so that the finalizer is not removed in the FatalError
// case. This way a human operator has to intervene and we don't leave garbage in the cluster.
if planStatus.Status.IsFinished() {
i.ObjectMeta.Finalizers = remove(i.ObjectMeta.Finalizers, instanceCleanupFinalizerName)
return true
}
} else {
// We have a finalizer but no cleanup plan. This could be due to an updated instance.
// Let's remove the finalizer.
i.ObjectMeta.Finalizers = remove(i.ObjectMeta.Finalizers, instanceCleanupFinalizerName)
return true
}
}

return false
}

func remove(values []string, s string) []string {
return funk.FilterString(values, func(str string) bool {
return str != s
})
}

// wasRunAfter returns true if p1 was run after p2
func wasRunAfter(p1 PlanStatus, p2 PlanStatus) bool {
if p1.Status == ExecutionNeverRun || p2.Status == ExecutionNeverRun || p1.LastUpdatedTimestamp == nil || p2.LastUpdatedTimestamp == nil {
Expand Down Expand Up @@ -160,6 +251,13 @@ func ParameterDiff(old, new map[string]string) map[string]string {
return diff
}

func CleanupPlanExists(ov *OperatorVersion) bool { return PlanExists(CleanupPlanName, ov) }

func PlanExists(plan string, ov *OperatorVersion) bool {
_, ok := ov.Spec.Plans[plan]
return ok
}

// SelectPlan returns nil if none of the plan exists, otherwise the first one in list that exists
func SelectPlan(possiblePlans []string, ov *OperatorVersion) *string {
for _, n := range possiblePlans {
Expand Down
Loading