Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions pkg/utils/kube/resource_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,6 @@ func (k *kubectlResourceOperations) ReplaceResource(ctx context.Context, obj *un
return err
}

if err := replaceOptions.Validate(); err != nil {
return fmt.Errorf("error validating replace options: %w", err)
}

return replaceOptions.Run(k.fact)
})
}
Expand Down Expand Up @@ -433,6 +429,10 @@ func (k *kubectlServerSideDiffDryRunApplier) newApplyOptions(ioStreams genericcl
}

o.ForceConflicts = true

if err := o.Validate(); err != nil {
return nil, fmt.Errorf("error validating options: %w", err)
}
return o, nil
}

Expand Down Expand Up @@ -462,6 +462,10 @@ func (k *kubectlResourceOperations) newApplyOptions(ioStreams genericclioptions.
if serverSideApply {
o.ForceConflicts = true
}

if err := o.Validate(); err != nil {
return nil, fmt.Errorf("error validating options: %w", err)
}
return o, nil
}

Expand Down Expand Up @@ -496,6 +500,10 @@ func (k *kubectlResourceOperations) newCreateOptions(ioStreams genericclioptions
return printer.PrintObj(obj, o.Out)
}
o.FilenameOptions.Filenames = []string{fileName}

if err := o.Validate(); err != nil {
return nil, fmt.Errorf("error validating options: %w", err)
}
return o, nil
}

Expand Down Expand Up @@ -551,6 +559,9 @@ func (k *kubectlResourceOperations) newReplaceOptions(config *rest.Config, f cmd
o.DeleteOptions.ForceDeletion = force
}

if err := o.Validate(); err != nil {
return nil, fmt.Errorf("error validating options: %w", err)
}
return o, nil
}

Expand Down Expand Up @@ -580,6 +591,10 @@ func newReconcileOptions(f cmdutil.Factory, kubeClient *kubernetes.Clientset, fi
return nil, fmt.Errorf("error configuring printer: %w", err)
}
o.PrintObject = printer.PrintObj

if err := o.Validate(); err != nil {
return nil, fmt.Errorf("error validating options: %w", err)
}
return o, nil
}

Expand Down
167 changes: 167 additions & 0 deletions pkg/utils/kube/resource_ops_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package kube

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/rest"
k8stesting "k8s.io/client-go/testing"
"k8s.io/klog/v2/textlogger"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme"

testingutils "github.com/argoproj/gitops-engine/pkg/utils/testing"
"github.com/argoproj/gitops-engine/pkg/utils/tracing"
)

func newTestResourceOperations(client *fake.FakeDynamicClient) (*kubectlResourceOperations, func()) {
tf := cmdtesting.NewTestFactory()
tf.FakeDynamicClient = client
tf.UnstructuredClientForMappingFunc = func(version schema.GroupVersion) (resource.RESTClient, error) {
return testingutils.NewFakeRESTClientBackedByDynamic(version, client), nil
}

ops := &kubectlResourceOperations{
config: &rest.Config{},
log: textlogger.NewLogger(textlogger.NewConfig()).WithValues("application", "fake-app"),
tracer: tracing.NopTracer{},
fact: tf,
}
return ops, tf.Cleanup
}

func TestApplyResource_Success(t *testing.T) {
obj := testingutils.NewService()
obj.SetNamespace("test")

client := fake.NewSimpleDynamicClient(scheme.Scheme)
ops, cleanup := newTestResourceOperations(client)
defer cleanup()

called := false
client.PrependReactor("create", "*", func(_ k8stesting.Action) (_ bool, _ runtime.Object, _ error) {
called = true
return false, nil, nil
})

out, err := ops.ApplyResource(context.Background(), obj, cmdutil.DryRunNone, false, false, false, "test-manager")
require.NoError(t, err)
assert.True(t, called)
assert.Equal(t, "service/my-service created", out)
}

