Skip to content

Commit

Permalink
packagevariantset controller: support arbitrary object selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
natasha41575 committed Feb 17, 2023
1 parent c3e832e commit 402fdb6
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package v1alpha1

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"
)
Expand Down Expand Up @@ -101,6 +102,21 @@ type Selector struct {
Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"`
}

func (s *Selector) ToKptfileSelector() kptfilev1.Selector {
var labels map[string]string
if s.Labels != nil {
labels = s.Labels.MatchLabels
}
return kptfilev1.Selector{
APIVersion: s.APIVersion,
Kind: s.Kind,
Name: s.Name,
Namespace: s.Namespace,
Labels: labels,
Annotations: s.Annotations,
}
}

type PackageName struct {
Name *ValueOrFromField `json:"baseName,omitempty"`

Expand Down
3 changes: 3 additions & 0 deletions porch/controllers/packagevariantsets/config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ rules:
- get
- patch
- update
- apiGroups: ["*"]
resources: ["*"]
verbs: ["list"]
11 changes: 10 additions & 1 deletion porch/controllers/packagevariantsets/config/samples/pvs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,13 @@ spec:
packageName:
baseName:
value: beta

- objects:
selectors:
- apiVersion: v1
kind: Pod
name: my-pod
repoName:
value: blueprints
packageName:
baseName:
value: gamma
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package packagevariantset

import (
"context"
"fmt"

pkgvarapi "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariants/api/v1alpha1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
)

type fakeClient struct {
output []string
client.Client
}

var _ client.Client = &fakeClient{}

func (f *fakeClient) Create(_ context.Context, obj client.Object, _ ...client.CreateOption) error {
f.output = append(f.output, fmt.Sprintf("creating object: %s", obj.GetName()))
return nil
}

func (f *fakeClient) Delete(_ context.Context, obj client.Object, _ ...client.DeleteOption) error {
f.output = append(f.output, fmt.Sprintf("deleting object: %s", obj.GetName()))
return nil
}

func (f *fakeClient) List(_ context.Context, obj client.ObjectList, _ ...client.ListOption) error {
f.output = append(f.output, fmt.Sprintf("listing objects"))
podList := `apiVersion: v1
kind: PodList
metadata:
name: my-pod-list
items:
- apiVersion: v1
kind: Pod
metadata:
name: my-pod-1
labels:
foo: bar
abc: def
- apiVersion: v1
kind: Pod
metadata:
name: my-pod-2
labels:
abc: def
efg: hij`

pvList := `apiVersion: config.porch.kpt.dev
kind: PackageVariantList
metadata:
name: my-pv-list
items:
- apiVersion: config.porch.kpt.dev
kind: PackageVariant
metadata:
name: my-pv-1
spec:
upstream:
repo: up
package: up
revision: up
downstream:
repo: dn-1
package: dn-1
- apiVersion: config.porch.kpt.dev
kind: PackageVariant
metadata:
name: my-pv-2
spec:
upstream:
repo: up
package: up
revision: up
downstream:
repo: dn-2
package: dn-2`

switch v := obj.(type) {
case *unstructured.UnstructuredList:
return yaml.Unmarshal([]byte(podList), v)
case *pkgvarapi.PackageVariantList:
return yaml.Unmarshal([]byte(pvList), v)
default:
return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ import (
pkgvarapi "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariants/api/v1alpha1"
api "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariantsets/api/v1alpha1"

"github.com/GoogleContainerTools/kpt/internal/fnruntime"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand All @@ -40,6 +43,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/handler"
"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"
)
Expand Down Expand Up @@ -161,6 +165,14 @@ func validatePackageVariantSet(pvs *api.PackageVariantSet) []error {
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 {
Expand Down Expand Up @@ -245,7 +257,11 @@ func (r *PackageVariantSetReconciler) unrollDownstreamTargets(ctx context.Contex

case target.Objects != nil:
// a selector against a set of arbitrary objects
pkgs, err := r.objectSet(ctx, &target, upstreamPackageName)
selectedObjects, err := r.getSelectedObjects(ctx, target.Objects.Selectors)
if err != nil {
return nil, err
}
pkgs, err := r.objectSet(&target, upstreamPackageName, selectedObjects)
if err != nil {
return nil, fmt.Errorf("error when selecting object set: %v", err)
}
Expand All @@ -271,7 +287,6 @@ 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)
Expand All @@ -291,11 +306,65 @@ func (r *PackageVariantSetReconciler) repositorySet(
return result, nil
}

func (r *PackageVariantSetReconciler) objectSet(ctx context.Context,
target *api.Target,
upstreamPackageName string) ([]*pkgvarapi.Downstream, error) {
// TODO: Implement this
return nil, fmt.Errorf("specifying a set of objects in the target is not yet supported")
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")
}
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 {
uList := &unstructured.UnstructuredList{}
group, version := resid.ParseGroupVersion(selector.APIVersion)
uList.SetGroupVersionKind(schema.GroupVersionKind{
Group: group,
Version: version,
Kind: selector.Kind,
})

labelSelector, err := metav1.LabelSelectorAsSelector(selector.Labels)
if err != nil {
return nil, err
}

if err := r.Client.List(ctx, uList,
client.InNamespace(selector.Namespace),
client.MatchingLabelsSelector{Selector: labelSelector}); err != nil {
return nil, fmt.Errorf("unable to list objects in cluster: %v", 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
}
}
}
return selectedObjects, nil
}

func (r *PackageVariantSetReconciler) getDownstreamPackageName(targetName *api.PackageName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
package packagevariantset

import (
"context"
"testing"

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"
"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"
)
Expand Down Expand Up @@ -286,3 +289,77 @@ packageName:
},
}, 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"},
}
pvs := &api.PackageVariantSet{ObjectMeta: metav1.ObjectMeta{Name: "my-pvs"}}

fc := &fakeClient{}
reconciler := &PackageVariantSetReconciler{Client: fc}
require.NoError(t, reconciler.ensurePackageVariants(context.Background(), upstream, downstreams, pvs))
require.Equal(t, []string{"listing objects",
"deleting object: my-pv-2",
"creating object: my-pvs-59bfcf77a6b032a656d78b14e2d92fa8c1e978a3",
}, fc.output)
}

0 comments on commit 402fdb6

Please sign in to comment.