Unverified Commit 6bdd3253 authored by Oren Shomron's avatar Oren Shomron Committed by GitHub
Browse files

Merge pull request #811 from shomron/issue-809-diff-all-objects

ks diff support for objects outside of the "all" category
parents f10518ec f9486157
......@@ -72,7 +72,7 @@ type Apply struct {
// these make it easier to test Apply.
findObjectsFn findObjectsFn
resourceClientFactory resourceClientFactoryFn
clientOpts *clientOpts
clientOpts *Clients
objectInfo ObjectInfo
ksonnetObjectFactory func() ksonnetObject
upserterFactory func() Upserter
......@@ -102,7 +102,7 @@ func RunApply(config ApplyConfig, opts ...ApplyOpts) error {
}
if a.clientOpts == nil {
co, err := genClientOpts(a.App, a.ClientConfig, a.EnvName)
co, err := GenClients(a.App, a.ClientConfig, a.EnvName)
if err != nil {
return err
}
......@@ -279,16 +279,3 @@ func (a *Apply) dryRunText() string {
return text
}
func genClientOpts(a app.App, clientConfig *client.Config, envName string) (clientOpts, error) {
clientPool, discovery, namespace, err := clientConfig.RestClient(a, &envName)
if err != nil {
return clientOpts{}, err
}
return clientOpts{
clientPool: clientPool,
discovery: discovery,
namespace: namespace,
}, nil
}
......@@ -59,7 +59,7 @@ func Test_Apply(t *testing.T) {
setupApp := func(apply *Apply) {
obj := &unstructured.Unstructured{Object: genObject()}
apply.clientOpts = &clientOpts{}
apply.clientOpts = &Clients{}
apply.findObjectsFn = func(a app.App, envName string, componentNames []string) ([]*unstructured.Unstructured, error) {
objects := []*unstructured.Unstructured{obj}
......@@ -96,7 +96,7 @@ func Test_Apply_dry_run(t *testing.T) {
setupApp := func(apply *Apply) {
obj := &unstructured.Unstructured{Object: genObject()}
apply.clientOpts = &clientOpts{}
apply.clientOpts = &Clients{}
apply.findObjectsFn = func(a app.App, envName string, componentNames []string) ([]*unstructured.Unstructured, error) {
objects := []*unstructured.Unstructured{obj}
......@@ -132,8 +132,8 @@ func Test_Apply_retry_on_conflict(t *testing.T) {
setupApp := func(apply *Apply) {
obj := &unstructured.Unstructured{Object: genObject()}
apply.clientOpts = &clientOpts{}
apply.resourceClientFactory = func(opts clientOpts, object runtime.Object) (ResourceClient, error) {
apply.clientOpts = &Clients{}
apply.resourceClientFactory = func(opts Clients, object runtime.Object) (ResourceClient, error) {
rc := &mocks.ResourceClient{}
rc.On("Get", mock.Anything).Return(obj, nil)
return rc, nil
......
......@@ -16,6 +16,8 @@
package cluster
import (
"github.com/ksonnet/ksonnet/pkg/app"
"github.com/ksonnet/ksonnet/pkg/client"
"github.com/ksonnet/ksonnet/utils"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
......@@ -40,7 +42,8 @@ type ResourceClient interface {
Patch(pt types.PatchType, data []byte) (*unstructured.Unstructured, error)
}
type clientOpts struct {
// Clients is a tuple of Kubernetes clients: dynamic.ClientPool, discovery.DiscoveryInterface.
type Clients struct {
clientPool dynamic.ClientPool
discovery discovery.DiscoveryInterface
namespace string
......@@ -49,22 +52,22 @@ type clientOpts struct {
type resourceClientOpt func(*resourceClient)
type resourceClient struct {
object *unstructured.Unstructured
opts clientOpts
c dynamic.ResourceInterface
object *unstructured.Unstructured
clients Clients
c dynamic.ResourceInterface
}
var _ ResourceClient = (*resourceClient)(nil)
func newResourceClient(opts clientOpts, object runtime.Object, rcOpts ...resourceClientOpt) (*resourceClient, error) {
func newResourceClient(clients Clients, object runtime.Object, rcOpts ...resourceClientOpt) (*resourceClient, error) {
o, ok := object.(*unstructured.Unstructured)
if !ok {
return nil, errors.Errorf("unsupported runtime object type %T", object)
}
rc := &resourceClient{
object: o,
opts: opts,
object: o,
clients: clients,
}
for _, opt := range rcOpts {
......@@ -72,7 +75,7 @@ func newResourceClient(opts clientOpts, object runtime.Object, rcOpts ...resourc
}
if rc.c == nil {
c, err := utils.ClientForResource(opts.clientPool, opts.discovery, object, opts.namespace)
c, err := utils.ClientForResource(clients.clientPool, clients.discovery, object, clients.namespace)
if err != nil {
return nil, err
}
......@@ -83,7 +86,7 @@ func newResourceClient(opts clientOpts, object runtime.Object, rcOpts ...resourc
return rc, nil
}
func resourceClientFactory(opts clientOpts, object runtime.Object) (ResourceClient, error) {
func resourceClientFactory(opts Clients, object runtime.Object) (ResourceClient, error) {
return newResourceClient(opts, object)
}
......@@ -105,3 +108,17 @@ func (c *resourceClient) Patch(pt types.PatchType, data []byte) (*unstructured.U
name := c.object.GetName()
return c.c.Patch(name, pt, data)
}
// GenClients returns a cluster.Clients structure initialized to the provided environment cluster/namespace
func GenClients(a app.App, clientConfig *client.Config, envName string) (Clients, error) {
clientPool, discovery, namespace, err := clientConfig.RestClient(a, &envName)
if err != nil {
return Clients{}, err
}
return Clients{
clientPool: clientPool,
discovery: discovery,
namespace: namespace,
}, nil
}
......@@ -28,7 +28,7 @@ import (
)
func Test_newResourceClient(t *testing.T) {
aOpts := clientOpts{}
aOpts := Clients{}
aObject := &unstructured.Unstructured{}
mockDI := &mockDynamicInterface{}
......@@ -101,7 +101,7 @@ func Test_resourceClient_Patch(t *testing.T) {
}
func withMockResourceClient(t *testing.T, fn func(rc *resourceClient, di *mockDynamicInterface, ob *unstructured.Unstructured)) {
aOpts := clientOpts{}
aOpts := Clients{}
aObject := &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
......
......@@ -34,11 +34,11 @@ import (
"k8s.io/client-go/dynamic"
)
type genClientOptsFn func(a app.App, c *client.Config, envName string) (clientOpts, error)
type genClientOptsFn func(a app.App, c *client.Config, envName string) (Clients, error)
type clientFactoryFn func(dynamic.ClientPool, discovery.DiscoveryInterface, runtime.Object, string) (dynamic.ResourceInterface, error)
type resourceClientFactoryFn func(opts clientOpts, object runtime.Object) (ResourceClient, error)
type resourceClientFactoryFn func(opts Clients, object runtime.Object) (ResourceClient, error)
type discoveryFn func(a app.App, clientConfig *client.Config, envName string) (discovery.DiscoveryInterface, error)
......@@ -48,11 +48,6 @@ type validateObjectFn func(d discovery.DiscoveryInterface,
type findObjectsFn func(a app.App, envName string,
componentNames []string) ([]*unstructured.Unstructured, error)
func loadDiscovery(a app.App, clientConfig *client.Config, envName string) (discovery.DiscoveryInterface, error) {
_, d, _, err := clientConfig.RestClient(a, &envName)
return d, err
}
func findObjects(a app.App, envName string, componentNames []string) ([]*unstructured.Unstructured, error) {
p := pipeline.New(a, envName)
return p.Objects(componentNames)
......@@ -68,7 +63,7 @@ func stringListContains(list []string, value string) bool {
}
// func gcDelete(clientpool dynamic.ClientPool, disco discovery.DiscoveryInterface, version *utils.ServerVersion, o runtime.Object) error {
func gcDelete(options clientOpts, rcFactory resourceClientFactoryFn, version *utils.ServerVersion, o runtime.Object) error {
func gcDelete(options Clients, rcFactory resourceClientFactoryFn, version *utils.ServerVersion, o runtime.Object) error {
obj, err := meta.Accessor(o)
if err != nil {
return fmt.Errorf("Unexpected object type: %s", err)
......@@ -108,7 +103,7 @@ func gcDelete(options clientOpts, rcFactory resourceClientFactoryFn, version *ut
return nil
}
func walkObjects(co clientOpts, listopts metav1.ListOptions, callback func(runtime.Object) error) error {
func walkObjects(co Clients, listopts metav1.ListOptions, callback func(runtime.Object) error) error {
rsrclists, err := co.discovery.ServerResources()
if err != nil {
return err
......
......@@ -56,7 +56,7 @@ func RunDelete(config DeleteConfig, opts ...DeleteOpts) error {
d := &Delete{
DeleteConfig: config,
findObjectsFn: findObjects,
genClientOptsFn: genClientOpts,
genClientOptsFn: GenClients,
resourceClientFactory: resourceClientFactory,
objectInfo: &objectInfo{},
}
......
// Copyright 2018 The ksonnet 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 cluster
import (
"sort"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// sortResources sources resources by moving extensions to the end of the slice. The order of all
// the other resources is preserved.
func sortResources(resources []*metav1.APIResourceList) {
sort.SliceStable(resources, func(i, j int) bool {
left := resources[i]
leftGV, _ := schema.ParseGroupVersion(left.GroupVersion)
// not checking error because it should be impossible to fail to parse data coming from the
// apiserver
if leftGV.Group == "extensions" {
// always sort extensions at the bottom by saying left is "greater"
return false
}
right := resources[j]
rightGV, _ := schema.ParseGroupVersion(right.GroupVersion)
// not checking error because it should be impossible to fail to parse data coming from the
// apiserver
if rightGV.Group == "extensions" {
// always sort extensions at the bottom by saying left is "less"
return true
}
return i < j
})
}
......@@ -25,7 +25,7 @@ import (
// ksonnetObject can merge an object with its cluster state. This is required because
// some fields will be overwritten if applied again (e.g. Server NodePort).
type ksonnetObject interface {
MergeFromCluster(co clientOpts, obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
MergeFromCluster(co Clients, obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
}
type defaultKsonnetObject struct {
......@@ -42,7 +42,7 @@ func newDefaultKsonnetObject(factory cmdutil.Factory) *defaultKsonnetObject {
}
}
func (ko *defaultKsonnetObject) MergeFromCluster(co clientOpts, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
func (ko *defaultKsonnetObject) MergeFromCluster(co Clients, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
mergedObject, err := ko.objectMerger.Merge(co.namespace, obj)
if err != nil {
cause := errors.Cause(err)
......
......@@ -85,7 +85,7 @@ func Test_defaultKsonnetObject_MergeFromCluster(t *testing.T) {
factory := cmdtesting.NewTestFactory()
defer factory.Cleanup()
co := clientOpts{}
co := Clients{}
ko := newDefaultKsonnetObject(factory)
ko.objectMerger = tc.objectMerger
......@@ -109,6 +109,6 @@ type fakeKsonnetObject struct {
var _ (ksonnetObject) = (*fakeKsonnetObject)(nil)
func (ko *fakeKsonnetObject) MergeFromCluster(co clientOpts, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
func (ko *fakeKsonnetObject) MergeFromCluster(co Clients, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return ko.obj, ko.err
}
......@@ -17,16 +17,16 @@ package cluster
import (
"encoding/json"
"fmt"
"strings"
clustermetadata "github.com/ksonnet/ksonnet/pkg/metadata"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/clientcmd"
kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/client-go/discovery"
"k8s.io/kubernetes/pkg/kubectl/resource"
)
......@@ -50,68 +50,100 @@ func SetMetaDataAnnotation(obj metav1.Object, key, value string) {
obj.SetAnnotations(a)
}
// DefaultResourceInfo fetches objects from the cluster.
func DefaultResourceInfo(namespace string, config clientcmd.ClientConfig, components []string) *resource.Result {
factory := kcmdutil.NewFactory(config)
f := factory.NewBuilder().
Unstructured().
NamespaceParam(namespace).
ExportParam(false).
ResourceTypeOrNameArgs(true, "all").
LabelSelectorParam("app.kubernetes.io/deploy-manager=ksonnet").
ContinueOnError().
Flatten().
IncludeUninitialized(false).
RequireObject(true).
Latest()
if len(components) > 0 {
selector := fmt.Sprintf("%s in (%s)", clustermetadata.LabelComponent, strings.Join(components, ","))
f = f.LabelSelectorParam(selector)
}
return f.Do()
}
// ResourceInfo holds information about cluster resources.
type ResourceInfo interface {
Err() error
Infos() ([]*resource.Info, error)
// Resoucetypes to exclude when fetching managed objects
var unmanagedKinds = map[string]bool{
"ComponentStatus": true,
"Endpoints": true,
}
var _ ResourceInfo = (*resource.Result)(nil)
// ManagedObjects returns a slice of ksonnet managed objects.
func ManagedObjects(r ResourceInfo) ([]*unstructured.Unstructured, error) {
if err := r.Err(); err != nil {
return nil, errors.WithStack(err)
// DefaultResourceInfo fetches objects from the cluster.
func fetchManagedObjects(namespace string, clients Clients, components []string) ([]*unstructured.Unstructured, error) {
log := log.WithFields(log.Fields{
"action": "fetchManagedObjects",
"namespace": namespace,
})
if clients.discovery == nil {
return nil, errors.New("nil discovery client")
}
if clients.clientPool == nil {
return nil, errors.New("nil client pool")
}
infos, err := r.Infos()
// TODO address cluster-wide-resources defined in other environments (see ServerPreferredResources)
resources, err := clients.discovery.ServerPreferredNamespacedResources()
if err != nil {
return nil, errors.WithStack(err)
return nil, errors.Wrap(err, "ServerPreferredNamespacedResources")
}
sortResources(resources) // Sift "extensions" to the end because it duplicates resources, e.g. Deployments
// Filter out resources we can't list
filtered := discovery.FilteredBy(
discovery.ResourcePredicateFunc(
func(groupVersion string, r *metav1.APIResource) bool {
return (!unmanagedKinds[r.Kind]) &&
discovery.SupportsAllVerbs{Verbs: []string{"list", "get"}}.Match(groupVersion, r)
},
),
resources,
)
uids := make(map[types.UID]bool)
results := make([]*unstructured.Unstructured, 0)
for _, lst := range filtered {
gv, err := schema.ParseGroupVersion(lst.GroupVersion)
if err != nil {
return nil, errors.Wrapf(err, "parsing GroupVersion: %s", lst.GroupVersion)
}
lookup := make(map[types.UID]map[string]interface{})
var objects []*unstructured.Unstructured
for i := range infos {
if obj, ok := infos[i].Object.(*unstructured.Unstructured); ok {
if _, ok := lookup[obj.GetUID()]; ok {
// we've seen this object already
for _, resource := range lst.APIResources {
// Create a dynamic client for this resource type
gvr := gv.WithKind(resource.Kind)
dynamic, err := clients.clientPool.ClientForGroupVersionKind(gvr)
log.Debugf("listing resources: %s", gvr.String())
if err != nil {
return nil, errors.Wrapf(err, "creating client for resource: %s", gvr.String())
}
resourceClient := dynamic.Resource(&resource, namespace)
// List managed resources of this type from the cluster
obj, err := resourceClient.List(metav1.ListOptions{
LabelSelector: "app.kubernetes.io/deploy-manager=ksonnet",
})
if err != nil {
log.Warnf("skipping %s due to error: %v", resource.Kind, err)
continue
}
lookup[obj.GetUID()] = obj.Object
objects = append(objects, obj)
if ul, ok := obj.(*unstructured.UnstructuredList); ok {
if err := ul.EachListItem(func(o runtime.Object) error {
if u, ok := o.(*unstructured.Unstructured); ok {
// Filter out duplicates, e.g apps/v1/Deployment vs. extensions/v1beta1/Deployment
if uids[u.GetUID()] {
return nil
}
uids[u.GetUID()] = true
results = append(results, u)
}
return nil
}); err != nil {
return nil, errors.Wrapf(err, "iterating %s", resource.Kind)
}
}
}
}
return results, nil
}
return objects, nil
// ResourceInfo holds information about cluster resources.
type ResourceInfo interface {
Err() error
Infos() ([]*resource.Info, error)
}
var _ ResourceInfo = (*resource.Result)(nil)
// RebuildObject rebuilds the ksonnet generated object from an object on
// the cluster.
func RebuildObject(m map[string]interface{}) (map[string]interface{}, error) {
......@@ -121,7 +153,7 @@ func RebuildObject(m map[string]interface{}) (map[string]interface{}, error) {
}
annotations, ok := metadata["annotations"].(map[string]interface{})
if !ok {
return nil, errors.New("metadata annotations not found")
return nil, errors.Errorf("metadata annotations not found: %v", metadata)
}
descriptor, ok := annotations[clustermetadata.AnnotationManaged].(string)
if !ok {
......@@ -136,10 +168,30 @@ func RebuildObject(m map[string]interface{}) (map[string]interface{}, error) {
return mm.Decode()
}
// filterManagedObjects filters out any non-managed objects according to their labels
func filterManagedObjects(objects []*unstructured.Unstructured) []*unstructured.Unstructured {
// see Filtering without allocating - https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
filtered := objects[:0]
for _, o := range objects {
labels := o.GetLabels()
if labels == nil {
continue
}
if labels["app.kubernetes.io/deploy-manager"] == "ksonnet" {
filtered = append(filtered, o)
}
}
return filtered
}
// CollectObjects collects objects in a cluster namespace.
func CollectObjects(namespace string, config clientcmd.ClientConfig, components []string) ([]*unstructured.Unstructured, error) {
res := DefaultResourceInfo(namespace, config, components)
objects, err := ManagedObjects(res)
func CollectObjects(namespace string, clients Clients, components []string) ([]*unstructured.Unstructured, error) {
objects, err := fetchManagedObjects(namespace, clients, components)
if err != nil {
return nil, err
}
objects = filterManagedObjects(objects)
if err != nil {
return nil, err
}
......
......@@ -32,7 +32,7 @@ type objectDescriber interface {
// defaultObjectDescriber is the default implementation of objectDescriber.
type defaultObjectDescriber struct {
// clientOpts are Kubernetes client otpions.
clientOpts clientOpts
clientOpts Clients
// objectInfo locates information for Kubernetes objects.
objectInfo ObjectInfo
......@@ -41,7 +41,7 @@ type defaultObjectDescriber struct {
var _ objectDescriber = (*defaultObjectDescriber)(nil)
// newDefaultObjectDescriber creates an instance of defaultObjectDescriber.
func newDefaultObjectDescriber(co clientOpts, oi ObjectInfo) (*defaultObjectDescriber, error) {
func newDefaultObjectDescriber(co Clients, oi ObjectInfo) (*defaultObjectDescriber, error) {
if oi == nil {
return nil, errors.Errorf("object info is required")
}
......
......@@ -25,7 +25,7 @@ import (
)
func Test_defaultObjectDescriber_Describe(t *testing.T) {
co := clientOpts{}
co := Clients{}
oi := &fakeObjectInfo{resourceName: "name"}
od, err := newDefaultObjectDescriber(co, oi)
......
......@@ -21,10 +21,8 @@ import (
"path/filepath"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
......@@ -147,71 +145,6 @@ func TestSetMetadataAnnotation(t *testing.T) {
}
}
func TestManagedObjects(t *testing.T) {
obj1 := &unstructured.Unstructured{}
obj1.SetUID(types.UID("uid"))
obj2 := &unstructured.Unstructured{}
obj2.SetUID(types.UID("uid"))
obj3 := &unstructured.Unstructured{}
obj3.SetUID(types.UID("uid2"))
cases := []struct {
name string
resourceInfo ResourceInfo
expected []*unstructured.Unstructured
isErr bool
}{
{
name: "no error",
resourceInfo: func() *fakeResourceInfo {
return &fakeResourceInfo{
infos: []*resource.Info{
{Object: obj1},
{Object: obj2},
{Object: obj3},
},
}
}(),
expected: []*unstructured.Unstructured{obj1, obj3},
},
{
name: "resource info error",
resourceInfo: func() *fakeResourceInfo {
return &fakeResourceInfo{
err: errors.New("error"),
}
}(),
isErr: true,
},
{
name: "infos error",
resourceInfo: func() *fakeResourceInfo {
return &fakeResourceInfo{
infosErr: errors.New("error"),
}
}(),
isErr: true,
},
}
for _, tc := range cases {