diff --git a/docs/design-docs/08-package-variant.md b/docs/design-docs/08-package-variant.md index 3c0e516273..8c500b9b1c 100644 --- a/docs/design-docs/08-package-variant.md +++ b/docs/design-docs/08-package-variant.md @@ -914,45 +914,89 @@ template API is shown below. ```go type PackageVariantTemplate struct { - Downstream *pkgvarapi.Downstream `json:"downstream,omitempty"` - DownstreamExprs *DownstreamExprs `json:"downstreamExprs,omitempty"` + // Downstream allows overriding the default downstream package and repository name + // +optional + Downstream *DownstreamTemplate `json:"downstream,omitempty"` + // AdoptionPolicy allows overriding the PackageVariant adoption policy + // +optional AdoptionPolicy *pkgvarapi.AdoptionPolicy `json:"adoptionPolicy,omitempty"` + + // DeletionPolicy allows overriding the PackageVariant deletion policy + // +optional DeletionPolicy *pkgvarapi.DeletionPolicy `json:"deletionPolicy,omitempty"` - Labels map[string]string `json:"labels,omitempty"` - LabelExprs []MapExpr `json:"labelExprs,omitemtpy"` + // Labels allows specifying the spec.Labels field of the generated PackageVariant + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // LabelsExprs allows specifying the spec.Labels field of the generated PackageVariant + // using CEL to dynamically create the keys and values. Entries in this field take precedent over + // those with the same keys that are present in Labels. + // +optional + LabelExprs []MapExpr `json:"labelExprs,omitemtpy"` - Annotations map[string]string `json:"annotations,omitempty"` - AnnotationExprs []MapExpr `json:"annotationExprs,omitempty"` + // Annotations allows specifying the spec.Annotations field of the generated PackageVariant + // +optional + Annotations map[string]string `json:"annotations,omitempty"` - PackageContext map[string]string `json:"packageContext,omitempty"` - PackageContextExprs *PackageContextExprs `json:"packageContextExprs,omitempty"` + // AnnotationsExprs allows specifying the spec.Annotations field of the generated PackageVariant + // using CEL to dynamically create the keys and values. Entries in this field take precedent over + // those with the same keys that are present in Annotations. + // +optional + AnnotationExprs []MapExpr `json:"annotationExprs,omitempty"` + // PackageContext allows specifying the spec.PackageContext field of the generated PackageVariant + // +optional + PackageContext *PackageContextTemplate `json:"packageContext,omitempty"` + + // Pipeline allows specifying the spec.Pipeline field of the generated PackageVariant + // +optional Pipeline *kptfilev1.Pipeline `json:"pipeline,omitempty"` - Injectors []pkgvarapi.InjectionSelector `json:"injectors,omitempty"` - InjectorExprs []InjectionSelectorExprs `json:"injectorExprs,omitempty"` + // Injectors allows specifying the spec.Injectors field of the generated PackageVariant + // +optional + Injectors *InfectionSelectorTemplate `json:"injectors,omitempty"` } -type DownstreamExprs struct { +// DownstreamTemplate is used to calculate the downstream field of the resulting +// package variants. Only one of Repo and RepoExpr may be specified; +// similarly only one of Package and PackageExpr may be specified. +type DownstreamTemplate struct { + Repo *string `json:"repo,omitempty"` + Package *string `json:"package,omitempty"` RepoExpr *string `json:"repoExpr,omitempty"` PackageExpr *string `json:"packageExpr,omitempty"` } -type PackageContextExprs struct { - DataExprs []MapExpr `json:"dataExprs,omitempty"` - RemoveKeyExprs []string `json:"removeKeyExprs,omitempty"` +// PackageContextTemplate is used to calculate the packageContext field of the +// resulting package variants. The plain fields and Exprs fields will be +// merged, with the Exprs fields taking precedence. +type PackageContextTemplate struct { + Data map[string]string `json:"data,omitempty"` + RemoveKeys []string `json:"removeKeys,omitempty"` + DataExprs []MapExpr `json:"dataExprs,omitempty"` + RemoveKeyExprs []string `json:"removeKeyExprs,omitempty"` } -type InjectionSelectorExprs struct { - GroupExpr *string `json:"groupExpr,omitempty"` - VersionExpr *string `json:"versionExpr,omitempty"` - KindExpr *string `json:"kindExpr,omitempty"` - NameExpr string `json:"nameExpr"` +// InjectionSelectorTemplate is used to calculate the injectors field of the +// resulting package variants. Only one of the Name and NameExpr fields may be +// specified. +type InjectionSelectorTemplate struct { + Group *string `json:"group,omitempty"` + Version *string `json:"version,omitempty"` + Kind *string `json:"kind,omitempty"` + Name *string `json:"name,omitempty"` + + NameExpr *string `json:"nameExpr,omitempty"` } +// MapExpr is used for various fields to calculate map entries. Only one of +// Key and KeyExpr may be specified; similarly only on of Value and ValueExpr +// may be specified. type MapExpr struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` KeyExpr *string `json:"keyExpr,omitempty"` ValueExpr *string `json:"valueExpr,omitempty"` } @@ -960,10 +1004,9 @@ type MapExpr struct { This is a pretty complicated structure. To make it more understandable, the first thing to notice is that many fields have a plain version, and an `Expr` -version. Only one of these should be used for any given field. For example, you -can use either `downstream` or `downstreamExpr`, but not both. The plain -version is used when the value is static across all the PackageVariants; the -`Expr` version is used when the value needs to vary across PackageVariants. +version. The plain version is used when the value is static across all the +PackageVariants; the `Expr` version is used when the value needs to vary across +PackageVariants. Let's consider a simple example. Suppose we have a package for provisioning namespaces called "base-ns". We want to instantiate this several times in the @@ -1108,7 +1151,7 @@ spec: org: hr template: labelExprs: - keyExpr: "'org'" + key: org valueExpr: "repository.labels['org']" injectorExprs: - nameExpr: "repository.labels['region'] + '-endpoints'" @@ -1183,16 +1226,16 @@ them. That is, there are certain objects that are accessible within the CEL expression. For CEL expressions used in the PackageVariantSet `template` field, the following variables are available: -| CEL Variable | Variable Contents | -| -------------| ------------------------------------------------------------ | -| repo | The default repository name based on the targeting criteria. | -| package | The default package name based on the targeting criteria. | -| upstream | The upstream PackageRevision. | -| repository | The downstream Repository. | +| CEL Variable | Variable Contents | +| -------------- | ------------------------------------------------------------ | +| repoDefault | The default repository name based on the targeting criteria. | +| packageDefault | The default package name based on the targeting criteria. | +| upstream | The upstream PackageRevision. | +| repository | The downstream Repository. | There is one expression that is an exception to the table above. Since the `repository` value corresponds to the Repository of the downstream, we must -first evaluate the `downstreamExpr.repoExpr` expression to *find* that +first evaluate the `downstream.repoExpr` expression to *find* that repository. Thus, for that expression only, `repository` is not a valid variable. @@ -1202,10 +1245,16 @@ target, as follows: | Target Type | `target` Variable Contents | | ------------------- | -------------------------- | -| Repo/Package List | A struct with two fields: `name` and `packageName`, the same as the `repo` and `package` values. | -| Repository Selector | The Repository selected by the selector. Although not recommended, this could be different than the `repository` value, which can be altered with `downstream.repo` or `downstreamExprs.repoExpr`. | +| Repo/Package List | A struct with two fields: `name` and `packageName`, the same as the `repoDefault` and `packageDefault` values. | +| Repository Selector | The Repository selected by the selector. Although not recommended, this could be different than the `repository` value, which can be altered with `downstream.repo` or `downstream.repoExpr`. | | Object Selector | The Object selected by the selector. | +For the various resource variables - `upstream`, `repository`, and `target` - +arbitrary access to all fields of the object could lead to security concerns. +Therefore, only a subset of the data is available for use in CEL exressions. +Specifically, the following fields: `name`, `namespace`, `labels`, and +`annotations`. + Given the slight quirk with the `repoExpr`, it may be helpful to state the processing flow for the template evaluation: @@ -1213,15 +1262,15 @@ processing flow for the template evaluation: the PackageVariantSet[^multi-ns-reg]. 1. The targets are determined. 1. For each target: - 1. The CEL environment is prepared with `repo`, `package`, `upstream`, and - `target` variables. + 1. The CEL environment is prepared with `repoDefault`, `packageDefault`, + `upstream`, and `target` variables. 1. The downstream repository is determined and loaded, as follows: - - If present, `downstreamExprs.repoExpr` is evaluated using the CEL + - If present, `downstream.repoExpr` is evaluated using the CEL environment, and the result used as the downstream repository name. - Otherwise, if `downstream.repo` is set, that is used as the downstream repository name. - If neither is present, the default repository name based on the target is - used (i.e., the same value as the `repo` variable). + used (i.e., the same value as the `repoDefault` variable). - The resulting downstream repository name is used to load the corresponding Repository object in the same namespace as the PackageVariantSet. 1. The downstream Repository is added to the CEL environment. diff --git a/porch/controllers/config/crd/bases/config.porch.kpt.dev_packagevariantsets.yaml b/porch/controllers/config/crd/bases/config.porch.kpt.dev_packagevariantsets.yaml index 8a1b669bfb..216761c98e 100644 --- a/porch/controllers/config/crd/bases/config.porch.kpt.dev_packagevariantsets.yaml +++ b/porch/controllers/config/crd/bases/config.porch.kpt.dev_packagevariantsets.yaml @@ -11,7 +11,6 @@ # 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 @@ -317,6 +316,346 @@ spec: type: array type: object type: object + served: false + storage: false + subresources: + status: {} + - name: v1alpha2 + schema: + openAPIV3Schema: + description: PackageVariantSet represents an upstream package revision and + a way to target specific downstream repositories where a variant of the + upstream package should be created. + 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: PackageVariantSetSpec defines the desired state of PackageVariantSet + properties: + targets: + items: + properties: + objectSelector: + description: 'option 3: a selector against a set of arbitrary + objects' + properties: + apiVersion: + description: APIVersion of the target resources + type: string + kind: + description: Kind of the target resources + type: string + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists or + DoesNotExist, the values array must be empty. This + array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is + "key", the operator is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + name: + description: Name of the target resource + type: string + type: object + repositories: + description: 'Exactly one of Repositories, RepositorySeletor, + and ObjectSelector must be populated option 1: an explicit + repositories and package names' + items: + properties: + name: + description: Name contains the name of the Repository + resource, which must be in the same namespace as the + PackageVariantSet resource. + type: string + packageNames: + description: PackageNames contains names to use for package + instances in this repository; that is, the same upstream + will be instantiated multiple times using these names. + items: + type: string + type: array + required: + - name + type: object + type: array + repositorySelector: + description: 'option 2: a label selector against a set of repositories' + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists or + DoesNotExist, the values array must be empty. This + array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is + "key", the operator is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + template: + description: Template specifies how to generate a PackageVariant + from a target + properties: + adoptionPolicy: + description: AdoptionPolicy allows overriding the PackageVariant + adoption policy + type: string + annotationExprs: + description: AnnotationsExprs allows specifying the spec.Annotations + field of the generated PackageVariant using CEL to dynamically + create the keys and values. Entries in this field take + precedent over those with the same keys that are present + in Annotations. + items: + properties: + key: + type: string + keyExpr: + type: string + value: + type: string + valueExpr: + type: string + type: object + type: array + annotations: + additionalProperties: + type: string + description: Annotations allows specifying the spec.Annotations + field of the generated PackageVariant + type: object + deletionPolicy: + description: DeletionPolicy allows overriding the PackageVariant + deletion policy + type: string + downstream: + description: Downstream allows overriding the default downstream + package and repository name + properties: + package: + type: string + packageExpr: + type: string + repo: + type: string + repoExpr: + type: string + type: object + labelExprs: + description: LabelsExprs allows specifying the spec.Labels + field of the generated PackageVariant using CEL to dynamically + create the keys and values. Entries in this field take + precedent over those with the same keys that are present + in Labels. + items: + properties: + key: + type: string + keyExpr: + type: string + value: + type: string + valueExpr: + type: string + type: object + type: array + labels: + additionalProperties: + type: string + description: Labels allows specifying the spec.Labels field + of the generated PackageVariant + type: object + packageContext: + description: PackageContext allows specifying the spec.PackageContext + field of the generated PackageVariant + properties: + data: + additionalProperties: + type: string + type: object + dataExprs: + items: + properties: + key: + type: string + keyExpr: + type: string + value: + type: string + valueExpr: + type: string + type: object + type: array + removeKeyExprs: + items: + type: string + type: array + removeKeys: + items: + type: string + type: array + type: object + type: object + type: object + type: array + upstream: + properties: + package: + type: string + repo: + type: string + revision: + type: string + type: object + type: object + status: + description: PackageVariantSetStatus defines the observed state of PackageVariantSet + properties: + conditions: + description: Conditions describes the reconciliation state of the + object. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object served: true storage: true subresources: diff --git a/porch/controllers/go.mod b/porch/controllers/go.mod index d0d1c5de2b..963ec1bbb0 100644 --- a/porch/controllers/go.mod +++ b/porch/controllers/go.mod @@ -15,6 +15,7 @@ require ( github.com/GoogleContainerTools/kpt-functions-sdk/go/fn v0.0.0-20230427202446-3255accc518d github.com/GoogleContainerTools/kpt/porch v0.0.0-00010101000000-000000000000 github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20230121152246-dc44dbd18a33 + github.com/google/cel-go v0.14.0 github.com/google/go-containerregistry v0.14.0 github.com/stretchr/testify v1.8.1 go.opentelemetry.io/otel v0.20.0 @@ -28,6 +29,7 @@ require ( k8s.io/apimachinery v0.25.4 k8s.io/client-go v0.25.3 k8s.io/klog/v2 v2.90.1 + k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2 sigs.k8s.io/cli-utils v0.34.0 sigs.k8s.io/controller-runtime v0.13.1 sigs.k8s.io/kustomize/kyaml v0.13.9 @@ -48,9 +50,8 @@ require ( github.com/GoogleContainerTools/kpt-functions-sdk/go/api v0.0.0-20220720212527-133180134b93 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/acomagu/bufpipe v1.0.4-0.20210605013841-cd7a5f79d3c4 // indirect - github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bytecodealliance/wasmtime-go v0.39.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect @@ -96,7 +97,6 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -108,7 +108,6 @@ require ( github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prep/wasmexec v0.0.0-20220807105708-6554945c1dec // indirect github.com/prometheus/client_golang v1.12.2 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect @@ -117,6 +116,7 @@ require ( github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/cobra v1.6.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect github.com/vbatts/tar-split v0.11.2 // indirect github.com/xlab/treeprint v1.1.0 // indirect go.opencensus.io v0.24.0 // indirect @@ -142,7 +142,6 @@ require ( k8s.io/component-base v0.25.3 // indirect k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596 // indirect k8s.io/kubectl v0.25.3 // indirect - k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect sigs.k8s.io/kustomize/api v0.12.1 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect diff --git a/porch/controllers/go.sum b/porch/controllers/go.sum index c41632229d..83ba4153fc 100644 --- a/porch/controllers/go.sum +++ b/porch/controllers/go.sum @@ -80,17 +80,15 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/bytecodealliance/wasmtime-go v0.39.0 h1:35AXy5+py5ZXRSpfoxqh+dWJ7nJnIrW1avjDfaJinxU= -github.com/bytecodealliance/wasmtime-go v0.39.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -216,6 +214,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.14.0 h1:LFobwuUDslWUHdQ48SXVXvQgPH2X1XVhsgOGNioAEZ4= +github.com/google/cel-go v0.14.0/go.mod h1:YzWEoI07MC/a/wj9in8GeVatqfypkldgBlwXh9bCwqY= github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -313,8 +313,6 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= @@ -351,8 +349,6 @@ github.com/platkrm/go-git/v5 v5.4.3-0.20220410165046-c76b262044ce h1:HuO44EsG+4+ github.com/platkrm/go-git/v5 v5.4.3-0.20220410165046-c76b262044ce/go.mod h1:U7oc8MDRtQhVD6StooNkBMVsh/Y4J/2Vl36Mo4IclvM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prep/wasmexec v0.0.0-20220807105708-6554945c1dec h1:Yz85KaIsPzF0YRrznNaD35o4GEk65/3mqio3m9sgqi4= -github.com/prep/wasmexec v0.0.0-20220807105708-6554945c1dec/go.mod h1:/AG7CoBOwtk42jdCTLbd280PA4h50oaFK88jee+BaVA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= @@ -397,6 +393,7 @@ github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/porch/controllers/packagevariantsets/api/v1alpha2/groupversion_info.go b/porch/controllers/packagevariantsets/api/v1alpha2/groupversion_info.go new file mode 100644 index 0000000000..44f22a354e --- /dev/null +++ b/porch/controllers/packagevariantsets/api/v1alpha2/groupversion_info.go @@ -0,0 +1,36 @@ +// Copyright 2023 The kpt 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 v1alpha1 contains API Schema definitions for the config.porch.kpt.dev v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=config.porch.kpt.dev +package v1alpha2 + +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: "v1alpha2"} + + // 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/packagevariantsets/api/v1alpha2/packagevariantset_types.go b/porch/controllers/packagevariantsets/api/v1alpha2/packagevariantset_types.go new file mode 100644 index 0000000000..7d13cae770 --- /dev/null +++ b/porch/controllers/packagevariantsets/api/v1alpha2/packagevariantset_types.go @@ -0,0 +1,201 @@ +// Copyright 2023 The kpt 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 v1alpha2 + +import ( + //kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + pkgvarapi "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariants/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +// PackageVariantSet represents an upstream package revision and a way to +// target specific downstream repositories where a variant of the upstream +// package should be created. +type PackageVariantSet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PackageVariantSetSpec `json:"spec,omitempty"` + Status PackageVariantSetStatus `json:"status,omitempty"` +} + +func (o *PackageVariantSet) GetSpec() *PackageVariantSetSpec { + if o == nil { + return nil + } + return &o.Spec +} + +// PackageVariantSetSpec defines the desired state of PackageVariantSet +type PackageVariantSetSpec struct { + Upstream *pkgvarapi.Upstream `json:"upstream,omitempty"` + Targets []Target `json:"targets,omitempty"` +} + +type Target struct { + // Exactly one of Repositories, RepositorySeletor, and ObjectSelector must be + // populated + // option 1: an explicit repositories and package names + Repositories []RepositoryTarget `json:"repositories,omitempty"` + + // option 2: a label selector against a set of repositories + RepositorySelector *metav1.LabelSelector `json:"repositorySelector,omitempty"` + + // option 3: a selector against a set of arbitrary objects + ObjectSelector *ObjectSelector `json:"objectSelector,omitempty"` + + // Template specifies how to generate a PackageVariant from a target + Template *PackageVariantTemplate `json:"template,omitempty"` +} + +type RepositoryTarget struct { + // Name contains the name of the Repository resource, which must be in + // the same namespace as the PackageVariantSet resource. + // +required + Name string `json:"name"` + + // PackageNames contains names to use for package instances in this repository; + // that is, the same upstream will be instantiated multiple times using these names. + // +optional + PackageNames []string `json:"packageNames,omitempty"` +} + +type ObjectSelector struct { + metav1.LabelSelector `json:",inline"` + + // APIVersion of the target resources + APIVersion string `yaml:"apiVersion,omitempty" json:"apiVersion,omitempty"` + + // Kind of the target resources + Kind string `yaml:"kind,omitempty" json:"kind,omitempty"` + + // Name of the target resource + // +optional + Name *string `yaml:"name,omitempty" json:"name,omitempty"` + + // Note: while v1alpha1 had Namespace, that is not allowed; the namespace + // must match the namespace of the PackageVariantSet resource +} + +type PackageVariantTemplate struct { + // Downstream allows overriding the default downstream package and repository name + // +optional + Downstream *DownstreamTemplate `json:"downstream,omitempty"` + + // AdoptionPolicy allows overriding the PackageVariant adoption policy + // +optional + AdoptionPolicy *pkgvarapi.AdoptionPolicy `json:"adoptionPolicy,omitempty"` + + // DeletionPolicy allows overriding the PackageVariant deletion policy + // +optional + DeletionPolicy *pkgvarapi.DeletionPolicy `json:"deletionPolicy,omitempty"` + + // Labels allows specifying the spec.Labels field of the generated PackageVariant + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // LabelsExprs allows specifying the spec.Labels field of the generated PackageVariant + // using CEL to dynamically create the keys and values. Entries in this field take precedent over + // those with the same keys that are present in Labels. + // +optional + LabelExprs []MapExpr `json:"labelExprs,omitemtpy"` + + // Annotations allows specifying the spec.Annotations field of the generated PackageVariant + // +optional + Annotations map[string]string `json:"annotations,omitempty"` + + // AnnotationsExprs allows specifying the spec.Annotations field of the generated PackageVariant + // using CEL to dynamically create the keys and values. Entries in this field take precedent over + // those with the same keys that are present in Annotations. + // +optional + AnnotationExprs []MapExpr `json:"annotationExprs,omitempty"` + + // PackageContext allows specifying the spec.PackageContext field of the generated PackageVariant + // +optional + PackageContext *PackageContextTemplate `json:"packageContext,omitempty"` + + // Pipeline allows specifying the spec.Pipeline field of the generated PackageVariant + // +optional + //Pipeline *kptfilev1.Pipeline `json:"pipeline,omitempty"` + + // Injectors allows specifying the spec.Injectors field of the generated PackageVariant + // +optional + //Injectors *InfectionSelectorTemplate `json:"injectors,omitempty"` +} + +// DownstreamTemplate is used to calculate the downstream field of the resulting +// package variants. Only one of Repo and RepoExpr may be specified; +// similarly only one of Package and PackageExpr may be specified. +type DownstreamTemplate struct { + Repo *string `json:"repo,omitempty"` + Package *string `json:"package,omitempty"` + RepoExpr *string `json:"repoExpr,omitempty"` + PackageExpr *string `json:"packageExpr,omitempty"` +} + +// PackageContextTemplate is used to calculate the packageContext field of the +// resulting package variants. The plain fields and Exprs fields will be +// merged, with the Exprs fields taking precedence. +type PackageContextTemplate struct { + Data map[string]string `json:"data,omitempty"` + RemoveKeys []string `json:"removeKeys,omitempty"` + DataExprs []MapExpr `json:"dataExprs,omitempty"` + RemoveKeyExprs []string `json:"removeKeyExprs,omitempty"` +} + +// InjectionSelectorTemplate is used to calculate the injectors field of the +// resulting package variants. Only one of the Name and NameExpr fields may be +// specified. +type InjectionSelectorTemplate struct { + Group *string `json:"group,omitempty"` + Version *string `json:"version,omitempty"` + Kind *string `json:"kind,omitempty"` + Name *string `json:"name,omitempty"` + + NameExpr *string `json:"nameExpr,omitempty"` +} + +// MapExpr is used for various fields to calculate map entries. Only one of +// Key and KeyExpr may be specified; similarly only on of Value and ValueExpr +// may be specified. +type MapExpr struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` + KeyExpr *string `json:"keyExpr,omitempty"` + ValueExpr *string `json:"valueExpr,omitempty"` +} + +// PackageVariantSetStatus defines the observed state of PackageVariantSet +type PackageVariantSetStatus struct { + // Conditions describes the reconciliation state of the object. + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +//+kubebuilder:object:root=true + +// PackageVariantSetList contains a list of PackageVariantSet +type PackageVariantSetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PackageVariantSet `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PackageVariantSet{}, &PackageVariantSetList{}) +} diff --git a/porch/controllers/packagevariantsets/api/v1alpha2/zz_generated.deepcopy.go b/porch/controllers/packagevariantsets/api/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 0000000000..2045c374d8 --- /dev/null +++ b/porch/controllers/packagevariantsets/api/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,424 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2023 The kpt 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. + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariants/api/v1alpha1" + "k8s.io/apimachinery/pkg/apis/meta/v1" + 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 *DownstreamTemplate) DeepCopyInto(out *DownstreamTemplate) { + *out = *in + if in.Repo != nil { + in, out := &in.Repo, &out.Repo + *out = new(string) + **out = **in + } + if in.Package != nil { + in, out := &in.Package, &out.Package + *out = new(string) + **out = **in + } + if in.RepoExpr != nil { + in, out := &in.RepoExpr, &out.RepoExpr + *out = new(string) + **out = **in + } + if in.PackageExpr != nil { + in, out := &in.PackageExpr, &out.PackageExpr + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DownstreamTemplate. +func (in *DownstreamTemplate) DeepCopy() *DownstreamTemplate { + if in == nil { + return nil + } + out := new(DownstreamTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InjectionSelectorTemplate) DeepCopyInto(out *InjectionSelectorTemplate) { + *out = *in + if in.Group != nil { + in, out := &in.Group, &out.Group + *out = new(string) + **out = **in + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } + if in.Kind != nil { + in, out := &in.Kind, &out.Kind + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.NameExpr != nil { + in, out := &in.NameExpr, &out.NameExpr + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InjectionSelectorTemplate. +func (in *InjectionSelectorTemplate) DeepCopy() *InjectionSelectorTemplate { + if in == nil { + return nil + } + out := new(InjectionSelectorTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MapExpr) DeepCopyInto(out *MapExpr) { + *out = *in + if in.Key != nil { + in, out := &in.Key, &out.Key + *out = new(string) + **out = **in + } + if in.Value != nil { + in, out := &in.Value, &out.Value + *out = new(string) + **out = **in + } + if in.KeyExpr != nil { + in, out := &in.KeyExpr, &out.KeyExpr + *out = new(string) + **out = **in + } + if in.ValueExpr != nil { + in, out := &in.ValueExpr, &out.ValueExpr + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MapExpr. +func (in *MapExpr) DeepCopy() *MapExpr { + if in == nil { + return nil + } + out := new(MapExpr) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectSelector) DeepCopyInto(out *ObjectSelector) { + *out = *in + in.LabelSelector.DeepCopyInto(&out.LabelSelector) + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectSelector. +func (in *ObjectSelector) DeepCopy() *ObjectSelector { + if in == nil { + return nil + } + out := new(ObjectSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackageContextTemplate) DeepCopyInto(out *PackageContextTemplate) { + *out = *in + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.RemoveKeys != nil { + in, out := &in.RemoveKeys, &out.RemoveKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.DataExprs != nil { + in, out := &in.DataExprs, &out.DataExprs + *out = make([]MapExpr, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.RemoveKeyExprs != nil { + in, out := &in.RemoveKeyExprs, &out.RemoveKeyExprs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageContextTemplate. +func (in *PackageContextTemplate) DeepCopy() *PackageContextTemplate { + if in == nil { + return nil + } + out := new(PackageContextTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackageVariantSet) DeepCopyInto(out *PackageVariantSet) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageVariantSet. +func (in *PackageVariantSet) DeepCopy() *PackageVariantSet { + if in == nil { + return nil + } + out := new(PackageVariantSet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackageVariantSet) 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 *PackageVariantSetList) DeepCopyInto(out *PackageVariantSetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PackageVariantSet, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageVariantSetList. +func (in *PackageVariantSetList) DeepCopy() *PackageVariantSetList { + if in == nil { + return nil + } + out := new(PackageVariantSetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackageVariantSetList) 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 *PackageVariantSetSpec) DeepCopyInto(out *PackageVariantSetSpec) { + *out = *in + if in.Upstream != nil { + in, out := &in.Upstream, &out.Upstream + *out = new(v1alpha1.Upstream) + **out = **in + } + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]Target, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageVariantSetSpec. +func (in *PackageVariantSetSpec) DeepCopy() *PackageVariantSetSpec { + if in == nil { + return nil + } + out := new(PackageVariantSetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackageVariantSetStatus) DeepCopyInto(out *PackageVariantSetStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageVariantSetStatus. +func (in *PackageVariantSetStatus) DeepCopy() *PackageVariantSetStatus { + if in == nil { + return nil + } + out := new(PackageVariantSetStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackageVariantTemplate) DeepCopyInto(out *PackageVariantTemplate) { + *out = *in + if in.Downstream != nil { + in, out := &in.Downstream, &out.Downstream + *out = new(DownstreamTemplate) + (*in).DeepCopyInto(*out) + } + if in.AdoptionPolicy != nil { + in, out := &in.AdoptionPolicy, &out.AdoptionPolicy + *out = new(v1alpha1.AdoptionPolicy) + **out = **in + } + if in.DeletionPolicy != nil { + in, out := &in.DeletionPolicy, &out.DeletionPolicy + *out = new(v1alpha1.DeletionPolicy) + **out = **in + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.LabelExprs != nil { + in, out := &in.LabelExprs, &out.LabelExprs + *out = make([]MapExpr, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.AnnotationExprs != nil { + in, out := &in.AnnotationExprs, &out.AnnotationExprs + *out = make([]MapExpr, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.PackageContext != nil { + in, out := &in.PackageContext, &out.PackageContext + *out = new(PackageContextTemplate) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageVariantTemplate. +func (in *PackageVariantTemplate) DeepCopy() *PackageVariantTemplate { + if in == nil { + return nil + } + out := new(PackageVariantTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryTarget) DeepCopyInto(out *RepositoryTarget) { + *out = *in + if in.PackageNames != nil { + in, out := &in.PackageNames, &out.PackageNames + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryTarget. +func (in *RepositoryTarget) DeepCopy() *RepositoryTarget { + if in == nil { + return nil + } + out := new(RepositoryTarget) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Target) DeepCopyInto(out *Target) { + *out = *in + if in.Repositories != nil { + in, out := &in.Repositories, &out.Repositories + *out = make([]RepositoryTarget, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.RepositorySelector != nil { + in, out := &in.RepositorySelector, &out.RepositorySelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.ObjectSelector != nil { + in, out := &in.ObjectSelector, &out.ObjectSelector + *out = new(ObjectSelector) + (*in).DeepCopyInto(*out) + } + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = new(PackageVariantTemplate) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Target. +func (in *Target) DeepCopy() *Target { + if in == nil { + return nil + } + out := new(Target) + in.DeepCopyInto(out) + return out +} diff --git a/porch/controllers/packagevariantsets/config/samples/pvs.yaml b/porch/controllers/packagevariantsets/config/samples/pvs-v1alpha1.yaml similarity index 100% rename from porch/controllers/packagevariantsets/config/samples/pvs.yaml rename to porch/controllers/packagevariantsets/config/samples/pvs-v1alpha1.yaml diff --git a/porch/controllers/packagevariantsets/config/samples/pvs-v1alpha2.yaml b/porch/controllers/packagevariantsets/config/samples/pvs-v1alpha2.yaml new file mode 100644 index 0000000000..9c5492ce81 --- /dev/null +++ b/porch/controllers/packagevariantsets/config/samples/pvs-v1alpha2.yaml @@ -0,0 +1,64 @@ +# Copyright 2023 The kpt 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. + +apiVersion: config.porch.kpt.dev/v1alpha2 +kind: PackageVariantSet +metadata: + name: example +spec: + upstream: + repo: catalog + package: foo + revision: v2 + targets: + - repositories: + - name: cluster-01 + packageNames: + - foo-01 + - foo-02 + - foo-03 + name: cluster-02 + template: + downstream: + packageExpr: "packageName + '-' + repository.labels['env']" + - repositorySelector: + matchLabels: + env: prod + org: hr + template: + labels: + foo: bar + # injectors: + # group: infra.bigco.com + # kind: Endpoints + # nameExpr: "repository.labels['region'] + '-endpoints'" + - objectSelector: + apiVersion: hr.bigco.com/v1 + kind: Team + matchLabels: + org: hr + role: dev + template: + downstream: + repo: cluster-hr-dev + packageExpr: "target.name + '-shared'" + labels: + pkg-type: namespace + labelExprs: + - key: org + valueExpr: "target.labels['org']" + - key: role + valueExpr: "target.labels['role']" + - keyExpr: "target.labels['role'] + '-namespace'" + valueExpr: "target.name + '-shared'" diff --git a/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/packagevariantset_controller.go b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/packagevariantset_controller.go index 6f84583fbf..7d68648715 100644 --- a/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/packagevariantset_controller.go +++ b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/packagevariantset_controller.go @@ -21,15 +21,12 @@ import ( "encoding/hex" "flag" "fmt" - "strconv" - "strings" porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" configapi "github.com/GoogleContainerTools/kpt/porch/api/porchconfig/v1alpha1" pkgvarapi "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariants/api/v1alpha1" - api "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariantsets/api/v1alpha1" + api "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariantsets/api/v1alpha2" - "github.com/GoogleContainerTools/kpt/internal/fnruntime" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -37,7 +34,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/client-go/kubernetes/scheme" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" @@ -46,7 +42,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" "sigs.k8s.io/kustomize/kyaml/resid" - kyamlutils "sigs.k8s.io/kustomize/kyaml/utils" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -63,7 +58,12 @@ type PackageVariantSetReconciler struct { serializer *json.Serializer } -const PackageVariantSetOwnerLabel = "config.porch.kpt.dev/packagevariantset" +const ( + PackageVariantSetOwnerLabel = "config.porch.kpt.dev/packagevariantset" + + ConditionTypeStalled = "Stalled" // whether or not the resource reconciliation is making progress or not + ConditionTypeReady = "Ready" // whether or not the reconciliation succeeded +) //go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0 rbac:roleName=porch-controllers-packagevariantsets webhook paths="." output:rbac:artifacts:config=../../../config/rbac @@ -74,7 +74,7 @@ const PackageVariantSetOwnerLabel = "config.porch.kpt.dev/packagevariantset" // Reconcile implements the main kubernetes reconciliation loop. func (r *PackageVariantSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - pvs, err := r.init(ctx, req) + pvs, prList, repoList, err := r.init(ctx, req) if err != nil { return ctrl.Result{}, err } @@ -90,349 +90,161 @@ func (r *PackageVariantSetReconciler) Reconcile(ctx context.Context, req ctrl.Re }() if errs := validatePackageVariantSet(pvs); len(errs) > 0 { - validationErr := combineErrors(errs) - meta.SetStatusCondition(&pvs.Status.Conditions, metav1.Condition{ - Type: "Valid", - Status: "False", - Reason: "Invalid", - Message: validationErr, - }) - return ctrl.Result{}, fmt.Errorf(validationErr) - } else { - meta.SetStatusCondition(&pvs.Status.Conditions, metav1.Condition{ - Type: "Valid", - Status: "True", - Reason: "Valid", - Message: "all validation checks passed", - }) + setStalledConditionsToTrue(pvs, "ValidationError", combineErrors(errs)) + return ctrl.Result{}, nil } - upstream, err := r.getUpstreamPR(pvs.Spec.Upstream) + upstreamPR, err := r.getUpstreamPR(pvs.Spec.Upstream, prList) if err != nil { - return ctrl.Result{}, err - } - if upstream == nil { - // Currently, this code will never be reached, because the upstream.Tag option - // is not yet implemented. - return ctrl.Result{}, fmt.Errorf("could not find specified upstream") + setStalledConditionsToTrue(pvs, "UpstreamNotFound", err.Error()) + // Currently we watch all PackageRevisions, so no need to requeue + // here, as we will get triggered if a new upstream appears + return ctrl.Result{}, nil } - downstreams, err := r.unrollDownstreamTargets(ctx, pvs, upstream.Package) + downstreams, err := r.unrollDownstreamTargets(ctx, pvs) if err != nil { if meta.IsNoMatchError(err) { - meta.SetStatusCondition(&pvs.Status.Conditions, metav1.Condition{ - Type: "Valid", - Status: "False", - Reason: "Invalid", - Message: err.Error(), - }) + setStalledConditionsToTrue(pvs, "NoMatchingTargets", err.Error()) return ctrl.Result{}, nil } return ctrl.Result{}, err } - return ctrl.Result{}, r.ensurePackageVariants(ctx, upstream, downstreams, pvs) + meta.SetStatusCondition(&pvs.Status.Conditions, metav1.Condition{ + Type: ConditionTypeStalled, + Status: "False", + Reason: "Valid", + Message: "all validation checks passed", + }) + + err = r.ensurePackageVariants(ctx, pvs, repoList, upstreamPR, downstreams) + if err != nil { + return ctrl.Result{}, err + } + + meta.SetStatusCondition(&pvs.Status.Conditions, metav1.Condition{ + Type: ConditionTypeReady, + Status: "True", + Reason: "Reconciled", + Message: "package variants successfully reconciled", + }) + + return ctrl.Result{}, nil } -func (r *PackageVariantSetReconciler) init(ctx context.Context, req ctrl.Request) (*api.PackageVariantSet, error) { +func (r *PackageVariantSetReconciler) init(ctx context.Context, req ctrl.Request) (*api.PackageVariantSet, + *porchapi.PackageRevisionList, *configapi.RepositoryList, error) { var pvs api.PackageVariantSet if err := r.Client.Get(ctx, req.NamespacedName, &pvs); err != nil { - return nil, client.IgnoreNotFound(err) + return nil, nil, nil, client.IgnoreNotFound(err) } - return &pvs, nil -} -func validatePackageVariantSet(pvs *api.PackageVariantSet) []error { - var allErrs []error - if pvs.Spec.Upstream == nil { - allErrs = append(allErrs, fmt.Errorf("spec.upstream is a required field")) - } else { - if pvs.Spec.Upstream.Package == nil { - allErrs = append(allErrs, fmt.Errorf("spec.upstream.package is a required field")) - } else { - if pvs.Spec.Upstream.Package.Name == "" { - allErrs = append(allErrs, fmt.Errorf("spec.upstream.package.name is a required field")) - } - if pvs.Spec.Upstream.Package.Repo == "" { - allErrs = append(allErrs, fmt.Errorf("spec.upstream.package.repo is a required field")) - } - } - if (pvs.Spec.Upstream.Tag == "" && pvs.Spec.Upstream.Revision == "") || - (pvs.Spec.Upstream.Tag != "" && pvs.Spec.Upstream.Revision != "") { - allErrs = append(allErrs, fmt.Errorf("must have one of spec.upstream.revision and spec.upstream.tag")) - } + var prList porchapi.PackageRevisionList + if err := r.Client.List(ctx, &prList, client.InNamespace(pvs.Namespace)); err != nil { + return nil, nil, nil, err } - if len(pvs.Spec.Targets) == 0 { - allErrs = append(allErrs, fmt.Errorf("must specify at least one item in spec.targets")) - } - for i, target := range pvs.Spec.Targets { - count := 0 - if target.Package != nil { - if target.PackageName != nil { - allErrs = append(allErrs, fmt.Errorf("spec.targets[%d] cannot specify both fields `packageName` and `package`", i)) - } - if target.Package.Repo == "" { - allErrs = append(allErrs, fmt.Errorf("spec.targets[%d].package.repo cannot be empty when using `package`", i)) - } - count++ - } - if target.Repositories != nil { - count++ - } - if target.Objects != nil { - if target.Objects.Selectors == nil { - allErrs = append(allErrs, fmt.Errorf("spec.targets[%d].objects must have at least one selector", i)) - } - if target.Objects.RepoName == nil { - allErrs = append(allErrs, fmt.Errorf("spec.targets[%d].objects must specify `repoName` field", i)) - } - for j, selector := range target.Objects.Selectors { - if selector.APIVersion == "" { - allErrs = append(allErrs, fmt.Errorf("spec.targets[%d].objects.selectors[%d] must specify 'apiVersion'", i, j)) - } - if selector.Kind == "" { - allErrs = append(allErrs, fmt.Errorf("spec.targets[%d].objects.selectors[%d] must specify 'kind'", i, j)) - } - } - count++ - } - if count != 1 { - allErrs = append(allErrs, fmt.Errorf("spec.targets[%d] must specify one of `package`, `repositories`, or `objects`", i)) - } + var repoList configapi.RepositoryList + if err := r.Client.List(ctx, &repoList, client.InNamespace(pvs.Namespace)); err != nil { + return nil, nil, nil, err } - if pvs.Spec.AdoptionPolicy == "" { - pvs.Spec.AdoptionPolicy = pkgvarapi.AdoptionPolicyAdoptNone - } - if pvs.Spec.DeletionPolicy == "" { - pvs.Spec.DeletionPolicy = pkgvarapi.DeletionPolicyDelete - } - if pvs.Spec.AdoptionPolicy != pkgvarapi.AdoptionPolicyAdoptNone && pvs.Spec.AdoptionPolicy != pkgvarapi.AdoptionPolicyAdoptExisting { - allErrs = append(allErrs, field.Invalid( - field.NewPath("spec", "adoptionPolicy"), pvs.Spec.AdoptionPolicy, - fmt.Sprintf("field can only be %q or %q", - pkgvarapi.AdoptionPolicyAdoptNone, pkgvarapi.AdoptionPolicyAdoptExisting))) - } - if pvs.Spec.DeletionPolicy != pkgvarapi.DeletionPolicyOrphan && pvs.Spec.DeletionPolicy != pkgvarapi.DeletionPolicyDelete { - allErrs = append(allErrs, field.Invalid( - field.NewPath("spec", "deletionPolicy"), pvs.Spec.DeletionPolicy, - fmt.Sprintf("field can only be %q or %q", - pkgvarapi.DeletionPolicyOrphan, pkgvarapi.DeletionPolicyDelete))) - } - return allErrs + return &pvs, &prList, &repoList, nil } -func combineErrors(errs []error) string { - var errMsgs []string - for _, e := range errs { - if e.Error() != "" { - errMsgs = append(errMsgs, e.Error()) +func (r *PackageVariantSetReconciler) getUpstreamPR(upstream *pkgvarapi.Upstream, + prList *porchapi.PackageRevisionList) (*porchapi.PackageRevision, error) { + + for _, pr := range prList.Items { + if pr.Spec.RepositoryName == upstream.Repo && + pr.Spec.PackageName == upstream.Package && + pr.Spec.Revision == upstream.Revision { + return &pr, nil } } - return strings.Join(errMsgs, "; ") + return nil, fmt.Errorf("could not find upstream package revision '%s/%s' in repo '%s'", + upstream.Package, upstream.Revision, upstream.Repo) } -func (r *PackageVariantSetReconciler) getUpstreamPR( - upstream *api.Upstream) (*pkgvarapi.Upstream, error) { - - // one of upstream.Tag or upstream.Revision must have been specified, - // because this pvs has already been validated - if upstream.Tag != "" { - // TODO: Implement this. - // We need to figure out which published revision this refers to, - // so the controller needs to reach out to the Github repo and - // look at the commits on this tag. - // We will need to find this tag's most recent commit that refers - // to the relevant package, and then find the package revision - // that is on the same commit. - return nil, fmt.Errorf("specifying the upstream tag is not yet supported") - - } - - // upstream.Revision is specified - return &pkgvarapi.Upstream{ - Repo: upstream.Package.Repo, - Package: upstream.Package.Name, - Revision: upstream.Revision, - }, nil - +type pvContext struct { + template *api.PackageVariantTemplate + repoDefault string + packageDefault string + object *unstructured.Unstructured } func (r *PackageVariantSetReconciler) unrollDownstreamTargets(ctx context.Context, - pvs *api.PackageVariantSet, - upstreamPackageName string) ([]*pkgvarapi.Downstream, error) { - var result []*pkgvarapi.Downstream - for i, target := range pvs.Spec.Targets { - switch { - case target.Package != nil: - // an explicit repo/package name pair - result = append(result, r.repoPackagePair(&target, upstreamPackageName)) + pvs *api.PackageVariantSet) ([]pvContext, error) { - case target.Repositories != nil: - // a label selector against a set of repositories - selector, err := metav1.LabelSelectorAsSelector(target.Repositories) - if err != nil { - return nil, err - } - var repoList configapi.RepositoryList - if err := r.Client.List(ctx, &repoList, - client.InNamespace(pvs.Namespace), - client.MatchingLabelsSelector{Selector: selector}); err != nil { - return nil, err - } - if len(repoList.Items) == 0 { - klog.Warningf("no repositories selected by spec.targets[%d]", i) - } - pkgs, err := r.repositorySet(&target, upstreamPackageName, &repoList) - if err != nil { - return nil, fmt.Errorf("error when selecting repository set: %v", err) - } - result = append(result, pkgs...) + upstreamPackageName := pvs.Spec.Upstream.Package + var result []pvContext - case target.Objects != nil: - // a selector against a set of arbitrary objects - selectedObjects, err := r.getSelectedObjects(ctx, target.Objects.Selectors) - if err != nil { - return nil, err - } - if len(selectedObjects) == 0 { - klog.Warningf("no objects selected by spec.targets[%d]", i) - } - pkgs, err := r.objectSet(&target, upstreamPackageName, selectedObjects) - if err != nil { - return nil, fmt.Errorf("error when selecting object set: %v", err) - } - result = append(result, pkgs...) - } - } - return result, nil -} - -func (r *PackageVariantSetReconciler) repoPackagePair(target *api.Target, - upstreamPackageName string) *pkgvarapi.Downstream { - downstreamPackageName := target.Package.Name - if downstreamPackageName == "" { - downstreamPackageName = upstreamPackageName - } - return &pkgvarapi.Downstream{ - Repo: target.Package.Repo, - Package: downstreamPackageName, - } -} + for i, target := range pvs.Spec.Targets { + if len(target.Repositories) > 0 { + for _, rt := range target.Repositories { + pns := []string{upstreamPackageName} + if len(rt.PackageNames) > 0 { + pns = rt.PackageNames + } -func (r *PackageVariantSetReconciler) repositorySet( - target *api.Target, - upstreamPackageName string, - repoList *configapi.RepositoryList) ([]*pkgvarapi.Downstream, error) { - var result []*pkgvarapi.Downstream - for _, repo := range repoList.Items { - repoAsRNode, err := r.convertObjectToRNode(&repo) - if err != nil { - return nil, fmt.Errorf("error converting repo to RNode: %v", err) - } - downstreamPackageName, err := r.getDownstreamPackageName(target.PackageName, upstreamPackageName, repoAsRNode) - if err != nil { - return nil, err + for _, pn := range pns { + result = append(result, pvContext{ + template: target.Template, + repoDefault: rt.Name, + packageDefault: pn, + }) + } + } + continue } - result = append(result, &pkgvarapi.Downstream{ - Repo: repo.Name, - Package: downstreamPackageName, - }) - } - return result, nil -} + objSel := target.ObjectSelector + if target.RepositorySelector != nil { + // a label selector against a set of repositories + // equivlanet to object selector with apiVersion/kind pre-set -func (r *PackageVariantSetReconciler) objectSet(target *api.Target, - upstreamPackageName string, - selectedObjects map[resid.ResId]*yaml.RNode) ([]*pkgvarapi.Downstream, error) { - var result []*pkgvarapi.Downstream - for _, obj := range selectedObjects { - downstreamPackageName, err := r.getDownstreamPackageName(target.PackageName, - upstreamPackageName, obj) - if err != nil { - return nil, err - } - repo, err := r.fetchValue(target.Objects.RepoName, obj) - if err != nil { - return nil, err - } - if repo == "" { - return nil, fmt.Errorf("error evaluating repo name: received empty string") + objSel = &api.ObjectSelector{ + LabelSelector: *target.RepositorySelector, + APIVersion: configapi.TypeRepository.APIVersion(), + Kind: configapi.TypeRepository.Kind, + } } - result = append(result, &pkgvarapi.Downstream{ - Package: downstreamPackageName, - Repo: repo, - }) - } - return result, nil -} - -func (r *PackageVariantSetReconciler) getSelectedObjects(ctx context.Context, selectors []api.Selector) (map[resid.ResId]*yaml.RNode, error) { - selectedObjects := make(map[resid.ResId]*yaml.RNode) // this is a map to prevent duplicates - - for _, selector := range selectors { + // a selector against a set of arbitrary objects uList := &unstructured.UnstructuredList{} - group, version := resid.ParseGroupVersion(selector.APIVersion) + group, version := resid.ParseGroupVersion(objSel.APIVersion) uList.SetGroupVersionKind(schema.GroupVersionKind{ Group: group, Version: version, - Kind: selector.Kind, + Kind: objSel.Kind, }) - opts := []client.ListOption{client.InNamespace(selector.Namespace)} - if selector.Labels != nil { - labelSelector, err := metav1.LabelSelectorAsSelector(selector.Labels) - if err != nil { - return nil, err - } - opts = append(opts, client.MatchingLabelsSelector{Selector: labelSelector}) + opts := []client.ListOption{client.InNamespace(pvs.Namespace)} + labelSelector, err := metav1.LabelSelectorAsSelector(&objSel.LabelSelector) + if err != nil { + return nil, err } + opts = append(opts, client.MatchingLabelsSelector{Selector: labelSelector}) + if err := r.Client.List(ctx, uList, opts...); err != nil { return nil, err } - for _, u := range uList.Items { - objAsRNode, err := r.convertObjectToRNode(&u) - if err != nil { - return nil, fmt.Errorf("error converting unstructured object to RNode: %v", err) - } - if fnruntime.IsMatch(objAsRNode, selector.ToKptfileSelector()) { - selectedObjects[resid.FromRNode(objAsRNode)] = objAsRNode - } + // TODO: fire event; set condition? + if len(uList.Items) == 0 { + klog.Warningf("no objects selected by spec.targets[%d]", i) } - } - return selectedObjects, nil -} - -func (r *PackageVariantSetReconciler) getDownstreamPackageName(targetName *api.PackageName, - upstreamPackageName string, - obj *yaml.RNode) (string, error) { - - if targetName == nil { - return upstreamPackageName, nil - } - - packageName, err := r.fetchValue(targetName.Name, obj) - if err != nil { - return "", err - } - if packageName == "" { - packageName = upstreamPackageName - } - - suffix, err := r.fetchValue(targetName.NameSuffix, obj) - if err != nil { - return "", err - } + for _, u := range uList.Items { + result = append(result, pvContext{ + template: target.Template, + repoDefault: u.GetName(), + packageDefault: upstreamPackageName, + object: &u, + }) - prefix, err := r.fetchValue(targetName.NamePrefix, obj) - if err != nil { - return "", err + } } - - return prefix + packageName + suffix, nil + return result, nil } func (r *PackageVariantSetReconciler) convertObjectToRNode(obj runtime.Object) (*yaml.RNode, error) { @@ -443,48 +255,10 @@ func (r *PackageVariantSetReconciler) convertObjectToRNode(obj runtime.Object) ( return yaml.Parse(buffer.String()) } -func (r *PackageVariantSetReconciler) fetchValue(value *api.ValueOrFromField, - obj *yaml.RNode) (string, error) { - if value == nil { - return "", nil - } - if value.Value != "" { - return value.Value, nil - } - if value.FromField == "" { - return "", nil - } - - // The SmarterPathSplitter below splits on '.', and the yaml.Lookup filter expects - // a list of path elements to parse through, e.g. ["metadata", "ownerRefs", "1"], - // so we have to do a bit of a hack to support JSON path syntax. - // Adding a '.' before each '[' ensures that we split our path correctly, then - // we have to parse through and remove the [] around numbers; - // E.g. 'metadata.ownerRefs[1]' splits into 'metadata', 'ownerRefs', '1' before - // we call yaml.Lookup, so we first change it to 'metadata.ownerRefs.1 before calling the splitter. - // See TestFetchValue for examples of what this supports. - fromField := strings.ReplaceAll(value.FromField, "[", ".[") - fieldPath := kyamlutils.SmarterPathSplitter(fromField, ".") - for i := range fieldPath { - trimmed := strings.Trim(fieldPath[i], "[]") - if _, err := strconv.Atoi(trimmed); err == nil { - fieldPath[i] = trimmed - } - } - rn, err := obj.Pipe(yaml.Lookup(fieldPath...)) - if err != nil { - return "", err - } - if rn.IsNilOrEmpty() || rn.YNode().Value == "" { - return "", fmt.Errorf("value not found") - } +func (r *PackageVariantSetReconciler) ensurePackageVariants(ctx context.Context, pvs *api.PackageVariantSet, + repoList *configapi.RepositoryList, upstreamPR *porchapi.PackageRevision, + downstreams []pvContext) error { - return rn.YNode().Value, nil -} - -func (r *PackageVariantSetReconciler) ensurePackageVariants(ctx context.Context, - upstream *pkgvarapi.Upstream, downstreams []*pkgvarapi.Downstream, - pvs *api.PackageVariantSet) error { var pvList pkgvarapi.PackageVariantList if err := r.Client.List(ctx, &pvList, client.InNamespace(pvs.Namespace), @@ -509,15 +283,11 @@ func (r *PackageVariantSetReconciler) ensurePackageVariants(ctx context.Context, tr := true for _, downstream := range downstreams { - pvSpec := pkgvarapi.PackageVariantSpec{ - Upstream: upstream, - Downstream: downstream, - AdoptionPolicy: pvs.Spec.AdoptionPolicy, - DeletionPolicy: pvs.Spec.DeletionPolicy, - Labels: pvs.Spec.Labels, - Annotations: pvs.Spec.Annotations, + pvSpec, err := renderPackageVariantSpec(ctx, pvs, repoList, upstreamPR, downstream) + if err != nil { + return err } - hash, err := hashFromPackageVariantSpec(&pvSpec) + hash, err := hashFromPackageVariantSpec(pvSpec) if err != nil { return err } @@ -540,7 +310,7 @@ func (r *PackageVariantSetReconciler) ensurePackageVariants(ctx context.Context, BlockOwnerDeletion: nil, }}, }, - Spec: pvSpec, + Spec: *pvSpec, } desiredPackageVariantMap[hash] = &pv } @@ -634,3 +404,18 @@ func (r *PackageVariantSetReconciler) mapObjectsToRequests(obj client.Object) [] } return requests } + +func setStalledConditionsToTrue(pvs *api.PackageVariantSet, reason, message string) { + meta.SetStatusCondition(&pvs.Status.Conditions, metav1.Condition{ + Type: ConditionTypeStalled, + Status: "True", + Reason: reason, + Message: message, + }) + meta.SetStatusCondition(&pvs.Status.Conditions, metav1.Condition{ + Type: ConditionTypeReady, + Status: "False", + Reason: reason, + Message: message, + }) +} diff --git a/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/packagevariantset_controller_test.go b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/packagevariantset_controller_test.go index e8e9c16548..69c12135a1 100644 --- a/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/packagevariantset_controller_test.go +++ b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/packagevariantset_controller_test.go @@ -18,132 +18,17 @@ import ( "context" "testing" + porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" configapi "github.com/GoogleContainerTools/kpt/porch/api/porchconfig/v1alpha1" pkgvarapi "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariants/api/v1alpha1" - api "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariantsets/api/v1alpha1" + api "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariantsets/api/v1alpha2" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/serializer/json" - "sigs.k8s.io/kustomize/kyaml/resid" - kyaml "sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/yaml" ) -func TestValidatePackageVariantSet(t *testing.T) { - packageVariantHeader := `apiVersion: config.porch.kpt.dev -kind: PackageVariantSet -metadata: - name: my-pv` - - testCases := map[string]struct { - packageVariant string - expectedErrs []string - }{ - "empty spec": { - packageVariant: packageVariantHeader, - expectedErrs: []string{"spec.upstream is a required field", - "must specify at least one item in spec.targets", - }, - }, - "missing upstream package, but has both revision and tag": { - packageVariant: packageVariantHeader + ` -spec: - upstream: - revision: v1 - tag: main`, - expectedErrs: []string{"spec.upstream.package is a required field", - "must specify at least one item in spec.targets", - }, - }, - "missing upstream package repo, revision, and tag": { - packageVariant: packageVariantHeader + ` -spec: - upstream: - package: - name: foo`, - expectedErrs: []string{"spec.upstream.package.repo is a required field", - "must have one of spec.upstream.revision and spec.upstream.tag", - "must specify at least one item in spec.targets", - }, - }, - "missing upstream package name, revision, and tag": { - packageVariant: packageVariantHeader + ` -spec: - upstream: - package: - repo: foo`, - expectedErrs: []string{"spec.upstream.package.name is a required field", - "must have one of spec.upstream.revision and spec.upstream.tag", - "must specify at least one item in spec.targets", - }, - }, - "invalid targets": { - packageVariant: packageVariantHeader + ` -spec: - targets: - - package: - name: foo - packageName: - name: - value: foo - - package: - repo: bar - repositories: - foo: bar - - package: - name: foo - repo: bar - objects: - repoName: - value: foo - `, - expectedErrs: []string{"spec.upstream is a required field", - "spec.targets[0] cannot specify both fields `packageName` and `package`", - "spec.targets[0].package.repo cannot be empty when using `package`", - "spec.targets[1] must specify one of `package`, `repositories`, or `objects`", - "spec.targets[2].objects must have at least one selector", - "spec.targets[2] must specify one of `package`, `repositories`, or `objects`", - }, - }, - "invalid adoption and deletion policies": { - packageVariant: packageVariantHeader + ` -spec: - adoptionPolicy: invalid - deletionPolicy: invalid -`, - expectedErrs: []string{"spec.upstream is a required field", - "must specify at least one item in spec.targets", - "spec.adoptionPolicy: Invalid value: \"invalid\": field can only be \"adoptNone\" or \"adoptExisting\"", - "spec.deletionPolicy: Invalid value: \"invalid\": field can only be \"orphan\" or \"delete\"", - }, - }, - "valid adoption and deletion policies": { - packageVariant: packageVariantHeader + ` -spec: - adoptionPolicy: adoptExisting - deletionPolicy: orphan -`, - expectedErrs: []string{"spec.upstream is a required field", - "must specify at least one item in spec.targets", - }, - }, - } - - for tn, tc := range testCases { - t.Run(tn, func(t *testing.T) { - var pvs api.PackageVariantSet - require.NoError(t, yaml.Unmarshal([]byte(tc.packageVariant), &pvs)) - actualErrs := validatePackageVariantSet(&pvs) - require.Equal(t, len(tc.expectedErrs), len(actualErrs)) - for i := range actualErrs { - require.EqualError(t, actualErrs[i], tc.expectedErrs[i]) - } - - }) - } -} - func TestConvertObjectToRNode(t *testing.T) { s := json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, json.SerializerOptions{Yaml: true}) r := PackageVariantSetReconciler{serializer: s} @@ -182,181 +67,23 @@ status: {} }) } -func TestFetchValue(t *testing.T) { - pod := kyaml.MustParse(` -apiVersion: v1 -kind: Pod -metadata: - name: nginx -spec: - containers: - - name: nginx - image: nginx:1.14.2 - ports: - - containerPort: 80 - - name: foo - image: image:1.2.3 - ports: - - containerPort: 8080`) - - testCases := map[string]struct { - input *api.ValueOrFromField - expected string - }{ - "nil input": { - input: nil, - expected: "", - }, - "empty struct input": { - input: &api.ValueOrFromField{}, - expected: "", - }, - "string literal value": { - input: &api.ValueOrFromField{ - Value: "literal", - }, - expected: "literal", - }, - "value from field using key-value selector": { - input: &api.ValueOrFromField{ - FromField: "spec.containers[name=foo].image", - }, - expected: "image:1.2.3", - }, - "value from field using integer selector": { - input: &api.ValueOrFromField{ - FromField: "spec.containers[1].image", - }, - expected: "image:1.2.3", - }, - } - - for tn, tc := range testCases { - r := &PackageVariantSetReconciler{} - t.Run(tn, func(t *testing.T) { - v, err := r.fetchValue(tc.input, pod) - require.NoError(t, err) - require.Equal(t, tc.expected, v) - }) - } -} - -func TestRepositorySet(t *testing.T) { - var repoList configapi.RepositoryList - require.NoError(t, yaml.Unmarshal([]byte(` -apiVersion: config.porch.kpt.dev/v1alpha1 -kind: RepositoryList -metadata: - name: my-repo-list -items: -- apiVersion: config.porch.kpt.dev/v1alpha1 - kind: Repository - metadata: - name: my-repo-1 - labels: - foo: bar - abc: def -- apiVersion: config.porch.kpt.dev/v1alpha1 - kind: Repository - metadata: - name: my-repo-2 - labels: - foo: bar - abc: def - efg: hij -`), &repoList)) - - var target api.Target - require.NoError(t, yaml.Unmarshal([]byte(` -repositories: - foo: bar - abc: def -packageName: - baseName: - value: dpn`), &target)) - - s := json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, json.SerializerOptions{Yaml: true}) - r := PackageVariantSetReconciler{serializer: s} - - result, err := r.repositorySet(&target, "upn", &repoList) - require.NoError(t, err) - require.Equal(t, []*pkgvarapi.Downstream{{ - Repo: "my-repo-1", - Package: "dpn", - }, { - Repo: "my-repo-2", - Package: "dpn", - }, - }, result) -} - -func TestGetSelectedObjects(t *testing.T) { - selectors := []api.Selector{{ - APIVersion: "v1", - Kind: "Pod", - Labels: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, - }} - reconciler := &PackageVariantSetReconciler{ - Client: new(fakeClient), - serializer: json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, json.SerializerOptions{Yaml: true}), - } - selectedObjects, err := reconciler.getSelectedObjects(context.Background(), selectors) - require.NoError(t, err) - require.Equal(t, 1, len(selectedObjects)) - - expectedResId := resid.NewResIdWithNamespace(resid.NewGvk("", "v1", "Pod"), "my-pod-1", "") - obj, found := selectedObjects[expectedResId] - require.True(t, found) - require.Equal(t, `apiVersion: v1 -kind: Pod -metadata: - labels: - abc: def - foo: bar - name: my-pod-1 -`, obj.MustString()) -} - -func TestObjectSet(t *testing.T) { - selectedObjects := map[resid.ResId]*kyaml.RNode{ - resid.NewResIdWithNamespace(resid.NewGvk("", "v1", "Pod"), "my-pod-1", ""): kyaml.MustParse(`apiVersion: v1 -kind: Pod -metadata: - labels: - repo: my-repo - name: downstream -`), - } - - target := &api.Target{ - PackageName: &api.PackageName{ - Name: &api.ValueOrFromField{FromField: "metadata.name"}, - }, - Objects: &api.ObjectSelector{ - RepoName: &api.ValueOrFromField{FromField: "metadata.labels.repo"}, - }, - } - - pvs := &PackageVariantSetReconciler{} - objectSet, err := pvs.objectSet(target, "upstream", selectedObjects) - require.NoError(t, err) - require.Equal(t, len(objectSet), 1) - require.Equal(t, pkgvarapi.Downstream{ - Repo: "my-repo", - Package: "downstream", - }, *objectSet[0]) -} - func TestEnsurePackageVariants(t *testing.T) { - upstream := &pkgvarapi.Upstream{Repo: "up", Package: "up", Revision: "up"} - downstreams := []*pkgvarapi.Downstream{ - {Repo: "dn-1", Package: "dn-1"}, - {Repo: "dn-3", Package: "dn-3"}, + downstreams := []pvContext{ + {repoDefault: "dn-1", packageDefault: "dn-1"}, + {repoDefault: "dn-3", packageDefault: "dn-3"}, } fc := &fakeClient{} reconciler := &PackageVariantSetReconciler{Client: fc} - require.NoError(t, reconciler.ensurePackageVariants(context.Background(), upstream, downstreams, - &api.PackageVariantSet{ObjectMeta: metav1.ObjectMeta{Name: "my-pvs"}})) + require.NoError(t, reconciler.ensurePackageVariants(context.Background(), + &api.PackageVariantSet{ + ObjectMeta: metav1.ObjectMeta{Name: "my-pvs"}, + Spec: api.PackageVariantSetSpec{ + Upstream: &pkgvarapi.Upstream{Repo: "up", Package: "up", Revision: "up"}, + }, + }, + &configapi.RepositoryList{}, + &porchapi.PackageRevision{}, + downstreams)) require.Equal(t, 2, len(fc.objects)) require.Equal(t, "my-pv-1", fc.objects[0].GetName()) require.Equal(t, "my-pvs-8680372821ea923a2c068ad9fa32ffd876e9fb80", fc.objects[1].GetName()) diff --git a/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/render.go b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/render.go new file mode 100644 index 0000000000..45d23ad4af --- /dev/null +++ b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/render.go @@ -0,0 +1,315 @@ +// Copyright 2023 The kpt 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 packagevariantset + +import ( + "context" + "fmt" + "reflect" + "sort" + + "github.com/google/cel-go/cel" + + porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" + configapi "github.com/GoogleContainerTools/kpt/porch/api/porchconfig/v1alpha1" + pkgvarapi "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariants/api/v1alpha1" + api "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariantsets/api/v1alpha2" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + // TODO: including this requires many dependency updates, at some point + // we should do that so the CEL evaluation here is consistent with + // K8s. There are a few other lines to uncomment in that case. + //"k8s.io/apiserver/pkg/cel/library" +) + +const ( + RepoDefaultVarName = "repoDefault" + PackageDefaultVarName = "packageDefault" + UpstreamVarName = "upstream" + RepositoryVarName = "repository" + TargetVarName = "target" +) + +func renderPackageVariantSpec(ctx context.Context, pvs *api.PackageVariantSet, repoList *configapi.RepositoryList, + upstreamPR *porchapi.PackageRevision, downstream pvContext) (*pkgvarapi.PackageVariantSpec, error) { + + spec := &pkgvarapi.PackageVariantSpec{ + Upstream: pvs.Spec.Upstream, + Downstream: &pkgvarapi.Downstream{ + Repo: downstream.repoDefault, + Package: downstream.packageDefault, + }, + } + + pvt := downstream.template + if pvt == nil { + return spec, nil + } + + inputs, err := buildBaseInputs(upstreamPR, downstream) + if err != nil { + return nil, err + } + + repo := downstream.repoDefault + + if pvt.Downstream != nil { + if pvt.Downstream.Repo != nil && *pvt.Downstream.Repo != "" { + repo = *pvt.Downstream.Repo + } + + if pvt.Downstream.RepoExpr != nil && *pvt.Downstream.RepoExpr != "" { + repo, err = evalExpr(*pvt.Downstream.RepoExpr, inputs) + if err != nil { + return nil, fmt.Errorf("spec.downstream.repoExpr: %s", err.Error()) + } + } + + spec.Downstream.Repo = repo + } + + for _, r := range repoList.Items { + if r.Name == repo { + repoInput, err := objectToInput(&r) + if err != nil { + return nil, err + } + inputs[RepositoryVarName] = repoInput + break + } + } + + if _, ok := inputs[RepositoryVarName]; !ok { + return nil, fmt.Errorf("repository %q could not be loaded", repo) + } + + if pvt.Downstream != nil { + if pvt.Downstream.Package != nil && *pvt.Downstream.Package != "" { + spec.Downstream.Package = *pvt.Downstream.Package + } + + if pvt.Downstream.PackageExpr != nil && *pvt.Downstream.PackageExpr != "" { + spec.Downstream.Package, err = evalExpr(*pvt.Downstream.PackageExpr, inputs) + if err != nil { + return nil, fmt.Errorf("spec.downstream.packageExpr: %s", err.Error()) + } + } + } + + if pvt.AdoptionPolicy != nil { + spec.AdoptionPolicy = *pvt.AdoptionPolicy + } + + if pvt.DeletionPolicy != nil { + spec.DeletionPolicy = *pvt.DeletionPolicy + } + spec.Labels, err = copyAndOverlayMapExpr("spec.labelExprs", pvt.Labels, pvt.LabelExprs, inputs) + if err != nil { + return nil, err + } + + spec.Annotations, err = copyAndOverlayMapExpr("spec.annotationExprs", pvt.Annotations, pvt.AnnotationExprs, inputs) + if err != nil { + return nil, err + } + + if pvt.PackageContext != nil { + data, err := copyAndOverlayMapExpr("spec.packageContext.dataExprs", pvt.PackageContext.Data, pvt.PackageContext.DataExprs, inputs) + if err != nil { + return nil, err + } + + removeKeys, err := copyAndOverlayStringSlice("spec.packageContext.removeKeyExprs", pvt.PackageContext.RemoveKeys, + pvt.PackageContext.RemoveKeyExprs, inputs) + if err != nil { + return nil, err + } + spec.PackageContext = &pkgvarapi.PackageContext{ + Data: data, + RemoveKeys: removeKeys, + } + } + return spec, nil +} + +func buildBaseInputs(upstreamPR *porchapi.PackageRevision, downstream pvContext) (map[string]interface{}, error) { + inputs := make(map[string]interface{}, 5) + inputs[RepoDefaultVarName] = downstream.repoDefault + inputs[PackageDefaultVarName] = downstream.packageDefault + + upstreamInput, err := objectToInput(upstreamPR) + if err != nil { + return nil, err + } + inputs[UpstreamVarName] = upstreamInput + + if downstream.object != nil { + targetInput, err := objectToInput(downstream.object) + if err != nil { + return nil, err + } + inputs[TargetVarName] = targetInput + } else { + inputs[TargetVarName] = map[string]string{ + "repo": downstream.repoDefault, + "package": downstream.packageDefault, + } + } + + return inputs, nil +} + +func objectToInput(obj interface{}) (map[string]interface{}, error) { + + result := make(map[string]interface{}) + + uo, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + + u := unstructured.Unstructured{Object: uo} + + //TODO: allow an administrator-configurable allow list of fields, + // on a per-GVK basis + result["name"] = u.GetName() + result["namespace"] = u.GetNamespace() + result["labels"] = u.GetLabels() + result["annotations"] = u.GetAnnotations() + + return result, nil +} + +func copyAndOverlayMapExpr(fieldName string, inMap map[string]string, mapExprs []api.MapExpr, inputs map[string]interface{}) (map[string]string, error) { + outMap := make(map[string]string, len(inMap)) + for k, v := range inMap { + outMap[k] = v + } + + var err error + for i, me := range mapExprs { + var k, v string + if me.Key != nil { + k = *me.Key + } + if me.KeyExpr != nil { + k, err = evalExpr(*me.KeyExpr, inputs) + if err != nil { + return nil, fmt.Errorf("%s[%d].keyExpr: %s", fieldName, i, err.Error()) + } + } + if me.Value != nil { + v = *me.Value + } + if me.ValueExpr != nil { + v, err = evalExpr(*me.ValueExpr, inputs) + if err != nil { + return nil, fmt.Errorf("%s[%d].valueExpr: %s", fieldName, i, err.Error()) + } + } + outMap[k] = v + } + + if len(outMap) == 0 { + return nil, nil + } + + return outMap, nil +} + +func copyAndOverlayStringSlice(fieldName string, in, exprs []string, inputs map[string]interface{}) ([]string, error) { + outMap := make(map[string]bool, len(in)+len(exprs)) + + for _, v := range in { + outMap[v] = true + } + for i, e := range exprs { + v, err := evalExpr(e, inputs) + if err != nil { + return nil, fmt.Errorf("%s[%d]: %s", fieldName, i, err.Error()) + } + outMap[v] = true + } + + if len(outMap) == 0 { + return nil, nil + } + + var out []string + for k := range outMap { + out = append(out, k) + } + sort.Strings(out) + return out, nil +} + +func evalExpr(expr string, inputs map[string]interface{}) (string, error) { + prog, err := compileExpr(expr) + if err != nil { + return "", err + } + + val, _, err := prog.Eval(inputs) + if err != nil { + return "", err + } + + result, err := val.ConvertToNative(reflect.TypeOf("")) + if err != nil { + return "", err + } + + s, ok := result.(string) + if !ok { + return "", fmt.Errorf("expression returned non-string value: %v", result) + } + + return s, nil +} + +// compileExpr returns a compiled CEL expression. +func compileExpr(expr string) (cel.Program, error) { + var opts []cel.EnvOption + opts = append(opts, cel.HomogeneousAggregateLiterals()) + opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true)) + // TODO: uncomment after updating to latest k8s + //opts = append(opts, library.ExtensionLibs...) + opts = append(opts, cel.Variable(RepoDefaultVarName, cel.StringType)) + opts = append(opts, cel.Variable(PackageDefaultVarName, cel.StringType)) + opts = append(opts, cel.Variable(UpstreamVarName, cel.DynType)) + opts = append(opts, cel.Variable(TargetVarName, cel.DynType)) + opts = append(opts, cel.Variable(RepositoryVarName, cel.DynType)) + + env, err := cel.NewEnv(opts...) + if err != nil { + return nil, err + } + + ast, issues := env.Compile(expr) + if issues != nil { + return nil, issues.Err() + } + + _, err = cel.AstToCheckedExpr(ast) + if err != nil { + return nil, err + } + return env.Program(ast, + cel.EvalOptions(cel.OptOptimize), + // TODO: uncomment after updating to latest k8s + //cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...), + ) +} diff --git a/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/render_test.go b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/render_test.go new file mode 100644 index 0000000000..a02982f057 --- /dev/null +++ b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/render_test.go @@ -0,0 +1,532 @@ +// Copyright 2023 The kpt 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 packagevariantset + +import ( + "context" + "testing" + + porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" + configapi "github.com/GoogleContainerTools/kpt/porch/api/porchconfig/v1alpha1" + pkgvarapi "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariants/api/v1alpha1" + api "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariantsets/api/v1alpha2" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + "sigs.k8s.io/yaml" +) + +var repoListYaml = []byte(` +apiVersion: config.porch.kpt.dev/v1alpha1 +kind: RepositoryList +metadata: + name: my-repo-list +items: +- apiVersion: config.porch.kpt.dev/v1alpha1 + kind: Repository + metadata: + name: my-repo-1 + labels: + foo: bar + abc: def +- apiVersion: config.porch.kpt.dev/v1alpha1 + kind: Repository + metadata: + name: my-repo-2 + labels: + foo: bar + abc: def + efg: hij +`) + +func TestRenderPackageVariantSpec(t *testing.T) { + var repoList configapi.RepositoryList + require.NoError(t, yaml.Unmarshal(repoListYaml, &repoList)) + + adoptExisting := pkgvarapi.AdoptionPolicyAdoptExisting + deletionPolicyDelete := pkgvarapi.DeletionPolicyDelete + pvs := api.PackageVariantSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pvs", + Namespace: "default", + }, + Spec: api.PackageVariantSetSpec{ + Upstream: &pkgvarapi.Upstream{Repo: "up-repo", Package: "up-pkg", Revision: "v2"}, + }, + } + upstreamPR := porchapi.PackageRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "p", + Namespace: "default", + Labels: map[string]string{ + "vendor": "bigco.com", + "product": "snazzy", + "version": "1.6.8", + }, + Annotations: map[string]string{ + "bigco.com/team": "us-platform", + }, + }, + } + testCases := map[string]struct { + downstream pvContext + expectedSpec pkgvarapi.PackageVariantSpec + expectedErrs []string + }{ + "no template": { + downstream: pvContext{ + repoDefault: "my-repo-1", + packageDefault: "p", + }, + expectedSpec: pkgvarapi.PackageVariantSpec{ + Upstream: pvs.Spec.Upstream, + Downstream: &pkgvarapi.Downstream{ + Repo: "my-repo-1", + Package: "p", + }, + }, + expectedErrs: nil, + }, + "template downstream.repo": { + downstream: pvContext{ + repoDefault: "my-repo-1", + packageDefault: "p", + template: &api.PackageVariantTemplate{ + Downstream: &api.DownstreamTemplate{ + Repo: pointer.String("my-repo-2"), + }, + }, + }, + expectedSpec: pkgvarapi.PackageVariantSpec{ + Upstream: pvs.Spec.Upstream, + Downstream: &pkgvarapi.Downstream{ + Repo: "my-repo-2", + Package: "p", + }, + }, + expectedErrs: nil, + }, + "template downstream.package": { + downstream: pvContext{ + repoDefault: "my-repo-1", + packageDefault: "p", + template: &api.PackageVariantTemplate{ + Downstream: &api.DownstreamTemplate{ + Package: pointer.String("new-p"), + }, + }, + }, + expectedSpec: pkgvarapi.PackageVariantSpec{ + Upstream: pvs.Spec.Upstream, + Downstream: &pkgvarapi.Downstream{ + Repo: "my-repo-1", + Package: "new-p", + }, + }, + expectedErrs: nil, + }, + "template adoption and deletion": { + downstream: pvContext{ + repoDefault: "my-repo-1", + packageDefault: "p", + template: &api.PackageVariantTemplate{ + AdoptionPolicy: &adoptExisting, + DeletionPolicy: &deletionPolicyDelete, + }, + }, + expectedSpec: pkgvarapi.PackageVariantSpec{ + Upstream: pvs.Spec.Upstream, + Downstream: &pkgvarapi.Downstream{ + Repo: "my-repo-1", + Package: "p", + }, + AdoptionPolicy: "adoptExisting", + DeletionPolicy: "delete", + }, + expectedErrs: nil, + }, + "template static labels and annotations": { + downstream: pvContext{ + repoDefault: "my-repo-1", + packageDefault: "p", + template: &api.PackageVariantTemplate{ + Labels: map[string]string{ + "foo": "bar", + "hello": "there", + }, + Annotations: map[string]string{ + "foobar": "barfoo", + }, + }, + }, + expectedSpec: pkgvarapi.PackageVariantSpec{ + Upstream: pvs.Spec.Upstream, + Downstream: &pkgvarapi.Downstream{ + Repo: "my-repo-1", + Package: "p", + }, + Labels: map[string]string{ + "foo": "bar", + "hello": "there", + }, + Annotations: map[string]string{ + "foobar": "barfoo", + }, + }, + expectedErrs: nil, + }, + "template static packageContext": { + downstream: pvContext{ + repoDefault: "my-repo-1", + packageDefault: "p", + template: &api.PackageVariantTemplate{ + PackageContext: &api.PackageContextTemplate{ + Data: map[string]string{ + "foo": "bar", + "hello": "there", + }, + RemoveKeys: []string{"foobar", "barfoo"}, + }, + }, + }, + expectedSpec: pkgvarapi.PackageVariantSpec{ + Upstream: pvs.Spec.Upstream, + Downstream: &pkgvarapi.Downstream{ + Repo: "my-repo-1", + Package: "p", + }, + PackageContext: &pkgvarapi.PackageContext{ + Data: map[string]string{ + "foo": "bar", + "hello": "there", + }, + RemoveKeys: []string{"barfoo", "foobar"}, + }, + }, + expectedErrs: nil, + }, + "template downstream with expressions": { + downstream: pvContext{ + repoDefault: "my-repo-1", + packageDefault: "p", + template: &api.PackageVariantTemplate{ + Downstream: &api.DownstreamTemplate{ + RepoExpr: pointer.String("'my-repo-2'"), + PackageExpr: pointer.String("repoDefault + '-' + packageDefault"), + }, + }, + }, + expectedSpec: pkgvarapi.PackageVariantSpec{ + Upstream: pvs.Spec.Upstream, + Downstream: &pkgvarapi.Downstream{ + Repo: "my-repo-2", + Package: "my-repo-1-p", + }, + }, + expectedErrs: nil, + }, + "template labels and annotations with expressions": { + downstream: pvContext{ + repoDefault: "my-repo-1", + packageDefault: "p", + template: &api.PackageVariantTemplate{ + Downstream: &api.DownstreamTemplate{ + RepoExpr: pointer.String("'my-repo-2'"), + PackageExpr: pointer.String("repoDefault + '-' + packageDefault"), + }, + Labels: map[string]string{ + "foo": "bar", + "hello": "there", + }, + LabelExprs: []api.MapExpr{ + { + Key: pointer.String("foo"), + ValueExpr: pointer.String("repoDefault"), + }, + { + KeyExpr: pointer.String("repository.labels['efg']"), + ValueExpr: pointer.String("packageDefault + '-' + repository.name"), + }, + { + Key: pointer.String("hello"), + Value: pointer.String("goodbye"), + }, + }, + Annotations: map[string]string{ + "bigco.com/sample-annotation": "some-annotation", + "foo.org/id": "123456", + }, + AnnotationExprs: []api.MapExpr{ + { + Key: pointer.String("foo.org/id"), + Value: pointer.String("54321"), + }, + { + Key: pointer.String("bigco.com/team"), + ValueExpr: pointer.String("upstream.annotations['bigco.com/team']"), + }, + }, + }, + }, + expectedSpec: pkgvarapi.PackageVariantSpec{ + Upstream: pvs.Spec.Upstream, + Downstream: &pkgvarapi.Downstream{ + Repo: "my-repo-2", + Package: "my-repo-1-p", + }, + Labels: map[string]string{ + "foo": "my-repo-1", + "hello": "goodbye", + "hij": "p-my-repo-2", + }, + Annotations: map[string]string{ + "bigco.com/sample-annotation": "some-annotation", + "foo.org/id": "54321", + "bigco.com/team": "us-platform", + }, + }, + expectedErrs: nil, + }, + "template with packageContext with expressions": { + downstream: pvContext{ + repoDefault: "my-repo-1", + packageDefault: "p", + template: &api.PackageVariantTemplate{ + PackageContext: &api.PackageContextTemplate{ + Data: map[string]string{ + "foo": "bar", + "hello": "there", + }, + DataExprs: []api.MapExpr{ + { + Key: pointer.String("foo"), + ValueExpr: pointer.String("upstream.name"), + }, + { + KeyExpr: pointer.String("upstream.namespace"), + ValueExpr: pointer.String("upstream.name"), + }, + { + KeyExpr: pointer.String("upstream.name"), + Value: pointer.String("foo"), + }, + }, + RemoveKeys: []string{"foobar", "barfoo"}, + RemoveKeyExprs: []string{"repository.labels['abc']"}, + }, + }, + }, + expectedSpec: pkgvarapi.PackageVariantSpec{ + Upstream: pvs.Spec.Upstream, + Downstream: &pkgvarapi.Downstream{ + Repo: "my-repo-1", + Package: "p", + }, + PackageContext: &pkgvarapi.PackageContext{ + Data: map[string]string{ + "foo": "p", + "hello": "there", + "default": "p", + "p": "foo", + }, + RemoveKeys: []string{"barfoo", "def", "foobar"}, + }, + }, + expectedErrs: nil, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + pvSpec, err := renderPackageVariantSpec(context.Background(), &pvs, &repoList, &upstreamPR, tc.downstream) + require.NoError(t, err) + require.Equal(t, &tc.expectedSpec, pvSpec) + }) + } +} + +func TestEvalExpr(t *testing.T) { + baseInputs := map[string]interface{}{ + "repoDefault": "foo-repo", + "packageDefault": "bar-package", + } + var repoList configapi.RepositoryList + require.NoError(t, yaml.Unmarshal(repoListYaml, &repoList)) + + r1Input, err := objectToInput(&repoList.Items[0]) + require.NoError(t, err) + + testCases := map[string]struct { + expr string + target interface{} + expectedResult string + expectedErr string + }{ + "no vars": { + expr: "'foo'", + expectedResult: "foo", + expectedErr: "", + }, + "repoDefault": { + expr: "repoDefault", + expectedResult: "foo-repo", + expectedErr: "", + }, + "packageDefault": { + expr: "packageDefault", + expectedResult: "bar-package", + expectedErr: "", + }, + "concat defaults": { + expr: "packageDefault + '-' + repoDefault", + expectedResult: "bar-package-foo-repo", + expectedErr: "", + }, + "repositories target": { + expr: "target.repo + '/' + target.package", + target: map[string]any{ + "repo": "my-repo", + "package": "my-package", + }, + expectedResult: "my-repo/my-package", + expectedErr: "", + }, + "repository target": { + expr: "target.name + '/' + target.labels['foo']", + target: r1Input, + expectedResult: "my-repo-1/bar", + expectedErr: "", + }, + "bad variable": { + expr: "badvar", + expectedErr: "ERROR: :1:1: undeclared reference to 'badvar' (in container '')\n | badvar\n | ^", + }, + "bad expr": { + expr: "/", + expectedErr: "ERROR: :1:1: Syntax error: mismatched input '/' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER}\n | /\n | ^\nERROR: :1:2: Syntax error: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER}\n | /\n | .^", + }, + "missing label": { + expr: "target.name + '/' + target.labels['no-such-label']", + target: r1Input, + expectedErr: "no such key: no-such-label", + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + inputs := map[string]any{} + for k, v := range baseInputs { + inputs[k] = v + } + inputs["target"] = tc.target + val, err := evalExpr(tc.expr, inputs) + if tc.expectedErr == "" { + require.NoError(t, err) + require.Equal(t, tc.expectedResult, val) + } else { + require.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestCopyAndOverlayMapExpr(t *testing.T) { + baseInputs := map[string]interface{}{ + "repoDefault": "foo-repo", + "packageDefault": "bar-package", + } + + testCases := map[string]struct { + inMap map[string]string + mapExprs []api.MapExpr + expectedResult map[string]string + expectedErr string + }{ + "empty starting map": { + inMap: map[string]string{}, + mapExprs: []api.MapExpr{ + { + Key: pointer.String("foo"), + Value: pointer.String("bar"), + }, + { + KeyExpr: pointer.String("repoDefault"), + Value: pointer.String("barbar"), + }, + { + Key: pointer.String("bar"), + ValueExpr: pointer.String("packageDefault"), + }, + }, + expectedResult: map[string]string{ + "foo": "bar", + "foo-repo": "barbar", + "bar": "bar-package", + }, + }, + "static overlay": { + inMap: map[string]string{ + "foo": "bar", + "bar": "foo", + }, + mapExprs: []api.MapExpr{ + { + Key: pointer.String("foo"), + Value: pointer.String("new-bar"), + }, + { + Key: pointer.String("foofoo"), + Value: pointer.String("barbar"), + }, + }, + expectedResult: map[string]string{ + "foo": "new-bar", + "bar": "foo", + "foofoo": "barbar", + }, + }, + "exprs overlay": { + inMap: map[string]string{ + "foo": "bar", + "bar": "foo", + }, + mapExprs: []api.MapExpr{ + { + KeyExpr: pointer.String("'foo'"), + Value: pointer.String("new-bar"), + }, + { + Key: pointer.String("bar"), + ValueExpr: pointer.String("packageDefault"), + }, + }, + expectedResult: map[string]string{ + "foo": "new-bar", + "bar": "bar-package", + }, + }, + } + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + outMap, err := copyAndOverlayMapExpr("f", tc.inMap, tc.mapExprs, baseInputs) + if tc.expectedErr == "" { + require.NoError(t, err) + require.Equal(t, tc.expectedResult, outMap) + } else { + require.EqualError(t, err, tc.expectedErr) + } + }) + } +} diff --git a/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/validate.go b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/validate.go new file mode 100644 index 0000000000..5c3ef6d8f7 --- /dev/null +++ b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/validate.go @@ -0,0 +1,159 @@ +// Copyright 2023 The kpt 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 packagevariantset + +import ( + "fmt" + "strings" + + pkgvarapi "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariants/api/v1alpha1" + api "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariantsets/api/v1alpha2" +) + +func validatePackageVariantSet(pvs *api.PackageVariantSet) []error { + var allErrs []error + if pvs.Spec.Upstream == nil { + allErrs = append(allErrs, fmt.Errorf("spec.upstream is a required field")) + } else { + if pvs.Spec.Upstream.Package == "" { + allErrs = append(allErrs, fmt.Errorf("spec.upstream.package is a required field")) + } + if pvs.Spec.Upstream.Repo == "" { + allErrs = append(allErrs, fmt.Errorf("spec.upstream.repo is a required field")) + } + if pvs.Spec.Upstream.Revision == "" { + allErrs = append(allErrs, fmt.Errorf("spec.upstream.revision is a required field")) + } + } + + if len(pvs.Spec.Targets) == 0 { + allErrs = append(allErrs, fmt.Errorf("must specify at least one item in spec.targets")) + } + for i, target := range pvs.Spec.Targets { + allErrs = append(allErrs, validateTarget(i, target)...) + } + + return allErrs +} + +func validateTarget(i int, target api.Target) []error { + var allErrs []error + count := 0 + if target.Repositories != nil { + count++ + + if len(target.Repositories) == 0 { + allErrs = append(allErrs, fmt.Errorf("spec.targets[%d].repositories must not be an empty list if specified", i)) + } + + for j, rt := range target.Repositories { + if rt.Name == "" { + allErrs = append(allErrs, fmt.Errorf("spec.targets[%d].repositories[%d].name cannot be empty", i, j)) + } + + for k, pn := range rt.PackageNames { + if pn == "" { + allErrs = append(allErrs, fmt.Errorf("spec.targets[%d].repositories[%d].packageNames[%d] cannot be empty", i, j, k)) + } + } + } + } + + if target.RepositorySelector != nil { + count++ + } + + if target.ObjectSelector != nil { + count++ + if target.ObjectSelector.APIVersion == "" { + allErrs = append(allErrs, fmt.Errorf("spec.targets[%d].objectselector.apiVersion cannot be empty", i)) + } + if target.ObjectSelector.Kind == "" { + allErrs = append(allErrs, fmt.Errorf("spec.targets[%d].objectselector.kind cannot be empty", i)) + } + } + + if count != 1 { + allErrs = append(allErrs, fmt.Errorf("spec.targets[%d] must specify one of `repositories`, `repositorySelector`, or `objectSelector`", i)) + } + + if target.Template == nil { + return allErrs + } + + return append(allErrs, validateTemplate(target.Template, fmt.Sprintf("spec.targets[%d].template", i))...) +} + +func validateTemplate(template *api.PackageVariantTemplate, field string) []error { + var allErrs []error + if template.AdoptionPolicy != nil && *template.AdoptionPolicy != pkgvarapi.AdoptionPolicyAdoptNone && + *template.AdoptionPolicy != pkgvarapi.AdoptionPolicyAdoptExisting { + allErrs = append(allErrs, fmt.Errorf("%s.adoptionPolicy can only be %q or %q", field, + pkgvarapi.AdoptionPolicyAdoptNone, pkgvarapi.AdoptionPolicyAdoptExisting)) + } + + if template.DeletionPolicy != nil && *template.DeletionPolicy != pkgvarapi.DeletionPolicyOrphan && + *template.DeletionPolicy != pkgvarapi.DeletionPolicyDelete { + allErrs = append(allErrs, fmt.Errorf("%s.deletionPolicy can only be %q or %q", field, + pkgvarapi.DeletionPolicyOrphan, pkgvarapi.DeletionPolicyDelete)) + } + + if template.Downstream != nil { + if template.Downstream.Repo != nil && template.Downstream.RepoExpr != nil { + allErrs = append(allErrs, fmt.Errorf("%s may specify only one of `downstream.repo` and `downstream.repoExpr`", field)) + } + if template.Downstream.Package != nil && template.Downstream.PackageExpr != nil { + allErrs = append(allErrs, fmt.Errorf("%s may specify only one of `downstream.package` and `downstream.packageExpr`", field)) + } + } + + if template.LabelExprs != nil { + allErrs = append(allErrs, validateMapExpr(template.LabelExprs, fmt.Sprintf("%s.labelExprs", field))...) + } + + if template.AnnotationExprs != nil { + allErrs = append(allErrs, validateMapExpr(template.AnnotationExprs, fmt.Sprintf("%s.annotationExprs", field))...) + } + + if template.PackageContext != nil && template.PackageContext.DataExprs != nil { + allErrs = append(allErrs, validateMapExpr(template.PackageContext.DataExprs, fmt.Sprintf("%s.packageContext.dataExprs", field))...) + } + + return allErrs +} + +func validateMapExpr(m []api.MapExpr, fieldName string) []error { + var allErrs []error + for j, me := range m { + if me.Key != nil && me.KeyExpr != nil { + allErrs = append(allErrs, fmt.Errorf("%s[%d] may specify only one of `key` and `keyExpr`", fieldName, j)) + } + if me.Value != nil && me.ValueExpr != nil { + allErrs = append(allErrs, fmt.Errorf("%s[%d] may specify only one of `value` and `valueExpr`", fieldName, j)) + } + } + + return allErrs +} + +func combineErrors(errs []error) string { + var errMsgs []string + for _, e := range errs { + if e.Error() != "" { + errMsgs = append(errMsgs, e.Error()) + } + } + return strings.Join(errMsgs, "; ") +} diff --git a/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/validate_test.go b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/validate_test.go new file mode 100644 index 0000000000..63571d2548 --- /dev/null +++ b/porch/controllers/packagevariantsets/pkg/controllers/packagevariantset/validate_test.go @@ -0,0 +1,186 @@ +// Copyright 2023 The kpt 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 packagevariantset + +import ( + "testing" + + api "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariantsets/api/v1alpha2" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" +) + +func TestValidatePackageVariantSet(t *testing.T) { + packageVariantHeader := `apiVersion: config.porch.kpt.dev +kind: PackageVariantSet +metadata: + name: my-pv` + + testCases := map[string]struct { + packageVariant string + expectedErrs []string + }{ + "empty spec": { + packageVariant: packageVariantHeader, + expectedErrs: []string{"spec.upstream is a required field", + "must specify at least one item in spec.targets", + }, + }, + "missing upstream package": { + packageVariant: packageVariantHeader + ` +spec: + upstream: + repo: foo + revision: v1`, + expectedErrs: []string{"spec.upstream.package is a required field", + "must specify at least one item in spec.targets", + }, + }, + "missing upstream repo": { + packageVariant: packageVariantHeader + ` +spec: + upstream: + package: foopkg + revision: v3`, + expectedErrs: []string{"spec.upstream.repo is a required field", + "must specify at least one item in spec.targets", + }, + }, + "missing upstream revision": { + packageVariant: packageVariantHeader + ` +spec: + upstream: + repo: foo + package: foopkg`, + expectedErrs: []string{"spec.upstream.revision is a required field", + "must specify at least one item in spec.targets", + }, + }, + "invalid targets": { + packageVariant: packageVariantHeader + ` +spec: + targets: + - repositories: + - name: "" + - repositories: + - name: bar + repositorySelector: + foo: bar + - repositories: + - name: bar + packageNames: + - "" + - foo + `, + expectedErrs: []string{"spec.upstream is a required field", + "spec.targets[0].repositories[0].name cannot be empty", + "spec.targets[1] must specify one of `repositories`, `repositorySelector`, or `objectSelector`", + "spec.targets[2].repositories[0].packageNames[0] cannot be empty", + }, + }, + "invalid adoption and deletion policies": { + packageVariant: packageVariantHeader + ` +spec: + targets: + - template: + adoptionPolicy: invalid + deletionPolicy: invalid +`, + expectedErrs: []string{"spec.upstream is a required field", + "spec.targets[0] must specify one of `repositories`, `repositorySelector`, or `objectSelector`", + "spec.targets[0].template.adoptionPolicy can only be \"adoptNone\" or \"adoptExisting\"", + "spec.targets[0].template.deletionPolicy can only be \"orphan\" or \"delete\"", + }, + }, + "valid adoption and deletion policies": { + packageVariant: packageVariantHeader + ` +spec: + adoptionPolicy: adoptExisting + deletionPolicy: orphan +`, + expectedErrs: []string{"spec.upstream is a required field", + "must specify at least one item in spec.targets", + }, + }, + "downstream values and expressions do not mix": { + packageVariant: packageVariantHeader + ` +spec: + targets: + - template: + downstream: + repo: "foo" + repoExpr: "'bar'" + package: "p" + packageExpr: "'p'" +`, + expectedErrs: []string{"spec.upstream is a required field", + "spec.targets[0] must specify one of `repositories`, `repositorySelector`, or `objectSelector`", + "spec.targets[0].template may specify only one of `downstream.repo` and `downstream.repoExpr`", + "spec.targets[0].template may specify only one of `downstream.package` and `downstream.packageExpr`", + }, + }, + "MapExprs do not allow both expr-and non-expr for same field": { + packageVariant: packageVariantHeader + ` +spec: + targets: + - template: + labelExprs: + - key: "foo" + keyExpr: "'bar'" + value: "bar" + - key: "foo" + value: "bar" + valueExpr: "'bar'" + annotationExprs: + - key: "foo" + keyExpr: "'bar'" + value: "bar" + - key: "foo" + value: "bar" + valueExpr: "'bar'" + packageContext: + dataExprs: + - key: "foo" + keyExpr: "'bar'" + value: "bar" + - key: "foo" + value: "bar" + valueExpr: "'bar'" +`, + expectedErrs: []string{"spec.upstream is a required field", + "spec.targets[0] must specify one of `repositories`, `repositorySelector`, or `objectSelector`", + "spec.targets[0].template.labelExprs[0] may specify only one of `key` and `keyExpr`", + "spec.targets[0].template.labelExprs[1] may specify only one of `value` and `valueExpr`", + "spec.targets[0].template.annotationExprs[0] may specify only one of `key` and `keyExpr`", + "spec.targets[0].template.annotationExprs[1] may specify only one of `value` and `valueExpr`", + "spec.targets[0].template.packageContext.dataExprs[0] may specify only one of `key` and `keyExpr`", + "spec.targets[0].template.packageContext.dataExprs[1] may specify only one of `value` and `valueExpr`", + }, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + var pvs api.PackageVariantSet + require.NoError(t, yaml.Unmarshal([]byte(tc.packageVariant), &pvs)) + actualErrs := validatePackageVariantSet(&pvs) + require.Equal(t, len(tc.expectedErrs), len(actualErrs)) + for i := range actualErrs { + require.EqualError(t, actualErrs[i], tc.expectedErrs[i]) + } + + }) + } +}