func Test_kubectlResourceOperations_ApplyResource(t *testing.T) {
newService := func() *unstructured.Unstructured {
obj := testingutils.NewService()
obj.SetNamespace("test")
return obj
}

type args struct {
obj *unstructured.Unstructured
dryRunStrategy cmdutil.DryRunStrategy
force bool
validate bool
serverSideApply bool
}
tests := []struct {
name string
args args
client *fake.FakeDynamicClient
want string
wantErr bool
}{
{
name: "success",
args: args{
obj: newService(),
dryRunStrategy: cmdutil.DryRunNone,
force: false,
validate: false,
serverSideApply: false,
},
client: fake.NewSimpleDynamicClient(scheme.Scheme),
want: "service/my-service created",
},
{
name: "success existing",
args: args{
obj: newService(),
dryRunStrategy: cmdutil.DryRunNone,
force: false,
validate: false,
serverSideApply: false,
},
client: fake.NewSimpleDynamicClient(scheme.Scheme, newService()),
want: "service/my-service created",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ops, cleanup := newTestResourceOperations(tt.client)
defer cleanup()

got, err := ops.ApplyResource(context.Background(), tt.args.obj, tt.args.dryRunStrategy, tt.args.force, tt.args.validate, tt.args.serverSideApply, "test-manager")
if tt.wantErr {
require.Error(t, err)
assert.ErrorContains(t, err, tt.want)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

// func TestApplyResource_RunResourceCommandError(t *testing.T) {
// obj := &unstructured.Unstructured{}
// obj.SetKind("ConfigMap")
// obj.SetName("fail-cm")
// obj.SetNamespace("default")

// mockOps := &mockKubectlResourceOperations{
// kubectlResourceOperations: kubectlResourceOperations{
// config: &fakeRestConfig{Host: "https://k8s.example.com"},
// log: testr.New(t),
// tracer: &mockTracer{},
// },
// runResourceCommandFunc: func(ctx context.Context, o *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, executor commandExecutor) (string, error) {
// return "", errors.New("runResourceCommand failed")
// },
// }

// out, err := mockOps.ApplyResource(context.Background(), obj, cmdutil.DryRunNone, false, false, false, "test-manager")
// assert.Error(t, err)
// assert.Contains(t, err.Error(), "runResourceCommand failed")
// assert.Empty(t, out)
// }

// func TestApplyResource_LogsWithDryRun(t *testing.T) {
// obj := &unstructured.Unstructured{}
// obj.SetKind("ConfigMap")
// obj.SetName("dryrun-cm")
// obj.SetNamespace("default")

// mockOps := &mockKubectlResourceOperations{
// kubectlResourceOperations: kubectlResourceOperations{
// config: &fakeRestConfig{Host: "https://k8s.example.com"},
// log: textlogger.NewLogger(textlogger.NewConfig()).WithValues("application", "fake-app"),
// tracer: &mockTracer{},
// },
// runResourceCommandFunc: func(ctx context.Context, o *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, executor commandExecutor) (string, error) {
// return "dryrun applied", nil
// },
// }

// out, err := mockOps.ApplyResource(context.Background(), obj, cmdutil.DryRunClient, false, false, false, "test-manager")
// assert.NoError(t, err)
// assert.Equal(t, "dryrun applied", out)
// }
81 changes: 81 additions & 0 deletions pkg/utils/testing/api_resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package testing

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

var (
commonVerbs = []string{"create", "get", "list", "watch", "update", "patch", "delete", "deletecollection"}
subresourceVerbs = []string{"get", "update", "patch"}
)

// StaticAPIResources defines the common Kubernetes API resources that are usually returned by a DiscoveryClient
var StaticAPIResources = []*metav1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
{Name: "pods", SingularName: "pod", Namespaced: true, Kind: "Pod", Verbs: commonVerbs},
{Name: "pods/status", SingularName: "", Namespaced: true, Kind: "Pod", Verbs: subresourceVerbs},
{Name: "pods/log", SingularName: "", Namespaced: true, Kind: "Pod", Verbs: []string{"get"}},
{Name: "pods/exec", SingularName: "", Namespaced: true, Kind: "Pod", Verbs: []string{"create"}},
{Name: "services", SingularName: "service", Namespaced: true, Kind: "Service", Verbs: commonVerbs},
{Name: "services/status", SingularName: "", Namespaced: true, Kind: "Service", Verbs: subresourceVerbs},
{Name: "configmaps", SingularName: "configmap", Namespaced: true, Kind: "ConfigMap", Verbs: commonVerbs},
{Name: "secrets", SingularName: "secret", Namespaced: true, Kind: "Secret", Verbs: commonVerbs},
{Name: "namespaces", SingularName: "namespace", Namespaced: false, Kind: "Namespace", Verbs: commonVerbs},
{Name: "namespaces/status", SingularName: "", Namespaced: false, Kind: "Namespace", Verbs: subresourceVerbs},
{Name: "nodes", SingularName: "node", Namespaced: false, Kind: "Node", Verbs: []string{"get", "list", "watch"}},
{Name: "persistentvolumes", SingularName: "persistentvolume", Namespaced: false, Kind: "PersistentVolume", Verbs: commonVerbs},
{Name: "persistentvolumeclaims", SingularName: "persistentvolumeclaim", Namespaced: true, Kind: "PersistentVolumeClaim", Verbs: commonVerbs},
{Name: "persistentvolumeclaims/status", SingularName: "", Namespaced: true, Kind: "PersistentVolumeClaim", Verbs: subresourceVerbs},
{Name: "events", SingularName: "event", Namespaced: true, Kind: "Event", Verbs: []string{"create", "get", "list", "watch"}},
{Name: "serviceaccounts", SingularName: "serviceaccount", Namespaced: true, Kind: "ServiceAccount", Verbs: commonVerbs},
},
},
{
GroupVersion: "apps/v1",
APIResources: []metav1.APIResource{
{Name: "deployments", SingularName: "deployment", Namespaced: true, Kind: "Deployment", Verbs: commonVerbs},
{Name: "deployments/status", SingularName: "", Namespaced: true, Kind: "Deployment", Verbs: subresourceVerbs},
{Name: "deployments/scale", SingularName: "", Namespaced: true, Kind: "Scale", Verbs: subresourceVerbs},
{Name: "statefulsets", SingularName: "statefulset", Namespaced: true, Kind: "StatefulSet", Verbs: commonVerbs},
{Name: "statefulsets/status", SingularName: "", Namespaced: true, Kind: "StatefulSet", Verbs: subresourceVerbs},
{Name: "statefulsets/scale", SingularName: "", Namespaced: true, Kind: "Scale", Verbs: subresourceVerbs},
{Name: "daemonsets", SingularName: "daemonset", Namespaced: true, Kind: "DaemonSet", Verbs: commonVerbs},
{Name: "daemonsets/status", SingularName: "", Namespaced: true, Kind: "DaemonSet", Verbs: subresourceVerbs},
{Name: "replicasets", SingularName: "replicaset", Namespaced: true, Kind: "ReplicaSet", Verbs: commonVerbs},
{Name: "replicasets/status", SingularName: "", Namespaced: true, Kind: "ReplicaSet", Verbs: subresourceVerbs},
},
},
{
GroupVersion: "batch/v1",
APIResources: []metav1.APIResource{
{Name: "jobs", SingularName: "job", Namespaced: true, Kind: "Job", Verbs: commonVerbs},
{Name: "jobs/status", SingularName: "", Namespaced: true, Kind: "Job", Verbs: subresourceVerbs},
{Name: "cronjobs", SingularName: "cronjob", Namespaced: true, Kind: "CronJob", Verbs: commonVerbs},
{Name: "cronjobs/status", SingularName: "", Namespaced: true, Kind: "CronJob", Verbs: subresourceVerbs},
},
},
{
GroupVersion: "rbac.authorization.k8s.io/v1",
APIResources: []metav1.APIResource{
{Name: "roles", SingularName: "role", Namespaced: true, Kind: "Role", Verbs: commonVerbs},
{Name: "rolebindings", SingularName: "rolebinding", Namespaced: true, Kind: "RoleBinding", Verbs: commonVerbs},
{Name: "clusterroles", SingularName: "clusterrole", Namespaced: false, Kind: "ClusterRole", Verbs: commonVerbs},
{Name: "clusterrolebindings", SingularName: "clusterrolebinding", Namespaced: false, Kind: "ClusterRoleBinding", Verbs: commonVerbs},
},
},
{
GroupVersion: "networking.k8s.io/v1",
APIResources: []metav1.APIResource{
{Name: "ingresses", SingularName: "ingress", Namespaced: true, Kind: "Ingress", Verbs: commonVerbs},
{Name: "ingresses/status", SingularName: "", Namespaced: true, Kind: "Ingress", Verbs: subresourceVerbs},
{Name: "networkpolicies", SingularName: "networkpolicy", Namespaced: true, Kind: "NetworkPolicy", Verbs: commonVerbs},
},
},
{
GroupVersion: "policy/v1",
APIResources: []metav1.APIResource{
{Name: "poddisruptionbudgets", SingularName: "poddisruptionbudget", Namespaced: true, Kind: "PodDisruptionBudget", Verbs: commonVerbs},
{Name: "poddisruptionbudgets/status", SingularName: "", Namespaced: true, Kind: "PodDisruptionBudget", Verbs: subresourceVerbs},
},
},
}
Loading
Loading