From f93a883dd8ac1c29f4ddcc80bfc10838892299ba Mon Sep 17 00:00:00 2001 From: Quentin Barrand Date: Wed, 23 Aug 2023 17:15:43 +0200 Subject: [PATCH] Add the NodeModulesConfig controller (#711) Reconcile NodeModulesConfig resources by creating and monitoring worker Pods. Use the NodeModulesConfig status to maintain the state of modules on nodes. Upstream-Commit: 04d0c1c04e052be2fbd9b041a3358e349f4e07c8 --- api/v1beta1/nodemodulesconfig_types.go | 7 +- ...dule-management.clusterserviceversion.yaml | 10 + .../kmm.sigs.x-k8s.io_nodemodulesconfigs.yaml | 3 + cmd/manager/main.go | 19 +- .../kmm.sigs.x-k8s.io_nodemodulesconfigs.yaml | 3 + config/manager/kustomization.yaml | 3 + .../manager/manager_worker_image_patch.yaml | 13 + config/rbac/role.yaml | 8 + .../mock_nodemodulesconfig_reconciler.go | 174 ++++ controllers/module_nmc_reconciler.go | 4 + controllers/nodemodulesconfig_reconciler.go | 702 ++++++++++++++ .../nodemodulesconfig_reconciler_test.go | 915 ++++++++++++++++++ go.mod | 4 +- go.sum | 4 + internal/cmd/cmdutils.go | 2 +- internal/filter/filter.go | 4 + .../budougumi0617/cmpmock/.golangci.yml | 45 + .../budougumi0617/cmpmock/.release-it.json | 17 + .../github.com/budougumi0617/cmpmock/LICENSE | 21 + .../budougumi0617/cmpmock/README.md | 66 ++ .../budougumi0617/cmpmock/diffmatcher.go | 42 + vendor/github.com/golang/mock/AUTHORS | 12 + vendor/github.com/golang/mock/CONTRIBUTORS | 37 + vendor/github.com/golang/mock/LICENSE | 202 ++++ vendor/github.com/golang/mock/gomock/call.go | 433 +++++++++ .../github.com/golang/mock/gomock/callset.go | 112 +++ .../golang/mock/gomock/controller.go | 333 +++++++ .../github.com/golang/mock/gomock/matchers.go | 269 +++++ .../google/go-cmp/cmp/cmpopts/equate.go | 156 +++ .../google/go-cmp/cmp/cmpopts/ignore.go | 206 ++++ .../google/go-cmp/cmp/cmpopts/sort.go | 147 +++ .../go-cmp/cmp/cmpopts/struct_filter.go | 189 ++++ .../google/go-cmp/cmp/cmpopts/xform.go | 36 + .../kubectl/pkg/cmd/util/podcmd/podcmd.go | 104 ++ vendor/modules.txt | 8 + 35 files changed, 4302 insertions(+), 8 deletions(-) create mode 100644 config/manager/manager_worker_image_patch.yaml create mode 100644 controllers/mock_nodemodulesconfig_reconciler.go create mode 100644 controllers/nodemodulesconfig_reconciler.go create mode 100644 controllers/nodemodulesconfig_reconciler_test.go create mode 100644 vendor/github.com/budougumi0617/cmpmock/.golangci.yml create mode 100644 vendor/github.com/budougumi0617/cmpmock/.release-it.json create mode 100644 vendor/github.com/budougumi0617/cmpmock/LICENSE create mode 100644 vendor/github.com/budougumi0617/cmpmock/README.md create mode 100644 vendor/github.com/budougumi0617/cmpmock/diffmatcher.go create mode 100644 vendor/github.com/golang/mock/AUTHORS create mode 100644 vendor/github.com/golang/mock/CONTRIBUTORS create mode 100644 vendor/github.com/golang/mock/LICENSE create mode 100644 vendor/github.com/golang/mock/gomock/call.go create mode 100644 vendor/github.com/golang/mock/gomock/callset.go create mode 100644 vendor/github.com/golang/mock/gomock/controller.go create mode 100644 vendor/github.com/golang/mock/gomock/matchers.go create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go create mode 100644 vendor/k8s.io/kubectl/pkg/cmd/util/podcmd/podcmd.go diff --git a/api/v1beta1/nodemodulesconfig_types.go b/api/v1beta1/nodemodulesconfig_types.go index f9d1f7e5f..830743434 100644 --- a/api/v1beta1/nodemodulesconfig_types.go +++ b/api/v1beta1/nodemodulesconfig_types.go @@ -31,9 +31,10 @@ type ModuleConfig struct { } type NodeModuleSpec struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Config ModuleConfig `json:"config"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Config ModuleConfig `json:"config"` + ServiceAccountName string `json:"serviceAccountName"` } // NodeModulesConfigSpec describes the desired state of modules on the node diff --git a/bundle/manifests/kernel-module-management.clusterserviceversion.yaml b/bundle/manifests/kernel-module-management.clusterserviceversion.yaml index 68b0f2354..321f29450 100644 --- a/bundle/manifests/kernel-module-management.clusterserviceversion.yaml +++ b/bundle/manifests/kernel-module-management.clusterserviceversion.yaml @@ -162,6 +162,8 @@ spec: resources: - pods verbs: + - create + - delete - get - list - patch @@ -224,6 +226,12 @@ spec: - list - patch - watch + - apiGroups: + - kmm.sigs.x-k8s.io + resources: + - nodemodulesconfigs/status + verbs: + - patch - apiGroups: - kmm.sigs.x-k8s.io resources: @@ -321,6 +329,8 @@ spec: command: - /usr/local/bin/manager env: + - name: RELATED_IMAGES_WORKER + value: quay.io/edge-infrastructure/kernel-module-management-worker:latest - name: SSL_CERT_DIR value: /etc/pki/ca-trust/extracted/pem - name: OPERATOR_NAMESPACE diff --git a/bundle/manifests/kmm.sigs.x-k8s.io_nodemodulesconfigs.yaml b/bundle/manifests/kmm.sigs.x-k8s.io_nodemodulesconfigs.yaml index dafa13c70..f35949f2c 100644 --- a/bundle/manifests/kmm.sigs.x-k8s.io_nodemodulesconfigs.yaml +++ b/bundle/manifests/kmm.sigs.x-k8s.io_nodemodulesconfigs.yaml @@ -153,10 +153,13 @@ spec: type: string namespace: type: string + serviceAccountName: + type: string required: - config - name - namespace + - serviceAccountName type: object type: array type: object diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 25c268463..bd14bae46 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -22,8 +22,6 @@ import ( "os" "strconv" - "github.com/rh-ecosystem-edge/kernel-module-management/internal/config" - ocpbuildutils "github.com/rh-ecosystem-edge/kernel-module-management/internal/utils/ocpbuild" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -48,6 +46,7 @@ import ( "github.com/rh-ecosystem-edge/kernel-module-management/internal/build" buildocpbuild "github.com/rh-ecosystem-edge/kernel-module-management/internal/build/ocpbuild" "github.com/rh-ecosystem-edge/kernel-module-management/internal/cmd" + "github.com/rh-ecosystem-edge/kernel-module-management/internal/config" "github.com/rh-ecosystem-edge/kernel-module-management/internal/constants" "github.com/rh-ecosystem-edge/kernel-module-management/internal/daemonset" "github.com/rh-ecosystem-edge/kernel-module-management/internal/filter" @@ -60,6 +59,7 @@ import ( signocpbuild "github.com/rh-ecosystem-edge/kernel-module-management/internal/sign/ocpbuild" "github.com/rh-ecosystem-edge/kernel-module-management/internal/statusupdater" "github.com/rh-ecosystem-edge/kernel-module-management/internal/syncronizedmap" + ocpbuildutils "github.com/rh-ecosystem-edge/kernel-module-management/internal/utils/ocpbuild" //+kubebuilder:scaffold:imports ) @@ -96,6 +96,7 @@ func main() { setupLogger.Info("Creating manager", "version", Version, "git commit", GitCommit) operatorNamespace := cmd.GetEnvOrFatalError(constants.OperatorNamespaceEnvVar, setupLogger) + workerImage := cmd.GetEnvOrFatalError("RELATED_IMAGES_WORKER", setupLogger) managed, err := GetBoolEnv("KMM_MANAGED") if err != nil { @@ -181,7 +182,19 @@ func main() { cmd.FatalError(setupLogger, err, "unable to create controller", "name", controllers.ModuleNMCReconcilerName) } + workerHelper := controllers.NewWorkerHelper( + client, + controllers.NewPodManager(client, workerImage, scheme), + ) + + ctx := ctrl.SetupSignalHandler() + + if err = controllers.NewNodeModulesConfigReconciler(client, workerHelper).SetupWithManager(ctx, mgr); err != nil { + cmd.FatalError(setupLogger, err, "unable to create controller", "name", controllers.NodeModulesConfigReconcilerName) + } + nodeKernelReconciler := controllers.NewNodeKernelReconciler(client, constants.KernelLabel, filterAPI, kernelOsDtkMapping) + if err = nodeKernelReconciler.SetupWithManager(mgr); err != nil { cmd.FatalError(setupLogger, err, "unable to create controller", "name", controllers.NodeKernelReconcilerName) } @@ -251,7 +264,7 @@ func main() { } setupLogger.Info("starting manager") - if err = mgr.Start(ctrl.SetupSignalHandler()); err != nil { + if err = mgr.Start(ctx); err != nil { cmd.FatalError(setupLogger, err, "problem running manager") } } diff --git a/config/crd/bases/kmm.sigs.x-k8s.io_nodemodulesconfigs.yaml b/config/crd/bases/kmm.sigs.x-k8s.io_nodemodulesconfigs.yaml index 7a9019359..d26315d70 100644 --- a/config/crd/bases/kmm.sigs.x-k8s.io_nodemodulesconfigs.yaml +++ b/config/crd/bases/kmm.sigs.x-k8s.io_nodemodulesconfigs.yaml @@ -149,10 +149,13 @@ spec: type: string namespace: type: string + serviceAccountName: + type: string required: - config - name - namespace + - serviceAccountName type: object type: array type: object diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index f282f5b28..532b82183 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -4,6 +4,9 @@ kind: Kustomization resources: - ../manager-base +patches: +- path: manager_worker_image_patch.yaml + images: - name: controller newName: quay.io/edge-infrastructure/kernel-module-management-operator diff --git a/config/manager/manager_worker_image_patch.yaml b/config/manager/manager_worker_image_patch.yaml new file mode 100644 index 000000000..84760f775 --- /dev/null +++ b/config/manager/manager_worker_image_patch.yaml @@ -0,0 +1,13 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + env: + - name: RELATED_IMAGES_WORKER + value: worker diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 79330efdc..2e26ec826 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -79,6 +79,8 @@ rules: resources: - pods verbs: + - create + - delete - get - list - patch @@ -141,6 +143,12 @@ rules: - list - patch - watch +- apiGroups: + - kmm.sigs.x-k8s.io + resources: + - nodemodulesconfigs/status + verbs: + - patch - apiGroups: - kmm.sigs.x-k8s.io resources: diff --git a/controllers/mock_nodemodulesconfig_reconciler.go b/controllers/mock_nodemodulesconfig_reconciler.go new file mode 100644 index 000000000..07da289a1 --- /dev/null +++ b/controllers/mock_nodemodulesconfig_reconciler.go @@ -0,0 +1,174 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: nodemodulesconfig_reconciler.go + +// Package controllers is a generated GoMock package. +package controllers + +import ( + context "context" + reflect "reflect" + + v1beta1 "github.com/rh-ecosystem-edge/kernel-module-management/api/v1beta1" + gomock "go.uber.org/mock/gomock" + v1 "k8s.io/api/core/v1" + client "sigs.k8s.io/controller-runtime/pkg/client" +) + +// MockWorkerHelper is a mock of WorkerHelper interface. +type MockWorkerHelper struct { + ctrl *gomock.Controller + recorder *MockWorkerHelperMockRecorder +} + +// MockWorkerHelperMockRecorder is the mock recorder for MockWorkerHelper. +type MockWorkerHelperMockRecorder struct { + mock *MockWorkerHelper +} + +// NewMockWorkerHelper creates a new mock instance. +func NewMockWorkerHelper(ctrl *gomock.Controller) *MockWorkerHelper { + mock := &MockWorkerHelper{ctrl: ctrl} + mock.recorder = &MockWorkerHelperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWorkerHelper) EXPECT() *MockWorkerHelperMockRecorder { + return m.recorder +} + +// ProcessModuleSpec mocks base method. +func (m *MockWorkerHelper) ProcessModuleSpec(ctx context.Context, nmc *v1beta1.NodeModulesConfig, spec *v1beta1.NodeModuleSpec, status *v1beta1.NodeModuleStatus) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessModuleSpec", ctx, nmc, spec, status) + ret0, _ := ret[0].(error) + return ret0 +} + +// ProcessModuleSpec indicates an expected call of ProcessModuleSpec. +func (mr *MockWorkerHelperMockRecorder) ProcessModuleSpec(ctx, nmc, spec, status interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessModuleSpec", reflect.TypeOf((*MockWorkerHelper)(nil).ProcessModuleSpec), ctx, nmc, spec, status) +} + +// ProcessOrphanModuleStatus mocks base method. +func (m *MockWorkerHelper) ProcessOrphanModuleStatus(ctx context.Context, nmc *v1beta1.NodeModulesConfig, status *v1beta1.NodeModuleStatus) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessOrphanModuleStatus", ctx, nmc, status) + ret0, _ := ret[0].(error) + return ret0 +} + +// ProcessOrphanModuleStatus indicates an expected call of ProcessOrphanModuleStatus. +func (mr *MockWorkerHelperMockRecorder) ProcessOrphanModuleStatus(ctx, nmc, status interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessOrphanModuleStatus", reflect.TypeOf((*MockWorkerHelper)(nil).ProcessOrphanModuleStatus), ctx, nmc, status) +} + +// RemoveOrphanFinalizers mocks base method. +func (m *MockWorkerHelper) RemoveOrphanFinalizers(ctx context.Context, nodeName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveOrphanFinalizers", ctx, nodeName) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveOrphanFinalizers indicates an expected call of RemoveOrphanFinalizers. +func (mr *MockWorkerHelperMockRecorder) RemoveOrphanFinalizers(ctx, nodeName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveOrphanFinalizers", reflect.TypeOf((*MockWorkerHelper)(nil).RemoveOrphanFinalizers), ctx, nodeName) +} + +// SyncStatus mocks base method. +func (m *MockWorkerHelper) SyncStatus(ctx context.Context, nmc *v1beta1.NodeModulesConfig) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SyncStatus", ctx, nmc) + ret0, _ := ret[0].(error) + return ret0 +} + +// SyncStatus indicates an expected call of SyncStatus. +func (mr *MockWorkerHelperMockRecorder) SyncStatus(ctx, nmc interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncStatus", reflect.TypeOf((*MockWorkerHelper)(nil).SyncStatus), ctx, nmc) +} + +// MockPodManager is a mock of PodManager interface. +type MockPodManager struct { + ctrl *gomock.Controller + recorder *MockPodManagerMockRecorder +} + +// MockPodManagerMockRecorder is the mock recorder for MockPodManager. +type MockPodManagerMockRecorder struct { + mock *MockPodManager +} + +// NewMockPodManager creates a new mock instance. +func NewMockPodManager(ctrl *gomock.Controller) *MockPodManager { + mock := &MockPodManager{ctrl: ctrl} + mock.recorder = &MockPodManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPodManager) EXPECT() *MockPodManagerMockRecorder { + return m.recorder +} + +// CreateLoaderPod mocks base method. +func (m *MockPodManager) CreateLoaderPod(ctx context.Context, nmc client.Object, nms *v1beta1.NodeModuleSpec) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateLoaderPod", ctx, nmc, nms) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateLoaderPod indicates an expected call of CreateLoaderPod. +func (mr *MockPodManagerMockRecorder) CreateLoaderPod(ctx, nmc, nms interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLoaderPod", reflect.TypeOf((*MockPodManager)(nil).CreateLoaderPod), ctx, nmc, nms) +} + +// CreateUnloaderPod mocks base method. +func (m *MockPodManager) CreateUnloaderPod(ctx context.Context, nmc client.Object, nms *v1beta1.NodeModuleStatus) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUnloaderPod", ctx, nmc, nms) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateUnloaderPod indicates an expected call of CreateUnloaderPod. +func (mr *MockPodManagerMockRecorder) CreateUnloaderPod(ctx, nmc, nms interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUnloaderPod", reflect.TypeOf((*MockPodManager)(nil).CreateUnloaderPod), ctx, nmc, nms) +} + +// DeletePod mocks base method. +func (m *MockPodManager) DeletePod(ctx context.Context, pod *v1.Pod) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePod", ctx, pod) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePod indicates an expected call of DeletePod. +func (mr *MockPodManagerMockRecorder) DeletePod(ctx, pod interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePod", reflect.TypeOf((*MockPodManager)(nil).DeletePod), ctx, pod) +} + +// ListWorkerPodsOnNode mocks base method. +func (m *MockPodManager) ListWorkerPodsOnNode(ctx context.Context, nodeName string) ([]v1.Pod, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListWorkerPodsOnNode", ctx, nodeName) + ret0, _ := ret[0].([]v1.Pod) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListWorkerPodsOnNode indicates an expected call of ListWorkerPodsOnNode. +func (mr *MockPodManagerMockRecorder) ListWorkerPodsOnNode(ctx, nodeName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListWorkerPodsOnNode", reflect.TypeOf((*MockPodManager)(nil).ListWorkerPodsOnNode), ctx, nodeName) +} diff --git a/controllers/module_nmc_reconciler.go b/controllers/module_nmc_reconciler.go index dda04eb96..2ca95ea10 100644 --- a/controllers/module_nmc_reconciler.go +++ b/controllers/module_nmc_reconciler.go @@ -243,6 +243,10 @@ func (mnrh *moduleNMCReconcilerHelper) enableModuleOnNode(ctx context.Context, m Modprobe: mld.Modprobe, } + if tls := mld.RegistryTLS; tls != nil { + moduleConfig.InsecurePull = tls.Insecure || tls.InsecureSkipTLSVerify + } + nmc := &kmmv1beta1.NodeModulesConfig{ ObjectMeta: metav1.ObjectMeta{Name: nodeName}, } diff --git a/controllers/nodemodulesconfig_reconciler.go b/controllers/nodemodulesconfig_reconciler.go new file mode 100644 index 000000000..6f6716cc9 --- /dev/null +++ b/controllers/nodemodulesconfig_reconciler.go @@ -0,0 +1,702 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/hashicorp/go-multierror" + kmmv1beta1 "github.com/rh-ecosystem-edge/kernel-module-management/api/v1beta1" + "github.com/rh-ecosystem-edge/kernel-module-management/internal/constants" + "github.com/rh-ecosystem-edge/kernel-module-management/internal/filter" + "github.com/rh-ecosystem-edge/kernel-module-management/internal/nmc" + v1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubectl/pkg/cmd/util/podcmd" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/yaml" +) + +type WorkerAction string + +const ( + WorkerActionLoad = "Load" + WorkerActionUnload = "Unload" + + NodeModulesConfigReconcilerName = "NodeModulesConfig" + + actionLabelKey = "kmm.node.kubernetes.io/worker-action" + configAnnotationKey = "kmm.node.kubernetes.io/worker-config" + nodeModulesConfigFinalizer = "kmm.node.kubernetes.io/nodemodulesconfig-reconciler" + volumeNameConfig = "config" + workerContainerName = "worker" +) + +//+kubebuilder:rbac:groups=kmm.sigs.x-k8s.io,resources=nodemodulesconfigs,verbs=get;list;watch +//+kubebuilder:rbac:groups=kmm.sigs.x-k8s.io,resources=nodemodulesconfigs/status,verbs=patch +//+kubebuilder:rbac:groups="core",resources=pods,verbs=create;delete;get;list;watch +//+kubebuilder:rbac:groups="core",resources=nodes,verbs=get;list;watch + +type NodeModulesConfigReconciler struct { + client client.Client + helper WorkerHelper +} + +func NewNodeModulesConfigReconciler(client client.Client, helper WorkerHelper) *NodeModulesConfigReconciler { + return &NodeModulesConfigReconciler{ + client: client, + helper: helper, + } +} + +func (r *NodeModulesConfigReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger := ctrl.LoggerFrom(ctx) + + nmcObj := kmmv1beta1.NodeModulesConfig{} + + if err := r.client.Get(ctx, req.NamespacedName, &nmcObj); err != nil { + if k8serrors.IsNotFound(err) { + // Pods are owned by the NMC, so the GC will have deleted them already. + // Remove the finalizer if we did not have a chance to do it before NMC deletion. + logger.Info("Clearing worker Pod finalizers") + + if err = r.helper.RemoveOrphanFinalizers(ctx, req.Name); err != nil { + return reconcile.Result{}, fmt.Errorf("could not clear all Pod finalizers for NMC %s: %v", req.Name, err) + } + + return reconcile.Result{}, nil + } + + return reconcile.Result{}, fmt.Errorf("could not get NodeModuleState %s: %v", req.NamespacedName, err) + } + + if err := r.helper.SyncStatus(ctx, &nmcObj); err != nil { + return reconcile.Result{}, fmt.Errorf("could not reconcile status for NodeModulesConfig %s: %v", nmcObj.Name, err) + } + + // Statuses are now up-to-date. + + statusMap := make(map[string]*kmmv1beta1.NodeModuleStatus, len(nmcObj.Status.Modules)) + + for i := 0; i < len(nmcObj.Status.Modules); i++ { + status := nmcObj.Status.Modules[i] + statusMap[status.Namespace+"/"+status.Name] = &nmcObj.Status.Modules[i] + } + + // TODO move to errors.Join() when we move to Go 1.20 + // errs := make([]error, 0, len(nmcObj.Spec.Modules)+len(nmcObj.Status.Modules)) + var errs *multierror.Error + + for _, mod := range nmcObj.Spec.Modules { + moduleNameKey := mod.Namespace + "/" + mod.Name + + logger := logger.WithValues("module", moduleNameKey) + + if err := r.helper.ProcessModuleSpec(ctrl.LoggerInto(ctx, logger), &nmcObj, &mod, statusMap[moduleNameKey]); err != nil { + errs = multierror.Append( + errs, + fmt.Errorf("error processing Module %s: %v", moduleNameKey, err), + ) + } + + delete(statusMap, moduleNameKey) + } + + // We have processed all module specs. + // Now, go through the remaining, "orphan" statuses that do not have a corresponding spec; those must be unloaded. + + for statusNameKey, status := range statusMap { + logger := logger.WithValues("status", statusNameKey) + + if err := r.helper.ProcessOrphanModuleStatus(ctrl.LoggerInto(ctx, logger), &nmcObj, status); err != nil { + errs = multierror.Append( + errs, + fmt.Errorf("erorr processing orphan status for Module %s: %v", statusNameKey, err), + ) + } + } + + return ctrl.Result{}, errs.ErrorOrNil() +} + +func (r *NodeModulesConfigReconciler) SetupWithManager(ctx context.Context, mgr manager.Manager) error { + // Cache pods by the name of the node they run on. + // Because NMC name == node name, we can efficiently reconcile the NMC status by listing all pods currently running + // or completed for it. + err := mgr.GetCache().IndexField(ctx, &v1.Pod{}, ".spec.nodeName", func(o client.Object) []string { + return []string{o.(*v1.Pod).Spec.NodeName} + }) + if err != nil { + return fmt.Errorf("could not start the worker Pod indexer: %v", err) + } + + nodeToNMCMapFunc := func(_ context.Context, o client.Object) []reconcile.Request { + return []reconcile.Request{ + {NamespacedName: types.NamespacedName{Name: o.GetName()}}, + } + } + + return ctrl.NewControllerManagedBy(mgr). + Named(NodeModulesConfigReconcilerName). + For(&kmmv1beta1.NodeModulesConfig{}). + Owns(&v1.Pod{}). + // TODO maybe replace this with Owns() if we make nodes the owners of NodeModulesConfigs. + Watches( + &v1.Node{}, + handler.EnqueueRequestsFromMapFunc(nodeToNMCMapFunc), + builder.WithPredicates(filter.SkipDeletions()), + ). + Complete(r) +} + +func workerPodName(nodeName, moduleName string) string { + return fmt.Sprintf("kmm-worker-%s-%s", nodeName, moduleName) +} + +func GetContainerStatus(statuses []v1.ContainerStatus, name string) v1.ContainerStatus { + for i := range statuses { + if statuses[i].Name == name { + return statuses[i] + } + } + + return v1.ContainerStatus{} +} + +func FindNodeCondition(cond []v1.NodeCondition, conditionType v1.NodeConditionType) *v1.NodeCondition { + for i := 0; i < len(cond); i++ { + c := cond[i] + + if c.Type == conditionType { + return &c + } + } + + return nil +} + +//go:generate mockgen -source=nodemodulesconfig_reconciler.go -package=controllers -destination=mock_nodemodulesconfig_reconciler.go WorkerHelper + +type WorkerHelper interface { + ProcessModuleSpec(ctx context.Context, nmc *kmmv1beta1.NodeModulesConfig, spec *kmmv1beta1.NodeModuleSpec, status *kmmv1beta1.NodeModuleStatus) error + ProcessOrphanModuleStatus(ctx context.Context, nmc *kmmv1beta1.NodeModulesConfig, status *kmmv1beta1.NodeModuleStatus) error + SyncStatus(ctx context.Context, nmc *kmmv1beta1.NodeModulesConfig) error + RemoveOrphanFinalizers(ctx context.Context, nodeName string) error +} + +type workerHelper struct { + client client.Client + pm PodManager +} + +func NewWorkerHelper(client client.Client, pm PodManager) WorkerHelper { + return &workerHelper{ + client: client, + pm: pm, + } +} + +// ProcessModuleSpec determines if a worker Pod should be created for a Module entry in a +// NodeModulesConfig .spec.modules. +// A loading worker pod is created when: +// - there is no corresponding entry in the NodeModulesConfig's .status.modules list; +// - the lastTransitionTime property in the .status.modules entry is older that the last transition time +// of the Ready condition on the node. This makes sure that we always load modules after maintenance operations +// that would make a node not Ready, such as a reboot. +// +// An unloading worker Pod is created when the entry in .spec.modules has a different config compared to the entry in +// .status.modules. +func (w *workerHelper) ProcessModuleSpec( + ctx context.Context, + nmc *kmmv1beta1.NodeModulesConfig, + spec *kmmv1beta1.NodeModuleSpec, + status *kmmv1beta1.NodeModuleStatus, +) error { + logger := ctrl.LoggerFrom(ctx) + + if status == nil { + logger.Info("Missing status; creating loader Pod") + + return w.pm.CreateLoaderPod(ctx, nmc, spec) + } + + if status.InProgress { + logger.Info("Worker pod is running; skipping") + return nil + } + + if !reflect.DeepEqual(spec.Config, *status.Config) { + logger.Info("Outdated config in status; creating unloader Pod") + + return w.pm.CreateUnloaderPod(ctx, nmc, status) + } + + node := v1.Node{} + + if err := w.client.Get(ctx, types.NamespacedName{Name: nmc.Name}, &node); err != nil { + return fmt.Errorf("could not get node %s: %v", nmc.Name, err) + } + + readyCondition := FindNodeCondition(node.Status.Conditions, v1.NodeReady) + if readyCondition == nil { + return fmt.Errorf("node %s has no Ready condition", nmc.Name) + } + + if readyCondition.Status == v1.ConditionTrue && status.LastTransitionTime.Before(&readyCondition.LastTransitionTime) { + logger.Info("Outdated last transition time status; creating unloader Pod") + + return w.pm.CreateLoaderPod(ctx, nmc, spec) + } + + logger.Info("Spec and status in sync; nothing to do") + + return nil +} + +func (w *workerHelper) ProcessOrphanModuleStatus( + ctx context.Context, + nmc *kmmv1beta1.NodeModulesConfig, + status *kmmv1beta1.NodeModuleStatus, +) error { + logger := ctrl.LoggerFrom(ctx) + + if status.InProgress { + logger.Info("Sync status is in progress; skipping") + return nil + } + + logger.Info("Creating unloader Pod") + + return w.pm.CreateUnloaderPod(ctx, nmc, status) +} + +func (w *workerHelper) RemoveOrphanFinalizers(ctx context.Context, nodeName string) error { + pods, err := w.pm.ListWorkerPodsOnNode(ctx, nodeName) + if err != nil { + return fmt.Errorf("could not delete orphan worker Pods on node %s: %v", nodeName, err) + } + + // TODO move to errors.Join() when we move to Go 1.20 + //errs := make([]error, 0, len(pods)) + var errs *multierror.Error + + for i := 0; i < len(pods); i++ { + pod := &pods[i] + + mergeFrom := client.MergeFrom(pod.DeepCopy()) + + if controllerutil.RemoveFinalizer(pod, nodeModulesConfigFinalizer) { + if err = w.client.Patch(ctx, pod, mergeFrom); err != nil { + errs = multierror.Append( + errs, + fmt.Errorf("could not patch Pod %s/%s: %v", pod.Namespace, pod.Name, err), + ) + + continue + } + } + } + + return errs.ErrorOrNil() +} + +func (w *workerHelper) SyncStatus(ctx context.Context, nmcObj *kmmv1beta1.NodeModulesConfig) error { + logger := ctrl.LoggerFrom(ctx) + + logger.Info("Syncing status") + + pods, err := w.pm.ListWorkerPodsOnNode(ctx, nmcObj.Name) + if err != nil { + return fmt.Errorf("could not list worker pods for NodeModulesConfig %s: %v", nmcObj.Name, err) + } + + logger.V(1).Info("List worker Pods", "count", len(pods)) + + if len(pods) == 0 { + return nil + } + + patchFrom := client.MergeFrom(nmcObj.DeepCopy()) + // TODO move to errors.Join() when we move to Go 1.20 + var errs *multierror.Error + + for _, p := range pods { + podNSN := types.NamespacedName{Namespace: p.Namespace, Name: p.Name} + + modNamespace := p.Namespace + modName := p.Labels[constants.ModuleNameLabel] + phase := p.Status.Phase + + logger := logger.WithValues("pod name", p.Name, "pod phase", p.Status.Phase) + + logger.Info("Processing worker Pod") + + status := kmmv1beta1.NodeModuleStatus{ + Namespace: modNamespace, + Name: modName, + } + + deletePod := false + statusDeleted := false + + switch phase { + case v1.PodFailed: + deletePod = true + case v1.PodSucceeded: + deletePod = true + + if p.Labels[actionLabelKey] == WorkerActionUnload { + nmc.RemoveModuleStatus(&nmcObj.Status.Modules, modNamespace, modName) + deletePod = true + statusDeleted = true + break + } + + config := kmmv1beta1.ModuleConfig{} + + if err = yaml.UnmarshalStrict([]byte(p.Annotations[configAnnotationKey]), &config); err != nil { + errs = multierror.Append( + errs, + fmt.Errorf("%s: could not unmarshal the ModuleConfig from YAML: %v", podNSN, err), + ) + + continue + } + + status.Config = &config + + podLTT := GetContainerStatus(p.Status.ContainerStatuses, workerContainerName). + State. + Terminated. + FinishedAt + + status.LastTransitionTime = &podLTT + + deletePod = true + case v1.PodPending, v1.PodRunning: + status.InProgress = true + // TODO: if the NMC's spec changed compared to the Pod's config, recreate the Pod + default: + errs = multierror.Append( + errs, + fmt.Errorf("%s: unhandled Pod phase %q", podNSN, phase), + ) + } + + if deletePod { + if err = w.pm.DeletePod(ctx, &p); err != nil { + errs = multierror.Append( + errs, + fmt.Errorf("could not delete worker Pod %s: %v", podNSN, errs), + ) + + continue + } + } + + if !statusDeleted { + nmc.SetModuleStatus(&nmcObj.Status.Modules, status) + } + } + + if err = errs.ErrorOrNil(); err != nil { + return fmt.Errorf("encountered errors while reconciling NMC %s status: %v", nmcObj.Name, err) + } + + if err = w.client.Status().Patch(ctx, nmcObj, patchFrom); err != nil { + return fmt.Errorf("could not patch NodeModulesConfig %s status: %v", nmcObj.Name, err) + } + + return nil +} + +const ( + configFileName = "config.yaml" + configFullPath = volMountPoingConfig + "/" + configFileName + + volNameConfig = "config" + volMountPoingConfig = "/etc/kmm-worker" +) + +//go:generate mockgen -source=nodemodulesconfig_reconciler.go -package=controllers -destination=mock_nodemodulesconfig_reconciler.go PodManager + +type PodManager interface { + CreateLoaderPod(ctx context.Context, nmc client.Object, nms *kmmv1beta1.NodeModuleSpec) error + CreateUnloaderPod(ctx context.Context, nmc client.Object, nms *kmmv1beta1.NodeModuleStatus) error + DeletePod(ctx context.Context, pod *v1.Pod) error + ListWorkerPodsOnNode(ctx context.Context, nodeName string) ([]v1.Pod, error) +} + +type podManager struct { + client client.Client + scheme *runtime.Scheme + workerImage string +} + +func NewPodManager(client client.Client, workerImage string, scheme *runtime.Scheme) PodManager { + return &podManager{ + client: client, + scheme: scheme, + workerImage: workerImage, + } +} + +func (p *podManager) CreateLoaderPod(ctx context.Context, nmc client.Object, nms *kmmv1beta1.NodeModuleSpec) error { + pod, err := p.baseWorkerPod(nmc.GetName(), nms.Namespace, nms.Name, nms.ServiceAccountName, nmc) + if err != nil { + return fmt.Errorf("could not create the base Pod: %v", err) + } + + if err = setWorkerContainerArgs(pod, []string{"kmod", "load", configFullPath}); err != nil { + return fmt.Errorf("could not set worker container args: %v", err) + } + + if err = setWorkerConfigAnnotation(pod, nms.Config); err != nil { + return fmt.Errorf("could not set worker config: %v", err) + } + + setWorkerActionLabel(pod, WorkerActionLoad) + pod.Spec.RestartPolicy = v1.RestartPolicyNever + + return p.client.Create(ctx, pod) +} + +func (p *podManager) CreateUnloaderPod(ctx context.Context, nmc client.Object, nms *kmmv1beta1.NodeModuleStatus) error { + pod, err := p.baseWorkerPod(nmc.GetName(), nms.Namespace, nms.Name, nms.ServiceAccountName, nmc) + if err != nil { + return fmt.Errorf("could not create the base Pod: %v", err) + } + + if err = setWorkerContainerArgs(pod, []string{"kmod", "unload", configFullPath}); err != nil { + return fmt.Errorf("could not set worker container args: %v", err) + } + + if err = setWorkerConfigAnnotation(pod, *nms.Config); err != nil { + return fmt.Errorf("could not set worker config: %v", err) + } + + setWorkerActionLabel(pod, WorkerActionUnload) + + return p.client.Create(ctx, pod) +} + +func (p *podManager) DeletePod(ctx context.Context, pod *v1.Pod) error { + logger := ctrl.LoggerFrom(ctx) + + logger.Info("Removing Pod finalizer") + + podPatch := client.MergeFrom(pod.DeepCopy()) + + controllerutil.RemoveFinalizer(pod, nodeModulesConfigFinalizer) + + if err := p.client.Patch(ctx, pod, podPatch); err != nil { + return fmt.Errorf("could not patch Pod %s/%s: %v", pod.Namespace, pod.Name, err) + } + + if pod.DeletionTimestamp == nil { + logger.Info("DeletionTimestamp not set; deleting Pod") + + if err := p.client.Delete(ctx, pod); client.IgnoreNotFound(err) != nil { + return fmt.Errorf("could not delete Pod %s/%s: %v", pod.Namespace, pod.Name, err) + } + } else { + logger.Info("DeletionTimestamp set; not deleting Pod") + } + + return nil +} + +func (p *podManager) ListWorkerPodsOnNode(ctx context.Context, nodeName string) ([]v1.Pod, error) { + logger := ctrl.LoggerFrom(ctx).WithValues("node name", nodeName) + + pl := v1.PodList{} + + hl := client.HasLabels{actionLabelKey} + mf := client.MatchingFields{".spec.nodeName": nodeName} + + logger.V(1).Info("Listing worker Pods") + + if err := p.client.List(ctx, &pl, hl, mf); err != nil { + return nil, fmt.Errorf("could not list worker pods for node %s: %v", nodeName, err) + } + + return pl.Items, nil +} + +func (p *podManager) PodExists(ctx context.Context, nodeName, modName, modNamespace string) (bool, error) { + pod := v1.Pod{} + + nsn := types.NamespacedName{ + Namespace: modNamespace, + Name: workerPodName(nodeName, modName), + } + + if err := p.client.Get(ctx, nsn, &pod); err != nil { + if k8serrors.IsNotFound(err) { + return false, nil + } + + return false, fmt.Errorf("error getting Pod %s: %v", nsn, err) + } + + return true, nil +} + +func (p *podManager) baseWorkerPod(nodeName, namespace, modName, serviceAccountName string, owner client.Object) (*v1.Pod, error) { + const ( + volNameLibModules = "lib-modules" + volNameUsrLibModules = "usr-lib-modules" + volNameVarLibFirmware = "var-lib-firmware" + ) + + hostPathDirectory := v1.HostPathDirectory + hostPathDirectoryOrCreate := v1.HostPathDirectoryOrCreate + + pod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: workerPodName(nodeName, modName), + Labels: map[string]string{constants.ModuleNameLabel: modName}, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: workerContainerName, + Image: p.workerImage, + VolumeMounts: []v1.VolumeMount{ + { + Name: volNameConfig, + MountPath: volMountPoingConfig, + ReadOnly: true, + }, + { + Name: volNameLibModules, + MountPath: "/lib/modules", + ReadOnly: true, + }, + { + Name: volNameUsrLibModules, + MountPath: "/usr/lib/modules", + ReadOnly: true, + }, + { + Name: volNameVarLibFirmware, + MountPath: "/var/lib/firmware", + }, + }, + SecurityContext: &v1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }, + }, + NodeName: nodeName, + RestartPolicy: v1.RestartPolicyNever, + ServiceAccountName: serviceAccountName, + Volumes: []v1.Volume{ + { + Name: volumeNameConfig, + VolumeSource: v1.VolumeSource{ + DownwardAPI: &v1.DownwardAPIVolumeSource{ + Items: []v1.DownwardAPIVolumeFile{ + { + Path: configFileName, + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: fmt.Sprintf("metadata.annotations['%s']", configAnnotationKey), + }, + }, + }, + }, + }, + }, + { + Name: volNameLibModules, + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: "/lib/modules", + Type: &hostPathDirectory, + }, + }, + }, + { + Name: volNameUsrLibModules, + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: "/usr/lib/modules", + Type: &hostPathDirectory, + }, + }, + }, + { + Name: volNameVarLibFirmware, + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: "/var/lib/firmware", + Type: &hostPathDirectoryOrCreate, + }, + }, + }, + }, + }, + } + + if err := ctrl.SetControllerReference(owner, &pod, p.scheme); err != nil { + return nil, fmt.Errorf("could not set the owner as controller: %v", err) + } + + controllerutil.AddFinalizer(&pod, nodeModulesConfigFinalizer) + + return &pod, nil +} + +func setWorkerActionLabel(pod *v1.Pod, action WorkerAction) { + labels := pod.GetLabels() + + if labels == nil { + labels = make(map[string]string) + } + + labels[actionLabelKey] = string(action) + + pod.SetLabels(labels) +} + +func setWorkerConfigAnnotation(pod *v1.Pod, cfg kmmv1beta1.ModuleConfig) error { + b, err := yaml.Marshal(cfg) + if err != nil { + return fmt.Errorf("could not marshal the ModuleConfig to YAML: %v", err) + } + + annotations := pod.GetAnnotations() + + if annotations == nil { + annotations = make(map[string]string) + } + + annotations[configAnnotationKey] = string(b) + + pod.SetAnnotations(annotations) + + return nil +} + +func setWorkerContainerArgs(pod *v1.Pod, args []string) error { + container, _ := podcmd.FindContainerByName(pod, workerContainerName) + if container == nil { + return errors.New("could not find the worker container") + } + + container.Args = args + + return nil +} diff --git a/controllers/nodemodulesconfig_reconciler_test.go b/controllers/nodemodulesconfig_reconciler_test.go new file mode 100644 index 000000000..598c271eb --- /dev/null +++ b/controllers/nodemodulesconfig_reconciler_test.go @@ -0,0 +1,915 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "reflect" + "time" + + "github.com/budougumi0617/cmpmock" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + kmmv1beta1 "github.com/rh-ecosystem-edge/kernel-module-management/api/v1beta1" + testclient "github.com/rh-ecosystem-edge/kernel-module-management/internal/client" + "github.com/rh-ecosystem-edge/kernel-module-management/internal/constants" + "go.uber.org/mock/gomock" + v1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const nmcName = "nmc" + +var _ = Describe("NodeModulesConfigReconciler_Reconcile", func() { + var ( + kubeClient *testclient.MockClient + wh *MockWorkerHelper + + r *NodeModulesConfigReconciler + + ctx = context.TODO() + nmcNsn = types.NamespacedName{Name: nmcName} + req = reconcile.Request{NamespacedName: nmcNsn} + ) + + BeforeEach(func() { + ctrl := gomock.NewController(GinkgoT()) + kubeClient = testclient.NewMockClient(ctrl) + wh = NewMockWorkerHelper(ctrl) + r = NewNodeModulesConfigReconciler(kubeClient, wh) + }) + + It("should clean worker Pod finalizers and return if the NMC does not exist", func() { + gomock.InOrder( + kubeClient. + EXPECT(). + Get(ctx, nmcNsn, &kmmv1beta1.NodeModulesConfig{}). + Return(k8serrors.NewNotFound(schema.GroupResource{}, nmcName)), + wh.EXPECT().RemoveOrphanFinalizers(ctx, nmcName), + ) + + Expect( + r.Reconcile(ctx, req), + ).To( + Equal(ctrl.Result{}), + ) + }) + + It("should fail if we could not synchronize the NMC status", func() { + nmc := &kmmv1beta1.NodeModulesConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nmcName}, + } + + gomock.InOrder( + kubeClient. + EXPECT(). + Get(ctx, nmcNsn, &kmmv1beta1.NodeModulesConfig{}). + Do(func(_ context.Context, _ types.NamespacedName, kubeNmc ctrlclient.Object, _ ...ctrlclient.Options) { + *kubeNmc.(*kmmv1beta1.NodeModulesConfig) = *nmc + }), + wh.EXPECT().SyncStatus(ctx, nmc).Return(errors.New("random error")), + ) + + _, err := r.Reconcile(ctx, req) + Expect(err).To(HaveOccurred()) + }) + + It("should process spec entries and orphan statuses", func() { + const ( + mod0Name = "mod0" + mod1Name = "mod1" + mod2Name = "mod2" + ) + spec0 := kmmv1beta1.NodeModuleSpec{ + Namespace: namespace, + Name: mod0Name, + } + + spec1 := kmmv1beta1.NodeModuleSpec{ + Namespace: namespace, + Name: mod1Name, + } + + status0 := kmmv1beta1.NodeModuleStatus{ + Namespace: namespace, + Name: mod0Name, + } + + status2 := kmmv1beta1.NodeModuleStatus{ + Namespace: namespace, + Name: mod2Name, + } + + nmc := &kmmv1beta1.NodeModulesConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nmcName}, + Spec: kmmv1beta1.NodeModulesConfigSpec{ + Modules: []kmmv1beta1.NodeModuleSpec{spec0, spec1}, + }, + Status: kmmv1beta1.NodeModulesConfigStatus{ + Modules: []kmmv1beta1.NodeModuleStatus{status0, status2}, + }, + } + + contextWithValueMatch := gomock.AssignableToTypeOf( + reflect.TypeOf((*context.Context)(nil)).Elem(), + ) + + gomock.InOrder( + kubeClient. + EXPECT(). + Get(ctx, nmcNsn, &kmmv1beta1.NodeModulesConfig{}). + Do(func(_ context.Context, _ types.NamespacedName, kubeNmc ctrlclient.Object, _ ...ctrlclient.Options) { + *kubeNmc.(*kmmv1beta1.NodeModulesConfig) = *nmc + }), + wh.EXPECT().SyncStatus(ctx, nmc), + wh.EXPECT().ProcessModuleSpec(contextWithValueMatch, nmc, &spec0, &status0), + wh.EXPECT().ProcessModuleSpec(contextWithValueMatch, nmc, &spec1, nil), + wh.EXPECT().ProcessOrphanModuleStatus(contextWithValueMatch, nmc, &status2), + ) + + Expect( + r.Reconcile(ctx, req), + ).To( + BeZero(), + ) + }) +}) + +var moduleConfig = kmmv1beta1.ModuleConfig{ + KernelVersion: "kernel version", + ContainerImage: "container image", + InsecurePull: true, + InTreeModuleToRemove: "intree", + Modprobe: kmmv1beta1.ModprobeSpec{ + ModuleName: "test", + Parameters: []string{"a", "b"}, + DirName: "/dir", + Args: nil, + RawArgs: nil, + FirmwarePath: "/firmware-path", + ModulesLoadingOrder: []string{"a", "b", "c"}, + }, +} + +var _ = Describe("workerHelper_ProcessModuleSpec", func() { + const ( + name = "name" + namespace = "namespace" + ) + + var ( + ctx = context.TODO() + + client *testclient.MockClient + pm *MockPodManager + wh WorkerHelper + ) + + BeforeEach(func() { + ctrl := gomock.NewController(GinkgoT()) + client = testclient.NewMockClient(ctrl) + pm = NewMockPodManager(ctrl) + wh = NewWorkerHelper(client, pm) + }) + + It("should create a loader Pod if the corresponding status is missing", func() { + nmc := &kmmv1beta1.NodeModulesConfig{} + spec := &kmmv1beta1.NodeModuleSpec{Name: name, Namespace: namespace} + + pm.EXPECT().CreateLoaderPod(ctx, nmc, spec) + + Expect( + wh.ProcessModuleSpec(ctx, nmc, spec, nil), + ).NotTo( + HaveOccurred(), + ) + }) + + It("should create an unloader Pod if the spec is different from the status", func() { + nmc := &kmmv1beta1.NodeModulesConfig{} + + spec := &kmmv1beta1.NodeModuleSpec{ + Name: name, + Namespace: namespace, + Config: kmmv1beta1.ModuleConfig{ContainerImage: "old-container-image"}, + } + + status := &kmmv1beta1.NodeModuleStatus{ + Name: name, + Namespace: namespace, + Config: &kmmv1beta1.ModuleConfig{ContainerImage: "new-container-image"}, + } + + pm.EXPECT().CreateUnloaderPod(ctx, nmc, status) + + Expect( + wh.ProcessModuleSpec(ctx, nmc, spec, status), + ).NotTo( + HaveOccurred(), + ) + }) + + It("should do nothing if InProgress is true, even though the config is different", func() { + spec := &kmmv1beta1.NodeModuleSpec{ + Name: name, + Namespace: namespace, + Config: kmmv1beta1.ModuleConfig{ContainerImage: "old-container-image"}, + } + + status := &kmmv1beta1.NodeModuleStatus{ + Name: name, + Namespace: namespace, + Config: &kmmv1beta1.ModuleConfig{ContainerImage: "new-container-image"}, + InProgress: true, + } + + Expect( + wh.ProcessModuleSpec(ctx, &kmmv1beta1.NodeModulesConfig{}, spec, status), + ).NotTo( + HaveOccurred(), + ) + }) + + It("should return an error if the node has no ready condition", func() { + nmc := &kmmv1beta1.NodeModulesConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nmcName}, + } + + spec := &kmmv1beta1.NodeModuleSpec{ + Config: moduleConfig, + Name: name, + Namespace: namespace, + } + + now := metav1.Now() + + status := &kmmv1beta1.NodeModuleStatus{ + Config: &moduleConfig, + Name: name, + Namespace: namespace, + LastTransitionTime: &now, + } + + client.EXPECT().Get(ctx, types.NamespacedName{Name: nmcName}, &v1.Node{}) + + Expect( + wh.ProcessModuleSpec(ctx, nmc, spec, status), + ).To( + HaveOccurred(), + ) + }) + + nmc := &kmmv1beta1.NodeModulesConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nmcName}, + } + + spec := &kmmv1beta1.NodeModuleSpec{ + Config: moduleConfig, + Name: name, + Namespace: namespace, + } + + now := metav1.Now() + + status := &kmmv1beta1.NodeModuleStatus{ + Config: &moduleConfig, + Name: name, + Namespace: namespace, + LastTransitionTime: &metav1.Time{Time: now.Add(-1 * time.Minute)}, + } + + DescribeTable( + "should create a loader Pod if status is older than the Ready condition", + func(cs v1.ConditionStatus, shouldCreate bool) { + getNode := client. + EXPECT(). + Get(ctx, types.NamespacedName{Name: nmcName}, &v1.Node{}). + Do(func(_ context.Context, _ types.NamespacedName, node *v1.Node, _ ...ctrl.Options) { + node.Status.Conditions = []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: cs, + LastTransitionTime: now, + }, + } + }) + + if shouldCreate { + pm.EXPECT().CreateLoaderPod(ctx, nmc, spec).After(getNode) + } + + Expect( + wh.ProcessModuleSpec(ctx, nmc, spec, status), + ).NotTo( + HaveOccurred(), + ) + }, + Entry(nil, v1.ConditionFalse, false), + Entry(nil, v1.ConditionTrue, true), + ) +}) + +var _ = Describe("workerHelper_ProcessOrphanModuleStatus", func() { + ctx := context.TODO() + + It("should do nothing if the status sync is in progress", func() { + nmc := &kmmv1beta1.NodeModulesConfig{} + status := &kmmv1beta1.NodeModuleStatus{InProgress: true} + + Expect( + NewWorkerHelper(nil, nil).ProcessOrphanModuleStatus(ctx, nmc, status), + ).NotTo( + HaveOccurred(), + ) + }) + + It("should create an unloader Pod", func() { + ctrl := gomock.NewController(GinkgoT()) + client := testclient.NewMockClient(ctrl) + pm := NewMockPodManager(ctrl) + wh := NewWorkerHelper(client, pm) + + nmc := &kmmv1beta1.NodeModulesConfig{} + status := &kmmv1beta1.NodeModuleStatus{} + + pm.EXPECT().CreateUnloaderPod(ctx, nmc, status) + + Expect( + wh.ProcessOrphanModuleStatus(ctx, nmc, status), + ).NotTo( + HaveOccurred(), + ) + }) +}) + +var _ = Describe("workerHelper_SyncStatus", func() { + var ( + ctx = context.TODO() + + ctrl *gomock.Controller + kubeClient *testclient.MockClient + pm *MockPodManager + wh WorkerHelper + sw *testclient.MockStatusWriter + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + kubeClient = testclient.NewMockClient(ctrl) + pm = NewMockPodManager(ctrl) + wh = NewWorkerHelper(kubeClient, pm) + sw = testclient.NewMockStatusWriter(ctrl) + }) + + It("should do nothing if there are no running pods for this NMC", func() { + pm.EXPECT().ListWorkerPodsOnNode(ctx, nmcName) + + nmc := &kmmv1beta1.NodeModulesConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nmcName}, + } + + Expect( + wh.SyncStatus(ctx, nmc), + ).NotTo( + HaveOccurred(), + ) + }) + + It("should delete the pod if it is failed", func() { + const ( + podName = "pod-name" + podNamespace = "pod-namespace" + ) + + pod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: podNamespace, + Name: podName, + }, + Status: v1.PodStatus{Phase: v1.PodFailed}, + } + + nmc := &kmmv1beta1.NodeModulesConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nmcName}, + } + + gomock.InOrder( + pm.EXPECT().ListWorkerPodsOnNode(ctx, nmcName).Return([]v1.Pod{pod}, nil), + pm.EXPECT().DeletePod(ctx, &pod), + kubeClient.EXPECT().Status().Return(sw), + sw.EXPECT().Patch(ctx, nmc, gomock.Any()), + ) + + Expect( + wh.SyncStatus(ctx, nmc), + ).NotTo( + HaveOccurred(), + ) + }) + + It("should set the in progress status if pods are pending or running", func() { + nmc := &kmmv1beta1.NodeModulesConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nmcName}, + } + + pods := []v1.Pod{ + { + Status: v1.PodStatus{Phase: v1.PodRunning}, + }, + { + Status: v1.PodStatus{Phase: v1.PodPending}, + }, + } + + gomock.InOrder( + pm.EXPECT().ListWorkerPodsOnNode(ctx, nmcName).Return(pods, nil), + kubeClient.EXPECT().Status().Return(sw), + sw.EXPECT().Patch(ctx, nmc, gomock.Any()), + ) + + Expect( + wh.SyncStatus(ctx, nmc), + ).NotTo( + HaveOccurred(), + ) + + Expect(nmc.Status.Modules).To(HaveLen(1)) + Expect(nmc.Status.Modules[0].InProgress).To(BeTrue()) + }) + + It("should remove the status if an unloader pod was successful", func() { + const ( + modName = "module" + modNamespace = "namespace" + ) + + nmc := &kmmv1beta1.NodeModulesConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nmcName}, + Status: kmmv1beta1.NodeModulesConfigStatus{ + Modules: []kmmv1beta1.NodeModuleStatus{ + { + Name: modName, + Namespace: modNamespace, + }, + }, + }, + } + + pod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: modNamespace, + Labels: map[string]string{ + actionLabelKey: WorkerActionUnload, + constants.ModuleNameLabel: modName, + }, + }, + Status: v1.PodStatus{Phase: v1.PodSucceeded}, + } + + gomock.InOrder( + pm.EXPECT().ListWorkerPodsOnNode(ctx, nmcName).Return([]v1.Pod{pod}, nil), + pm.EXPECT().DeletePod(ctx, &pod), + kubeClient.EXPECT().Status().Return(sw), + sw.EXPECT().Patch(ctx, nmc, gomock.Any()), + ) + + Expect( + wh.SyncStatus(ctx, nmc), + ).NotTo( + HaveOccurred(), + ) + + Expect(nmc.Status.Modules).To(BeEmpty()) + }) + + It("should add the status if a loader pod was successful", func() { + const ( + modName = "module" + modNamespace = "namespace" + ) + + nmc := &kmmv1beta1.NodeModulesConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nmcName}, + } + + now := metav1.Now() + + cfg := kmmv1beta1.ModuleConfig{ + KernelVersion: "some-kernel-version", + ContainerImage: "some-container-image", + InsecurePull: true, + InTreeModuleToRemove: "intree", + Modprobe: kmmv1beta1.ModprobeSpec{ModuleName: "test"}, + } + + pod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: modNamespace, + Labels: map[string]string{ + actionLabelKey: WorkerActionLoad, + constants.ModuleNameLabel: modName, + }, + }, + Status: v1.PodStatus{ + Phase: v1.PodSucceeded, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "worker", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{FinishedAt: now}, + }, + }, + }, + }, + } + + Expect( + setWorkerConfigAnnotation(&pod, cfg), + ).NotTo( + HaveOccurred(), + ) + + gomock.InOrder( + pm.EXPECT().ListWorkerPodsOnNode(ctx, nmcName).Return([]v1.Pod{pod}, nil), + pm.EXPECT().DeletePod(ctx, &pod), + kubeClient.EXPECT().Status().Return(sw), + sw.EXPECT().Patch(ctx, nmc, gomock.Any()), + ) + + Expect( + wh.SyncStatus(ctx, nmc), + ).NotTo( + HaveOccurred(), + ) + + Expect(nmc.Status.Modules).To(HaveLen(1)) + + expectedStatus := kmmv1beta1.NodeModuleStatus{ + Config: &cfg, + LastTransitionTime: &now, + Name: modName, + Namespace: modNamespace, + } + + Expect(nmc.Status.Modules[0]).To(BeComparableTo(expectedStatus)) + }) +}) + +var _ = Describe("workerHelper_RemoveOrphanFinalizers", func() { + const nodeName = "node-name" + + var ( + ctx = context.TODO() + + kubeClient *testclient.MockClient + pm *MockPodManager + wh WorkerHelper + ) + + BeforeEach(func() { + ctrl := gomock.NewController(GinkgoT()) + kubeClient = testclient.NewMockClient(ctrl) + pm = NewMockPodManager(ctrl) + wh = NewWorkerHelper(kubeClient, pm) + }) + + It("should do nothing if no pods are present", func() { + pm.EXPECT().ListWorkerPodsOnNode(ctx, nodeName) + + Expect( + wh.RemoveOrphanFinalizers(ctx, nodeName), + ).NotTo( + HaveOccurred(), + ) + }) + + It("should patch to remove the finalizer if it is set", func() { + const ( + name = "my-pod" + namespace = "my-namespace" + ) + + podWithFinalizer := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Finalizers: []string{nodeModulesConfigFinalizer}, + }, + } + + podWithoutFinalizer := podWithFinalizer + podWithoutFinalizer.Finalizers = []string{} + + gomock.InOrder( + pm.EXPECT().ListWorkerPodsOnNode(ctx, nodeName).Return([]v1.Pod{podWithFinalizer, {}}, nil), + kubeClient.EXPECT().Patch(ctx, &podWithoutFinalizer, gomock.Any()), + ) + + Expect( + wh.RemoveOrphanFinalizers(ctx, nodeName), + ).NotTo( + HaveOccurred(), + ) + }) +}) + +const ( + moduleName = "my-module" + serviceAccountName = "some-sa" + workerImage = "worker-image" +) + +var _ = Describe("podManager_CreateLoaderPod", func() { + It("should work as expected", func() { + ctrl := gomock.NewController(GinkgoT()) + client := testclient.NewMockClient(ctrl) + + nmc := &kmmv1beta1.NodeModulesConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nmcName}, + } + + nms := &kmmv1beta1.NodeModuleSpec{ + Name: moduleName, + Namespace: namespace, + Config: moduleConfig, + ServiceAccountName: serviceAccountName, + } + + expected := getBaseWorkerPod("load", WorkerActionLoad, nmc) + + Expect( + controllerutil.SetControllerReference(nmc, expected, scheme), + ).NotTo( + HaveOccurred(), + ) + + controllerutil.AddFinalizer(expected, nodeModulesConfigFinalizer) + + ctx := context.TODO() + + client.EXPECT().Create(ctx, cmpmock.DiffEq(expected)) + + Expect( + NewPodManager(client, workerImage, scheme).CreateLoaderPod(ctx, nmc, nms), + ).NotTo( + HaveOccurred(), + ) + }) +}) + +var _ = Describe("podManager_CreateUnloaderPod", func() { + It("should work as expected", func() { + ctrl := gomock.NewController(GinkgoT()) + client := testclient.NewMockClient(ctrl) + + nmc := &kmmv1beta1.NodeModulesConfig{ + ObjectMeta: metav1.ObjectMeta{Name: nmcName}, + } + + status := &kmmv1beta1.NodeModuleStatus{ + Name: moduleName, + Namespace: namespace, + Config: &moduleConfig, + ServiceAccountName: serviceAccountName, + } + + expected := getBaseWorkerPod("unload", WorkerActionUnload, nmc) + + ctx := context.TODO() + + client.EXPECT().Create(ctx, cmpmock.DiffEq(expected)) + + Expect( + NewPodManager(client, workerImage, scheme).CreateUnloaderPod(ctx, nmc, status), + ).NotTo( + HaveOccurred(), + ) + }) +}) + +var _ = Describe("podManager_DeletePod", func() { + ctx := context.TODO() + now := metav1.Now() + + DescribeTable( + "should work as expected", + func(deletionTimestamp *metav1.Time) { + ctrl := gomock.NewController(GinkgoT()) + kubeclient := testclient.NewMockClient(ctrl) + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: deletionTimestamp, + Finalizers: []string{nodeModulesConfigFinalizer}, + }, + } + + patchedPod := pod + patchedPod.Finalizers = nil + + patch := kubeclient.EXPECT().Patch(ctx, patchedPod, gomock.Any()) + + if deletionTimestamp == nil { + kubeclient.EXPECT().Delete(ctx, patchedPod).After(patch) + } + + Expect( + NewPodManager(kubeclient, workerImage, scheme).DeletePod(ctx, patchedPod), + ).NotTo( + HaveOccurred(), + ) + }, + Entry("deletionTimestamp not set", nil), + Entry("deletionTimestamp set", &now), + ) +}) + +var _ = Describe("podManager_ListWorkerPodsOnNode", func() { + const nodeName = "some-node" + + var ( + ctx = context.TODO() + + kubeClient *testclient.MockClient + pm PodManager + ) + + BeforeEach(func() { + ctrl := gomock.NewController(GinkgoT()) + kubeClient = testclient.NewMockClient(ctrl) + pm = NewPodManager(kubeClient, workerImage, scheme) + }) + + opts := []interface{}{ + ctrlclient.HasLabels{actionLabelKey}, + ctrlclient.MatchingFields{".spec.nodeName": nodeName}, + } + + It("should return an error if the kube client encountered one", func() { + kubeClient.EXPECT().List(ctx, &v1.PodList{}, opts...).Return(errors.New("random error")) + + _, err := pm.ListWorkerPodsOnNode(ctx, nodeName) + Expect(err).To(HaveOccurred()) + }) + + It("should the list of pods", func() { + pods := []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "pod-0"}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + }, + } + + kubeClient. + EXPECT(). + List(ctx, &v1.PodList{}, opts...). + Do(func(_ context.Context, pl *v1.PodList, _ ...ctrlclient.ListOption) { + pl.Items = pods + }) + + Expect( + pm.ListWorkerPodsOnNode(ctx, nodeName), + ).To( + Equal(pods), + ) + }) +}) + +func getBaseWorkerPod(subcommand string, action WorkerAction, owner ctrlclient.Object) *v1.Pod { + GinkgoHelper() + + const ( + volNameLibModules = "lib-modules" + volNameUsrLibModules = "usr-lib-modules" + volNameVarLibFirmware = "var-lib-firmware" + ) + + hostPathDirectory := v1.HostPathDirectory + hostPathDirectoryOrCreate := v1.HostPathDirectoryOrCreate + + pod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: workerPodName(nmcName, moduleName), + Namespace: namespace, + Labels: map[string]string{ + actionLabelKey: string(action), + constants.ModuleNameLabel: moduleName, + }, + Annotations: map[string]string{configAnnotationKey: `containerImage: container image +inTreeModuleToRemove: intree +insecurePull: true +kernelVersion: kernel version +modprobe: + dirName: /dir + firmwarePath: /firmware-path + moduleName: test + modulesLoadingOrder: + - a + - b + - c + parameters: + - a + - b +`, + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "worker", + Image: workerImage, + Args: []string{"kmod", subcommand, "/etc/kmm-worker/config.yaml"}, + SecurityContext: &v1.SecurityContext{Privileged: pointer.Bool(true)}, + VolumeMounts: []v1.VolumeMount{ + { + Name: volNameConfig, + MountPath: "/etc/kmm-worker", + ReadOnly: true, + }, + { + Name: volNameLibModules, + MountPath: "/lib/modules", + ReadOnly: true, + }, + { + Name: volNameUsrLibModules, + MountPath: "/usr/lib/modules", + ReadOnly: true, + }, + { + Name: volNameVarLibFirmware, + MountPath: "/var/lib/firmware", + }, + }, + }, + }, + NodeName: nmcName, + RestartPolicy: v1.RestartPolicyNever, + ServiceAccountName: serviceAccountName, + Volumes: []v1.Volume{ + { + Name: volumeNameConfig, + VolumeSource: v1.VolumeSource{ + DownwardAPI: &v1.DownwardAPIVolumeSource{ + Items: []v1.DownwardAPIVolumeFile{ + { + Path: "config.yaml", + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: fmt.Sprintf("metadata.annotations['%s']", configAnnotationKey), + }, + }, + }, + }, + }, + }, + { + Name: volNameLibModules, + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: "/lib/modules", + Type: &hostPathDirectory, + }, + }, + }, + { + Name: volNameUsrLibModules, + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: "/usr/lib/modules", + Type: &hostPathDirectory, + }, + }, + }, + { + Name: volNameVarLibFirmware, + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: "/var/lib/firmware", + Type: &hostPathDirectoryOrCreate, + }, + }, + }, + }, + }, + } + + Expect( + controllerutil.SetControllerReference(owner, &pod, scheme), + ).NotTo( + HaveOccurred(), + ) + + controllerutil.AddFinalizer(&pod, nodeModulesConfigFinalizer) + + return &pod +} diff --git a/go.mod b/go.mod index dc91bd745..d70017171 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/a8m/envsubst v1.4.2 + github.com/budougumi0617/cmpmock v0.0.4 github.com/go-logr/logr v1.2.4 github.com/google/go-cmp v0.5.9 github.com/google/go-containerregistry v0.16.1 @@ -25,6 +26,7 @@ require ( k8s.io/utils v0.0.0-20230505201702-9f6742963106 open-cluster-management.io/api v0.11.0 sigs.k8s.io/controller-runtime v0.15.1 + sigs.k8s.io/yaml v1.3.0 ) require ( @@ -46,6 +48,7 @@ require ( github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/mock v1.5.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.6.9 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -90,5 +93,4 @@ require ( k8s.io/kube-openapi v0.0.0-20230515203736-54b630e78af5 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index a61e65ad9..c4a0f5962 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGt github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 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/budougumi0617/cmpmock v0.0.4 h1:WSSSkN8zh57MLFsMbHe0svW3sP7ZksDyqg4j8tHijdw= +github.com/budougumi0617/cmpmock v0.0.4/go.mod h1:x5H3AmbT7AipA7u65AFnHxC+bYom+txnkkSH1X+ZIIw= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -71,6 +73,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/internal/cmd/cmdutils.go b/internal/cmd/cmdutils.go index 589fb670c..32dd3db8a 100644 --- a/internal/cmd/cmdutils.go +++ b/internal/cmd/cmdutils.go @@ -15,7 +15,7 @@ func FatalError(l logr.Logger, err error, msg string, fields ...interface{}) { func GetEnvOrFatalError(name string, logger logr.Logger) string { val := os.Getenv(name) if val == "" { - FatalError(logger, errors.New("empty value"), "Could not get the environment variable", "name", val) + FatalError(logger, errors.New("empty value"), "Could not get the environment variable", "name", name) } return val diff --git a/internal/filter/filter.go b/internal/filter/filter.go index b9ce0c03c..a7bb7c480 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -433,3 +433,7 @@ func NodeLabelModuleVersionUpdatePredicate(logger logr.Logger) predicate.Predica }, } } + +func SkipDeletions() predicate.Predicate { + return skipDeletions +} diff --git a/vendor/github.com/budougumi0617/cmpmock/.golangci.yml b/vendor/github.com/budougumi0617/cmpmock/.golangci.yml new file mode 100644 index 000000000..bd403a048 --- /dev/null +++ b/vendor/github.com/budougumi0617/cmpmock/.golangci.yml @@ -0,0 +1,45 @@ + +linters-settings: + govet: + check-shadowing: false + golint: + min-confidence: 0 + gocyclo: + min-complexity: 30 + maligned: + suggest-new: true + misspell: + locale: US + ignore-words: + - cancelled + - Cancelled + +linters: + disable-all: true + enable: + - goimports + - bodyclose + - deadcode + - errcheck + - gochecknoinits + - gocognit + - gocritic + - gocyclo + - gofmt + - golint + - govet + - ineffassign + - interfacer + - maligned + - misspell + - nakedret + - prealloc + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - varcheck + - whitespace \ No newline at end of file diff --git a/vendor/github.com/budougumi0617/cmpmock/.release-it.json b/vendor/github.com/budougumi0617/cmpmock/.release-it.json new file mode 100644 index 000000000..2b22d371c --- /dev/null +++ b/vendor/github.com/budougumi0617/cmpmock/.release-it.json @@ -0,0 +1,17 @@ +{ + "requireUpstream": false, + "requireCleanWorkingDir": false, + "github": { + "release": true + }, + "git": { + "commit": false, + "push": false, + "requireUpstream": false, + "requireCleanWorkingDir": false + }, + "npm": { + "publish": false, + "ignoreVersion": true + } +} \ No newline at end of file diff --git a/vendor/github.com/budougumi0617/cmpmock/LICENSE b/vendor/github.com/budougumi0617/cmpmock/LICENSE new file mode 100644 index 000000000..3adf0d307 --- /dev/null +++ b/vendor/github.com/budougumi0617/cmpmock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Yoichiro Shimizu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/budougumi0617/cmpmock/README.md b/vendor/github.com/budougumi0617/cmpmock/README.md new file mode 100644 index 000000000..b41e2b518 --- /dev/null +++ b/vendor/github.com/budougumi0617/cmpmock/README.md @@ -0,0 +1,66 @@ +# cmpmock + +[![Go Reference](https://pkg.go.dev/badge/github.com/budougumi0617/cmpmock.svg)](https://pkg.go.dev/github.com/budougumi0617/cmpmock) +[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) +[![test](https://github.com/budougumi0617/cmpmock/workflows/test/badge.svg)](https://github.com/budougumi0617/cmpmock/actions?query=workflow%3Atest) +[![reviewdog](https://github.com/budougumi0617/cmpmock/workflows/reviewdog/badge.svg)](https://github.com/budougumi0617/cmpmock/actions?query=workflow%3Areviewdog) + +Readable & Flexible matcher for https://github.com/golang/mock + +## Description +cmpmock provides a simple custom matcher. it is be able to modify behavior with `github.com/google/go-cmp/cmp/cmpopts`. + +```go +import "github.com/google/go-cmp/cmp" + +func DiffEq(v interface{}, opts ...cmp.Option) gomock.Matcher +``` + +If `DiffEq` is set no `opts`, default behavior ignores a time differences of less than a second. + + +### Readable ouput + +Default output +``` +expected call at /Users/budougumi0617/go/src/github.com/budougumi0617/cmpmock/_example/repo_test.go:26 doesn't match the argument at index 1. +Got: &{John Due Tokyo 2021-04-23 02:46:58.145696 +0900 JST m=+0.000595005} +Want: is equal to &{John Due Tokyo 2021-04-23 02:46:48.145646 +0900 JST m=-9.999455563} +``` + +use `cmpmock.DiffEq` +``` +expected call at /Users/budougumi0617/go/src/github.com/budougumi0617/cmpmock/_example/repo_test.go:27 doesn't match the argument at index 1. +Got: &{John Due Tokyo 2021-04-23 02:46:33.290458 +0900 JST m=+0.001035665} +Want: diff(-got +want) is &_example.User{ + Name: "John Due", + Address: "Tokyo", +- CreateAt: s"2021-04-23 02:46:33.290458 +0900 JST m=+0.001035665", ++ CreateAt: s"2021-04-23 02:46:23.290383 +0900 JST m=-9.999039004", +} +``` + +## Usage + +```go +type UserRepo interface { + Save(context.Context, *User) error +} + +wantUser := &User{} +mrepo := mock.NewMockUserRepo(ctrl) +mrepo.EXPECT().Save(ctx, cmpmock.DiffEq(wantUser)).Return(nil) +``` + +## Installation + +```bash +$ go get -u github.com/budougumi0617/cmpmock +``` + +## License + +[MIT](./LICENSE) + +## Author +Yocihiro Shimizu(@budougumi0617) \ No newline at end of file diff --git a/vendor/github.com/budougumi0617/cmpmock/diffmatcher.go b/vendor/github.com/budougumi0617/cmpmock/diffmatcher.go new file mode 100644 index 000000000..c8a3ba0e6 --- /dev/null +++ b/vendor/github.com/budougumi0617/cmpmock/diffmatcher.go @@ -0,0 +1,42 @@ +package cmpmock + +import ( + "fmt" + "time" + + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +// DiffEq is a simple custom matcher. it is be able to modify behavior with `github.com/google/go-cmp/cmp/cmpopts`. +// If `DiffEq` is set no `opts`, default behavior ignores a time differences of less than a second. +func DiffEq(v interface{}, opts ...cmp.Option) gomock.Matcher { + var lopts cmp.Options + if len(opts) == 0 { + lopts = append(lopts, cmpopts.EquateApproxTime(1*time.Second)) + } else { + lopts = append(lopts, opts...) + } + return &diffMatcher{want: v, opts: lopts} +} + +type diffMatcher struct { + want interface{} + diff string + opts cmp.Options +} + +// Matches implements golang/mock/gomock#Matcher interface. +func (d *diffMatcher) Matches(x interface{}) bool { + d.diff = cmp.Diff(x, d.want, d.opts...) + return len(d.diff) == 0 +} + +// String implements golang/mock/gomock#Matcher interface. +func (d *diffMatcher) String() string { + if d.diff == "" { + return "" + } + return fmt.Sprintf("diff(-got +want) is %s", d.diff) +} diff --git a/vendor/github.com/golang/mock/AUTHORS b/vendor/github.com/golang/mock/AUTHORS new file mode 100644 index 000000000..660b8ccc8 --- /dev/null +++ b/vendor/github.com/golang/mock/AUTHORS @@ -0,0 +1,12 @@ +# This is the official list of GoMock authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# Please keep the list sorted. + +Alex Reece +Google Inc. diff --git a/vendor/github.com/golang/mock/CONTRIBUTORS b/vendor/github.com/golang/mock/CONTRIBUTORS new file mode 100644 index 000000000..def849cab --- /dev/null +++ b/vendor/github.com/golang/mock/CONTRIBUTORS @@ -0,0 +1,37 @@ +# This is the official list of people who can contribute (and typically +# have contributed) code to the gomock repository. +# The AUTHORS file lists the copyright holders; this file +# lists people. For example, Google employees are listed here +# but not in AUTHORS, because Google holds the copyright. +# +# The submission process automatically checks to make sure +# that people submitting code are listed in this file (by email address). +# +# Names should be added to this file only after verifying that +# the individual or the individual's organization has agreed to +# the appropriate Contributor License Agreement, found here: +# +# http://code.google.com/legal/individual-cla-v1.0.html +# http://code.google.com/legal/corporate-cla-v1.0.html +# +# The agreement for individuals can be filled out on the web. +# +# When adding J Random Contributor's name to this file, +# either J's name or J's organization's name should be +# added to the AUTHORS file, depending on whether the +# individual or corporate CLA was used. + +# Names should be added to this file like so: +# Name +# +# An entry with two email addresses specifies that the +# first address should be used in the submit logs and +# that the second address should be recognized as the +# same person when interacting with Rietveld. + +# Please keep the list sorted. + +Aaron Jacobs +Alex Reece +David Symonds +Ryan Barrett diff --git a/vendor/github.com/golang/mock/LICENSE b/vendor/github.com/golang/mock/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/vendor/github.com/golang/mock/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/github.com/golang/mock/gomock/call.go b/vendor/github.com/golang/mock/gomock/call.go new file mode 100644 index 000000000..b18cc2d61 --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/call.go @@ -0,0 +1,433 @@ +// Copyright 2010 Google Inc. +// +// 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 gomock + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// Call represents an expected call to a mock. +type Call struct { + t TestHelper // for triggering test failures on invalid call setup + + receiver interface{} // the receiver of the method call + method string // the name of the method + methodType reflect.Type // the type of the method + args []Matcher // the args + origin string // file and line number of call setup + + preReqs []*Call // prerequisite calls + + // Expectations + minCalls, maxCalls int + + numCalls int // actual number made + + // actions are called when this Call is called. Each action gets the args and + // can set the return values by returning a non-nil slice. Actions run in the + // order they are created. + actions []func([]interface{}) []interface{} +} + +// newCall creates a *Call. It requires the method type in order to support +// unexported methods. +func newCall(t TestHelper, receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call { + t.Helper() + + // TODO: check arity, types. + margs := make([]Matcher, len(args)) + for i, arg := range args { + if m, ok := arg.(Matcher); ok { + margs[i] = m + } else if arg == nil { + // Handle nil specially so that passing a nil interface value + // will match the typed nils of concrete args. + margs[i] = Nil() + } else { + margs[i] = Eq(arg) + } + } + + // callerInfo's skip should be updated if the number of calls between the user's test + // and this line changes, i.e. this code is wrapped in another anonymous function. + // 0 is us, 1 is RecordCallWithMethodType(), 2 is the generated recorder, and 3 is the user's test. + origin := callerInfo(3) + actions := []func([]interface{}) []interface{}{func([]interface{}) []interface{} { + // Synthesize the zero value for each of the return args' types. + rets := make([]interface{}, methodType.NumOut()) + for i := 0; i < methodType.NumOut(); i++ { + rets[i] = reflect.Zero(methodType.Out(i)).Interface() + } + return rets + }} + return &Call{t: t, receiver: receiver, method: method, methodType: methodType, + args: margs, origin: origin, minCalls: 1, maxCalls: 1, actions: actions} +} + +// AnyTimes allows the expectation to be called 0 or more times +func (c *Call) AnyTimes() *Call { + c.minCalls, c.maxCalls = 0, 1e8 // close enough to infinity + return c +} + +// MinTimes requires the call to occur at least n times. If AnyTimes or MaxTimes have not been called or if MaxTimes +// was previously called with 1, MinTimes also sets the maximum number of calls to infinity. +func (c *Call) MinTimes(n int) *Call { + c.minCalls = n + if c.maxCalls == 1 { + c.maxCalls = 1e8 + } + return c +} + +// MaxTimes limits the number of calls to n times. If AnyTimes or MinTimes have not been called or if MinTimes was +// previously called with 1, MaxTimes also sets the minimum number of calls to 0. +func (c *Call) MaxTimes(n int) *Call { + c.maxCalls = n + if c.minCalls == 1 { + c.minCalls = 0 + } + return c +} + +// DoAndReturn declares the action to run when the call is matched. +// The return values from this function are returned by the mocked function. +// It takes an interface{} argument to support n-arity functions. +func (c *Call) DoAndReturn(f interface{}) *Call { + // TODO: Check arity and types here, rather than dying badly elsewhere. + v := reflect.ValueOf(f) + + c.addAction(func(args []interface{}) []interface{} { + vargs := make([]reflect.Value, len(args)) + ft := v.Type() + for i := 0; i < len(args); i++ { + if args[i] != nil { + vargs[i] = reflect.ValueOf(args[i]) + } else { + // Use the zero value for the arg. + vargs[i] = reflect.Zero(ft.In(i)) + } + } + vrets := v.Call(vargs) + rets := make([]interface{}, len(vrets)) + for i, ret := range vrets { + rets[i] = ret.Interface() + } + return rets + }) + return c +} + +// Do declares the action to run when the call is matched. The function's +// return values are ignored to retain backward compatibility. To use the +// return values call DoAndReturn. +// It takes an interface{} argument to support n-arity functions. +func (c *Call) Do(f interface{}) *Call { + // TODO: Check arity and types here, rather than dying badly elsewhere. + v := reflect.ValueOf(f) + + c.addAction(func(args []interface{}) []interface{} { + vargs := make([]reflect.Value, len(args)) + ft := v.Type() + for i := 0; i < len(args); i++ { + if args[i] != nil { + vargs[i] = reflect.ValueOf(args[i]) + } else { + // Use the zero value for the arg. + vargs[i] = reflect.Zero(ft.In(i)) + } + } + v.Call(vargs) + return nil + }) + return c +} + +// Return declares the values to be returned by the mocked function call. +func (c *Call) Return(rets ...interface{}) *Call { + c.t.Helper() + + mt := c.methodType + if len(rets) != mt.NumOut() { + c.t.Fatalf("wrong number of arguments to Return for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, len(rets), mt.NumOut(), c.origin) + } + for i, ret := range rets { + if got, want := reflect.TypeOf(ret), mt.Out(i); got == want { + // Identical types; nothing to do. + } else if got == nil { + // Nil needs special handling. + switch want.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + // ok + default: + c.t.Fatalf("argument %d to Return for %T.%v is nil, but %v is not nillable [%s]", + i, c.receiver, c.method, want, c.origin) + } + } else if got.AssignableTo(want) { + // Assignable type relation. Make the assignment now so that the generated code + // can return the values with a type assertion. + v := reflect.New(want).Elem() + v.Set(reflect.ValueOf(ret)) + rets[i] = v.Interface() + } else { + c.t.Fatalf("wrong type of argument %d to Return for %T.%v: %v is not assignable to %v [%s]", + i, c.receiver, c.method, got, want, c.origin) + } + } + + c.addAction(func([]interface{}) []interface{} { + return rets + }) + + return c +} + +// Times declares the exact number of times a function call is expected to be executed. +func (c *Call) Times(n int) *Call { + c.minCalls, c.maxCalls = n, n + return c +} + +// SetArg declares an action that will set the nth argument's value, +// indirected through a pointer. Or, in the case of a slice, SetArg +// will copy value's elements into the nth argument. +func (c *Call) SetArg(n int, value interface{}) *Call { + c.t.Helper() + + mt := c.methodType + // TODO: This will break on variadic methods. + // We will need to check those at invocation time. + if n < 0 || n >= mt.NumIn() { + c.t.Fatalf("SetArg(%d, ...) called for a method with %d args [%s]", + n, mt.NumIn(), c.origin) + } + // Permit setting argument through an interface. + // In the interface case, we don't (nay, can't) check the type here. + at := mt.In(n) + switch at.Kind() { + case reflect.Ptr: + dt := at.Elem() + if vt := reflect.TypeOf(value); !vt.AssignableTo(dt) { + c.t.Fatalf("SetArg(%d, ...) argument is a %v, not assignable to %v [%s]", + n, vt, dt, c.origin) + } + case reflect.Interface: + // nothing to do + case reflect.Slice: + // nothing to do + default: + c.t.Fatalf("SetArg(%d, ...) referring to argument of non-pointer non-interface non-slice type %v [%s]", + n, at, c.origin) + } + + c.addAction(func(args []interface{}) []interface{} { + v := reflect.ValueOf(value) + switch reflect.TypeOf(args[n]).Kind() { + case reflect.Slice: + setSlice(args[n], v) + default: + reflect.ValueOf(args[n]).Elem().Set(v) + } + return nil + }) + return c +} + +// isPreReq returns true if other is a direct or indirect prerequisite to c. +func (c *Call) isPreReq(other *Call) bool { + for _, preReq := range c.preReqs { + if other == preReq || preReq.isPreReq(other) { + return true + } + } + return false +} + +// After declares that the call may only match after preReq has been exhausted. +func (c *Call) After(preReq *Call) *Call { + c.t.Helper() + + if c == preReq { + c.t.Fatalf("A call isn't allowed to be its own prerequisite") + } + if preReq.isPreReq(c) { + c.t.Fatalf("Loop in call order: %v is a prerequisite to %v (possibly indirectly).", c, preReq) + } + + c.preReqs = append(c.preReqs, preReq) + return c +} + +// Returns true if the minimum number of calls have been made. +func (c *Call) satisfied() bool { + return c.numCalls >= c.minCalls +} + +// Returns true if the maximum number of calls have been made. +func (c *Call) exhausted() bool { + return c.numCalls >= c.maxCalls +} + +func (c *Call) String() string { + args := make([]string, len(c.args)) + for i, arg := range c.args { + args[i] = arg.String() + } + arguments := strings.Join(args, ", ") + return fmt.Sprintf("%T.%v(%s) %s", c.receiver, c.method, arguments, c.origin) +} + +// Tests if the given call matches the expected call. +// If yes, returns nil. If no, returns error with message explaining why it does not match. +func (c *Call) matches(args []interface{}) error { + if !c.methodType.IsVariadic() { + if len(args) != len(c.args) { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: %d", + c.origin, len(args), len(c.args)) + } + + for i, m := range c.args { + if !m.Matches(args[i]) { + return fmt.Errorf( + "expected call at %s doesn't match the argument at index %d.\nGot: %v\nWant: %v", + c.origin, i, formatGottenArg(m, args[i]), m, + ) + } + } + } else { + if len(c.args) < c.methodType.NumIn()-1 { + return fmt.Errorf("expected call at %s has the wrong number of matchers. Got: %d, want: %d", + c.origin, len(c.args), c.methodType.NumIn()-1) + } + if len(c.args) != c.methodType.NumIn() && len(args) != len(c.args) { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: %d", + c.origin, len(args), len(c.args)) + } + if len(args) < len(c.args)-1 { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: greater than or equal to %d", + c.origin, len(args), len(c.args)-1) + } + + for i, m := range c.args { + if i < c.methodType.NumIn()-1 { + // Non-variadic args + if !m.Matches(args[i]) { + return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v", + c.origin, strconv.Itoa(i), formatGottenArg(m, args[i]), m) + } + continue + } + // The last arg has a possibility of a variadic argument, so let it branch + + // sample: Foo(a int, b int, c ...int) + if i < len(c.args) && i < len(args) { + if m.Matches(args[i]) { + // Got Foo(a, b, c) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, someSliceMatcher) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC) + // Got Foo(a, b) want Foo(matcherA, matcherB) + // Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD) + continue + } + } + + // The number of actual args don't match the number of matchers, + // or the last matcher is a slice and the last arg is not. + // If this function still matches it is because the last matcher + // matches all the remaining arguments or the lack of any. + // Convert the remaining arguments, if any, into a slice of the + // expected type. + vargsType := c.methodType.In(c.methodType.NumIn() - 1) + vargs := reflect.MakeSlice(vargsType, 0, len(args)-i) + for _, arg := range args[i:] { + vargs = reflect.Append(vargs, reflect.ValueOf(arg)) + } + if m.Matches(vargs.Interface()) { + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, someSliceMatcher) + // Got Foo(a, b) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b) want Foo(matcherA, matcherB, someEmptySliceMatcher) + break + } + // Wrong number of matchers or not match. Fail. + // Got Foo(a, b) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD, matcherE) + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c) want Foo(matcherA, matcherB) + + return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v", + c.origin, strconv.Itoa(i), formatGottenArg(m, args[i:]), c.args[i]) + } + } + + // Check that all prerequisite calls have been satisfied. + for _, preReqCall := range c.preReqs { + if !preReqCall.satisfied() { + return fmt.Errorf("Expected call at %s doesn't have a prerequisite call satisfied:\n%v\nshould be called before:\n%v", + c.origin, preReqCall, c) + } + } + + // Check that the call is not exhausted. + if c.exhausted() { + return fmt.Errorf("expected call at %s has already been called the max number of times", c.origin) + } + + return nil +} + +// dropPrereqs tells the expected Call to not re-check prerequisite calls any +// longer, and to return its current set. +func (c *Call) dropPrereqs() (preReqs []*Call) { + preReqs = c.preReqs + c.preReqs = nil + return +} + +func (c *Call) call() []func([]interface{}) []interface{} { + c.numCalls++ + return c.actions +} + +// InOrder declares that the given calls should occur in order. +func InOrder(calls ...*Call) { + for i := 1; i < len(calls); i++ { + calls[i].After(calls[i-1]) + } +} + +func setSlice(arg interface{}, v reflect.Value) { + va := reflect.ValueOf(arg) + for i := 0; i < v.Len(); i++ { + va.Index(i).Set(v.Index(i)) + } +} + +func (c *Call) addAction(action func([]interface{}) []interface{}) { + c.actions = append(c.actions, action) +} + +func formatGottenArg(m Matcher, arg interface{}) string { + got := fmt.Sprintf("%v", arg) + if gs, ok := m.(GotFormatter); ok { + got = gs.Got(arg) + } + return got +} diff --git a/vendor/github.com/golang/mock/gomock/callset.go b/vendor/github.com/golang/mock/gomock/callset.go new file mode 100644 index 000000000..e4e85d602 --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/callset.go @@ -0,0 +1,112 @@ +// Copyright 2011 Google Inc. +// +// 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 gomock + +import ( + "bytes" + "fmt" +) + +// callSet represents a set of expected calls, indexed by receiver and method +// name. +type callSet struct { + // Calls that are still expected. + expected map[callSetKey][]*Call + // Calls that have been exhausted. + exhausted map[callSetKey][]*Call +} + +// callSetKey is the key in the maps in callSet +type callSetKey struct { + receiver interface{} + fname string +} + +func newCallSet() *callSet { + return &callSet{make(map[callSetKey][]*Call), make(map[callSetKey][]*Call)} +} + +// Add adds a new expected call. +func (cs callSet) Add(call *Call) { + key := callSetKey{call.receiver, call.method} + m := cs.expected + if call.exhausted() { + m = cs.exhausted + } + m[key] = append(m[key], call) +} + +// Remove removes an expected call. +func (cs callSet) Remove(call *Call) { + key := callSetKey{call.receiver, call.method} + calls := cs.expected[key] + for i, c := range calls { + if c == call { + // maintain order for remaining calls + cs.expected[key] = append(calls[:i], calls[i+1:]...) + cs.exhausted[key] = append(cs.exhausted[key], call) + break + } + } +} + +// FindMatch searches for a matching call. Returns error with explanation message if no call matched. +func (cs callSet) FindMatch(receiver interface{}, method string, args []interface{}) (*Call, error) { + key := callSetKey{receiver, method} + + // Search through the expected calls. + expected := cs.expected[key] + var callsErrors bytes.Buffer + for _, call := range expected { + err := call.matches(args) + if err != nil { + _, _ = fmt.Fprintf(&callsErrors, "\n%v", err) + } else { + return call, nil + } + } + + // If we haven't found a match then search through the exhausted calls so we + // get useful error messages. + exhausted := cs.exhausted[key] + for _, call := range exhausted { + if err := call.matches(args); err != nil { + _, _ = fmt.Fprintf(&callsErrors, "\n%v", err) + continue + } + _, _ = fmt.Fprintf( + &callsErrors, "all expected calls for method %q have been exhausted", method, + ) + } + + if len(expected)+len(exhausted) == 0 { + _, _ = fmt.Fprintf(&callsErrors, "there are no expected calls of the method %q for that receiver", method) + } + + return nil, fmt.Errorf(callsErrors.String()) +} + +// Failures returns the calls that are not satisfied. +func (cs callSet) Failures() []*Call { + failures := make([]*Call, 0, len(cs.expected)) + for _, calls := range cs.expected { + for _, call := range calls { + if !call.satisfied() { + failures = append(failures, call) + } + } + } + return failures +} diff --git a/vendor/github.com/golang/mock/gomock/controller.go b/vendor/github.com/golang/mock/gomock/controller.go new file mode 100644 index 000000000..3b6569091 --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/controller.go @@ -0,0 +1,333 @@ +// Copyright 2010 Google Inc. +// +// 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 gomock is a mock framework for Go. +// +// Standard usage: +// (1) Define an interface that you wish to mock. +// type MyInterface interface { +// SomeMethod(x int64, y string) +// } +// (2) Use mockgen to generate a mock from the interface. +// (3) Use the mock in a test: +// func TestMyThing(t *testing.T) { +// mockCtrl := gomock.NewController(t) +// defer mockCtrl.Finish() +// +// mockObj := something.NewMockMyInterface(mockCtrl) +// mockObj.EXPECT().SomeMethod(4, "blah") +// // pass mockObj to a real object and play with it. +// } +// +// By default, expected calls are not enforced to run in any particular order. +// Call order dependency can be enforced by use of InOrder and/or Call.After. +// Call.After can create more varied call order dependencies, but InOrder is +// often more convenient. +// +// The following examples create equivalent call order dependencies. +// +// Example of using Call.After to chain expected call order: +// +// firstCall := mockObj.EXPECT().SomeMethod(1, "first") +// secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall) +// mockObj.EXPECT().SomeMethod(3, "third").After(secondCall) +// +// Example of using InOrder to declare expected call order: +// +// gomock.InOrder( +// mockObj.EXPECT().SomeMethod(1, "first"), +// mockObj.EXPECT().SomeMethod(2, "second"), +// mockObj.EXPECT().SomeMethod(3, "third"), +// ) +package gomock + +import ( + "context" + "fmt" + "reflect" + "runtime" + "sync" +) + +// A TestReporter is something that can be used to report test failures. It +// is satisfied by the standard library's *testing.T. +type TestReporter interface { + Errorf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) +} + +// TestHelper is a TestReporter that has the Helper method. It is satisfied +// by the standard library's *testing.T. +type TestHelper interface { + TestReporter + Helper() +} + +// cleanuper is used to check if TestHelper also has the `Cleanup` method. A +// common pattern is to pass in a `*testing.T` to +// `NewController(t TestReporter)`. In Go 1.14+, `*testing.T` has a cleanup +// method. This can be utilized to call `Finish()` so the caller of this library +// does not have to. +type cleanuper interface { + Cleanup(func()) +} + +// A Controller represents the top-level control of a mock ecosystem. It +// defines the scope and lifetime of mock objects, as well as their +// expectations. It is safe to call Controller's methods from multiple +// goroutines. Each test should create a new Controller and invoke Finish via +// defer. +// +// func TestFoo(t *testing.T) { +// ctrl := gomock.NewController(t) +// defer ctrl.Finish() +// // .. +// } +// +// func TestBar(t *testing.T) { +// t.Run("Sub-Test-1", st) { +// ctrl := gomock.NewController(st) +// defer ctrl.Finish() +// // .. +// }) +// t.Run("Sub-Test-2", st) { +// ctrl := gomock.NewController(st) +// defer ctrl.Finish() +// // .. +// }) +// }) +type Controller struct { + // T should only be called within a generated mock. It is not intended to + // be used in user code and may be changed in future versions. T is the + // TestReporter passed in when creating the Controller via NewController. + // If the TestReporter does not implement a TestHelper it will be wrapped + // with a nopTestHelper. + T TestHelper + mu sync.Mutex + expectedCalls *callSet + finished bool +} + +// NewController returns a new Controller. It is the preferred way to create a +// Controller. +// +// New in go1.14+, if you are passing a *testing.T into this function you no +// longer need to call ctrl.Finish() in your test methods +func NewController(t TestReporter) *Controller { + h, ok := t.(TestHelper) + if !ok { + h = &nopTestHelper{t} + } + ctrl := &Controller{ + T: h, + expectedCalls: newCallSet(), + } + if c, ok := isCleanuper(ctrl.T); ok { + c.Cleanup(func() { + ctrl.T.Helper() + ctrl.finish(true, nil) + }) + } + + return ctrl +} + +type cancelReporter struct { + t TestHelper + cancel func() +} + +func (r *cancelReporter) Errorf(format string, args ...interface{}) { + r.t.Errorf(format, args...) +} +func (r *cancelReporter) Fatalf(format string, args ...interface{}) { + defer r.cancel() + r.t.Fatalf(format, args...) +} + +func (r *cancelReporter) Helper() { + r.t.Helper() +} + +// WithContext returns a new Controller and a Context, which is cancelled on any +// fatal failure. +func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context) { + h, ok := t.(TestHelper) + if !ok { + h = &nopTestHelper{t: t} + } + + ctx, cancel := context.WithCancel(ctx) + return NewController(&cancelReporter{t: h, cancel: cancel}), ctx +} + +type nopTestHelper struct { + t TestReporter +} + +func (h *nopTestHelper) Errorf(format string, args ...interface{}) { + h.t.Errorf(format, args...) +} +func (h *nopTestHelper) Fatalf(format string, args ...interface{}) { + h.t.Fatalf(format, args...) +} + +func (h nopTestHelper) Helper() {} + +// RecordCall is called by a mock. It should not be called by user code. +func (ctrl *Controller) RecordCall(receiver interface{}, method string, args ...interface{}) *Call { + ctrl.T.Helper() + + recv := reflect.ValueOf(receiver) + for i := 0; i < recv.Type().NumMethod(); i++ { + if recv.Type().Method(i).Name == method { + return ctrl.RecordCallWithMethodType(receiver, method, recv.Method(i).Type(), args...) + } + } + ctrl.T.Fatalf("gomock: failed finding method %s on %T", method, receiver) + panic("unreachable") +} + +// RecordCallWithMethodType is called by a mock. It should not be called by user code. +func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call { + ctrl.T.Helper() + + call := newCall(ctrl.T, receiver, method, methodType, args...) + + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + ctrl.expectedCalls.Add(call) + + return call +} + +// Call is called by a mock. It should not be called by user code. +func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{} { + ctrl.T.Helper() + + // Nest this code so we can use defer to make sure the lock is released. + actions := func() []func([]interface{}) []interface{} { + ctrl.T.Helper() + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + expected, err := ctrl.expectedCalls.FindMatch(receiver, method, args) + if err != nil { + // callerInfo's skip should be updated if the number of calls between the user's test + // and this line changes, i.e. this code is wrapped in another anonymous function. + // 0 is us, 1 is controller.Call(), 2 is the generated mock, and 3 is the user's test. + origin := callerInfo(3) + ctrl.T.Fatalf("Unexpected call to %T.%v(%v) at %s because: %s", receiver, method, args, origin, err) + } + + // Two things happen here: + // * the matching call no longer needs to check prerequite calls, + // * and the prerequite calls are no longer expected, so remove them. + preReqCalls := expected.dropPrereqs() + for _, preReqCall := range preReqCalls { + ctrl.expectedCalls.Remove(preReqCall) + } + + actions := expected.call() + if expected.exhausted() { + ctrl.expectedCalls.Remove(expected) + } + return actions + }() + + var rets []interface{} + for _, action := range actions { + if r := action(args); r != nil { + rets = r + } + } + + return rets +} + +// Finish checks to see if all the methods that were expected to be called +// were called. It should be invoked for each Controller. It is not idempotent +// and therefore can only be invoked once. +func (ctrl *Controller) Finish() { + // If we're currently panicking, probably because this is a deferred call. + // This must be recovered in the deferred function. + err := recover() + ctrl.finish(false, err) +} + +func (ctrl *Controller) finish(cleanup bool, panicErr interface{}) { + ctrl.T.Helper() + + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + if ctrl.finished { + if _, ok := isCleanuper(ctrl.T); !ok { + ctrl.T.Fatalf("Controller.Finish was called more than once. It has to be called exactly once.") + } + return + } + ctrl.finished = true + + // Short-circuit, pass through the panic. + if panicErr != nil { + panic(panicErr) + } + + // Check that all remaining expected calls are satisfied. + failures := ctrl.expectedCalls.Failures() + for _, call := range failures { + ctrl.T.Errorf("missing call(s) to %v", call) + } + if len(failures) != 0 { + if !cleanup { + ctrl.T.Fatalf("aborting test due to missing call(s)") + return + } + ctrl.T.Errorf("aborting test due to missing call(s)") + } +} + +// callerInfo returns the file:line of the call site. skip is the number +// of stack frames to skip when reporting. 0 is callerInfo's call site. +func callerInfo(skip int) string { + if _, file, line, ok := runtime.Caller(skip + 1); ok { + return fmt.Sprintf("%s:%d", file, line) + } + return "unknown file" +} + +// isCleanuper checks it if t's base TestReporter has a Cleanup method. +func isCleanuper(t TestReporter) (cleanuper, bool) { + tr := unwrapTestReporter(t) + c, ok := tr.(cleanuper) + return c, ok +} + +// unwrapTestReporter unwraps TestReporter to the base implementation. +func unwrapTestReporter(t TestReporter) TestReporter { + tr := t + switch nt := t.(type) { + case *cancelReporter: + tr = nt.t + if h, check := tr.(*nopTestHelper); check { + tr = h.t + } + case *nopTestHelper: + tr = nt.t + default: + // not wrapped + } + return tr +} diff --git a/vendor/github.com/golang/mock/gomock/matchers.go b/vendor/github.com/golang/mock/gomock/matchers.go new file mode 100644 index 000000000..770aba5a3 --- /dev/null +++ b/vendor/github.com/golang/mock/gomock/matchers.go @@ -0,0 +1,269 @@ +// Copyright 2010 Google Inc. +// +// 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 gomock + +import ( + "fmt" + "reflect" + "strings" +) + +// A Matcher is a representation of a class of values. +// It is used to represent the valid or expected arguments to a mocked method. +type Matcher interface { + // Matches returns whether x is a match. + Matches(x interface{}) bool + + // String describes what the matcher matches. + String() string +} + +// WantFormatter modifies the given Matcher's String() method to the given +// Stringer. This allows for control on how the "Want" is formatted when +// printing . +func WantFormatter(s fmt.Stringer, m Matcher) Matcher { + type matcher interface { + Matches(x interface{}) bool + } + + return struct { + matcher + fmt.Stringer + }{ + matcher: m, + Stringer: s, + } +} + +// StringerFunc type is an adapter to allow the use of ordinary functions as +// a Stringer. If f is a function with the appropriate signature, +// StringerFunc(f) is a Stringer that calls f. +type StringerFunc func() string + +// String implements fmt.Stringer. +func (f StringerFunc) String() string { + return f() +} + +// GotFormatter is used to better print failure messages. If a matcher +// implements GotFormatter, it will use the result from Got when printing +// the failure message. +type GotFormatter interface { + // Got is invoked with the received value. The result is used when + // printing the failure message. + Got(got interface{}) string +} + +// GotFormatterFunc type is an adapter to allow the use of ordinary +// functions as a GotFormatter. If f is a function with the appropriate +// signature, GotFormatterFunc(f) is a GotFormatter that calls f. +type GotFormatterFunc func(got interface{}) string + +// Got implements GotFormatter. +func (f GotFormatterFunc) Got(got interface{}) string { + return f(got) +} + +// GotFormatterAdapter attaches a GotFormatter to a Matcher. +func GotFormatterAdapter(s GotFormatter, m Matcher) Matcher { + return struct { + GotFormatter + Matcher + }{ + GotFormatter: s, + Matcher: m, + } +} + +type anyMatcher struct{} + +func (anyMatcher) Matches(interface{}) bool { + return true +} + +func (anyMatcher) String() string { + return "is anything" +} + +type eqMatcher struct { + x interface{} +} + +func (e eqMatcher) Matches(x interface{}) bool { + // In case, some value is nil + if e.x == nil || x == nil { + return reflect.DeepEqual(e.x, x) + } + + // Check if types assignable and convert them to common type + x1Val := reflect.ValueOf(e.x) + x2Val := reflect.ValueOf(x) + + if x1Val.Type().AssignableTo(x2Val.Type()) { + x1ValConverted := x1Val.Convert(x2Val.Type()) + return reflect.DeepEqual(x1ValConverted.Interface(), x2Val.Interface()) + } + + return false +} + +func (e eqMatcher) String() string { + return fmt.Sprintf("is equal to %v", e.x) +} + +type nilMatcher struct{} + +func (nilMatcher) Matches(x interface{}) bool { + if x == nil { + return true + } + + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, + reflect.Ptr, reflect.Slice: + return v.IsNil() + } + + return false +} + +func (nilMatcher) String() string { + return "is nil" +} + +type notMatcher struct { + m Matcher +} + +func (n notMatcher) Matches(x interface{}) bool { + return !n.m.Matches(x) +} + +func (n notMatcher) String() string { + // TODO: Improve this if we add a NotString method to the Matcher interface. + return "not(" + n.m.String() + ")" +} + +type assignableToTypeOfMatcher struct { + targetType reflect.Type +} + +func (m assignableToTypeOfMatcher) Matches(x interface{}) bool { + return reflect.TypeOf(x).AssignableTo(m.targetType) +} + +func (m assignableToTypeOfMatcher) String() string { + return "is assignable to " + m.targetType.Name() +} + +type allMatcher struct { + matchers []Matcher +} + +func (am allMatcher) Matches(x interface{}) bool { + for _, m := range am.matchers { + if !m.Matches(x) { + return false + } + } + return true +} + +func (am allMatcher) String() string { + ss := make([]string, 0, len(am.matchers)) + for _, matcher := range am.matchers { + ss = append(ss, matcher.String()) + } + return strings.Join(ss, "; ") +} + +type lenMatcher struct { + i int +} + +func (m lenMatcher) Matches(x interface{}) bool { + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == m.i + default: + return false + } +} + +func (m lenMatcher) String() string { + return fmt.Sprintf("has length %d", m.i) +} + +// Constructors + +// All returns a composite Matcher that returns true if and only all of the +// matchers return true. +func All(ms ...Matcher) Matcher { return allMatcher{ms} } + +// Any returns a matcher that always matches. +func Any() Matcher { return anyMatcher{} } + +// Eq returns a matcher that matches on equality. +// +// Example usage: +// Eq(5).Matches(5) // returns true +// Eq(5).Matches(4) // returns false +func Eq(x interface{}) Matcher { return eqMatcher{x} } + +// Len returns a matcher that matches on length. This matcher returns false if +// is compared to a type that is not an array, chan, map, slice, or string. +func Len(i int) Matcher { + return lenMatcher{i} +} + +// Nil returns a matcher that matches if the received value is nil. +// +// Example usage: +// var x *bytes.Buffer +// Nil().Matches(x) // returns true +// x = &bytes.Buffer{} +// Nil().Matches(x) // returns false +func Nil() Matcher { return nilMatcher{} } + +// Not reverses the results of its given child matcher. +// +// Example usage: +// Not(Eq(5)).Matches(4) // returns true +// Not(Eq(5)).Matches(5) // returns false +func Not(x interface{}) Matcher { + if m, ok := x.(Matcher); ok { + return notMatcher{m} + } + return notMatcher{Eq(x)} +} + +// AssignableToTypeOf is a Matcher that matches if the parameter to the mock +// function is assignable to the type of the parameter to this function. +// +// Example usage: +// var s fmt.Stringer = &bytes.Buffer{} +// AssignableToTypeOf(s).Matches(time.Second) // returns true +// AssignableToTypeOf(s).Matches(99) // returns false +// +// var ctx = reflect.TypeOf((*context.Context)(nil)).Elem() +// AssignableToTypeOf(ctx).Matches(context.Background()) // returns true +func AssignableToTypeOf(x interface{}) Matcher { + if xt, ok := x.(reflect.Type); ok { + return assignableToTypeOfMatcher{xt} + } + return assignableToTypeOfMatcher{reflect.TypeOf(x)} +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go new file mode 100644 index 000000000..e54a76c7e --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go @@ -0,0 +1,156 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package cmpopts provides common options for the cmp package. +package cmpopts + +import ( + "errors" + "math" + "reflect" + "time" + + "github.com/google/go-cmp/cmp" +) + +func equateAlways(_, _ interface{}) bool { return true } + +// EquateEmpty returns a Comparer option that determines all maps and slices +// with a length of zero to be equal, regardless of whether they are nil. +// +// EquateEmpty can be used in conjunction with SortSlices and SortMaps. +func EquateEmpty() cmp.Option { + return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways)) +} + +func isEmpty(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) && + (vx.Len() == 0 && vy.Len() == 0) +} + +// EquateApprox returns a Comparer option that determines float32 or float64 +// values to be equal if they are within a relative fraction or absolute margin. +// This option is not used when either x or y is NaN or infinite. +// +// The fraction determines that the difference of two values must be within the +// smaller fraction of the two values, while the margin determines that the two +// values must be within some absolute margin. +// To express only a fraction or only a margin, use 0 for the other parameter. +// The fraction and margin must be non-negative. +// +// The mathematical expression used is equivalent to: +// +// |x-y| ≤ max(fraction*min(|x|, |y|), margin) +// +// EquateApprox can be used in conjunction with EquateNaNs. +func EquateApprox(fraction, margin float64) cmp.Option { + if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) { + panic("margin or fraction must be a non-negative number") + } + a := approximator{fraction, margin} + return cmp.Options{ + cmp.FilterValues(areRealF64s, cmp.Comparer(a.compareF64)), + cmp.FilterValues(areRealF32s, cmp.Comparer(a.compareF32)), + } +} + +type approximator struct{ frac, marg float64 } + +func areRealF64s(x, y float64) bool { + return !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0) +} +func areRealF32s(x, y float32) bool { + return areRealF64s(float64(x), float64(y)) +} +func (a approximator) compareF64(x, y float64) bool { + relMarg := a.frac * math.Min(math.Abs(x), math.Abs(y)) + return math.Abs(x-y) <= math.Max(a.marg, relMarg) +} +func (a approximator) compareF32(x, y float32) bool { + return a.compareF64(float64(x), float64(y)) +} + +// EquateNaNs returns a Comparer option that determines float32 and float64 +// NaN values to be equal. +// +// EquateNaNs can be used in conjunction with EquateApprox. +func EquateNaNs() cmp.Option { + return cmp.Options{ + cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)), + cmp.FilterValues(areNaNsF32s, cmp.Comparer(equateAlways)), + } +} + +func areNaNsF64s(x, y float64) bool { + return math.IsNaN(x) && math.IsNaN(y) +} +func areNaNsF32s(x, y float32) bool { + return areNaNsF64s(float64(x), float64(y)) +} + +// EquateApproxTime returns a Comparer option that determines two non-zero +// time.Time values to be equal if they are within some margin of one another. +// If both times have a monotonic clock reading, then the monotonic time +// difference will be used. The margin must be non-negative. +func EquateApproxTime(margin time.Duration) cmp.Option { + if margin < 0 { + panic("margin must be a non-negative number") + } + a := timeApproximator{margin} + return cmp.FilterValues(areNonZeroTimes, cmp.Comparer(a.compare)) +} + +func areNonZeroTimes(x, y time.Time) bool { + return !x.IsZero() && !y.IsZero() +} + +type timeApproximator struct { + margin time.Duration +} + +func (a timeApproximator) compare(x, y time.Time) bool { + // Avoid subtracting times to avoid overflow when the + // difference is larger than the largest representable duration. + if x.After(y) { + // Ensure x is always before y + x, y = y, x + } + // We're within the margin if x+margin >= y. + // Note: time.Time doesn't have AfterOrEqual method hence the negation. + return !x.Add(a.margin).Before(y) +} + +// AnyError is an error that matches any non-nil error. +var AnyError anyError + +type anyError struct{} + +func (anyError) Error() string { return "any error" } +func (anyError) Is(err error) bool { return err != nil } + +// EquateErrors returns a Comparer option that determines errors to be equal +// if errors.Is reports them to match. The AnyError error can be used to +// match any non-nil error. +func EquateErrors() cmp.Option { + return cmp.FilterValues(areConcreteErrors, cmp.Comparer(compareErrors)) +} + +// areConcreteErrors reports whether x and y are types that implement error. +// The input types are deliberately of the interface{} type rather than the +// error type so that we can handle situations where the current type is an +// interface{}, but the underlying concrete types both happen to implement +// the error interface. +func areConcreteErrors(x, y interface{}) bool { + _, ok1 := x.(error) + _, ok2 := y.(error) + return ok1 && ok2 +} + +func compareErrors(x, y interface{}) bool { + xe := x.(error) + ye := y.(error) + return errors.Is(xe, ye) || errors.Is(ye, xe) +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go new file mode 100644 index 000000000..80c60617e --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go @@ -0,0 +1,206 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts + +import ( + "fmt" + "reflect" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/function" +) + +// IgnoreFields returns an Option that ignores fields of the +// given names on a single struct type. It respects the names of exported fields +// that are forwarded due to struct embedding. +// The struct type is specified by passing in a value of that type. +// +// The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a +// specific sub-field that is embedded or nested within the parent struct. +func IgnoreFields(typ interface{}, names ...string) cmp.Option { + sf := newStructFilter(typ, names...) + return cmp.FilterPath(sf.filter, cmp.Ignore()) +} + +// IgnoreTypes returns an Option that ignores all values assignable to +// certain types, which are specified by passing in a value of each type. +func IgnoreTypes(typs ...interface{}) cmp.Option { + tf := newTypeFilter(typs...) + return cmp.FilterPath(tf.filter, cmp.Ignore()) +} + +type typeFilter []reflect.Type + +func newTypeFilter(typs ...interface{}) (tf typeFilter) { + for _, typ := range typs { + t := reflect.TypeOf(typ) + if t == nil { + // This occurs if someone tries to pass in sync.Locker(nil) + panic("cannot determine type; consider using IgnoreInterfaces") + } + tf = append(tf, t) + } + return tf +} +func (tf typeFilter) filter(p cmp.Path) bool { + if len(p) < 1 { + return false + } + t := p.Last().Type() + for _, ti := range tf { + if t.AssignableTo(ti) { + return true + } + } + return false +} + +// IgnoreInterfaces returns an Option that ignores all values or references of +// values assignable to certain interface types. These interfaces are specified +// by passing in an anonymous struct with the interface types embedded in it. +// For example, to ignore sync.Locker, pass in struct{sync.Locker}{}. +func IgnoreInterfaces(ifaces interface{}) cmp.Option { + tf := newIfaceFilter(ifaces) + return cmp.FilterPath(tf.filter, cmp.Ignore()) +} + +type ifaceFilter []reflect.Type + +func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) { + t := reflect.TypeOf(ifaces) + if ifaces == nil || t.Name() != "" || t.Kind() != reflect.Struct { + panic("input must be an anonymous struct") + } + for i := 0; i < t.NumField(); i++ { + fi := t.Field(i) + switch { + case !fi.Anonymous: + panic("struct cannot have named fields") + case fi.Type.Kind() != reflect.Interface: + panic("embedded field must be an interface type") + case fi.Type.NumMethod() == 0: + // This matches everything; why would you ever want this? + panic("cannot ignore empty interface") + default: + tf = append(tf, fi.Type) + } + } + return tf +} +func (tf ifaceFilter) filter(p cmp.Path) bool { + if len(p) < 1 { + return false + } + t := p.Last().Type() + for _, ti := range tf { + if t.AssignableTo(ti) { + return true + } + if t.Kind() != reflect.Ptr && reflect.PtrTo(t).AssignableTo(ti) { + return true + } + } + return false +} + +// IgnoreUnexported returns an Option that only ignores the immediate unexported +// fields of a struct, including anonymous fields of unexported types. +// In particular, unexported fields within the struct's exported fields +// of struct types, including anonymous fields, will not be ignored unless the +// type of the field itself is also passed to IgnoreUnexported. +// +// Avoid ignoring unexported fields of a type which you do not control (i.e. a +// type from another repository), as changes to the implementation of such types +// may change how the comparison behaves. Prefer a custom Comparer instead. +func IgnoreUnexported(typs ...interface{}) cmp.Option { + ux := newUnexportedFilter(typs...) + return cmp.FilterPath(ux.filter, cmp.Ignore()) +} + +type unexportedFilter struct{ m map[reflect.Type]bool } + +func newUnexportedFilter(typs ...interface{}) unexportedFilter { + ux := unexportedFilter{m: make(map[reflect.Type]bool)} + for _, typ := range typs { + t := reflect.TypeOf(typ) + if t == nil || t.Kind() != reflect.Struct { + panic(fmt.Sprintf("%T must be a non-pointer struct", typ)) + } + ux.m[t] = true + } + return ux +} +func (xf unexportedFilter) filter(p cmp.Path) bool { + sf, ok := p.Index(-1).(cmp.StructField) + if !ok { + return false + } + return xf.m[p.Index(-2).Type()] && !isExported(sf.Name()) +} + +// isExported reports whether the identifier is exported. +func isExported(id string) bool { + r, _ := utf8.DecodeRuneInString(id) + return unicode.IsUpper(r) +} + +// IgnoreSliceElements returns an Option that ignores elements of []V. +// The discard function must be of the form "func(T) bool" which is used to +// ignore slice elements of type V, where V is assignable to T. +// Elements are ignored if the function reports true. +func IgnoreSliceElements(discardFunc interface{}) cmp.Option { + vf := reflect.ValueOf(discardFunc) + if !function.IsType(vf.Type(), function.ValuePredicate) || vf.IsNil() { + panic(fmt.Sprintf("invalid discard function: %T", discardFunc)) + } + return cmp.FilterPath(func(p cmp.Path) bool { + si, ok := p.Index(-1).(cmp.SliceIndex) + if !ok { + return false + } + if !si.Type().AssignableTo(vf.Type().In(0)) { + return false + } + vx, vy := si.Values() + if vx.IsValid() && vf.Call([]reflect.Value{vx})[0].Bool() { + return true + } + if vy.IsValid() && vf.Call([]reflect.Value{vy})[0].Bool() { + return true + } + return false + }, cmp.Ignore()) +} + +// IgnoreMapEntries returns an Option that ignores entries of map[K]V. +// The discard function must be of the form "func(T, R) bool" which is used to +// ignore map entries of type K and V, where K and V are assignable to T and R. +// Entries are ignored if the function reports true. +func IgnoreMapEntries(discardFunc interface{}) cmp.Option { + vf := reflect.ValueOf(discardFunc) + if !function.IsType(vf.Type(), function.KeyValuePredicate) || vf.IsNil() { + panic(fmt.Sprintf("invalid discard function: %T", discardFunc)) + } + return cmp.FilterPath(func(p cmp.Path) bool { + mi, ok := p.Index(-1).(cmp.MapIndex) + if !ok { + return false + } + if !mi.Key().Type().AssignableTo(vf.Type().In(0)) || !mi.Type().AssignableTo(vf.Type().In(1)) { + return false + } + k := mi.Key() + vx, vy := mi.Values() + if vx.IsValid() && vf.Call([]reflect.Value{k, vx})[0].Bool() { + return true + } + if vy.IsValid() && vf.Call([]reflect.Value{k, vy})[0].Bool() { + return true + } + return false + }, cmp.Ignore()) +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go new file mode 100644 index 000000000..0eb2a758c --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go @@ -0,0 +1,147 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts + +import ( + "fmt" + "reflect" + "sort" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/function" +) + +// SortSlices returns a Transformer option that sorts all []V. +// The less function must be of the form "func(T, T) bool" which is used to +// sort any slice with element type V that is assignable to T. +// +// The less function must be: +// - Deterministic: less(x, y) == less(x, y) +// - Irreflexive: !less(x, x) +// - Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// +// The less function does not have to be "total". That is, if !less(x, y) and +// !less(y, x) for two elements x and y, their relative order is maintained. +// +// SortSlices can be used in conjunction with EquateEmpty. +func SortSlices(lessFunc interface{}) cmp.Option { + vf := reflect.ValueOf(lessFunc) + if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { + panic(fmt.Sprintf("invalid less function: %T", lessFunc)) + } + ss := sliceSorter{vf.Type().In(0), vf} + return cmp.FilterValues(ss.filter, cmp.Transformer("cmpopts.SortSlices", ss.sort)) +} + +type sliceSorter struct { + in reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (ss sliceSorter) filter(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + if !(x != nil && y != nil && vx.Type() == vy.Type()) || + !(vx.Kind() == reflect.Slice && vx.Type().Elem().AssignableTo(ss.in)) || + (vx.Len() <= 1 && vy.Len() <= 1) { + return false + } + // Check whether the slices are already sorted to avoid an infinite + // recursion cycle applying the same transform to itself. + ok1 := sort.SliceIsSorted(x, func(i, j int) bool { return ss.less(vx, i, j) }) + ok2 := sort.SliceIsSorted(y, func(i, j int) bool { return ss.less(vy, i, j) }) + return !ok1 || !ok2 +} +func (ss sliceSorter) sort(x interface{}) interface{} { + src := reflect.ValueOf(x) + dst := reflect.MakeSlice(src.Type(), src.Len(), src.Len()) + for i := 0; i < src.Len(); i++ { + dst.Index(i).Set(src.Index(i)) + } + sort.SliceStable(dst.Interface(), func(i, j int) bool { return ss.less(dst, i, j) }) + ss.checkSort(dst) + return dst.Interface() +} +func (ss sliceSorter) checkSort(v reflect.Value) { + start := -1 // Start of a sequence of equal elements. + for i := 1; i < v.Len(); i++ { + if ss.less(v, i-1, i) { + // Check that first and last elements in v[start:i] are equal. + if start >= 0 && (ss.less(v, start, i-1) || ss.less(v, i-1, start)) { + panic(fmt.Sprintf("incomparable values detected: want equal elements: %v", v.Slice(start, i))) + } + start = -1 + } else if start == -1 { + start = i + } + } +} +func (ss sliceSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i), v.Index(j) + return ss.fnc.Call([]reflect.Value{vx, vy})[0].Bool() +} + +// SortMaps returns a Transformer option that flattens map[K]V types to be a +// sorted []struct{K, V}. The less function must be of the form +// "func(T, T) bool" which is used to sort any map with key K that is +// assignable to T. +// +// Flattening the map into a slice has the property that cmp.Equal is able to +// use Comparers on K or the K.Equal method if it exists. +// +// The less function must be: +// - Deterministic: less(x, y) == less(x, y) +// - Irreflexive: !less(x, x) +// - Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// - Total: if x != y, then either less(x, y) or less(y, x) +// +// SortMaps can be used in conjunction with EquateEmpty. +func SortMaps(lessFunc interface{}) cmp.Option { + vf := reflect.ValueOf(lessFunc) + if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { + panic(fmt.Sprintf("invalid less function: %T", lessFunc)) + } + ms := mapSorter{vf.Type().In(0), vf} + return cmp.FilterValues(ms.filter, cmp.Transformer("cmpopts.SortMaps", ms.sort)) +} + +type mapSorter struct { + in reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (ms mapSorter) filter(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Map && vx.Type().Key().AssignableTo(ms.in)) && + (vx.Len() != 0 || vy.Len() != 0) +} +func (ms mapSorter) sort(x interface{}) interface{} { + src := reflect.ValueOf(x) + outType := reflect.StructOf([]reflect.StructField{ + {Name: "K", Type: src.Type().Key()}, + {Name: "V", Type: src.Type().Elem()}, + }) + dst := reflect.MakeSlice(reflect.SliceOf(outType), src.Len(), src.Len()) + for i, k := range src.MapKeys() { + v := reflect.New(outType).Elem() + v.Field(0).Set(k) + v.Field(1).Set(src.MapIndex(k)) + dst.Index(i).Set(v) + } + sort.Slice(dst.Interface(), func(i, j int) bool { return ms.less(dst, i, j) }) + ms.checkSort(dst) + return dst.Interface() +} +func (ms mapSorter) checkSort(v reflect.Value) { + for i := 1; i < v.Len(); i++ { + if !ms.less(v, i-1, i) { + panic(fmt.Sprintf("partial order detected: want %v < %v", v.Index(i-1), v.Index(i))) + } + } +} +func (ms mapSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i).Field(0), v.Index(j).Field(0) + return ms.fnc.Call([]reflect.Value{vx, vy})[0].Bool() +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go new file mode 100644 index 000000000..ca11a4024 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go @@ -0,0 +1,189 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp" +) + +// filterField returns a new Option where opt is only evaluated on paths that +// include a specific exported field on a single struct type. +// The struct type is specified by passing in a value of that type. +// +// The name may be a dot-delimited string (e.g., "Foo.Bar") to select a +// specific sub-field that is embedded or nested within the parent struct. +func filterField(typ interface{}, name string, opt cmp.Option) cmp.Option { + // TODO: This is currently unexported over concerns of how helper filters + // can be composed together easily. + // TODO: Add tests for FilterField. + + sf := newStructFilter(typ, name) + return cmp.FilterPath(sf.filter, opt) +} + +type structFilter struct { + t reflect.Type // The root struct type to match on + ft fieldTree // Tree of fields to match on +} + +func newStructFilter(typ interface{}, names ...string) structFilter { + // TODO: Perhaps allow * as a special identifier to allow ignoring any + // number of path steps until the next field match? + // This could be useful when a concrete struct gets transformed into + // an anonymous struct where it is not possible to specify that by type, + // but the transformer happens to provide guarantees about the names of + // the transformed fields. + + t := reflect.TypeOf(typ) + if t == nil || t.Kind() != reflect.Struct { + panic(fmt.Sprintf("%T must be a non-pointer struct", typ)) + } + var ft fieldTree + for _, name := range names { + cname, err := canonicalName(t, name) + if err != nil { + panic(fmt.Sprintf("%s: %v", strings.Join(cname, "."), err)) + } + ft.insert(cname) + } + return structFilter{t, ft} +} + +func (sf structFilter) filter(p cmp.Path) bool { + for i, ps := range p { + if ps.Type().AssignableTo(sf.t) && sf.ft.matchPrefix(p[i+1:]) { + return true + } + } + return false +} + +// fieldTree represents a set of dot-separated identifiers. +// +// For example, inserting the following selectors: +// +// Foo +// Foo.Bar.Baz +// Foo.Buzz +// Nuka.Cola.Quantum +// +// Results in a tree of the form: +// +// {sub: { +// "Foo": {ok: true, sub: { +// "Bar": {sub: { +// "Baz": {ok: true}, +// }}, +// "Buzz": {ok: true}, +// }}, +// "Nuka": {sub: { +// "Cola": {sub: { +// "Quantum": {ok: true}, +// }}, +// }}, +// }} +type fieldTree struct { + ok bool // Whether this is a specified node + sub map[string]fieldTree // The sub-tree of fields under this node +} + +// insert inserts a sequence of field accesses into the tree. +func (ft *fieldTree) insert(cname []string) { + if ft.sub == nil { + ft.sub = make(map[string]fieldTree) + } + if len(cname) == 0 { + ft.ok = true + return + } + sub := ft.sub[cname[0]] + sub.insert(cname[1:]) + ft.sub[cname[0]] = sub +} + +// matchPrefix reports whether any selector in the fieldTree matches +// the start of path p. +func (ft fieldTree) matchPrefix(p cmp.Path) bool { + for _, ps := range p { + switch ps := ps.(type) { + case cmp.StructField: + ft = ft.sub[ps.Name()] + if ft.ok { + return true + } + if len(ft.sub) == 0 { + return false + } + case cmp.Indirect: + default: + return false + } + } + return false +} + +// canonicalName returns a list of identifiers where any struct field access +// through an embedded field is expanded to include the names of the embedded +// types themselves. +// +// For example, suppose field "Foo" is not directly in the parent struct, +// but actually from an embedded struct of type "Bar". Then, the canonical name +// of "Foo" is actually "Bar.Foo". +// +// Suppose field "Foo" is not directly in the parent struct, but actually +// a field in two different embedded structs of types "Bar" and "Baz". +// Then the selector "Foo" causes a panic since it is ambiguous which one it +// refers to. The user must specify either "Bar.Foo" or "Baz.Foo". +func canonicalName(t reflect.Type, sel string) ([]string, error) { + var name string + sel = strings.TrimPrefix(sel, ".") + if sel == "" { + return nil, fmt.Errorf("name must not be empty") + } + if i := strings.IndexByte(sel, '.'); i < 0 { + name, sel = sel, "" + } else { + name, sel = sel[:i], sel[i:] + } + + // Type must be a struct or pointer to struct. + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return nil, fmt.Errorf("%v must be a struct", t) + } + + // Find the canonical name for this current field name. + // If the field exists in an embedded struct, then it will be expanded. + sf, _ := t.FieldByName(name) + if !isExported(name) { + // Avoid using reflect.Type.FieldByName for unexported fields due to + // buggy behavior with regard to embeddeding and unexported fields. + // See https://golang.org/issue/4876 for details. + sf = reflect.StructField{} + for i := 0; i < t.NumField() && sf.Name == ""; i++ { + if t.Field(i).Name == name { + sf = t.Field(i) + } + } + } + if sf.Name == "" { + return []string{name}, fmt.Errorf("does not exist") + } + var ss []string + for i := range sf.Index { + ss = append(ss, t.FieldByIndex(sf.Index[:i+1]).Name) + } + if sel == "" { + return ss, nil + } + ssPost, err := canonicalName(sf.Type, sel) + return append(ss, ssPost...), err +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go new file mode 100644 index 000000000..8812443a2 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go @@ -0,0 +1,36 @@ +// Copyright 2018, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts + +import ( + "github.com/google/go-cmp/cmp" +) + +type xformFilter struct{ xform cmp.Option } + +func (xf xformFilter) filter(p cmp.Path) bool { + for _, ps := range p { + if t, ok := ps.(cmp.Transform); ok && t.Option() == xf.xform { + return false + } + } + return true +} + +// AcyclicTransformer returns a Transformer with a filter applied that ensures +// that the transformer cannot be recursively applied upon its own output. +// +// An example use case is a transformer that splits a string by lines: +// +// AcyclicTransformer("SplitLines", func(s string) []string{ +// return strings.Split(s, "\n") +// }) +// +// Had this been an unfiltered Transformer instead, this would result in an +// infinite cycle converting a string to []string to [][]string and so on. +func AcyclicTransformer(name string, xformFunc interface{}) cmp.Option { + xf := xformFilter{cmp.Transformer(name, xformFunc)} + return cmp.FilterPath(xf.filter, xf.xform) +} diff --git a/vendor/k8s.io/kubectl/pkg/cmd/util/podcmd/podcmd.go b/vendor/k8s.io/kubectl/pkg/cmd/util/podcmd/podcmd.go new file mode 100644 index 000000000..bf760645d --- /dev/null +++ b/vendor/k8s.io/kubectl/pkg/cmd/util/podcmd/podcmd.go @@ -0,0 +1,104 @@ +/* +Copyright 2021 The Kubernetes 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 podcmd + +import ( + "fmt" + "io" + "strings" + + v1 "k8s.io/api/core/v1" + "k8s.io/klog/v2" +) + +// DefaultContainerAnnotationName is an annotation name that can be used to preselect the interesting container +// from a pod when running kubectl. +const DefaultContainerAnnotationName = "kubectl.kubernetes.io/default-container" + +// FindContainerByName selects the named container from the spec of +// the provided pod or return nil if no such container exists. +func FindContainerByName(pod *v1.Pod, name string) (*v1.Container, string) { + for i := range pod.Spec.Containers { + if pod.Spec.Containers[i].Name == name { + return &pod.Spec.Containers[i], fmt.Sprintf("spec.containers{%s}", name) + } + } + for i := range pod.Spec.InitContainers { + if pod.Spec.InitContainers[i].Name == name { + return &pod.Spec.InitContainers[i], fmt.Sprintf("spec.initContainers{%s}", name) + } + } + for i := range pod.Spec.EphemeralContainers { + if pod.Spec.EphemeralContainers[i].Name == name { + return (*v1.Container)(&pod.Spec.EphemeralContainers[i].EphemeralContainerCommon), fmt.Sprintf("spec.ephemeralContainers{%s}", name) + } + } + return nil, "" +} + +// FindOrDefaultContainerByName defaults a container for a pod to the first container if any +// exists, or returns an error. It will print a message to the user indicating a default was +// selected if there was more than one container. +func FindOrDefaultContainerByName(pod *v1.Pod, name string, quiet bool, warn io.Writer) (*v1.Container, error) { + var container *v1.Container + + if len(name) > 0 { + container, _ = FindContainerByName(pod, name) + if container == nil { + return nil, fmt.Errorf("container %s not found in pod %s", name, pod.Name) + } + return container, nil + } + + // this should never happen, but just in case + if len(pod.Spec.Containers) == 0 { + return nil, fmt.Errorf("pod %s/%s does not have any containers", pod.Namespace, pod.Name) + } + + // read the default container the annotation as per + // https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/2227-kubectl-default-container + if name := pod.Annotations[DefaultContainerAnnotationName]; len(name) > 0 { + if container, _ = FindContainerByName(pod, name); container != nil { + klog.V(4).Infof("Defaulting container name from annotation %s", container.Name) + return container, nil + } + klog.V(4).Infof("Default container name from annotation %s was not found in the pod", name) + } + + // pick the first container as per existing behavior + container = &pod.Spec.Containers[0] + if !quiet && (len(pod.Spec.Containers) > 1 || len(pod.Spec.InitContainers) > 0 || len(pod.Spec.EphemeralContainers) > 0) { + fmt.Fprintf(warn, "Defaulted container %q out of: %s\n", container.Name, AllContainerNames(pod)) + } + + klog.V(4).Infof("Defaulting container name to %s", container.Name) + return &pod.Spec.Containers[0], nil +} + +func AllContainerNames(pod *v1.Pod) string { + var containers []string + for _, container := range pod.Spec.Containers { + containers = append(containers, container.Name) + } + for _, container := range pod.Spec.EphemeralContainers { + containers = append(containers, fmt.Sprintf("%s (ephem)", container.Name)) + } + for _, container := range pod.Spec.InitContainers { + containers = append(containers, fmt.Sprintf("%s (init)", container.Name)) + } + return strings.Join(containers, ", ") +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 98dfa07b8..19595537a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,6 +4,9 @@ github.com/a8m/envsubst/parse # github.com/beorn7/perks v1.0.1 ## explicit; go 1.11 github.com/beorn7/perks/quantile +# github.com/budougumi0617/cmpmock v0.0.4 +## explicit; go 1.16 +github.com/budougumi0617/cmpmock # github.com/cespare/xxhash/v2 v2.2.0 ## explicit; go 1.11 github.com/cespare/xxhash/v2 @@ -67,6 +70,9 @@ github.com/gogo/protobuf/sortkeys # github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da ## explicit github.com/golang/groupcache/lru +# github.com/golang/mock v1.5.0 +## explicit; go 1.11 +github.com/golang/mock/gomock # github.com/golang/protobuf v1.5.3 ## explicit; go 1.9 github.com/golang/protobuf/proto @@ -84,6 +90,7 @@ github.com/google/gnostic/openapiv3 # github.com/google/go-cmp v0.5.9 ## explicit; go 1.13 github.com/google/go-cmp/cmp +github.com/google/go-cmp/cmp/cmpopts github.com/google/go-cmp/cmp/internal/diff github.com/google/go-cmp/cmp/internal/flags github.com/google/go-cmp/cmp/internal/function @@ -712,6 +719,7 @@ k8s.io/kube-openapi/pkg/util/proto k8s.io/kube-openapi/pkg/validation/spec # k8s.io/kubectl v0.27.4 ## explicit; go 1.20 +k8s.io/kubectl/pkg/cmd/util/podcmd k8s.io/kubectl/pkg/util/podutils # k8s.io/utils v0.0.0-20230505201702-9f6742963106 ## explicit; go 1.18