diff --git a/controllers/main.go b/controllers/main.go index a011043a..87711729 100644 --- a/controllers/main.go +++ b/controllers/main.go @@ -137,7 +137,8 @@ func run(ctx context.Context) error { Client: client.Options{ Cache: &client.CacheOptions{ DisableFor: []client.Object{ - &porchapi.PackageRevisionResources{}}, + &porchapi.PackageRevisionResources{}, + &porchapi.PackageRevision{}}, }, }, } diff --git a/controllers/packagevariants/pkg/controllers/packagevariant/injection.go b/controllers/packagevariants/pkg/controllers/packagevariant/injection.go index 3fbf859d..022b7a90 100644 --- a/controllers/packagevariants/pkg/controllers/packagevariant/injection.go +++ b/controllers/packagevariants/pkg/controllers/packagevariant/injection.go @@ -18,14 +18,16 @@ import ( "context" "fmt" "path/filepath" - "sigs.k8s.io/kustomize/kyaml/kio" "sort" "strings" + "sigs.k8s.io/kustomize/kyaml/kio" + "github.com/GoogleContainerTools/kpt-functions-sdk/go/fn" porchapi "github.com/nephio-project/porch/api/porch/v1alpha1" api "github.com/nephio-project/porch/controllers/packagevariants/api/v1alpha1" kptfilev1 "github.com/nephio-project/porch/pkg/kpt/api/kptfile/v1" + "github.com/nephio-project/porch/pkg/util" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -124,7 +126,7 @@ func ensureConfigInjection(ctx context.Context, return err } - prr.Spec.Resources["Kptfile"] = kptfile.String() + prr.Spec.Resources[kptfilev1.KptFileName] = util.KubeObjectToYaml(kptfile) return nil } diff --git a/controllers/packagevariants/pkg/controllers/packagevariant/packagevariant_controller.go b/controllers/packagevariants/pkg/controllers/packagevariant/packagevariant_controller.go index 8b2dda28..6286d076 100644 --- a/controllers/packagevariants/pkg/controllers/packagevariant/packagevariant_controller.go +++ b/controllers/packagevariants/pkg/controllers/packagevariant/packagevariant_controller.go @@ -1,4 +1,4 @@ -// Copyright 2023-2024 The kpt and Nephio Authors +// Copyright 2022, 2024 The kpt and Nephio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import ( "github.com/GoogleContainerTools/kpt-functions-sdk/go/fn" kptfilev1 "github.com/nephio-project/porch/pkg/kpt/api/kptfile/v1" "github.com/nephio-project/porch/pkg/kpt/kptfileutil" + "github.com/nephio-project/porch/pkg/util" "golang.org/x/mod/semver" "k8s.io/apimachinery/pkg/api/meta" @@ -56,8 +57,24 @@ type PackageVariantReconciler struct { const ( workspaceNamePrefix = "packagevariant-" - ConditionTypeStalled = "Stalled" // whether or not the packagevariant object is making progress or not - ConditionTypeReady = "Ready" // whether or not the reconciliation succeeded + ConditionTypeStalled = "Stalled" // whether or not the packagevariant object is making progress + ConditionTypeReady = "Ready" // whether or not the reconciliation succeeded + ConditionTypePipelinePassed = "PackagePipelinePassed" // whether or not the package's pipeline has completed successfully +) + +var ( + conditionPipelineNotPassed = porchapi.Condition{ + Type: ConditionTypePipelinePassed, + Status: porchapi.ConditionFalse, + Reason: "WaitingOnPipeline", + Message: "waiting for package pipeline to pass", + } + conditionPipelinePassed = porchapi.Condition{ + Type: ConditionTypePipelinePassed, + Status: porchapi.ConditionTrue, + Reason: "PipelinePassed", + Message: "package pipeline completed successfully", + } ) //go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.1 rbac:headerFile=../../../../../scripts/boilerplate.yaml.txt,roleName=porch-controllers-packagevariants webhook paths="." output:rbac:artifacts:config=../../../config/rbac @@ -334,6 +351,13 @@ func (r *PackageVariantReconciler) ensurePackageVariant(ctx context.Context, if err = r.Client.Create(ctx, newPR); err != nil { return nil, err } + + setPrReadinessGate(newPR, ConditionTypePipelinePassed) + setPrStatusCondition(newPR, conditionPipelineNotPassed) + if err := r.Client.Update(ctx, newPR); err != nil { + return nil, err + } + klog.Infoln(fmt.Sprintf("package variant %q created package revision %q", pv.Name, newPR.Name)) prr, changed, err := r.calculateDraftResources(ctx, pv, newPR) @@ -347,6 +371,15 @@ func (r *PackageVariantReconciler) ensurePackageVariant(ctx context.Context, } } + if err := r.Client.Get(ctx, types.NamespacedName{Name: newPR.GetName(), Namespace: newPR.GetNamespace()}, newPR); err != nil { + return nil, err + } + + setPrStatusCondition(newPR, conditionPipelinePassed) + if err := r.Client.Update(ctx, newPR); err != nil { + return nil, err + } + return []*porchapi.PackageRevision{newPR}, nil } @@ -369,10 +402,18 @@ func (r *PackageVariantReconciler) findAndUpdateExistingRevisions(ctx context.Co downstream.Spec.Lifecycle = porchapi.PackageRevisionLifecyclePublished // We update this now, because later we may use a Porch call to clone or update // and we want to make sure the server is in sync with us + + setPrReadinessGate(downstream, ConditionTypePipelinePassed) + setPrStatusCondition(downstream, conditionPipelineNotPassed) if err := r.Client.Update(ctx, downstream); err != nil { klog.Errorf("error updating package revision lifecycle: %v", err) return nil, err } + + setPrStatusCondition(downstream, conditionPipelinePassed) + if err := r.Client.Update(ctx, downstream); err != nil { + return nil, err + } } // see if the package needs updating due to an upstream change @@ -423,9 +464,20 @@ func (r *PackageVariantReconciler) findAndUpdateExistingRevisions(ctx context.Co } // Save the updated PackageRevisionResources + + setPrReadinessGate(downstream, ConditionTypePipelinePassed) + setPrStatusCondition(downstream, conditionPipelineNotPassed) + if err := r.Client.Update(ctx, downstream); err != nil { + return nil, err + } if err := r.updatePackageResources(ctx, prr, pv); err != nil { return nil, err } + + setPrStatusCondition(downstream, conditionPipelinePassed) + if err := r.Client.Update(ctx, downstream); err != nil { + return nil, err + } } } return downstreams, nil @@ -694,8 +746,15 @@ func (r *PackageVariantReconciler) updateDraft(ctx context.Context, updateTask.Update.Upstream.UpstreamRef.Name = newUpstreamPR.Name draft.Spec.Tasks = append(tasks, updateTask) - err := r.Client.Update(ctx, draft) - if err != nil { + setPrReadinessGate(draft, ConditionTypePipelinePassed) + setPrStatusCondition(draft, conditionPipelineNotPassed) + + if err := r.Client.Update(ctx, draft); err != nil { + return nil, err + } + + setPrStatusCondition(draft, conditionPipelinePassed) + if err := r.Client.Update(ctx, draft); err != nil { return nil, err } return draft, nil @@ -724,10 +783,33 @@ func setTargetStatusConditions(pv *api.PackageVariant, targets []*porchapi.Packa Type: ConditionTypeReady, Status: "True", Reason: "NoErrors", - Message: "successfully ensured downstream package variant", + Message: "successfully ensured downstream target package revision", }) } +func setPrReadinessGate(pr *porchapi.PackageRevision, conditionType string) { + for _, aGate := range pr.Spec.ReadinessGates { + if aGate.ConditionType == conditionType { + return + } + } + + pr.Spec.ReadinessGates = append(pr.Spec.ReadinessGates, porchapi.ReadinessGate{ + ConditionType: conditionType, + }) +} + +func setPrStatusCondition(pr *porchapi.PackageRevision, condition porchapi.Condition) { + for index, aCondition := range pr.Status.Conditions { + if aCondition.Type == condition.Type { + pr.Status.Conditions[index] = condition + return + } + } + + pr.Status.Conditions = append(pr.Status.Conditions, condition) +} + // SetupWithManager sets up the controller with the Manager. func (r *PackageVariantReconciler) SetupWithManager(mgr ctrl.Manager) error { if err := api.AddToScheme(mgr.GetScheme()); err != nil { @@ -1019,7 +1101,7 @@ func ensureKRMFunctions(pv *api.PackageVariant, } // update kptfile - prr.Spec.Resources[kptfilev1.KptFileName] = kptfile.String() + prr.Spec.Resources[kptfilev1.KptFileName] = util.KubeObjectToYaml(kptfile) return nil } diff --git a/controllers/packagevariantsets/config/samples/pvs-v1alpha2.yaml b/controllers/packagevariantsets/config/samples/pvs-v1alpha2.yaml index 88cd7228..75529aba 100644 --- a/controllers/packagevariantsets/config/samples/pvs-v1alpha2.yaml +++ b/controllers/packagevariantsets/config/samples/pvs-v1alpha2.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 The kpt and Nephio Authors +# Copyright 2023-2024 The kpt and Nephio Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ spec: - foo-01 - foo-02 - foo-03 - name: cluster-02 + - name: cluster-02 template: downstream: packageExpr: "target.package + '-' + repository.labels['env']" diff --git a/func/internal/podevaluator.go b/func/internal/podevaluator.go index 643c7d1a..061e0d92 100644 --- a/func/internal/podevaluator.go +++ b/func/internal/podevaluator.go @@ -1,4 +1,4 @@ -// Copyright 2022 The kpt and Nephio Authors +// Copyright 2022, 2024 The kpt and Nephio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -238,7 +238,7 @@ type imagePodAndGRPCClient struct { func (pcm *podCacheManager) warmupCache(podTTLConfig string) error { start := time.Now() defer func() { - klog.Infof("cache warning is completed and it took %v", time.Since(start)) + klog.Infof("cache warming is complete after %v", time.Since(start)) }() content, err := os.ReadFile(podTTLConfig) if err != nil { diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index c80eb5e6..6a52c665 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -201,6 +201,7 @@ func (cad *cadEngine) CreatePackageRevision(ctx context.Context, repositoryObj * return nil, err } repoPkgRev.SetMeta(pkgRevMeta) + sent := cad.watcherManager.NotifyPackageRevisionChange(watch.Added, repoPkgRev) klog.Infof("engine: sent %d for new PackageRevision %s/%s", sent, repoPkgRev.KubeObjectNamespace(), repoPkgRev.KubeObjectName()) return repoPkgRev, nil diff --git a/pkg/kpt/api/kptfile/v1/types.go b/pkg/kpt/api/kptfile/v1/types.go index 4dd90ead..29849eb7 100644 --- a/pkg/kpt/api/kptfile/v1/types.go +++ b/pkg/kpt/api/kptfile/v1/types.go @@ -1,4 +1,4 @@ -// Copyright 2021 The kpt and Nephio Authors +// Copyright 2021, 2024 The kpt and Nephio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ package v1 import ( "fmt" + "github.com/GoogleContainerTools/kpt-functions-sdk/go/fn" + "github.com/nephio-project/porch/pkg/util" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -76,13 +78,34 @@ type KptFile struct { // Info contains metadata such as license, documentation, etc. Info *PackageInfo `yaml:"info,omitempty" json:"info,omitempty"` + Status *Status `yaml:"status,omitempty" json:"status,omitempty"` + // Pipeline declares the pipeline of functions. Pipeline *Pipeline `yaml:"pipeline,omitempty" json:"pipeline,omitempty"` // Inventory contains parameters for the inventory object used in apply. Inventory *Inventory `yaml:"inventory,omitempty" json:"inventory,omitempty"` +} - Status *Status `yaml:"status,omitempty" json:"status,omitempty"` +func FromKubeObject(kptfileKubeObject *fn.KubeObject) (KptFile, error) { + var apiKptfile KptFile + if err := kptfileKubeObject.As(&apiKptfile); err != nil { + return KptFile{}, err + } + return apiKptfile, nil +} + +func (file *KptFile) ToYamlString() (string, error) { + b, err := yaml.MarshalWithOptions(file, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle}) + if err != nil { + return "", err + } + kptfileKubeObject, err := util.YamlToKubeObject(string(b)) + if err != nil { + return "", err + } + + return kptfileKubeObject.String(), nil } // OriginType defines the type of origin for a package. @@ -210,6 +233,8 @@ type PackageInfo struct { // Relative slash-delimited path to the license file (e.g. LICENSE.txt) LicenseFile string `yaml:"licenseFile,omitempty" json:"licenseFile,omitempty"` + ReadinessGates []ReadinessGate `yaml:"readinessGates,omitempty" json:"readinessGates,omitempty"` + // Description contains a short description of the package. Description string `yaml:"description,omitempty" json:"description,omitempty"` @@ -218,8 +243,6 @@ type PackageInfo struct { // Man is the path to documentation about the package Man string `yaml:"man,omitempty" json:"man,omitempty"` - - ReadinessGates []ReadinessGate `yaml:"readinessGates,omitempty" json:"readinessGates,omitempty"` } type ReadinessGate struct { @@ -289,6 +312,11 @@ func (p *Pipeline) IsEmpty() bool { // Function specifies a KRM function. // +kubebuilder:object:generate=true type Function struct { + + // `Name` is used to uniquely identify the function declaration + // this is primarily used for merging function declaration with upstream counterparts + Name string `yaml:"name,omitempty" json:"name,omitempty"` + // `Image` specifies the function container image. // It can either be fully qualified, e.g.: // @@ -318,10 +346,6 @@ type Function struct { // `ConfigMap` is a convenient way to specify a function config of kind ConfigMap. ConfigMap map[string]string `yaml:"configMap,omitempty" json:"configMap,omitempty"` - // `Name` is used to uniquely identify the function declaration - // this is primarily used for merging function declaration with upstream counterparts - Name string `yaml:"name,omitempty" json:"name,omitempty"` - // `Selectors` are used to specify resources on which the function should be executed // if not specified, all resources are selected Selectors []Selector `yaml:"selectors,omitempty" json:"selectors,omitempty"` @@ -386,9 +410,9 @@ type Condition struct { Status ConditionStatus `yaml:"status" json:"status"` - Reason string `yaml:"reason,omitempty" json:"reason,omitempty"` - Message string `yaml:"message,omitempty" json:"message,omitempty"` + + Reason string `yaml:"reason,omitempty" json:"reason,omitempty"` } type ConditionStatus string diff --git a/pkg/kpt/clone.go b/pkg/kpt/clone.go index 43076b87..72293535 100644 --- a/pkg/kpt/clone.go +++ b/pkg/kpt/clone.go @@ -20,7 +20,6 @@ import ( internalpkg "github.com/nephio-project/porch/internal/kpt/pkg" kptfilev1 "github.com/nephio-project/porch/pkg/kpt/api/kptfile/v1" - "sigs.k8s.io/kustomize/kyaml/yaml" ) // TODO: Accept a virtual filesystem or other package abstraction @@ -38,12 +37,12 @@ func UpdateUpstream(kptfileContents string, name string, upstream kptfilev1.Upst kptfile.Name = name } - b, err := yaml.MarshalWithOptions(kptfile, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle}) + kptfileYaml, err := kptfile.ToYamlString() if err != nil { return "", fmt.Errorf("cannot save Kptfile: %w", err) } - return string(b), nil + return kptfileYaml, nil } func UpdateName(kptfileContents string, name string) (string, error) { @@ -55,12 +54,12 @@ func UpdateName(kptfileContents string, name string) (string, error) { // update the name of the package kptfile.Name = name - b, err := yaml.MarshalWithOptions(kptfile, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle}) + kptfileYaml, err := kptfile.ToYamlString() if err != nil { return "", fmt.Errorf("cannot save Kptfile: %w", err) } - return string(b), nil + return kptfileYaml, nil } // TODO: accept a virtual filesystem diff --git a/pkg/kpt/kptfileutil/util_test.go b/pkg/kpt/kptfileutil/util_test.go index 2234b51e..f87a0bb9 100644 --- a/pkg/kpt/kptfileutil/util_test.go +++ b/pkg/kpt/kptfileutil/util_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 The kpt and Nephio Authors +// Copyright 2020, 2024 The kpt and Nephio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -364,8 +364,8 @@ status: conditions: - type: foo status: "True" - reason: reason message: message + reason: reason `, }, "additional readinessGate and condition added in upstream": { @@ -381,8 +381,8 @@ status: conditions: - type: foo status: "True" - reason: reason message: message + reason: reason `, updated: ` apiVersion: kpt.dev/v1 @@ -397,12 +397,12 @@ status: conditions: - type: foo status: "True" - reason: reason message: message + reason: reason - type: bar status: "False" - reason: reason message: message + reason: reason `, local: ` apiVersion: kpt.dev/v1 @@ -416,8 +416,8 @@ status: conditions: - type: foo status: "True" - reason: reason message: message + reason: reason `, updateUpstream: false, expected: ` @@ -433,12 +433,12 @@ status: conditions: - type: foo status: "True" - reason: reason message: message + reason: reason - type: bar status: "False" - reason: reason message: message + reason: reason `, }, "readinessGate added removed in upstream": { @@ -502,12 +502,12 @@ status: conditions: - type: foo status: "True" - reason: reason message: message + reason: reason - type: bar status: "False" - reason: reason message: message + reason: reason `, updated: ` apiVersion: kpt.dev/v1 @@ -522,12 +522,12 @@ status: conditions: - type: foo status: "True" - reason: reason message: message + reason: reason - type: zork status: "Unknown" - reason: reason message: message + reason: reason `, local: ` apiVersion: kpt.dev/v1 @@ -542,12 +542,12 @@ status: conditions: - type: xandar status: "True" - reason: reason message: message + reason: reason - type: foo status: "True" - reason: reason message: message + reason: reason `, updateUpstream: false, expected: ` @@ -563,12 +563,12 @@ status: conditions: - type: foo status: "True" - reason: reason message: message + reason: reason - type: zork status: Unknown - reason: reason message: message + reason: reason `, }, } @@ -1151,27 +1151,27 @@ metadata: name: pipeline pipeline: mutators: - - image: gcr.io/kpt-fn/search-replace:v0.1 + - name: my-new-function + image: gcr.io/kpt-fn/search-replace:v0.1 configMap: by-value: YOUR_TEAM put-value: my-team - name: my-new-function - - image: gcr.io/kpt-fn/generate-folders:v0.1 - name: gf1 - - image: gcr.io/kpt-fn/search-replace:v0.1 + - name: gf1 + image: gcr.io/kpt-fn/generate-folders:v0.1 + - name: sr1 + image: gcr.io/kpt-fn/search-replace:v0.1 configMap: by-value: foo put-value: bar-new - name: sr1 - - image: gcr.io/kpt-fn/set-labels:v0.1 + - name: sl1 + image: gcr.io/kpt-fn/set-labels:v0.1 configMap: app: db - name: sl1 - - image: gcr.io/kpt-fn/search-replace:v0.1 + - name: sr2 + image: gcr.io/kpt-fn/search-replace:v0.1 configMap: by-value: abc put-comment: ${updated-setter-name} - name: sr2 `, }, @@ -1215,10 +1215,10 @@ metadata: name: pipeline pipeline: mutators: - - image: gcr.io/kpt/folder-ref - name: folder-ref - - image: gcr.io/kpt/gen-folders - name: gen-folders + - name: folder-ref + image: gcr.io/kpt/folder-ref + - name: gen-folders + image: gcr.io/kpt/gen-folders `, }, @@ -1264,14 +1264,14 @@ metadata: name: pipeline pipeline: mutators: - - image: x-gcr.io/kpt/gen-folders - name: x-local - - image: b-gcr.io/kpt/gen-folders - name: b-local - - image: z-gcr.io/kpt/gen-folders - name: z-upstream - - image: a-gcr.io/kpt/gen-folders - name: a-upstream + - name: x-local + image: x-gcr.io/kpt/gen-folders + - name: b-local + image: b-gcr.io/kpt/gen-folders + - name: z-upstream + image: z-gcr.io/kpt/gen-folders + - name: a-upstream + image: a-gcr.io/kpt/gen-folders `, }, @@ -1320,15 +1320,15 @@ metadata: name: pipeline pipeline: mutators: - - image: gcr.io/kpt/ref-folders + - name: ref-folders + image: gcr.io/kpt/ref-folders configMap: bar: foo foo: bar - name: ref-folders - - image: gcr.io/kpt/gen-folders - name: gen-folder-local - - image: gcr.io/kpt/gen-folders - name: gen-folder-upstream + - name: gen-folder-local + image: gcr.io/kpt/gen-folders + - name: gen-folder-upstream + image: gcr.io/kpt/gen-folders `, }, @@ -1372,10 +1372,10 @@ metadata: name: pipeline pipeline: mutators: - - image: gcr.io/kpt/ref-folders + - name: ref-folders + image: gcr.io/kpt/ref-folders configMap: band: sleater-kinney - name: ref-folders `, }, } diff --git a/pkg/registry/porch/packagecommon.go b/pkg/registry/porch/packagecommon.go index 78400c2d..dda487e7 100644 --- a/pkg/registry/porch/packagecommon.go +++ b/pkg/registry/porch/packagecommon.go @@ -309,19 +309,7 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string, parentPackage = p } - if !isCreate { - rev, err := r.cad.UpdatePackageRevision(ctx, "", &repositoryObj, oldRepoPkgRev, oldApiPkgRev.(*api.PackageRevision), newApiPkgRev, parentPackage) - if err != nil { - return nil, false, apierrors.NewInternalError(err) - } - - updated, err := rev.GetPackageRevision(ctx) - if err != nil { - return nil, false, apierrors.NewInternalError(err) - } - - return updated, false, nil - } else { + if isCreate { rev, err := r.cad.CreatePackageRevision(ctx, &repositoryObj, newApiPkgRev, parentPackage) if err != nil { klog.Infof("error creating package: %v", err) @@ -334,6 +322,18 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string, return createdApiPkgRev, true, nil } + + rev, err := r.cad.UpdatePackageRevision(ctx, "", &repositoryObj, oldRepoPkgRev, oldApiPkgRev.(*api.PackageRevision), newApiPkgRev, parentPackage) + if err != nil { + return nil, false, apierrors.NewInternalError(err) + } + + updated, err := rev.GetPackageRevision(ctx) + if err != nil { + return nil, false, apierrors.NewInternalError(err) + } + + return updated, false, nil } // Common implementation of Package update logic. diff --git a/pkg/registry/porch/packagerevisionresources.go b/pkg/registry/porch/packagerevisionresources.go index 10388484..073cb5b0 100644 --- a/pkg/registry/porch/packagerevisionresources.go +++ b/pkg/registry/porch/packagerevisionresources.go @@ -154,13 +154,13 @@ func (r *packageRevisionResources) Update(ctx context.Context, name string, objI klog.Infof("update failed to construct UpdatedObject: %v", err) return nil, false, err } - newObj, ok := newRuntimeObj.(*api.PackageRevisionResources) + newPkgRevResources, ok := newRuntimeObj.(*api.PackageRevisionResources) if !ok { return nil, false, apierrors.NewBadRequest(fmt.Sprintf("expected PackageRevisionResources object, got %T", newRuntimeObj)) } if updateValidation != nil { - err := updateValidation(ctx, newObj, oldApiPkgRevResources) + err := updateValidation(ctx, newPkgRevResources, oldApiPkgRevResources) if err != nil { klog.Infof("update failed validation: %v", err) return nil, false, err @@ -181,7 +181,7 @@ func (r *packageRevisionResources) Update(ctx context.Context, name string, objI return nil, false, apierrors.NewInternalError(fmt.Errorf("error getting repository %v: %w", repositoryID, err)) } - rev, renderStatus, err := r.cad.UpdatePackageResources(ctx, &repositoryObj, oldRepoPkgRev, oldApiPkgRevResources, newObj) + rev, renderStatus, err := r.cad.UpdatePackageResources(ctx, &repositoryObj, oldRepoPkgRev, oldApiPkgRevResources, newPkgRevResources) if err != nil { return nil, false, apierrors.NewInternalError(err) } diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index dfb8cf3b..36c7fc6a 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -85,7 +85,7 @@ type PackageRevision interface { // GetUpstreamLock returns the kpt lock information. GetUpstreamLock(context.Context) (kptfile.Upstream, kptfile.UpstreamLock, error) - // GetKptfile returns the Kptfile for hte package + // GetKptfile returns the Kptfile for the package GetKptfile(context.Context) (kptfile.KptFile, error) // GetLock returns the current revision's lock information. diff --git a/pkg/task/generictaskhandler.go b/pkg/task/generictaskhandler.go index 1bd938e2..db3b189b 100644 --- a/pkg/task/generictaskhandler.go +++ b/pkg/task/generictaskhandler.go @@ -15,9 +15,11 @@ package task import ( - "bytes" "context" + "encoding/json" "fmt" + "reflect" + "slices" api "github.com/nephio-project/porch/api/porch/v1alpha1" configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1" @@ -34,7 +36,26 @@ import ( "sigs.k8s.io/kustomize/kyaml/yaml" ) -var _ TaskHandler = &genericTaskHandler{} +var ( + _ TaskHandler = &genericTaskHandler{} + + conditionPipelineNotPassed = api.Condition{ + Type: ConditionTypePipelinePassed, + Status: api.ConditionFalse, + Reason: "WaitingOnPipeline", + Message: "waiting for package pipeline to pass", + } + conditionPipelinePassed = api.Condition{ + Type: ConditionTypePipelinePassed, + Status: api.ConditionTrue, + Reason: "PipelinePassed", + Message: "package pipeline completed successfully", + } +) + +const ( + ConditionTypePipelinePassed = "PackagePipelinePassed" // whether or not the package's pipeline has completed successfully +) type genericTaskHandler struct { runnerOptionsResolver func(namespace string) fnruntime.RunnerOptions @@ -154,21 +175,30 @@ func (th *genericTaskHandler) DoPRMutations(ctx context.Context, namespace strin // If any of the fields in the API that are projections from the Kptfile // must be updated in the Kptfile as well. - kfPatchTask, created, err := createKptfilePatchTask(ctx, repoPR, newObj) + kfPatchTask, kfPatchCreated, err := createKptfilePatchTask(ctx, repoPR, newObj) if err != nil { return err } - if created { - kfPatchMutation, err := buildPatchMutation(ctx, kfPatchTask) + var kfPatchMutation mutation + if kfPatchCreated { + kfPatchMutation, err = buildPatchMutation(ctx, kfPatchTask) if err != nil { return err } + mutations = append(mutations, kfPatchMutation) } // Re-render if we are making changes. mutations = th.conditionalAddRender(newObj, mutations) + // if all this update does is set the PackagePipelinePassed + // readiness condition, we don't need to run the full mutation + // pipeline - just update the Kptfile and leave it at that + if updateOnlySetsPipelineCondition(oldObj, newObj) { + mutations = []mutation{kfPatchMutation} + } + // TODO: Handle the case if alongside lifecycle change, tasks are changed too. // Update package contents only if the package is in draft state if oldObj.Spec.Lifecycle == api.PackageRevisionLifecycleDraft { @@ -339,14 +369,9 @@ func createKptfilePatchTask(ctx context.Context, oldPackage repository.PackageRe return nil, false, err } - var orgKfString string - { - var buf bytes.Buffer - d := yaml.NewEncoder(&buf) - if err := d.Encode(kf); err != nil { - return nil, false, err - } - orgKfString = buf.String() + var origKfString string + if origKfString, err = kf.ToYamlString(); err != nil { + return nil, false, fmt.Errorf("cannot read original Kptfile: %w", err) } var readinessGates []kptfile.ReadinessGate @@ -381,15 +406,11 @@ func createKptfilePatchTask(ctx context.Context, oldPackage repository.PackageRe } var newKfString string - { - var buf bytes.Buffer - d := yaml.NewEncoder(&buf) - if err := d.Encode(kf); err != nil { - return nil, false, err - } - newKfString = buf.String() + if newKfString, err = kf.ToYamlString(); err != nil { + return nil, false, fmt.Errorf("cannot read Kptfile after updating: %w", err) } - patchSpec, err := GeneratePatch(kptfile.KptFileName, orgKfString, newKfString) + + patchSpec, err := GeneratePatch(kptfile.KptFileName, origKfString, newKfString) if err != nil { return nil, false, err } @@ -441,6 +462,32 @@ func isRenderMutation(m mutation) bool { return isRender } +func updateOnlySetsPipelineCondition(oldObj *api.PackageRevision, newObj *api.PackageRevision) bool { + setsCondition := func() bool { + oldObjHasCondNotPassed := slices.Contains(oldObj.Status.Conditions, conditionPipelineNotPassed) + oldObjHasCondPassed := slices.Contains(oldObj.Status.Conditions, conditionPipelinePassed) + newObjHasCondNotPassed := slices.Contains(newObj.Status.Conditions, conditionPipelineNotPassed) + newObjHasCondPassed := slices.Contains(newObj.Status.Conditions, conditionPipelinePassed) + return (!oldObjHasCondNotPassed && newObjHasCondNotPassed) || + (!oldObjHasCondPassed && newObjHasCondPassed) || + (oldObjHasCondNotPassed && newObjHasCondPassed) || + (oldObjHasCondPassed && newObjHasCondNotPassed) + }() + + noOtherChanges := func() bool { + copyOld := oldObj.DeepCopy() + copyOld.Spec.ReadinessGates = newObj.Spec.ReadinessGates + copyOld.Status.Conditions = newObj.Status.Conditions + + oldJson, _ := json.Marshal(copyOld) + newJson, _ := json.Marshal(newObj) + equalExceptReadinessInfo := reflect.DeepEqual(oldJson, newJson) + return equalExceptReadinessInfo + }() + + return setsCondition && noOtherChanges +} + // applyResourceMutations mutates the resources and returns the most recent renderResult. func applyResourceMutations(ctx context.Context, draft repository.PackageRevisionDraft, baseResources repository.PackageResources, mutations []mutation) (applied repository.PackageResources, renderStatus *api.RenderStatus, err error) { var lastApplied mutation diff --git a/pkg/task/kio.go b/pkg/task/kio.go index 4c94e87f..aaaaf883 100644 --- a/pkg/task/kio.go +++ b/pkg/task/kio.go @@ -51,6 +51,9 @@ func (r *packageReader) Read() ([]*yaml.RNode, error) { kioutil.PathAnnotation: k, }, DisableUnwrapping: true, + // need to preserve indentation to avoid Git conflicts + // in written-out YAML + PreserveSeqIndent: true, } nodes, err := reader.Read() if err != nil { diff --git a/pkg/task/patch_test.go b/pkg/task/patch_test.go index 32961e88..9de49736 100644 --- a/pkg/task/patch_test.go +++ b/pkg/task/patch_test.go @@ -26,7 +26,7 @@ import ( "github.com/nephio-project/porch/pkg/repository/fake" ) -func TestSomething(t *testing.T) { +func TestGenerateKptfilePatches(t *testing.T) { testCases := map[string]struct { repoPkgRev repository.PackageRevision newApiPkgRev *api.PackageRevision @@ -145,12 +145,12 @@ func TestSomething(t *testing.T) { conditions: - type: foo status: "True" -+ reason: reason + message: message ++ reason: reason + - type: bar + status: "False" -+ reason: reason + message: message ++ reason: reason `) + "\n", PatchType: api.PatchTypePatchFile, }, @@ -279,12 +279,12 @@ func TestSomething(t *testing.T) { conditions: - type: foo status: "True" -- reason: reason - message: message +- reason: reason - - type: bar - status: "False" -- reason: reason - message: message +- reason: reason `) + "\n", PatchType: api.PatchTypePatchFile, }, diff --git a/pkg/task/patchgen.go b/pkg/task/patchgen.go index 1046ba0d..458d0b02 100644 --- a/pkg/task/patchgen.go +++ b/pkg/task/patchgen.go @@ -100,22 +100,23 @@ func (m *applyPatchMutation) apply(ctx context.Context, resources repository.Pac return result, nil, fmt.Errorf("patch had unexpected preamble %q", preamble) } - if files[0].OldName != patchSpec.File { + file := files[0] + if file.OldName != patchSpec.File { return result, nil, fmt.Errorf("patch contained unexpected name; got %q, want %q", files[0].OldName, patchSpec.File) } - if files[0].IsBinary { + if file.IsBinary { return result, nil, fmt.Errorf("patch was a binary diff; expected text diff") } - if files[0].IsCopy || files[0].IsDelete || files[0].IsNew || files[0].IsRename { + if file.IsCopy || file.IsDelete || file.IsNew || file.IsRename { return result, nil, fmt.Errorf("patch was of an unexpected type (copy/delete/new/rename)") } - if files[0].OldMode != files[0].NewMode { + if file.OldMode != file.NewMode { return result, nil, fmt.Errorf("patch contained file mode change") } var output bytes.Buffer - if err := gitdiff.Apply(&output, strings.NewReader(oldContents), files[0]); err != nil { - return result, nil, fmt.Errorf("error applying patch: %w", err) + if err := gitdiff.Apply(&output, strings.NewReader(oldContents), file); err != nil { + return result, nil, fmt.Errorf("error applying patch %q to file %q: %w", patchSpec.Contents, oldContents, err) } patched := output.String() diff --git a/pkg/task/testdata/replace/Kptfile b/pkg/task/testdata/replace/Kptfile index 9529df9a..bb3c1ce0 100644 --- a/pkg/task/testdata/replace/Kptfile +++ b/pkg/task/testdata/replace/Kptfile @@ -15,9 +15,9 @@ metadata: pipeline: # Kptfile mutators mutators: - - configMap: - name: updated-bucket-name - namespace: updated-namespace - project-id: updated-project-id - storage-class: updated-storage-class - image: gcr.io/kpt-fn/apply-setters:v0.2.0 + - configMap: + name: updated-bucket-name # line comment + namespace: updated-namespace + project-id: updated-project-id + storage-class: updated-storage-class + image: gcr.io/kpt-fn/apply-setters:v0.2.0 diff --git a/pkg/util/util.go b/pkg/util/util.go index c331b586..06876ce0 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -1,4 +1,4 @@ -// Copyright 2023, 2024 The kpt and Nephio Authors +// Copyright 2023-2024 The kpt and Nephio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import ( "os" "strings" + "github.com/GoogleContainerTools/kpt-functions-sdk/go/fn" porchapi "github.com/nephio-project/porch/api/porch/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -109,3 +110,21 @@ func ParseRepositoryName(name string) (string, error) { } return name[:lastDash], nil } + +// Parses valid YAML into a KubeObject using the kpt-functions-sdk's fn package. +// Wrapped in this function to unify deserialisation into a single approach +// everywhere in Porch. +func YamlToKubeObject(yaml string) (kubeObject *fn.KubeObject, err error) { + if kubeObject, err = fn.ParseKubeObject([]byte(yaml)); err != nil { + return nil, err + } + + return kubeObject, nil +} + +// Writes a KubeObject out to string-form YAML. +// Wrapped in this function to unify serialisation into a single approach +// everywhere in Porch. +func KubeObjectToYaml(kubeObject *fn.KubeObject) string { + return kubeObject.String() +} diff --git a/test/e2e/suite.go b/test/e2e/suite.go index db6c8ffb..3bee1295 100644 --- a/test/e2e/suite.go +++ b/test/e2e/suite.go @@ -795,11 +795,11 @@ func (t *TestSuite) ParseKptfileF(resources *porchapi.PackageRevisionResources) func (t *TestSuite) SaveKptfileF(resources *porchapi.PackageRevisionResources, kptfile *kptfilev1.KptFile) { t.Helper() - b, err := yaml.MarshalWithOptions(kptfile, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle}) + kptfileYaml, err := kptfile.ToYamlString() if err != nil { t.Fatalf("Failed saving Kptfile: %v", err) } - resources.Spec.Resources[kptfilev1.KptFileName] = string(b) + resources.Spec.Resources[kptfilev1.KptFileName] = kptfileYaml } func (t *TestSuite) FindAndDecodeF(resources *porchapi.PackageRevisionResources, name string, value interface{}) {