From a0887930d4d5963af6d5446b65a78b7452b0d34d Mon Sep 17 00:00:00 2001 From: Justin SB Date: Wed, 9 Nov 2022 09:21:50 -0500 Subject: [PATCH 1/2] Better status functions We pass in a function to query the current state of objects, this allows us to cache the results (if using SSA) and allows for cluster targeting. --- pkg/patterns/addon/pkg/status/aggregate.go | 98 +++++++-------- pkg/patterns/addon/pkg/status/basic.go | 11 +- pkg/patterns/addon/pkg/status/kstatus.go | 95 +++++++++------ .../declarative/pkg/manifest/objects.go | 11 ++ pkg/patterns/declarative/reconciler.go | 115 ++++++++++++++---- pkg/patterns/declarative/status.go | 44 ++++++- pkg/patterns/declarative/statusinfo.go | 32 +++++ 7 files changed, 285 insertions(+), 121 deletions(-) create mode 100644 pkg/patterns/declarative/statusinfo.go diff --git a/pkg/patterns/addon/pkg/status/aggregate.go b/pkg/patterns/addon/pkg/status/aggregate.go index 7ae498e3..cde8aa7a 100644 --- a/pkg/patterns/addon/pkg/status/aggregate.go +++ b/pkg/patterns/addon/pkg/status/aggregate.go @@ -25,11 +25,13 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" - "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" ) const successfulDeployment = appsv1.DeploymentAvailable @@ -37,54 +39,56 @@ const successfulDeployment = appsv1.DeploymentAvailable // NewAggregator provides an implementation of declarative.Reconciled that // aggregates the status of deployed objects to configure the 'Healthy' // field on an addon that derives from CommonStatus -func NewAggregator(client client.Client) *aggregator { - return &aggregator{client} +// +// TODO: Create a version that doesn't require the unused client arg +func NewAggregator(_ client.Client) *aggregator { + return &aggregator{} } type aggregator struct { - client client.Client } -func (a *aggregator) Reconciled(ctx context.Context, src declarative.DeclarativeObject, objs *manifest.Objects, reconcileErr error) error { +func (a *aggregator) BuildStatus(ctx context.Context, info *declarative.StatusInfo) error { log := log.FromContext(ctx) statusHealthy := true statusErrors := []string{} - if reconcileErr != nil { + shouldComputeHealthFromObjects := info.Manifest != nil && info.LiveObjects != nil + if info.Err != nil { statusHealthy = false + shouldComputeHealthFromObjects = false } - for _, o := range objs.GetItems() { - gk := o.Group + "/" + o.Kind - healthy := true - objKey := client.ObjectKey{ - Name: o.GetName(), - Namespace: o.GetNamespace(), - } - // If the namespace isn't set on the object, we would want to use the namespace of src - if objKey.Namespace == "" { - objKey.Namespace = src.GetNamespace() - } - var err error - switch gk { - case "/Service": - healthy, err = a.service(ctx, objKey) - case "extensions/Deployment", "apps/Deployment": - healthy, err = a.deployment(ctx, objKey) - default: - log.WithValues("type", gk).V(2).Info("type not implemented for status aggregation, skipping") - } - - statusHealthy = statusHealthy && healthy - if err != nil { - statusErrors = append(statusErrors, fmt.Sprintf("%v", err)) + if shouldComputeHealthFromObjects { + for _, o := range info.Manifest.GetItems() { + gvk := o.GroupVersionKind() + nn := o.NamespacedName() + + log := log.WithValues("kind", gvk.Kind).WithValues("name", nn.Name).WithValues("namespace", nn.Namespace) + + healthy := true + + var err error + switch gvk.Group + "/" + gvk.Kind { + case "/Service": + healthy, err = a.serviceIsHealthy(ctx, info.LiveObjects, gvk, nn) + case "extensions/Deployment", "apps/Deployment": + healthy, err = a.deploymentIsHealthy(ctx, info.LiveObjects, gvk, nn) + default: + log.V(4).Info("type not implemented for status aggregation, skipping") + } + + statusHealthy = statusHealthy && healthy + if err != nil { + statusErrors = append(statusErrors, fmt.Sprintf("%v", err)) + } } } - log.WithValues("object", src).WithValues("status", statusHealthy).V(2).Info("built status") + log.WithValues("status", statusHealthy).V(2).Info("built status") - currentStatus, err := utils.GetCommonStatus(src) + currentStatus, err := utils.GetCommonStatus(info.Subject) if err != nil { return err } @@ -94,27 +98,24 @@ func (a *aggregator) Reconciled(ctx context.Context, src declarative.Declarative status.Errors = statusErrors if !reflect.DeepEqual(status, currentStatus) { - err := utils.SetCommonStatus(src, status) + err := utils.SetCommonStatus(info.Subject, status) if err != nil { return err } - - log.WithValues("name", src.GetName()).WithValues("status", status).Info("updating status") - err = a.client.Status().Update(ctx, src) - if err != nil { - log.Error(err, "updating status") - return err - } } return nil } -func (a *aggregator) deployment(ctx context.Context, key client.ObjectKey) (bool, error) { - dep := &appsv1.Deployment{} +func (a *aggregator) deploymentIsHealthy(ctx context.Context, liveObjects declarative.LiveObjectReader, gvk schema.GroupVersionKind, nn types.NamespacedName) (bool, error) { + u, err := liveObjects(ctx, gvk, nn) + if err != nil { + return false, fmt.Errorf("error reading deployment: %w", err) + } - if err := a.client.Get(ctx, key, dep); err != nil { - return false, fmt.Errorf("error reading deployment (%s): %v", key, err) + dep := &appsv1.Deployment{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, dep); err != nil { + return false, fmt.Errorf("error converting deployment from unstructured: %w", err) } for _, cond := range dep.Status.Conditions { @@ -123,14 +124,13 @@ func (a *aggregator) deployment(ctx context.Context, key client.ObjectKey) (bool } } - return false, fmt.Errorf("deployment (%s) does not meet condition: %s", key, successfulDeployment) + return false, fmt.Errorf("deployment does not meet condition: %s", successfulDeployment) } -func (a *aggregator) service(ctx context.Context, key client.ObjectKey) (bool, error) { - svc := &corev1.Service{} - err := a.client.Get(ctx, key, svc) +func (a *aggregator) serviceIsHealthy(ctx context.Context, liveObjects declarative.LiveObjectReader, gvk schema.GroupVersionKind, nn types.NamespacedName) (bool, error) { + _, err := liveObjects(ctx, gvk, nn) if err != nil { - return false, fmt.Errorf("error reading service (%s): %v", key, err) + return false, fmt.Errorf("error reading service: %w", err) } return true, nil diff --git a/pkg/patterns/addon/pkg/status/basic.go b/pkg/patterns/addon/pkg/status/basic.go index 694144f1..a14e3590 100644 --- a/pkg/patterns/addon/pkg/status/basic.go +++ b/pkg/patterns/addon/pkg/status/basic.go @@ -21,13 +21,13 @@ import ( "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" ) -// Deprecated: This function exists for backward compatibility, please use NewKstatusCheck - // NewBasic provides an implementation of declarative.Status that // performs no preflight checks. +// +// Deprecated: This function exists for backward compatibility, please use NewKstatusCheck func NewBasic(client client.Client) declarative.Status { return &declarative.StatusBuilder{ - ReconciledImpl: NewAggregator(client), + BuildStatusImpl: NewAggregator(client), // no preflight checks } } @@ -41,14 +41,15 @@ func NewBasicVersionChecks(client client.Client, version string) (declarative.St } return &declarative.StatusBuilder{ - ReconciledImpl: NewAggregator(client), + BuildStatusImpl: NewAggregator(client), VersionCheckImpl: v, // no preflight checks }, nil } +// TODO: Create a version that doesn't take (unusued) client & reconciler args func NewKstatusCheck(client client.Client, d *declarative.Reconciler) declarative.Status { return &declarative.StatusBuilder{ - ReconciledImpl: NewKstatusAgregator(client, d), + BuildStatusImpl: NewKstatusAgregator(client, d), } } diff --git a/pkg/patterns/addon/pkg/status/kstatus.go b/pkg/patterns/addon/pkg/status/kstatus.go index bc69fa5a..77da31f1 100644 --- a/pkg/patterns/addon/pkg/status/kstatus.go +++ b/pkg/patterns/addon/pkg/status/kstatus.go @@ -2,7 +2,6 @@ package status import ( "context" - "fmt" "sigs.k8s.io/cli-utils/pkg/kstatus/status" "sigs.k8s.io/controller-runtime/pkg/client" @@ -10,63 +9,79 @@ import ( "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/utils" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" - "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" ) type kstatusAggregator struct { - client client.Client - reconciler *declarative.Reconciler } -func NewKstatusAgregator(c client.Client, reconciler *declarative.Reconciler) *kstatusAggregator { - return &kstatusAggregator{client: c, reconciler: reconciler} +// TODO: Create a version that doesn't need reconciler or client? +func NewKstatusAgregator(_ client.Client, _ *declarative.Reconciler) *kstatusAggregator { + return &kstatusAggregator{} } -func (k *kstatusAggregator) Reconciled(ctx context.Context, src declarative.DeclarativeObject, - objs *manifest.Objects, _ error) error { +func (k *kstatusAggregator) BuildStatus(ctx context.Context, info *declarative.StatusInfo) error { log := log.FromContext(ctx) - statusMap := make(map[status.Status]bool) - for _, object := range objs.Items { + currentStatus, err := utils.GetCommonStatus(info.Subject) + if err != nil { + log.Error(err, "error retrieving status") + return err + } - unstruct, err := declarative.GetObjectFromCluster(object, k.reconciler) - if err != nil { - log.WithValues("object", object.Kind+"/"+object.GetName()).Error(err, "Unable to get status of object") - return err + shouldComputeHealthFromObjects := info.Manifest != nil && info.LiveObjects != nil + if info.Err != nil { + currentStatus.Healthy = false + switch info.KnownError { + case declarative.KnownErrorApplyFailed: + currentStatus.Phase = "Applying" + // computeHealthFromObjects if we can (leave unchanged) + case declarative.KnownErrorVersionCheckFailed: + currentStatus.Phase = "VersionMismatch" + shouldComputeHealthFromObjects = false + default: + currentStatus.Phase = "InternalError" + shouldComputeHealthFromObjects = false } + } - res, err := status.Compute(unstruct) - if err != nil { - log.WithValues("kind", object.Kind).WithValues("name", object.GetName()).WithValues("status", res.Status).WithValues( - "message", res.Message).Info("Got status of resource:") - statusMap[status.NotFoundStatus] = true + if shouldComputeHealthFromObjects { + statusMap := make(map[status.Status]bool) + for _, object := range info.Manifest.Items { + gvk := object.GroupVersionKind() + nn := object.NamespacedName() + + log := log.WithValues("kind", gvk.Kind).WithValues("name", nn.Name).WithValues("namespace", nn.Namespace) + + unstruct, err := info.LiveObjects(ctx, gvk, nn) + if err != nil { + log.Error(err, "unable to get object to determine status") + statusMap[status.UnknownStatus] = true + continue + } + + res, err := status.Compute(unstruct) + if err != nil { + log.Error(err, "error getting status of resource") + statusMap[status.UnknownStatus] = true + } else if res != nil { + log.WithValues("status", res.Status).WithValues("message", res.Message).Info("Got status of resource:") + statusMap[res.Status] = true + } else { + log.Info("resource status was nil") + statusMap[status.UnknownStatus] = true + } } - if res != nil { - log.WithValues("kind", object.Kind).WithValues("name", object.GetName()).WithValues("status", res.Status).WithValues("message", res.Message).Info("Got status of resource:") - statusMap[res.Status] = true + + aggregatedPhase := string(aggregateStatus(statusMap)) + + if currentStatus.Phase != aggregatedPhase { + currentStatus.Phase = aggregatedPhase } } - aggregatedPhase := string(aggregateStatus(statusMap)) - - currentStatus, err := utils.GetCommonStatus(src) - if err != nil { - log.Error(err, "error retrieving status") + if err := utils.SetCommonStatus(info.Subject, currentStatus); err != nil { return err } - if currentStatus.Phase != aggregatedPhase { - currentStatus.Phase = aggregatedPhase - err := utils.SetCommonStatus(src, currentStatus) - if err != nil { - return err - } - log.WithValues("name", src.GetName()).WithValues("phase", aggregatedPhase).Info("updating status") - err = k.client.Status().Update(ctx, src) - if err != nil { - log.Error(err, "error updating status") - return fmt.Errorf("error error status: %v", err) - } - } return nil } diff --git a/pkg/patterns/declarative/pkg/manifest/objects.go b/pkg/patterns/declarative/pkg/manifest/objects.go index b6d065f8..d863b272 100644 --- a/pkg/patterns/declarative/pkg/manifest/objects.go +++ b/pkg/patterns/declarative/pkg/manifest/objects.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" k8syaml "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -320,6 +321,16 @@ func (o *Object) GroupVersionKind() schema.GroupVersionKind { return o.object.GroupVersionKind() } +// NamespacedName returns the name and namespace of the object in a types.NamespacedName. +// Note that this reflects the state of the object; if the namespace is not yet set, +// it will returned as "" here, even though it would likely be defaulted before apply. +func (o *Object) NamespacedName() types.NamespacedName { + return types.NamespacedName{ + Namespace: o.GetNamespace(), + Name: o.GetName(), + } +} + func (o *Objects) JSONManifest() (string, error) { var b bytes.Buffer diff --git a/pkg/patterns/declarative/reconciler.go b/pkg/patterns/declarative/reconciler.go index 22a35951..33f7026b 100644 --- a/pkg/patterns/declarative/reconciler.go +++ b/pkg/patterns/declarative/reconciler.go @@ -21,16 +21,15 @@ import ( "errors" "fmt" "path/filepath" + "reflect" "strings" - "k8s.io/apimachinery/pkg/api/meta" - "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/kustomize" - apierrors "k8s.io/apimachinery/pkg/api/errors" - + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" @@ -42,6 +41,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/kustomize/kyaml/filesys" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/utils" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/kustomize" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/applier" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" ) @@ -116,7 +117,7 @@ func (r *Reconciler) Init(mgr manager.Manager, prototype DeclarativeObject, opts r.restMapper = mgr.GetRESTMapper() - if err = r.applyOptions(opts...); err != nil { + if err := r.applyOptions(opts...); err != nil { return err } @@ -138,6 +139,7 @@ func (r *Reconciler) Init(mgr manager.Manager, prototype DeclarativeObject, opts // +rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (result reconcile.Result, err error) { var objects *manifest.Objects + log := log.FromContext(ctx) defer r.collectMetrics(request, result, err) @@ -170,6 +172,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( } }() + original := instance.DeepCopyObject().(DeclarativeObject) + if r.options.status != nil { if err := r.options.status.Preflight(ctx, instance); err != nil { log.Error(err, "preflight check failed, not reconciling") @@ -177,19 +181,52 @@ func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( } } - objects, err = r.reconcileExists(ctx, request.NamespacedName, instance) + statusInfo, err := r.reconcileExists(ctx, request.NamespacedName, instance) + objects = statusInfo.Manifest // for the defer block + if err != nil { + statusInfo.Err = err + } if err != nil { r.recorder.Eventf(instance, "Warning", "InternalError", "internal error: %v", err) } + if r.options.status != nil { + if err := r.options.status.BuildStatus(ctx, statusInfo); err != nil { + log.Error(err, "error building status") + return result, err + } + } + + // Write the status if it has changed + oldStatus, err := utils.GetCommonStatus(original) + if err != nil { + log.Error(err, "error getting status") + return result, err + } + newStatus, err := utils.GetCommonStatus(instance) + if err != nil { + log.Error(err, "error getting status") + return result, err + } + if !reflect.DeepEqual(oldStatus, newStatus) { + if err := r.client.Status().Update(ctx, instance); err != nil { + log.Error(err, "error updating status") + return result, err + } + } + return result, err } -func (r *Reconciler) reconcileExists(ctx context.Context, name types.NamespacedName, instance DeclarativeObject) (*manifest.Objects, error) { +func (r *Reconciler) reconcileExists(ctx context.Context, name types.NamespacedName, instance DeclarativeObject) (*StatusInfo, error) { log := log.FromContext(ctx) log.WithValues("object", name.String()).Info("reconciling") + statusInfo := &StatusInfo{ + Subject: instance, + } + var fs filesys.FileSystem if r.IsKustomizeOptionUsed() { fs = filesys.MakeFsInMemory() @@ -198,42 +235,41 @@ func (r *Reconciler) reconcileExists(ctx context.Context, name types.NamespacedN objects, err := r.BuildDeploymentObjectsWithFs(ctx, name, instance, fs) if err != nil { log.Error(err, "building deployment objects") - return nil, fmt.Errorf("error building deployment objects: %v", err) + return statusInfo, fmt.Errorf("error building deployment objects: %v", err) } + statusInfo.Manifest = objects log.WithValues("objects", fmt.Sprintf("%d", len(objects.Items))).Info("built deployment objects") if r.options.status != nil { isValidVersion, err := r.options.status.VersionCheck(ctx, instance, objects) if err != nil { if !isValidVersion { - // r.client isn't exported so can't be updated in version check function - if err := r.client.Status().Update(ctx, instance); err != nil { - return objects, err - } + statusInfo.KnownError = KnownErrorVersionCheckFailed r.recorder.Event(instance, "Warning", "Failed version check", err.Error()) log.Error(err, "Version check failed, not reconciling") - return objects, nil + return statusInfo, nil + } else { + log.Error(err, "Version check failed") + return statusInfo, err } - log.Error(err, "Version check failed, trying to reconcile") - return objects, err } } objects, err = parseListKind(objects) - if err != nil { log.Error(err, "Parsing list kind") - return objects, fmt.Errorf("error parsing list kind: %v", err) + return statusInfo, fmt.Errorf("error parsing list kind: %v", err) } + statusInfo.Manifest = objects err = r.setNamespaces(ctx, instance, objects) if err != nil { - return objects, err + return statusInfo, err } err = r.injectOwnerRef(ctx, instance, objects) if err != nil { - return objects, err + return statusInfo, err } var newItems []*manifest.Object @@ -315,20 +351,46 @@ func (r *Reconciler) reconcileExists(ctx context.Context, name types.NamespacedN if beforeApply, ok := hook.(BeforeApply); ok { if err := beforeApply.BeforeApply(ctx, applyOperation); err != nil { log.Error(err, "calling BeforeApply hook") - return objects, fmt.Errorf("error calling BeforeApply hook: %v", err) + return statusInfo, fmt.Errorf("error calling BeforeApply hook: %v", err) } } } if err := applier.Apply(ctx, applierOpt); err != nil { log.Error(err, "applying manifest") - return objects, fmt.Errorf("error applying manifest: %v", err) + statusInfo.KnownError = KnownErrorApplyFailed + return statusInfo, fmt.Errorf("error applying manifest: %v", err) + } + + statusInfo.LiveObjects = func(ctx context.Context, gvk schema.GroupVersionKind, nn types.NamespacedName) (*unstructured.Unstructured, error) { + // TODO: Applier should return the objects in their post-apply state, so we don't have to requery + + mapping, err := r.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return nil, fmt.Errorf("unable to get mapping for resource %v: %w", gvk, err) + } + + var resource dynamic.ResourceInterface + switch mapping.Scope { + case meta.RESTScopeNamespace: + resource = r.dynamicClient.Resource(mapping.Resource).Namespace(nn.Namespace) + case meta.RESTScopeRoot: + resource = r.dynamicClient.Resource(mapping.Resource) + default: + return nil, fmt.Errorf("unknown scope %v", mapping.Scope) + } + u, err := resource.Get(ctx, nn.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("error getting object: %w", err) + } + return u, nil + } if r.options.sink != nil { if err := r.options.sink.Notify(ctx, instance, objects); err != nil { log.Error(err, "notifying sink") - return objects, err + return statusInfo, err } } @@ -336,12 +398,12 @@ func (r *Reconciler) reconcileExists(ctx context.Context, name types.NamespacedN if afterApply, ok := hook.(AfterApply); ok { if err := afterApply.AfterApply(ctx, applyOperation); err != nil { log.Error(err, "calling AfterApply hook") - return objects, fmt.Errorf("error calling AfterApply hook: %w", err) + return statusInfo, fmt.Errorf("error calling AfterApply hook: %w", err) } } } - return objects, nil + return statusInfo, nil } // BuildDeploymentObjects performs all manifest operations to build a final set of objects for deployment @@ -688,6 +750,9 @@ func (r *Reconciler) CollectMetrics() bool { return r.options.metrics } +// GetObjectFromCluster gets the current state of the object from the cluster. +// +// deprecated: use LiveObjectReader instead when computing status func GetObjectFromCluster(obj *manifest.Object, r *Reconciler) (*unstructured.Unstructured, error) { getOptions := metav1.GetOptions{} gvk := obj.GroupVersionKind() @@ -701,7 +766,7 @@ func GetObjectFromCluster(obj *manifest.Object, r *Reconciler) (*unstructured.Un if mapping.Scope.Name() == meta.RESTScopeNameNamespace { ns = obj.GetNamespace() } - unstruct, err := r.dynamicClient.Resource(mapping.Resource).Namespace(ns).Get(context.Background(), name, getOptions) + unstruct, err := r.dynamicClient.Resource(mapping.Resource).Namespace(ns).Get(context.TODO(), name, getOptions) if err != nil { return nil, fmt.Errorf("unable to get object: %w", err) } diff --git a/pkg/patterns/declarative/status.go b/pkg/patterns/declarative/status.go index 3243eb4c..90db4a35 100644 --- a/pkg/patterns/declarative/status.go +++ b/pkg/patterns/declarative/status.go @@ -19,6 +19,9 @@ package declarative import ( "context" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" ) @@ -27,13 +30,36 @@ type Status interface { Reconciled Preflight VersionCheck + + // BuildStatus computes the new status for the object after a reconcile operation, + // and writes it into statusInfo.Subject. + // This function is should broadly map any reconciliation errors into the status, + // and if no errors were reported should check that the applied objects are healthy and ready. + // If this function returns a nil error, changes to the `status` of the object + // will then be written back to the kube-apiserver. + // If this function returns an error, the reconciliation function will return + // an error without updating the status to the apiserver, the interpretation is that + // we were unable to compute a new status. + BuildStatus(ctx context.Context, statusInfo *StatusInfo) error } +// LiveObjectReader exposes the state of objects on the cluster after the apply operation. +// Currently this is done by querying the cluster, but we will move this to record the state of objects after applying them. +type LiveObjectReader func(ctx context.Context, gvk schema.GroupVersionKind, nn types.NamespacedName) (*unstructured.Unstructured, error) + type Reconciled interface { // Reconciled is triggered when Reconciliation has occured. // The caller is encouraged to determine and surface the health of the reconcilation // on the DeclarativeObject. - Reconciled(context.Context, DeclarativeObject, *manifest.Objects, error) error + // + // Deprecated: Prefer the BuildStatus method + Reconciled(ctx context.Context, subject DeclarativeObject, manifest *manifest.Objects, err error) error +} + +// BuildStatus computes the new status after a reconcile operation. +type BuildStatus interface { + // BuildStatus computes the new status after a reconcile operation. + BuildStatus(ctx context.Context, statusInfo *StatusInfo) error } type Preflight interface { @@ -52,9 +78,16 @@ type VersionCheck interface { // StatusBuilder provides a pluggable implementation of Status type StatusBuilder struct { - ReconciledImpl Reconciled + // ReconciledImpl is called after reconciliation + // + // Deprecated: Prefer the BuildStatus method + ReconciledImpl Reconciled + PreflightImpl Preflight VersionCheckImpl VersionCheck + + // BuildStatus computes the status after a reconcile operation + BuildStatusImpl BuildStatus } func (s *StatusBuilder) Reconciled(ctx context.Context, src DeclarativeObject, objs *manifest.Objects, err error) error { @@ -78,4 +111,11 @@ func (s *StatusBuilder) VersionCheck(ctx context.Context, src DeclarativeObject, return true, nil } +func (s *StatusBuilder) BuildStatus(ctx context.Context, statusInfo *StatusInfo) error { + if s.BuildStatusImpl != nil { + return s.BuildStatusImpl.BuildStatus(ctx, statusInfo) + } + return nil +} + var _ Status = &StatusBuilder{} diff --git a/pkg/patterns/declarative/statusinfo.go b/pkg/patterns/declarative/statusinfo.go new file mode 100644 index 00000000..331aea4e --- /dev/null +++ b/pkg/patterns/declarative/statusinfo.go @@ -0,0 +1,32 @@ +// Copyright 2022 The Kubernetes Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package declarative + +import "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" + +type StatusInfo struct { + Subject DeclarativeObject + Manifest *manifest.Objects + LiveObjects LiveObjectReader + KnownError KnownErrorCode + Err error +} + +type KnownErrorCode string + +const ( + KnownErrorApplyFailed KnownErrorCode = "FailedToApply" + KnownErrorVersionCheckFailed KnownErrorCode = "VersionCheckFailed" +) From 355b1bc642be1e6c9cd2ee963ed6add906fd53d0 Mon Sep 17 00:00:00 2001 From: Justin SB Date: Wed, 16 Nov 2022 05:56:25 -0500 Subject: [PATCH 2/2] Extend tests to cover health checking. --- .../testreconciler/simpletest/controller.go | 4 +- .../simpletest/controller_test.go | 11 +- .../packages/simpletest/0.1.0/manifest.yaml | 18 +++ .../direct/create/expected-http.yaml | 134 +++++++++++++++-- .../reconcile/ssa/create/expected-http.yaml | 135 ++++++++++++++++-- 5 files changed, 282 insertions(+), 20 deletions(-) diff --git a/pkg/test/testreconciler/simpletest/controller.go b/pkg/test/testreconciler/simpletest/controller.go index a47c493b..fb321a63 100644 --- a/pkg/test/testreconciler/simpletest/controller.go +++ b/pkg/test/testreconciler/simpletest/controller.go @@ -27,7 +27,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon" - "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/status" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/applier" @@ -48,6 +47,7 @@ type SimpleTestReconciler struct { manifestController declarative.ManifestController applier applier.Applier + status declarative.Status } func (r *SimpleTestReconciler) setupReconciler(mgr ctrl.Manager) error { @@ -61,7 +61,7 @@ func (r *SimpleTestReconciler) setupReconciler(mgr ctrl.Manager) error { declarative.WithObjectTransform(declarative.AddLabels(labels)), declarative.WithOwner(declarative.SourceAsOwner), declarative.WithLabels(r.watchLabels), - declarative.WithStatus(status.NewBasic(mgr.GetClient())), + declarative.WithStatus(r.status), // TODO: Readd prune //declarative.WithApplyPrune(), diff --git a/pkg/test/testreconciler/simpletest/controller_test.go b/pkg/test/testreconciler/simpletest/controller_test.go index 4d23500c..41e7367e 100644 --- a/pkg/test/testreconciler/simpletest/controller_test.go +++ b/pkg/test/testreconciler/simpletest/controller_test.go @@ -18,6 +18,8 @@ import ( "sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/loaders" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/status" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/applier" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/restmapper" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/test/httprecorder" @@ -30,26 +32,29 @@ func TestSimpleReconciler(t *testing.T) { appliers := []struct { Key string Applier applier.Applier + Status declarative.Status }{ { Key: "direct", Applier: applier.NewDirectApplier(), + Status: status.NewBasic(nil), }, { Key: "ssa", Applier: applier.NewApplySetApplier(metav1.PatchOptions{FieldManager: "kdp-test"}), + Status: status.NewKstatusCheck(nil, nil), }, } for _, applier := range appliers { t.Run(applier.Key, func(t *testing.T) { testharness.RunGoldenTests(t, "testdata/reconcile/"+applier.Key+"/", func(h *testharness.Harness, testdir string) { - testSimpleReconciler(h, testdir, applier.Applier) + testSimpleReconciler(h, testdir, applier.Applier, applier.Status) }) }) } } -func testSimpleReconciler(h *testharness.Harness, testdir string, applier applier.Applier) { +func testSimpleReconciler(h *testharness.Harness, testdir string, applier applier.Applier, status declarative.Status) { ctx := context.Background() k8s, err := mockkubeapiserver.NewMockKubeAPIServer(":0") @@ -60,6 +65,7 @@ func testSimpleReconciler(h *testharness.Harness, testdir string, applier applie k8s.RegisterType(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, "namespaces", meta.RESTScopeRoot) k8s.RegisterType(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}, "configmaps", meta.RESTScopeNamespace) k8s.RegisterType(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Event"}, "events", meta.RESTScopeNamespace) + k8s.RegisterType(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, "deployments", meta.RESTScopeNamespace) k8s.RegisterType(schema.GroupVersionKind{Group: "addons.example.org", Version: "v1alpha1", Kind: "SimpleTest"}, "simpletests", meta.RESTScopeNamespace) defer func() { @@ -110,6 +116,7 @@ func testSimpleReconciler(h *testharness.Harness, testdir string, applier applie Client: mgr.GetClient(), Scheme: mgr.GetScheme(), applier: applier, + status: status, } mc, err := loaders.NewManifestLoader("testdata/channels") diff --git a/pkg/test/testreconciler/simpletest/testdata/channels/packages/simpletest/0.1.0/manifest.yaml b/pkg/test/testreconciler/simpletest/testdata/channels/packages/simpletest/0.1.0/manifest.yaml index 7285b7af..9a09a024 100644 --- a/pkg/test/testreconciler/simpletest/testdata/channels/packages/simpletest/0.1.0/manifest.yaml +++ b/pkg/test/testreconciler/simpletest/testdata/channels/packages/simpletest/0.1.0/manifest.yaml @@ -6,3 +6,21 @@ metadata: l1: v1 data: k1: v1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mydeployment +spec: + replicas: 3 + selector: + matchLabels: + app: bar + template: + metadata: + labels: + app: bar + spec: + containers: + - name: main + image: registry.k8s.io/pause:3.9 diff --git a/pkg/test/testreconciler/simpletest/testdata/reconcile/direct/create/expected-http.yaml b/pkg/test/testreconciler/simpletest/testdata/reconcile/direct/create/expected-http.yaml index dc31e705..8d116707 100644 --- a/pkg/test/testreconciler/simpletest/testdata/reconcile/direct/create/expected-http.yaml +++ b/pkg/test/testreconciler/simpletest/testdata/reconcile/direct/create/expected-http.yaml @@ -49,6 +49,19 @@ Date: (removed) --- +GET http://kube-apiserver/apis/apps/v1?timeout=32s +Accept: application/json, */* + +200 OK +Cache-Control: no-cache, private +Content-Length: 831 +Content-Type: application/json +Date: (removed) + +{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"apps/v1","resources":[{"name":"controllerrevisions","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"ControllerRevision","verbs":null},{"name":"daemonsets","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"DaemonSet","verbs":null},{"name":"deployments","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"Deployment","verbs":null},{"name":"deployments","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"Deployment","verbs":null},{"name":"replicasets","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"ReplicaSet","verbs":null},{"name":"statefulsets","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"StatefulSet","verbs":null}]} + +--- + GET http://kube-apiserver/api/v1/namespaces/ns1/configmaps/foo Accept: application/json @@ -61,6 +74,20 @@ X-Content-Type-Options: nosniff Not Found +--- + +GET http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment +Accept: application/json + +404 Not Found +Content-Length: 10 +Content-Type: text/plain; charset=utf-8 +Date: (removed) +X-Content-Type-Options: nosniff + +Not Found + + --- GET http://kube-apiserver/api/v1/configmaps?allowWatchBookmarks=true&labelSelector=addons.example.org%2Fsimpletest%3Dsimple1&watch=true @@ -75,6 +102,18 @@ Date: (removed) --- +GET http://kube-apiserver/apis/apps/v1/deployments?allowWatchBookmarks=true&labelSelector=addons.example.org%2Fsimpletest%3Dsimple1&watch=true +Accept: application/json + +200 OK +Cache-Control: no-cache, private +Content-Type: application/json +Date: (removed) + + + +--- + GET http://kube-apiserver/api/v1/namespaces/ns1/configmaps/foo Accept: application/json @@ -119,20 +158,77 @@ Date: (removed) --- +GET http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment +Accept: application/json + +404 Not Found +Content-Length: 10 +Content-Type: text/plain; charset=utf-8 +Date: (removed) +X-Content-Type-Options: nosniff + +Not Found + + +--- + +GET http://kube-apiserver/api/v1/namespaces/ns1 +Accept: application/json + +200 OK +Cache-Control: no-cache, private +Content-Length: 286 +Content-Type: application/json +Date: (removed) + +{"apiVersion":"v1","kind":"Namespace","metadata":{"creationTimestamp":"2022-01-01T00:00:00Z","labels":{"kubernetes.io/metadata.name":"ns1"},"name":"ns1","resourceVersion":"1","uid":"00000000-0000-0000-0000-000000000001"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} + +--- + +POST http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments?fieldManager=kubectl-client-side-apply&fieldValidation=Strict +Accept: application/json +Content-Type: application/json + +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"addons.example.org/simpletest\":\"simple1\",\"example-app\":\"simpletest\"},\"name\":\"mydeployment\",\"namespace\":\"ns1\",\"ownerReferences\":[{\"apiVersion\":\"addons.example.org/v1alpha1\",\"blockOwnerDeletion\":true,\"controller\":true,\"kind\":\"SimpleTest\",\"name\":\"simple1\",\"uid\":\"00000000-0000-0000-0000-000000000002\"}]},\"spec\":{\"replicas\":3,\"selector\":{\"matchLabels\":{\"app\":\"bar\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"bar\"}},\"spec\":{\"containers\":[{\"image\":\"registry.k8s.io/pause:3.9\",\"name\":\"main\"}]}}}}\n"},"labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}]},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} + + +200 OK +Cache-Control: no-cache, private +Content-Length: 1397 +Content-Type: application/json +Date: (removed) + +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"addons.example.org/simpletest\":\"simple1\",\"example-app\":\"simpletest\"},\"name\":\"mydeployment\",\"namespace\":\"ns1\",\"ownerReferences\":[{\"apiVersion\":\"addons.example.org/v1alpha1\",\"blockOwnerDeletion\":true,\"controller\":true,\"kind\":\"SimpleTest\",\"name\":\"simple1\",\"uid\":\"00000000-0000-0000-0000-000000000002\"}]},\"spec\":{\"replicas\":3,\"selector\":{\"matchLabels\":{\"app\":\"bar\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"bar\"}},\"spec\":{\"containers\":[{\"image\":\"registry.k8s.io/pause:3.9\",\"name\":\"main\"}]}}}}\n"},"creationTimestamp":"2022-01-01T00:00:03Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000004"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} + +--- + +GET http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment +Accept: application/json + +200 OK +Cache-Control: no-cache, private +Content-Length: 1397 +Content-Type: application/json +Date: (removed) + +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"addons.example.org/simpletest\":\"simple1\",\"example-app\":\"simpletest\"},\"name\":\"mydeployment\",\"namespace\":\"ns1\",\"ownerReferences\":[{\"apiVersion\":\"addons.example.org/v1alpha1\",\"blockOwnerDeletion\":true,\"controller\":true,\"kind\":\"SimpleTest\",\"name\":\"simple1\",\"uid\":\"00000000-0000-0000-0000-000000000002\"}]},\"spec\":{\"replicas\":3,\"selector\":{\"matchLabels\":{\"app\":\"bar\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"bar\"}},\"spec\":{\"containers\":[{\"image\":\"registry.k8s.io/pause:3.9\",\"name\":\"main\"}]}}}}\n"},"creationTimestamp":"2022-01-01T00:00:03Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000004"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} + +--- + PUT http://kube-apiserver/apis/addons.example.org/v1alpha1/namespaces/ns1/simpletests/simple1/status Accept: application/json, */* Content-Type: application/json -{"kind":"SimpleTest","apiVersion":"addons.example.org/v1alpha1","metadata":{"name":"simple1","namespace":"ns1","uid":"00000000-0000-0000-0000-000000000002","resourceVersion":"2","creationTimestamp":"2022-01-01T00:00:01Z"},"spec":{"channel":"stable"},"status":{"healthy":true}} +{"kind":"SimpleTest","apiVersion":"addons.example.org/v1alpha1","metadata":{"name":"simple1","namespace":"ns1","uid":"00000000-0000-0000-0000-000000000002","resourceVersion":"2","creationTimestamp":"2022-01-01T00:00:01Z"},"spec":{"channel":"stable"},"status":{"healthy":false,"errors":["deployment does not meet condition: Available"]}} 200 OK Cache-Control: no-cache, private -Content-Length: 276 +Content-Length: 336 Content-Type: application/json Date: (removed) -{"apiVersion":"addons.example.org/v1alpha1","kind":"SimpleTest","metadata":{"creationTimestamp":"2022-01-01T00:00:01Z","name":"simple1","namespace":"ns1","resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"channel":"stable"},"status":{"healthy":true}} +{"apiVersion":"addons.example.org/v1alpha1","kind":"SimpleTest","metadata":{"creationTimestamp":"2022-01-01T00:00:01Z","name":"simple1","namespace":"ns1","resourceVersion":"5","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"channel":"stable"},"status":{"errors":["deployment does not meet condition: Available"],"healthy":false}} --- @@ -149,6 +245,19 @@ Date: (removed) --- +GET http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment +Accept: application/json + +200 OK +Cache-Control: no-cache, private +Content-Length: 1397 +Content-Type: application/json +Date: (removed) + +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"addons.example.org/simpletest\":\"simple1\",\"example-app\":\"simpletest\"},\"name\":\"mydeployment\",\"namespace\":\"ns1\",\"ownerReferences\":[{\"apiVersion\":\"addons.example.org/v1alpha1\",\"blockOwnerDeletion\":true,\"controller\":true,\"kind\":\"SimpleTest\",\"name\":\"simple1\",\"uid\":\"00000000-0000-0000-0000-000000000002\"}]},\"spec\":{\"replicas\":3,\"selector\":{\"matchLabels\":{\"app\":\"bar\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"bar\"}},\"spec\":{\"containers\":[{\"image\":\"registry.k8s.io/pause:3.9\",\"name\":\"main\"}]}}}}\n"},"creationTimestamp":"2022-01-01T00:00:03Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000004"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} + +--- + GET http://kube-apiserver/api/v1/namespaces/ns1/configmaps/foo Accept: application/json @@ -162,17 +271,26 @@ Date: (removed) --- -PUT http://kube-apiserver/apis/addons.example.org/v1alpha1/namespaces/ns1/simpletests/simple1/status -Accept: application/json, */* +GET http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment +Accept: application/json + +200 OK +Cache-Control: no-cache, private +Content-Length: 1397 Content-Type: application/json +Date: (removed) -{"kind":"SimpleTest","apiVersion":"addons.example.org/v1alpha1","metadata":{"name":"simple1","namespace":"ns1","uid":"00000000-0000-0000-0000-000000000002","resourceVersion":"4","creationTimestamp":"2022-01-01T00:00:01Z"},"spec":{"channel":"stable"},"status":{"healthy":true}} +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"addons.example.org/simpletest\":\"simple1\",\"example-app\":\"simpletest\"},\"name\":\"mydeployment\",\"namespace\":\"ns1\",\"ownerReferences\":[{\"apiVersion\":\"addons.example.org/v1alpha1\",\"blockOwnerDeletion\":true,\"controller\":true,\"kind\":\"SimpleTest\",\"name\":\"simple1\",\"uid\":\"00000000-0000-0000-0000-000000000002\"}]},\"spec\":{\"replicas\":3,\"selector\":{\"matchLabels\":{\"app\":\"bar\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"bar\"}},\"spec\":{\"containers\":[{\"image\":\"registry.k8s.io/pause:3.9\",\"name\":\"main\"}]}}}}\n"},"creationTimestamp":"2022-01-01T00:00:03Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000004"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} +--- + +GET http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment +Accept: application/json 200 OK Cache-Control: no-cache, private -Content-Length: 276 +Content-Length: 1397 Content-Type: application/json Date: (removed) -{"apiVersion":"addons.example.org/v1alpha1","kind":"SimpleTest","metadata":{"creationTimestamp":"2022-01-01T00:00:01Z","name":"simple1","namespace":"ns1","resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"channel":"stable"},"status":{"healthy":true}} +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"addons.example.org/simpletest\":\"simple1\",\"example-app\":\"simpletest\"},\"name\":\"mydeployment\",\"namespace\":\"ns1\",\"ownerReferences\":[{\"apiVersion\":\"addons.example.org/v1alpha1\",\"blockOwnerDeletion\":true,\"controller\":true,\"kind\":\"SimpleTest\",\"name\":\"simple1\",\"uid\":\"00000000-0000-0000-0000-000000000002\"}]},\"spec\":{\"replicas\":3,\"selector\":{\"matchLabels\":{\"app\":\"bar\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"bar\"}},\"spec\":{\"containers\":[{\"image\":\"registry.k8s.io/pause:3.9\",\"name\":\"main\"}]}}}}\n"},"creationTimestamp":"2022-01-01T00:00:03Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000004"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} diff --git a/pkg/test/testreconciler/simpletest/testdata/reconcile/ssa/create/expected-http.yaml b/pkg/test/testreconciler/simpletest/testdata/reconcile/ssa/create/expected-http.yaml index 82005810..79a9ac10 100644 --- a/pkg/test/testreconciler/simpletest/testdata/reconcile/ssa/create/expected-http.yaml +++ b/pkg/test/testreconciler/simpletest/testdata/reconcile/ssa/create/expected-http.yaml @@ -49,6 +49,19 @@ Date: (removed) --- +GET http://kube-apiserver/apis/apps/v1?timeout=32s +Accept: application/json, */* + +200 OK +Cache-Control: no-cache, private +Content-Length: 831 +Content-Type: application/json +Date: (removed) + +{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"apps/v1","resources":[{"name":"controllerrevisions","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"ControllerRevision","verbs":null},{"name":"daemonsets","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"DaemonSet","verbs":null},{"name":"deployments","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"Deployment","verbs":null},{"name":"deployments","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"Deployment","verbs":null},{"name":"replicasets","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"ReplicaSet","verbs":null},{"name":"statefulsets","singularName":"","namespaced":true,"group":"apps","version":"v1","kind":"StatefulSet","verbs":null}]} + +--- + GET http://kube-apiserver/api/v1/namespaces/ns1/configmaps/foo Accept: application/json @@ -61,6 +74,20 @@ X-Content-Type-Options: nosniff Not Found +--- + +GET http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment +Accept: application/json + +404 Not Found +Content-Length: 10 +Content-Type: text/plain; charset=utf-8 +Date: (removed) +X-Content-Type-Options: nosniff + +Not Found + + --- GET http://kube-apiserver/api/v1/configmaps?allowWatchBookmarks=true&labelSelector=addons.example.org%2Fsimpletest%3Dsimple1&watch=true @@ -75,6 +102,18 @@ Date: (removed) --- +GET http://kube-apiserver/apis/apps/v1/deployments?allowWatchBookmarks=true&labelSelector=addons.example.org%2Fsimpletest%3Dsimple1&watch=true +Accept: application/json + +200 OK +Cache-Control: no-cache, private +Content-Type: application/json +Date: (removed) + + + +--- + PATCH http://kube-apiserver/api/v1/namespaces/ns1/configmaps/foo?fieldManager=kdp-test&force=true Accept: application/json Content-Type: application/apply-patch+yaml @@ -91,20 +130,62 @@ Date: (removed) --- +PATCH http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment?fieldManager=kdp-test&force=true +Accept: application/json +Content-Type: application/apply-patch+yaml + +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}]},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} + +200 OK +Cache-Control: no-cache, private +Content-Length: 666 +Content-Type: application/json +Date: (removed) + +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"creationTimestamp":"2022-01-01T00:00:03Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000004"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} + +--- + +GET http://kube-apiserver/api/v1/namespaces/ns1/configmaps/foo +Accept: application/json + +200 OK +Cache-Control: no-cache, private +Content-Length: 492 +Content-Type: application/json +Date: (removed) + +{"apiVersion":"v1","data":{"k1":"v1"},"kind":"ConfigMap","metadata":{"creationTimestamp":"2022-01-01T00:00:02Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest","l1":"v1"},"name":"foo","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"3","uid":"00000000-0000-0000-0000-000000000003"}} + +--- + +GET http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment +Accept: application/json + +200 OK +Cache-Control: no-cache, private +Content-Length: 666 +Content-Type: application/json +Date: (removed) + +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"creationTimestamp":"2022-01-01T00:00:03Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000004"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} + +--- + PUT http://kube-apiserver/apis/addons.example.org/v1alpha1/namespaces/ns1/simpletests/simple1/status Accept: application/json, */* Content-Type: application/json -{"kind":"SimpleTest","apiVersion":"addons.example.org/v1alpha1","metadata":{"name":"simple1","namespace":"ns1","uid":"00000000-0000-0000-0000-000000000002","resourceVersion":"2","creationTimestamp":"2022-01-01T00:00:01Z"},"spec":{"channel":"stable"},"status":{"healthy":true}} +{"kind":"SimpleTest","apiVersion":"addons.example.org/v1alpha1","metadata":{"name":"simple1","namespace":"ns1","uid":"00000000-0000-0000-0000-000000000002","resourceVersion":"2","creationTimestamp":"2022-01-01T00:00:01Z"},"spec":{"channel":"stable"},"status":{"healthy":false,"phase":"InProgress"}} 200 OK Cache-Control: no-cache, private -Content-Length: 276 +Content-Length: 298 Content-Type: application/json Date: (removed) -{"apiVersion":"addons.example.org/v1alpha1","kind":"SimpleTest","metadata":{"creationTimestamp":"2022-01-01T00:00:01Z","name":"simple1","namespace":"ns1","resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"channel":"stable"},"status":{"healthy":true}} +{"apiVersion":"addons.example.org/v1alpha1","kind":"SimpleTest","metadata":{"creationTimestamp":"2022-01-01T00:00:01Z","name":"simple1","namespace":"ns1","resourceVersion":"5","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"channel":"stable"},"status":{"healthy":false,"phase":"InProgress"}} --- @@ -121,6 +202,19 @@ Date: (removed) --- +GET http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment +Accept: application/json + +200 OK +Cache-Control: no-cache, private +Content-Length: 666 +Content-Type: application/json +Date: (removed) + +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"creationTimestamp":"2022-01-01T00:00:03Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000004"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} + +--- + PATCH http://kube-apiserver/api/v1/namespaces/ns1/configmaps/foo?fieldManager=kdp-test&force=true Accept: application/json Content-Type: application/apply-patch+yaml @@ -137,17 +231,42 @@ Date: (removed) --- -PUT http://kube-apiserver/apis/addons.example.org/v1alpha1/namespaces/ns1/simpletests/simple1/status -Accept: application/json, */* +PATCH http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment?fieldManager=kdp-test&force=true +Accept: application/json +Content-Type: application/apply-patch+yaml + +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}]},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} + +200 OK +Cache-Control: no-cache, private +Content-Length: 666 +Content-Type: application/json +Date: (removed) + +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"creationTimestamp":"2022-01-01T00:00:03Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000004"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}} + +--- + +GET http://kube-apiserver/api/v1/namespaces/ns1/configmaps/foo +Accept: application/json + +200 OK +Cache-Control: no-cache, private +Content-Length: 492 Content-Type: application/json +Date: (removed) + +{"apiVersion":"v1","data":{"k1":"v1"},"kind":"ConfigMap","metadata":{"creationTimestamp":"2022-01-01T00:00:02Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest","l1":"v1"},"name":"foo","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"3","uid":"00000000-0000-0000-0000-000000000003"}} -{"kind":"SimpleTest","apiVersion":"addons.example.org/v1alpha1","metadata":{"name":"simple1","namespace":"ns1","uid":"00000000-0000-0000-0000-000000000002","resourceVersion":"4","creationTimestamp":"2022-01-01T00:00:01Z"},"spec":{"channel":"stable"},"status":{"healthy":true}} +--- +GET http://kube-apiserver/apis/apps/v1/namespaces/ns1/deployments/mydeployment +Accept: application/json 200 OK Cache-Control: no-cache, private -Content-Length: 276 +Content-Length: 666 Content-Type: application/json Date: (removed) -{"apiVersion":"addons.example.org/v1alpha1","kind":"SimpleTest","metadata":{"creationTimestamp":"2022-01-01T00:00:01Z","name":"simple1","namespace":"ns1","resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"channel":"stable"},"status":{"healthy":true}} +{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"creationTimestamp":"2022-01-01T00:00:03Z","labels":{"addons.example.org/simpletest":"simple1","example-app":"simpletest"},"name":"mydeployment","namespace":"ns1","ownerReferences":[{"apiVersion":"addons.example.org/v1alpha1","blockOwnerDeletion":true,"controller":true,"kind":"SimpleTest","name":"simple1","uid":"00000000-0000-0000-0000-000000000002"}],"resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000004"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"bar"}},"template":{"metadata":{"labels":{"app":"bar"}},"spec":{"containers":[{"image":"registry.k8s.io/pause:3.9","name":"main"}]}}}}