Skip to content

Commit

Permalink
add instance history
Browse files Browse the repository at this point in the history
  • Loading branch information
linyguo committed Jan 24, 2025
1 parent d9e089f commit f27240a
Show file tree
Hide file tree
Showing 22 changed files with 1,157 additions and 0 deletions.
9 changes: 9 additions & 0 deletions k8s/PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,13 @@ resources:
kind: Diagnostic
path: gopls-workspace/apis/monitor/v1
version: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: symphony
group: solution
kind: InstanceHistory
path: gopls-workspace/apis/solution/v1
version: v1
version: "3"
16 changes: 16 additions & 0 deletions k8s/apis/model/v1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@ type InstanceSpec struct {
ReconciliationPolicy *ReconciliationPolicySpec `json:"reconciliationPolicy,omitempty"`
}

// +kubebuilder:object:generate=true
type InstanceStatusHistory struct {
Properties map[string]string `json:"properties,omitempty"`
ProvisioningStatus model.ProvisioningStatus `json:"provisioningStatus"`
LastModified string `json:"lastModified,omitempty"`
}

// +kubebuilder:object:generate=true
type InstanceHistorySpec struct {
// Snapshot of the instance spec
InstanceSpec `json:",inline"`
InstanceStatus InstanceStatusHistory `json:"instanceStatus,omitempty"`
// Add rootresoure to the instance history spec
RootResource string `json:"rootResource,omitempty"`
}

