diff --git a/porch/Makefile b/porch/Makefile index 34b1823fc6..fbdd149f82 100644 --- a/porch/Makefile +++ b/porch/Makefile @@ -49,7 +49,7 @@ TEST_GIT_SERVER_IMAGE ?= test-git-server # Only enable a subset of reconcilers in porch controllers by default. Use the RECONCILERS # env variable to specify a specific list of reconcilers or use # RECONCILERS=* to enable all known reconcilers. -ALL_RECONCILERS="rootsyncsets,remoterootsyncsets,workloadidentitybindings,rootsyncdeployments,functiondiscovery" +ALL_RECONCILERS="rootsyncsets,remoterootsyncsets,workloadidentitybindings,rootsyncdeployments,functiondiscovery,downstreampackages" ifndef RECONCILERS ENABLED_RECONCILERS="rootsyncsets,remoterootsyncsets,workloadidentitybindings,functiondiscovery" else diff --git a/porch/controllers/config/crd/bases/config.porch.kpt.dev_downstreampackages.yaml b/porch/controllers/config/crd/bases/config.porch.kpt.dev_downstreampackages.yaml new file mode 100644 index 0000000000..22848caa93 --- /dev/null +++ b/porch/controllers/config/crd/bases/config.porch.kpt.dev_downstreampackages.yaml @@ -0,0 +1,89 @@ +# Copyright 2022 Google LLC +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: downstreampackages.config.porch.kpt.dev +spec: + group: config.porch.kpt.dev + names: + kind: DownstreamPackage + listKind: DownstreamPackageList + plural: downstreampackages + singular: downstreampackage + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: DownstreamPackage represents an upstream and downstream porch + package pair. The upstream package should already exist. The DownstreamPackage + controller is responsible for creating the downstream package revisions + based on the spec. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: DownstreamPackageSpec defines the desired state of DownstreamPackage + properties: + adoptionPolicy: + type: string + deletionPolicy: + type: string + downstream: + properties: + package: + type: string + repo: + type: string + type: object + upstream: + properties: + package: + type: string + repo: + type: string + revision: + type: string + type: object + type: object + status: + description: DownstreamPackageStatus defines the observed state of DownstreamPackage + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/porch/controllers/downstreampackages/api/v1alpha1/downstreampackage_types.go b/porch/controllers/downstreampackages/api/v1alpha1/downstreampackage_types.go new file mode 100644 index 0000000000..7caaf71674 --- /dev/null +++ b/porch/controllers/downstreampackages/api/v1alpha1/downstreampackage_types.go @@ -0,0 +1,87 @@ +// Copyright 2022 Google LLC +// +// 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// DownstreamPackage represents an upstream and downstream porch package pair. +// The upstream package should already exist. The DownstreamPackage controller is +// responsible for creating the downstream package revisions based on the spec. +type DownstreamPackage struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DownstreamPackageSpec `json:"spec,omitempty"` + Status DownstreamPackageStatus `json:"status,omitempty"` +} + +func (o *DownstreamPackage) GetSpec() *DownstreamPackageSpec { + if o == nil { + return nil + } + return &o.Spec +} + +type AdoptionPolicy string +type DeletionPolicy string + +const ( + AdoptionPolicyAdoptExisting AdoptionPolicy = "adoptExisting" + AdoptionPolicyAdoptNone AdoptionPolicy = "adoptNone" + + DeletionPolicyDelete DeletionPolicy = "delete" + DeletionPolicyOrphan DeletionPolicy = "orphan" +) + +// DownstreamPackageSpec defines the desired state of DownstreamPackage +type DownstreamPackageSpec struct { + Upstream *Upstream `json:"upstream,omitempty"` + Downstream *Downstream `json:"downstream,omitempty"` + + AdoptionPolicy AdoptionPolicy `json:"adoptionPolicy,omitempty"` + DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"` +} + +type Upstream struct { + Repo string `json:"repo,omitempty"` + Package string `json:"package,omitempty"` + Revision string `json:"revision,omitempty"` +} + +type Downstream struct { + Repo string `json:"repo,omitempty"` + Package string `json:"package,omitempty"` +} + +// DownstreamPackageStatus defines the observed state of DownstreamPackage +type DownstreamPackageStatus struct{} + +//+kubebuilder:object:root=true + +// DownstreamPackageList contains a list of DownstreamPackage +type DownstreamPackageList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DownstreamPackage `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DownstreamPackage{}, &DownstreamPackageList{}) +} diff --git a/porch/controllers/downstreampackages/api/v1alpha1/groupversion_info.go b/porch/controllers/downstreampackages/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000000..aa52eab1de --- /dev/null +++ b/porch/controllers/downstreampackages/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +// Copyright 2022 Google LLC +// +// 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 v1alpha1 contains API Schema definitions for the config.porch.kpt.dev v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=config.porch.kpt.dev +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0 object object:headerFile="../../../../scripts/boilerplate.go.txt" paths="./..." + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "config.porch.kpt.dev", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/porch/controllers/downstreampackages/api/v1alpha1/zz_generated.deepcopy.go b/porch/controllers/downstreampackages/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..4b1f2d743d --- /dev/null +++ b/porch/controllers/downstreampackages/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,153 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2022 Google LLC +// +// 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. + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Downstream) DeepCopyInto(out *Downstream) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Downstream. +func (in *Downstream) DeepCopy() *Downstream { + if in == nil { + return nil + } + out := new(Downstream) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DownstreamPackage) DeepCopyInto(out *DownstreamPackage) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DownstreamPackage. +func (in *DownstreamPackage) DeepCopy() *DownstreamPackage { + if in == nil { + return nil + } + out := new(DownstreamPackage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DownstreamPackage) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DownstreamPackageList) DeepCopyInto(out *DownstreamPackageList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DownstreamPackage, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DownstreamPackageList. +func (in *DownstreamPackageList) DeepCopy() *DownstreamPackageList { + if in == nil { + return nil + } + out := new(DownstreamPackageList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DownstreamPackageList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DownstreamPackageSpec) DeepCopyInto(out *DownstreamPackageSpec) { + *out = *in + if in.Upstream != nil { + in, out := &in.Upstream, &out.Upstream + *out = new(Upstream) + **out = **in + } + if in.Downstream != nil { + in, out := &in.Downstream, &out.Downstream + *out = new(Downstream) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DownstreamPackageSpec. +func (in *DownstreamPackageSpec) DeepCopy() *DownstreamPackageSpec { + if in == nil { + return nil + } + out := new(DownstreamPackageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DownstreamPackageStatus) DeepCopyInto(out *DownstreamPackageStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DownstreamPackageStatus. +func (in *DownstreamPackageStatus) DeepCopy() *DownstreamPackageStatus { + if in == nil { + return nil + } + out := new(DownstreamPackageStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Upstream) DeepCopyInto(out *Upstream) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Upstream. +func (in *Upstream) DeepCopy() *Upstream { + if in == nil { + return nil + } + out := new(Upstream) + in.DeepCopyInto(out) + return out +} diff --git a/porch/controllers/downstreampackages/config/rbac/role.yaml b/porch/controllers/downstreampackages/config/rbac/role.yaml new file mode 100644 index 0000000000..4a90e3a8e5 --- /dev/null +++ b/porch/controllers/downstreampackages/config/rbac/role.yaml @@ -0,0 +1,47 @@ +# Copyright 2022 Google LLC +# +# 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. + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: porch-controllers-downstreampackages +rules: +- apiGroups: + - config.porch.kpt.dev + resources: + - downstreampackages + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - config.porch.kpt.dev + resources: + - downstreampackages/finalizers + verbs: + - update +- apiGroups: + - config.porch.kpt.dev + resources: + - downstreampackages/status + verbs: + - get + - patch + - update diff --git a/porch/controllers/downstreampackages/config/rbac/rolebinding.yaml b/porch/controllers/downstreampackages/config/rbac/rolebinding.yaml new file mode 100644 index 0000000000..a89009c671 --- /dev/null +++ b/porch/controllers/downstreampackages/config/rbac/rolebinding.yaml @@ -0,0 +1,26 @@ +# Copyright 2022 Google LLC +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: porch-system:porch-controllers-downstreampackages +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: porch-controllers-downstreampackages +subjects: +- kind: ServiceAccount + name: porch-controllers + namespace: porch-system \ No newline at end of file diff --git a/porch/controllers/downstreampackages/config/samples/dp.yaml b/porch/controllers/downstreampackages/config/samples/dp.yaml new file mode 100644 index 0000000000..e1964af5a0 --- /dev/null +++ b/porch/controllers/downstreampackages/config/samples/dp.yaml @@ -0,0 +1,28 @@ +# Copyright 2022 Google LLC +# +# 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. + +apiVersion: config.porch.kpt.dev/v1alpha1 +kind: DownstreamPackage +metadata: + name: my-dp + namespace: default +spec: + upstream: + repo: blueprints + package: foo + revision: v3 + downstream: + repo: deployments + package: bar + diff --git a/porch/controllers/downstreampackages/pkg/controllers/downstreampackage/downstreampackage_controller.go b/porch/controllers/downstreampackages/pkg/controllers/downstreampackage/downstreampackage_controller.go new file mode 100644 index 0000000000..a9d4e54611 --- /dev/null +++ b/porch/controllers/downstreampackages/pkg/controllers/downstreampackage/downstreampackage_controller.go @@ -0,0 +1,351 @@ +// Copyright 2022 Google LLC +// +// 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 downstreampackage + +import ( + "context" + "flag" + "fmt" + "strconv" + "strings" + + porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" + api "github.com/GoogleContainerTools/kpt/porch/controllers/downstreampackages/api/v1alpha1" + "golang.org/x/mod/semver" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + configapi "github.com/GoogleContainerTools/kpt/porch/api/porchconfig/v1alpha1" +) + +type Options struct{} + +func (o *Options) InitDefaults() {} +func (o *Options) BindFlags(_ string, _ *flag.FlagSet) {} + +// DownstreamPackageReconciler reconciles a DownstreamPackage object +type DownstreamPackageReconciler struct { + client.Client + Options +} + +const ( + workspaceNamePrefix = "downstreampackage-" +) + +//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0 rbac:roleName=porch-controllers-downstreampackages webhook paths="." output:rbac:artifacts:config=../../../config/rbac + +//+kubebuilder:rbac:groups=config.porch.kpt.dev,resources=downstreampackages,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=config.porch.kpt.dev,resources=downstreampackages/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=config.porch.kpt.dev,resources=downstreampackages/finalizers,verbs=update + +// Reconcile implements the main kubernetes reconciliation loop. +func (r *DownstreamPackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + dp, prList, err := r.init(ctx, req) + if err != nil { + return ctrl.Result{}, err + } + if dp == nil { + // maybe the dp was deleted + return ctrl.Result{}, nil + } + + upstream := r.getUpstreamPR(dp.Spec.Upstream, prList) + if upstream == nil { + return ctrl.Result{}, fmt.Errorf("could not find upstream package revision") + } + + if err := r.ensureDownstreamPackage(ctx, dp, upstream, prList); err != nil { + return ctrl.Result{}, err + } + + // TODO: Prune (propose deletion of) deployment packages created by this controller + // that are no longer needed. Part of this will be to implement the DeletionPolicy + // field, which will allow the user to specify whether to "orphan" or "delete". + + return ctrl.Result{}, nil +} + +func (r *DownstreamPackageReconciler) init(ctx context.Context, + req ctrl.Request) (*api.DownstreamPackage, *porchapi.PackageRevisionList, error) { + var dp api.DownstreamPackage + if err := r.Client.Get(ctx, req.NamespacedName, &dp); err != nil { + return nil, nil, client.IgnoreNotFound(err) + } + + var prList porchapi.PackageRevisionList + if err := r.Client.List(ctx, &prList, client.InNamespace(dp.Namespace)); err != nil { + return nil, nil, err + } + + return &dp, &prList, nil +} + +func (r *DownstreamPackageReconciler) getUpstreamPR(upstream *api.Upstream, + prList *porchapi.PackageRevisionList) *porchapi.PackageRevision { + for _, pr := range prList.Items { + if pr.Spec.RepositoryName == upstream.Repo && + pr.Spec.PackageName == upstream.Package && + pr.Spec.Revision == upstream.Revision { + return &pr + } + } + return nil +} + +// ensureDownstreamPackage needs to: +// - Check if the downstream package revision already exists. If not, create it. +// - If it does already exist, we need to make sure it is up-to-date. If there is +// a downstream package draft, we look at the draft. Otherwise, we look at the latest +// published downstream package revision. +// - Compare pd.Spec.Upstream.Revision to the revision number that the downstream +// package is based on. If it is different, we need to do an update (could be an upgrade +// or a downgrade). +func (r *DownstreamPackageReconciler) ensureDownstreamPackage(ctx context.Context, + dp *api.DownstreamPackage, + upstream *porchapi.PackageRevision, + prList *porchapi.PackageRevisionList) error { + existing, err := r.findAndUpdateExistingRevision(ctx, dp, upstream, prList) + if err != nil { + return err + } + if existing != nil { + return nil + } + + // No downstream package created by this controller exists. Create one. + tr := true + newPR := &porchapi.PackageRevision{ + TypeMeta: metav1.TypeMeta{ + Kind: "PackageRevision", + APIVersion: porchapi.SchemeGroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: dp.Namespace, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: dp.APIVersion, + Kind: dp.Kind, + Name: dp.Name, + UID: dp.UID, + Controller: &tr, + BlockOwnerDeletion: nil, + }}, + }, + Spec: porchapi.PackageRevisionSpec{ + PackageName: dp.Spec.Downstream.Package, + WorkspaceName: porchapi.WorkspaceName(workspaceNamePrefix + "1"), + RepositoryName: dp.Spec.Downstream.Repo, + Tasks: []porchapi.Task{ + { + Type: porchapi.TaskTypeClone, + Clone: &porchapi.PackageCloneTaskSpec{ + Upstream: porchapi.UpstreamPackage{ + UpstreamRef: &porchapi.PackageRevisionRef{ + Name: upstream.Name, + }, + }, + }, + }, + }, + }, + } + + if err := r.Client.Create(ctx, newPR); err != nil { + return err + } + + return nil +} + +func (r *DownstreamPackageReconciler) findAndUpdateExistingRevision(ctx context.Context, + dp *api.DownstreamPackage, + upstream *porchapi.PackageRevision, + prList *porchapi.PackageRevisionList) (*porchapi.PackageRevision, error) { + // First, check if a downstream package exists. If not, just return nil. The + // caller will create one. + downstream := r.getDownstreamPR(dp, prList) + if downstream == nil { + return nil, nil + } + + // Determine if the downstream package needs to be updated. If not, return + // the downstream package as-is. + if r.isUpToDate(dp, downstream) { + return downstream, nil + } + + if downstream.Spec.Lifecycle == porchapi.PackageRevisionLifecyclePublished { + var err error + downstream, err = r.copyPublished(ctx, downstream, dp) + if err != nil { + return nil, err + } + } + + return r.updateDraft(ctx, downstream, upstream) +} + +func (r *DownstreamPackageReconciler) getDownstreamPR(dp *api.DownstreamPackage, + prList *porchapi.PackageRevisionList) *porchapi.PackageRevision { + downstream := dp.Spec.Downstream + + var latestPublished *porchapi.PackageRevision + // the first package revision number that porch assigns is "v1", + // so use v0 as a placeholder for comparison + latestVersion := "v0" + + for _, pr := range prList.Items { + // look for the downstream package in the target repo + if pr.Spec.RepositoryName != downstream.Repo || + pr.Spec.PackageName != downstream.Package { + continue + } + // check that the downstream package was created by this DownstreamPackage object + owned := false + + // TODO: Implement the "AdoptionPolicy" field, which allows the user to decide if + // the controller should adopt existing package revisions or ignore them. For now, + // we just ignore them. + for _, owner := range pr.OwnerReferences { + if owner.UID == dp.UID { + owned = true + } + } + if !owned { + // this downstream package doesn't belong to us + continue + } + + // Check if this PR is a draft. We should only have one draft created by this controller at a time, + // so we can just return it. + if pr.Spec.Lifecycle != porchapi.PackageRevisionLifecyclePublished { + return &pr + } else { + latestPublished, latestVersion = compare(&pr, latestPublished, latestVersion) + } + } + + return latestPublished +} + +func compare(pr, latestPublished *porchapi.PackageRevision, latestVersion string) (*porchapi.PackageRevision, string) { + switch cmp := semver.Compare(pr.Spec.Revision, latestVersion); { + case cmp == 0: + // Same revision. + case cmp < 0: + // current < latest; no change + case cmp > 0: + // current > latest; update latest + latestVersion = pr.Spec.Revision + latestPublished = pr.DeepCopy() + } + return latestPublished, latestVersion +} + +// determine if the downstream PR needs to be updated +func (r *DownstreamPackageReconciler) isUpToDate(dp *api.DownstreamPackage, downstream *porchapi.PackageRevision) bool { + upstreamLock := downstream.Status.UpstreamLock + lastIndex := strings.LastIndex(upstreamLock.Git.Ref, "/") + if strings.HasPrefix(upstreamLock.Git.Ref, "drafts") { + // the current upstream is a draft, so it needs to be updated. + return false + } + currentUpstreamRevision := upstreamLock.Git.Ref[lastIndex+1:] + return currentUpstreamRevision == dp.Spec.Upstream.Revision +} + +func (r *DownstreamPackageReconciler) copyPublished(ctx context.Context, + source *porchapi.PackageRevision, + dp *api.DownstreamPackage) (*porchapi.PackageRevision, error) { + tr := true + newPR := &porchapi.PackageRevision{ + TypeMeta: metav1.TypeMeta{ + Kind: "PackageRevision", + APIVersion: porchapi.SchemeGroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: source.Namespace, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: dp.APIVersion, + Kind: dp.Kind, + Name: dp.Name, + UID: dp.UID, + Controller: &tr, + BlockOwnerDeletion: nil, + }}, + }, + Spec: source.Spec, + } + + newPR.Spec.Revision = "" + newPR.Spec.WorkspaceName = newWorkspaceName(newPR.Spec.WorkspaceName) + newPR.Spec.Lifecycle = porchapi.PackageRevisionLifecycleDraft + + if err := r.Client.Create(ctx, newPR); err != nil { + return nil, err + } + + return newPR, nil +} + +func newWorkspaceName(oldWorkspaceName porchapi.WorkspaceName) porchapi.WorkspaceName { + wsNumStr := strings.TrimPrefix(string(oldWorkspaceName), workspaceNamePrefix) + wsNum, _ := strconv.Atoi(wsNumStr) + wsNum++ + return porchapi.WorkspaceName(fmt.Sprintf(workspaceNamePrefix+"%d", wsNum)) +} + +func (r *DownstreamPackageReconciler) updateDraft(ctx context.Context, + draft *porchapi.PackageRevision, + newUpstreamPR *porchapi.PackageRevision) (*porchapi.PackageRevision, error) { + + draft = draft.DeepCopy() + tasks := draft.Spec.Tasks + + updateTask := porchapi.Task{ + Type: porchapi.TaskTypeUpdate, + Update: &porchapi.PackageUpdateTaskSpec{ + Upstream: tasks[0].Clone.Upstream, + }, + } + updateTask.Update.Upstream.UpstreamRef.Name = newUpstreamPR.Name + draft.Spec.Tasks = append(tasks, updateTask) + + err := r.Client.Update(ctx, draft) + if err != nil { + return nil, err + } + return draft, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DownstreamPackageReconciler) SetupWithManager(mgr ctrl.Manager) error { + if err := api.AddToScheme(mgr.GetScheme()); err != nil { + return err + } + if err := porchapi.AddToScheme(mgr.GetScheme()); err != nil { + return err + } + if err := configapi.AddToScheme(mgr.GetScheme()); err != nil { + return err + } + + r.Client = mgr.GetClient() + + return ctrl.NewControllerManagedBy(mgr). + For(&api.DownstreamPackage{}). + Complete(r) +} diff --git a/porch/controllers/main.go b/porch/controllers/main.go index 9514622952..d56b19b542 100644 --- a/porch/controllers/main.go +++ b/porch/controllers/main.go @@ -39,6 +39,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/GoogleContainerTools/kpt/porch/controllers/downstreampackages/pkg/controllers/downstreampackage" "github.com/GoogleContainerTools/kpt/porch/controllers/functiondiscovery" "github.com/GoogleContainerTools/kpt/porch/controllers/klippy/pkg/controllers/klippy" "github.com/GoogleContainerTools/kpt/porch/controllers/remoterootsyncsets/pkg/controllers/remoterootsyncset" @@ -51,6 +52,7 @@ import ( var ( reconcilers = map[string]Reconciler{ + "downstreampackages": &downstreampackage.DownstreamPackageReconciler{}, "rootsyncsets": &rootsyncset.RootSyncSetReconciler{}, "remoterootsyncsets": &remoterootsyncset.RemoteRootSyncSetReconciler{}, "workloadidentitybindings": &workloadidentitybinding.WorkloadIdentityBindingReconciler{},