// +kubebuilder:object:generate=true
type SolutionSpec struct {
DisplayName string `json:"displayName,omitempty"`
Expand Down
40 changes: 40 additions & 0 deletions k8s/apis/model/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions k8s/apis/solution/v1/instance_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import (
configv1 "gopls-workspace/apis/config/v1"
"gopls-workspace/apis/dynamicclient"
"gopls-workspace/apis/metrics/v1"
k8smodel "gopls-workspace/apis/model/v1"
v1 "gopls-workspace/apis/model/v1"
"gopls-workspace/configutils"
"gopls-workspace/constants"
"gopls-workspace/history"
"gopls-workspace/utils/diagnostic"
"time"

Expand All @@ -27,7 +29,9 @@ import (
"github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/validation"
"github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2"

"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand All @@ -41,12 +45,15 @@ import (
// log is for logging in this package.
var instancelog = logf.Log.WithName("instance-resource")
var myInstanceClient client.Reader
var upsertHistoryClient client.Client
var instanceWebhookValidationMetrics *metrics.Metrics
var instanceProjectConfig *configv1.ProjectConfig
var instanceValidator validation.InstanceValidator
var instanceHistory history.InstanceHistory

func (r *Instance) SetupWebhookWithManager(mgr ctrl.Manager) error {
myInstanceClient = mgr.GetAPIReader()
upsertHistoryClient = mgr.GetClient()
mgr.GetFieldIndexer().IndexField(context.Background(), &Instance{}, "spec.solution", func(rawObj client.Object) []string {
instance := rawObj.(*Instance)
return []string{instance.Spec.Solution}
Expand Down Expand Up @@ -81,6 +88,54 @@ func (r *Instance) SetupWebhookWithManager(mgr ctrl.Manager) error {
instanceValidator = validation.NewInstanceValidator(nil, solutionLookupFunc, targetLookupFunc)
}

saveInstanceHistoryFunc := func(ctx context.Context, objectName string, namespace string, object interface{}) error {
instance, ok := object.(*Instance)
if !ok {
err := fmt.Errorf("expected an Instance object")
diagnostic.ErrorWithCtx(instancelog, ctx, err, "failed to convert old object to Instance", "name", r.Name, "namespace", r.Namespace)
return err
}
currentTime := time.Now()
diagnostic.InfoWithCtx(instancelog, ctx, "Saving old instance history", "Current time", currentTime, "name", instance.Name, "instance", instance)

var history InstanceHistory
history.ObjectMeta = metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%d", instance.Name, currentTime.Unix()),
Namespace: instance.Namespace,
}
history.Spec = k8smodel.InstanceHistorySpec{
InstanceSpec: instance.Spec,
RootResource: instance.Name,
}

// If the instance has a status, save it in the history
if !instance.Status.LastModified.IsZero() {
history.Spec.InstanceStatus = k8smodel.InstanceStatusHistory{
Properties: instance.Status.Properties,
ProvisioningStatus: instance.Status.ProvisioningStatus,
LastModified: instance.Status.LastModified.Format(time.RFC3339),
}
}

var result InstanceHistory
err := upsertHistoryClient.Get(ctx, client.ObjectKey{Name: history.GetName(), Namespace: history.GetNamespace()}, &result)
if err != nil && errors.IsNotFound(err) {
// Resource does not exist, create it
err = upsertHistoryClient.Create(ctx, &history)
if err != nil {
err := fmt.Errorf("upsert instance history failed, instance: %s, error: %v", instance.Name, err)
diagnostic.ErrorWithCtx(instancelog, ctx, err, "failed to save instance history for instance", "name", r.Name, "namespace", r.Namespace)
return err
}
diagnostic.InfoWithCtx(instancelog, ctx, "Saved instance history", "instance history", history)
} else if err != nil {
diagnostic.ErrorWithCtx(instancelog, ctx, err, "Unexpected error saving instance history", "name", r.Name, "namespace", r.Namespace)
}

return nil
}
instanceHistory = history.NewInstanceHistory(saveInstanceHistoryFunc)

return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
Expand Down Expand Up @@ -166,6 +221,11 @@ func (r *Instance) ValidateUpdate(old runtime.Object) (admission.Warnings, error
diagnostic.ErrorWithCtx(instancelog, ctx, err, "failed to convert old object to Instance", "name", r.Name, "namespace", r.Namespace)
return nil, err
}

// Save the old object
diagnostic.InfoWithCtx(instancelog, ctx, "saving instance history", "oldInstance", oldInstance)
instanceHistory.SaveInstanceHistoryFunc(ctx, oldInstance.Name, oldInstance.Namespace, oldInstance)

validationError := r.validateUpdateInstance(ctx, oldInstance)
if validationError != nil {
instanceWebhookValidationMetrics.ControllerValidationLatency(
Expand Down
47 changes: 47 additions & 0 deletions k8s/apis/solution/v1/instancehistory_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
* SPDX-License-Identifier: MIT
*/

package v1

import (
k8smodel "gopls-workspace/apis/model/v1"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// SolutionStatus defines the observed state of Solution
type InstanceHistoryStatus struct {
// Important: Run "make" to regenerate code after modifying this file
Properties map[string]string `json:"properties,omitempty"`
}

// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// InstanceHistory is the Schema for the instancehistories API
type InstanceHistory struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec k8smodel.InstanceHistorySpec `json:"spec,omitempty"`
Status InstanceHistoryStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// InstanceHistoryList contains a list of InstanceHistory
type InstanceHistoryList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []InstanceHistory `json:"items"`
}

func init() {
SchemeBuilder.Register(&InstanceHistory{}, &InstanceHistoryList{})
}
120 changes: 120 additions & 0 deletions k8s/apis/solution/v1/instancehistory_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
* SPDX-License-Identifier: MIT
*/

package v1

import (
"context"
"fmt"
"gopls-workspace/configutils"
"gopls-workspace/constants"
"gopls-workspace/utils/diagnostic"
"os"

api_constants "github.com/eclipse-symphony/symphony/api/constants"
observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// log is for logging in this package.
var historyLog = logf.Log.WithName("instance-history-resource")

var historyReaderClient client.Reader

func (r *InstanceHistory) SetupWebhookWithManager(mgr ctrl.Manager) error {
historyReaderClient = mgr.GetAPIReader()

return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
}

// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!

//+kubebuilder:webhook:path=/mutate-solution-symphony-v1-instancehistory,mutating=true,failurePolicy=fail,sideEffects=None,groups=solution.symphony,resources=instancehistories,verbs=create,versions=v1,name=minstancehistory.kb.io,admissionReviewVersions=v1

var _ webhook.Defaulter = &InstanceHistory{}

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *InstanceHistory) Default() {
ctx := diagnostic.ConstructDiagnosticContextFromAnnotations(r.Annotations, context.TODO(), historyLog)
diagnostic.InfoWithCtx(historyLog, ctx, "default", "name", r.Name, "namespace", r.Namespace, "spec", r.Spec, "status", r.Status)

// Set owner reference for the instance history
if r.Spec.RootResource != "" {
var instance Instance
err := historyReaderClient.Get(context.Background(), client.ObjectKey{Name: r.Spec.RootResource, Namespace: r.Namespace}, &instance)
if err != nil {
diagnostic.ErrorWithCtx(historyLog, ctx, err, "failed to get instance", "name", r.Spec.RootResource)
} else {
ownerReference := metav1.OwnerReference{
APIVersion: GroupVersion.String(),
Kind: "Instance",
Name: instance.Name,
UID: instance.UID,
}

if !configutils.CheckOwnerReferenceAlreadySet(r.OwnerReferences, ownerReference) {
r.OwnerReferences = append(r.OwnerReferences, ownerReference)
}
if r.Labels == nil {
r.Labels = make(map[string]string)
}
r.Labels[api_constants.RootResource] = r.Spec.RootResource
}
}

// Set annotation for the instance history
annotation_name := os.Getenv("ANNOTATION_KEY")
if annotation_name != "" {
annotations := r.ObjectMeta.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[annotation_name] = r.Name
r.ObjectMeta.SetAnnotations(annotations)
}
}

//+kubebuilder:webhook:path=/validate-solution-symphony-v1-instancehistory,mutating=false,failurePolicy=fail,sideEffects=None,groups=solution.symphony,resources=instancehistories,verbs=create,versions=v1,name=vinstancehistory.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &InstanceHistory{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *InstanceHistory) ValidateCreate() (admission.Warnings, error) {
resourceK8SId := r.GetNamespace() + "/" + r.GetName()
operationName := fmt.Sprintf("%s/%s", constants.InstanceHistoryOperationNamePrefix, constants.ActivityOperation_Write)
ctx := configutils.PopulateActivityAndDiagnosticsContextFromAnnotations(r.GetNamespace(), resourceK8SId, r.Annotations, operationName, historyReaderClient, context.TODO(), historyLog)

diagnostic.InfoWithCtx(historyLog, ctx, "validate create", "name", r.Name, "namespace", r.Namespace)
observ_utils.EmitUserAuditsLogs(ctx, "Instance history %s is being created on namespace %s", r.Name, r.Namespace)

return nil, nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *InstanceHistory) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
// TODO: instance history is readonly and should not be updated
return nil, nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *InstanceHistory) ValidateDelete() (admission.Warnings, error) {
resourceK8SId := r.GetNamespace() + "/" + r.GetName()
operationName := fmt.Sprintf("%s/%s", constants.InstanceHistoryOperationNamePrefix, constants.ActivityOperation_Delete)
ctx := configutils.PopulateActivityAndDiagnosticsContextFromAnnotations(r.GetNamespace(), resourceK8SId, r.Annotations, operationName, historyReaderClient, context.TODO(), historyLog)

diagnostic.InfoWithCtx(historyLog, ctx, "validate delete", "name", r.Name, "namespace", r.Namespace)
observ_utils.EmitUserAuditsLogs(ctx, "Instance history %s is being deleted on namespace %s", r.Name, r.Namespace)

return nil, nil
}
Loading

0 comments on commit f27240a

Please sign in to comment.