diff --git a/README.md b/README.md index fe9e33b3..740b4b2c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Learn about the capabilities of this extension in our [Reliability Hub](https:// |------------------------------------------------------------------|---------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------------------------------------------------------------------| | `STEADYBIT_EXTENSION_KUBERNETES_CLUSTER_NAME` | `kubernetes.clusterName` | The name of the kubernetes cluster | yes | | | `STEADYBIT_EXTENSION_DISABLE_DISCOVERY_EXCLUDES` | `discovery.disableExcludes` | Ignore discovery excludes specified by `steadybit.com/discovery-disabled` | false | `false` | +| `STEADYBIT_EXTENSION_DISCOVERY_DISABLE_ARGO_ROLLOUTS` | `discovery.disableArgoRollouts` | Disable discovery of Argo rollouts | false | `true` | | `STEADYBIT_EXTENSION_LABEL_FILTER` | | These labels will be ignored and not added to the discovered targets | false | `controller-revision-hash,pod-template-generation,pod-template-hash` | | `STEADYBIT_EXTENSION_ACTIVE_ADVICE_LIST` | | List of active advice definitions, default is all (*). You can define a list of active adviceDefinitionId. See UI -> Settings -> Extension -> Advice -> Column: ID | false | `*` | | `STEADYBIT_EXTENSION_ADVICE_SINGLE_REPLICA_MIN_REPLICAS` | | Minimal required replicas for the "Redundant Pod" advice | false | 2 | diff --git a/charts/steadybit-extension-kubernetes/templates/_permissions.tpl b/charts/steadybit-extension-kubernetes/templates/_permissions.tpl index 4361c2c2..fe8fdced 100644 --- a/charts/steadybit-extension-kubernetes/templates/_permissions.tpl +++ b/charts/steadybit-extension-kubernetes/templates/_permissions.tpl @@ -93,5 +93,14 @@ permissions for clusterrole or role verbs: - create {{- end }} + {{- if not .Values.discovery.disableArgoRollouts }} + {{/* Required for Argo Discovery */}} + - apiGroups: ["argoproj.io"] + resources: + - rollouts + verbs: + - get + - list + - watch + {{- end }} {{- end -}} - diff --git a/charts/steadybit-extension-kubernetes/templates/deployment.yaml b/charts/steadybit-extension-kubernetes/templates/deployment.yaml index faafbfaa..7acbe62c 100644 --- a/charts/steadybit-extension-kubernetes/templates/deployment.yaml +++ b/charts/steadybit-extension-kubernetes/templates/deployment.yaml @@ -143,6 +143,10 @@ spec: {{- end }} - name: STEADYBIT_EXTENSION_DISCOVERY_MAX_POD_COUNT value: "{{ .Values.discovery.maxPodCount }}" + {{- if .Values.discovery.disableArgoRollouts }} + - name: STEADYBIT_EXTENSION_DISCOVERY_DISABLE_ARGO_ROLLOUTS + value: "true" + {{- end }} - name: STEADYBIT_EXTENSION_DISCOVERY_REFRESH_THROTTLE value: "{{ .Values.discovery.refreshThrottle }}" {{- with .Values.extraEnvFrom }} diff --git a/charts/steadybit-extension-kubernetes/values.yaml b/charts/steadybit-extension-kubernetes/values.yaml index e65101d4..4ac67bd8 100644 --- a/charts/steadybit-extension-kubernetes/values.yaml +++ b/charts/steadybit-extension-kubernetes/values.yaml @@ -169,6 +169,8 @@ discovery: disableAdvice: false # discovery.maxPodCount -- Skip listing pods, containers and hosts for deployments, statefulsets, etc. if there are more then the given pods. maxPodCount: 50 + # discovery.disableArgoRollouts -- Disable discovery of Argo rollouts (requires argoproj.io rollout CRDs) + disableArgoRollouts: true # discovery.refreshThrottle -- Number of seconds between successive refreshes of the target data. refreshThrottle: 20 attributes: diff --git a/client/client.go b/client/client.go index a9c913f8..cd389f75 100644 --- a/client/client.go +++ b/client/client.go @@ -9,7 +9,14 @@ import ( "errors" "flag" "fmt" + "path/filepath" + "sort" + "strings" + "sync" + "time" + "github.com/google/uuid" + "github.com/rs/zerolog/log" "github.com/steadybit/extension-kubernetes/v2/extconfig" "golang.org/x/exp/slices" @@ -19,8 +26,12 @@ import ( networkingv1 "k8s.io/api/networking/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" networkingv1client "k8s.io/client-go/kubernetes/typed/networking/v1" @@ -33,11 +44,6 @@ import ( "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/remotecommand" "k8s.io/client-go/util/homedir" - "path/filepath" - "sort" - "strings" - "sync" - "time" ) var K8S *Client @@ -46,6 +52,11 @@ type Client struct { Distribution string permissions *PermissionCheckResult + argoRollout struct { + lister RolloutLister + informer cache.SharedIndexInformer + } + daemonSet struct { lister listerAppsv1.DaemonSetLister informer cache.SharedIndexInformer @@ -114,6 +125,73 @@ type Client struct { clientset kubernetes.Interface } +type RolloutLister interface { + List(selector labels.Selector) ([]*unstructured.Unstructured, error) + Rollouts(namespace string) RolloutNamespaceLister +} + +type RolloutNamespaceLister interface { + List(selector labels.Selector) ([]*unstructured.Unstructured, error) + Get(name string) (*unstructured.Unstructured, error) +} + +type rolloutLister struct { + indexer cache.Indexer +} + +func (l *rolloutLister) List(selector labels.Selector) ([]*unstructured.Unstructured, error) { + items := l.indexer.List() + result := make([]*unstructured.Unstructured, 0, len(items)) + for _, item := range items { + if obj, ok := item.(*unstructured.Unstructured); ok { + result = append(result, obj) + } + } + return result, nil +} + +func (l *rolloutLister) Rollouts(namespace string) RolloutNamespaceLister { + return &rolloutNamespaceLister{ + indexer: l.indexer, + namespace: namespace, + } +} + +type rolloutNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +func (l *rolloutNamespaceLister) List(selector labels.Selector) ([]*unstructured.Unstructured, error) { + items, err := l.indexer.ByIndex(cache.NamespaceIndex, l.namespace) + if err != nil { + return nil, err + } + result := make([]*unstructured.Unstructured, 0, len(items)) + for _, item := range items { + if obj, ok := item.(*unstructured.Unstructured); ok { + result = append(result, obj) + } + } + return result, nil +} + +func (l *rolloutNamespaceLister) Get(name string) (*unstructured.Unstructured, error) { + obj, exists, err := l.indexer.GetByKey(l.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, k8sErrors.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "rollouts"}, name) + } + + if rolloutObj, ok := obj.(*unstructured.Unstructured); ok { + return rolloutObj, nil + } else { + return nil, fmt.Errorf("expected *unstructured.Unstructured, got %T", obj) + } +} + func (c *Client) Permissions() *PermissionCheckResult { return c.permissions } @@ -175,12 +253,12 @@ func (c *Client) PodsByLabelSelector(labelSelector *metav1.LabelSelector, namesp // ExecInPod executes a command in a pod container and returns the output func (c *Client) ExecInPod(ctx context.Context, namespace, podName, containerName string, command []string) (string, error) { config := c.GetConfig() - + // Check if we have a valid REST config if config.Host == "" { return "", fmt.Errorf("no valid Kubernetes REST config available - cannot execute pod commands") } - + req := c.clientset.CoreV1().RESTClient().Post(). Resource("pods"). Name(podName). @@ -190,7 +268,7 @@ func (c *Client) ExecInPod(ctx context.Context, namespace, podName, containerNam Param("command", command[0]). Param("stdout", "true"). Param("stderr", "true") - + // Add additional command arguments for _, arg := range command[1:] { req.Param("command", arg) @@ -206,11 +284,11 @@ func (c *Client) ExecInPod(ctx context.Context, namespace, podName, containerNam Stdout: &stdout, Stderr: &stderr, }) - + if err != nil { return "", fmt.Errorf("error executing command: %w, stderr: %s", err, stderr.String()) } - + return stdout.String(), nil } @@ -259,6 +337,25 @@ func (c *Client) DeploymentByNamespaceAndName(namespace string, name string) *ap return item } +func (c *Client) ArgoRollouts() []*unstructured.Unstructured { + if extconfig.HasNamespaceFilter() { + log.Info().Msgf("Fetching Argo Rollouts for namespace %s", extconfig.Config.Namespace) + rollouts, err := c.argoRollout.lister.Rollouts(extconfig.Config.Namespace).List(labels.Everything()) + if err != nil { + log.Error().Err(err).Msgf("Error while fetching Argo Rollouts") + return []*unstructured.Unstructured{} + } + return rollouts + } else { + rollouts, err := c.argoRollout.lister.List(labels.Everything()) + if err != nil { + log.Error().Err(err).Msgf("Error while fetching Argo Rollouts") + return []*unstructured.Unstructured{} + } + return rollouts + } +} + func (c *Client) ServicesByPod(pod *corev1.Pod) []*corev1.Service { services, err := c.service.lister.Services(pod.Namespace).List(labels.Everything()) if err != nil { @@ -542,13 +639,23 @@ func filterEvents(events []interface{}, since time.Time) []corev1.Event { } func PrepareClient(stopCh <-chan struct{}) { - clientset, rootApiPath := createClientset() + clientset, rootApiPath, config := createClientset() permissions := checkPermissions(clientset) - K8S = CreateClient(clientset, stopCh, rootApiPath, permissions) + + var dynamicClient dynamic.Interface + if !extconfig.Config.DiscoveryDisabledArgoRollout { + var err error + dynamicClient, err = dynamic.NewForConfig(config) + if err != nil { + log.Fatal().Err(err).Msg("Failed to create dynamic client") + } + } + + K8S = CreateClient(clientset, stopCh, rootApiPath, permissions, config, dynamicClient) } // CreateClient is visible for testing -func CreateClient(clientset kubernetes.Interface, stopCh <-chan struct{}, rootApiPath string, permissions *PermissionCheckResult) *Client { +func CreateClient(clientset kubernetes.Interface, stopCh <-chan struct{}, rootApiPath string, permissions *PermissionCheckResult, config *rest.Config, dynamicClient dynamic.Interface) *Client { client := &Client{ Distribution: "kubernetes", permissions: permissions, @@ -565,6 +672,7 @@ func CreateClient(clientset kubernetes.Interface, stopCh <-chan struct{}, rootAp } else { factory = informers.NewSharedInformerFactory(clientset, informerResyncDuration) } + client.resourceEventHandler = cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { client.doNotify(obj) @@ -578,6 +686,30 @@ func CreateClient(clientset kubernetes.Interface, stopCh <-chan struct{}, rootAp } var informerSyncList []cache.InformerSynced + var argoRolloutInformer informers.GenericInformer + + // Initialize Argo Rollouts informer if enabled + if !extconfig.Config.DiscoveryDisabledArgoRollout { + argoRolloutGVR := schema.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "rollouts", + } + argoRolloutInformer = dynamicinformer.NewFilteredDynamicInformer( + dynamicClient, + argoRolloutGVR, + extconfig.Config.Namespace, + 0, + cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, + nil, + ) + client.argoRollout.informer = argoRolloutInformer.Informer() + client.argoRollout.lister = &rolloutLister{indexer: argoRolloutInformer.Informer().GetIndexer()} + informerSyncList = append(informerSyncList, client.argoRollout.informer.HasSynced) + if _, err := client.argoRollout.informer.AddEventHandler(client.resourceEventHandler); err != nil { + log.Fatal().Msg("failed to add argo rollout event handler") + } + } daemonSets := factory.Apps().V1().DaemonSets() client.daemonSet.informer = daemonSets.Informer() @@ -722,6 +854,9 @@ func CreateClient(clientset kubernetes.Interface, stopCh <-chan struct{}, rootAp defer runtime.HandleCrash() go factory.Start(stopCh) + if argoRolloutInformer != nil { + go argoRolloutInformer.Informer().Run(stopCh) + } log.Info().Msgf("Start Kubernetes cache sync.") if !cache.WaitForCacheSync(stopCh, informerSyncList...) { @@ -972,7 +1107,7 @@ func isOpenShift(rootApiPath string) bool { return rootApiPath == "/oapi" || rootApiPath == "oapi" } -func createClientset() (*kubernetes.Clientset, string) { +func createClientset() (*kubernetes.Clientset, string, *rest.Config) { config, err := rest.InClusterConfig() if err == nil { log.Info().Msgf("Extension is running inside a cluster, config found") @@ -1007,7 +1142,7 @@ func createClientset() (*kubernetes.Clientset, string) { log.Info().Msgf("Cluster connected! Kubernetes Server Version %+v", info) - return clientset, config.APIPath + return clientset, config.APIPath, config } func IsExcludedFromDiscovery(objectMeta metav1.ObjectMeta) bool { diff --git a/client/permissions.go b/client/permissions.go index 417241f9..80453f5c 100644 --- a/client/permissions.go +++ b/client/permissions.go @@ -5,9 +5,10 @@ import ( "github.com/rs/zerolog/log" "github.com/steadybit/extension-kubernetes/v2/extconfig" - authorizationv1 "k8s.io/api/authorization/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + + authorizationv1 "k8s.io/api/authorization/v1" ) type PermissionCheckResult struct { @@ -62,6 +63,7 @@ var requiredPermissions = []requiredPermission{ {group: "", resource: "pods", subresource: "eviction", verbs: []string{"create"}, allowGracefulFailure: true}, {group: "", resource: "nodes", verbs: []string{"patch"}, allowGracefulFailure: true}, {group: "", resource: "pods", subresource: "exec", verbs: []string{"create"}, allowGracefulFailure: true}, + {group: "argoproj.io", resource: "rollouts", verbs: []string{"get", "list", "watch"}, allowGracefulFailure: true}, {group: "networking.k8s.io", resource: "ingresses", verbs: []string{"get", "list", "watch", "update", "patch"}, allowGracefulFailure: true}, {group: "networking.k8s.io", resource: "ingressclasses", verbs: []string{"get", "list", "watch"}, allowGracefulFailure: true}, } @@ -71,6 +73,13 @@ func checkPermissions(client *kubernetes.Clientset) *PermissionCheckResult { reviews := client.AuthorizationV1().SelfSubjectAccessReviews() errors := false + // Conditionally add Argo rollouts permissions + if !extconfig.Config.DiscoveryDisabledArgoRollout { + requiredPermissions = append(requiredPermissions, requiredPermission{ + group: "argoproj.io", resource: "rollouts", verbs: []string{"get", "list", "watch"}, allowGracefulFailure: true, + }) + } + for _, p := range requiredPermissions { for _, verb := range p.verbs { sar := authorizationv1.SelfSubjectAccessReview{ @@ -242,6 +251,14 @@ func (p *PermissionCheckResult) IsSetImageDeploymentPermitted() bool { func MockAllPermitted() *PermissionCheckResult { result := make(map[string]PermissionCheckOutcome) + + // Conditionally add Argo rollouts permissions + if !extconfig.Config.DiscoveryDisabledArgoRollout { + requiredPermissions = append(requiredPermissions, requiredPermission{ + group: "argoproj.io", resource: "rollouts", verbs: []string{"get", "list", "watch"}, allowGracefulFailure: true, + }) + } + for _, p := range requiredPermissions { for _, verb := range p.verbs { result[p.Key(verb)] = OK diff --git a/client/transformers_test.go b/client/transformers_test.go index 5606b731..93aae8ff 100644 --- a/client/transformers_test.go +++ b/client/transformers_test.go @@ -27,11 +27,11 @@ func TestTransformIngressClass(t *testing.T) { Name: "nginx-steadybit", Annotations: map[string]string{ "ingressclass.kubernetes.io/is-default-class": "true", - "operator-sdk/primary-resource": "nginx-system/nginx-controller", - "meta.helm.sh/release-namespace": "nginx-system", - "meta.helm.sh/release-name": "nginx-ingress", - "other.annotation/should-be-removed": "some-value", - "yet.another/annotation": "another-value", + "operator-sdk/primary-resource": "nginx-system/nginx-controller", + "meta.helm.sh/release-namespace": "nginx-system", + "meta.helm.sh/release-name": "nginx-ingress", + "other.annotation/should-be-removed": "some-value", + "yet.another/annotation": "another-value", }, ManagedFields: []metav1.ManagedFieldsEntry{ { @@ -48,9 +48,9 @@ func TestTransformIngressClass(t *testing.T) { Name: "nginx-steadybit", Annotations: map[string]string{ "ingressclass.kubernetes.io/is-default-class": "true", - "operator-sdk/primary-resource": "nginx-system/nginx-controller", - "meta.helm.sh/release-namespace": "nginx-system", - "meta.helm.sh/release-name": "nginx-ingress", + "operator-sdk/primary-resource": "nginx-system/nginx-controller", + "meta.helm.sh/release-namespace": "nginx-system", + "meta.helm.sh/release-name": "nginx-ingress", }, ManagedFields: nil, // Should be cleared }, @@ -67,8 +67,8 @@ func TestTransformIngressClass(t *testing.T) { Name: "nginx-basic", Annotations: map[string]string{ "ingressclass.kubernetes.io/is-default-class": "false", - "meta.helm.sh/release-namespace": "nginx-system", - "irrelevant.annotation/should-be-removed": "some-value", + "meta.helm.sh/release-namespace": "nginx-system", + "irrelevant.annotation/should-be-removed": "some-value", }, ManagedFields: []metav1.ManagedFieldsEntry{ { @@ -85,7 +85,7 @@ func TestTransformIngressClass(t *testing.T) { Name: "nginx-basic", Annotations: map[string]string{ "ingressclass.kubernetes.io/is-default-class": "false", - "meta.helm.sh/release-namespace": "nginx-system", + "meta.helm.sh/release-namespace": "nginx-system", }, ManagedFields: nil, }, @@ -101,11 +101,11 @@ func TestTransformIngressClass(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "nginx-empty-annotations", Annotations: map[string]string{ - "ingressclass.kubernetes.io/is-default-class": "", // Empty - should not be kept - "operator-sdk/primary-resource": "namespace/controller", // Non-empty - should be kept - "meta.helm.sh/release-namespace": "", // Empty - should not be kept - "meta.helm.sh/release-name": "", // Empty - should not be kept - "other.annotation/irrelevant": "value", // Irrelevant - should not be kept + "ingressclass.kubernetes.io/is-default-class": "", // Empty - should not be kept + "operator-sdk/primary-resource": "namespace/controller", // Non-empty - should be kept + "meta.helm.sh/release-namespace": "", // Empty - should not be kept + "meta.helm.sh/release-name": "", // Empty - should not be kept + "other.annotation/irrelevant": "value", // Irrelevant - should not be kept }, ManagedFields: []metav1.ManagedFieldsEntry{ { @@ -137,8 +137,8 @@ func TestTransformIngressClass(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "nginx-no-relevant-annotations", Annotations: map[string]string{ - "some.other.annotation/value": "test", - "another.irrelevant/annotation": "value", + "some.other.annotation/value": "test", + "another.irrelevant/annotation": "value", }, ManagedFields: []metav1.ManagedFieldsEntry{ { @@ -211,7 +211,7 @@ func TestTransformIngressClass(t *testing.T) { wantErr: false, }, { - name: "passes through string objects unchanged", + name: "passes through string objects unchanged", input: "not-an-ingress-class", expected: "not-an-ingress-class", wantErr: false, @@ -262,7 +262,7 @@ func TestTransformIngressClass_SpecificBehaviors(t *testing.T) { ic, ok := result.(*networkingv1.IngressClass) require.True(t, ok) - + // Spec should be preserved exactly assert.Equal(t, input.Spec, ic.Spec) }) @@ -283,7 +283,7 @@ func TestTransformIngressClass_SpecificBehaviors(t *testing.T) { ic, ok := result.(*networkingv1.IngressClass) require.True(t, ok) - + assert.Nil(t, ic.ObjectMeta.ManagedFields) }) @@ -293,9 +293,9 @@ func TestTransformIngressClass_SpecificBehaviors(t *testing.T) { Name: "test", Annotations: map[string]string{ "ingressclass.kubernetes.io/is-default-class": "true", - "operator-sdk/primary-resource": "ns/deploy", - "meta.helm.sh/release-namespace": "helm-ns", - "meta.helm.sh/release-name": "helm-release", + "operator-sdk/primary-resource": "ns/deploy", + "meta.helm.sh/release-namespace": "helm-ns", + "meta.helm.sh/release-name": "helm-release", }, }, } @@ -305,14 +305,14 @@ func TestTransformIngressClass_SpecificBehaviors(t *testing.T) { ic, ok := result.(*networkingv1.IngressClass) require.True(t, ok) - + expectedAnnotations := map[string]string{ "ingressclass.kubernetes.io/is-default-class": "true", - "operator-sdk/primary-resource": "ns/deploy", - "meta.helm.sh/release-namespace": "helm-ns", - "meta.helm.sh/release-name": "helm-release", + "operator-sdk/primary-resource": "ns/deploy", + "meta.helm.sh/release-namespace": "helm-ns", + "meta.helm.sh/release-name": "helm-release", } - + assert.Equal(t, expectedAnnotations, ic.Annotations) }) } @@ -320,4 +320,4 @@ func TestTransformIngressClass_SpecificBehaviors(t *testing.T) { // Helper function for string pointer func stringPtr(s string) *string { return &s -} \ No newline at end of file +} diff --git a/extcommon/kubeclient_notifications.go b/extcommon/kubeclient_notifications.go index 1bf07007..2ee2c41f 100644 --- a/extcommon/kubeclient_notifications.go +++ b/extcommon/kubeclient_notifications.go @@ -1,12 +1,23 @@ package extcommon import ( - "github.com/rs/zerolog/log" - "github.com/steadybit/extension-kubernetes/v2/client" "reflect" "slices" + + "github.com/rs/zerolog/log" + "github.com/steadybit/extension-kubernetes/v2/client" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" ) +// ArgoRolloutGVK is the GroupVersionKind for Argo Rollouts +var ArgoRolloutGVK = schema.GroupVersionKind{ + Group: "argoproj.io", + Version: "v1alpha1", + Kind: "Rollout", +} + func TriggerOnKubernetesResourceChange(k8s *client.Client, t ...reflect.Type) chan struct{} { chRefresh := make(chan struct{}) chNotification := make(chan interface{}) @@ -29,8 +40,23 @@ func triggerNotificationsForType(in <-chan interface{}, out chan<- struct{}, typ eventType = eventType.Elem() } - forward := slices.Index(types, eventType) >= 0 - log.Trace().Type("type", event).Bool("forward", forward).Strs("types", s).Msg("resource event") + forward := false + if obj, ok := event.(*unstructured.Unstructured); ok { + // For unstructured resources, check if it's an Argo Rollout + gvk := obj.GetObjectKind().GroupVersionKind() + if gvk == ArgoRolloutGVK { + forward = true + } + } else { + forward = slices.Index(types, eventType) >= 0 + } + + log.Trace(). + Str("eventType", eventType.String()). + Bool("forward", forward). + Strs("types", s). + Msg("resource event") + if forward { out <- struct{}{} } diff --git a/extconfig/extconfig.go b/extconfig/extconfig.go index 9bc92e88..7a4fc95f 100644 --- a/extconfig/extconfig.go +++ b/extconfig/extconfig.go @@ -19,6 +19,7 @@ type Specification struct { AdviceSingleReplicaMinReplicas int `json:"adviceSingleReplicaMinReplicas" split_words:"true" required:"false" default:"2"` DisableDiscoveryExcludes bool `required:"false" split_words:"true" default:"false"` LogKubernetesHttpRequests bool `required:"false" split_words:"true" default:"false"` + DiscoveryDisabledArgoRollout bool `json:"discoveryDisabledArgoRollout" required:"false" split_words:"true" default:"true"` DiscoveryDisabledCluster bool `json:"discoveryDisabledCluster" required:"false" split_words:"true" default:"false"` DiscoveryDisabledContainer bool `json:"discoveryDisabledContainer" required:"false" split_words:"true" default:"false"` DiscoveryDisabledDaemonSet bool `json:"discoveryDisabledDaemonSet" required:"false" split_words:"true" default:"false"` diff --git a/extcontainer/container_discovery_test.go b/extcontainer/container_discovery_test.go index 43993a19..ae5b4521 100644 --- a/extcontainer/container_discovery_test.go +++ b/extcontainer/container_discovery_test.go @@ -5,8 +5,13 @@ package extcontainer import ( "context" + "sort" + "testing" + "time" + kclient "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" @@ -14,9 +19,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" - "sort" - "testing" - "time" + "k8s.io/client-go/rest" ) func Test_containerDiscovery(t *testing.T) { @@ -256,6 +259,7 @@ func Test_getDiscoveredContainerShouldNotIgnoreLabeledPodsIfExcludesDisabled(t * func getTestClient(stopCh <-chan struct{}) (*kclient.Client, kubernetes.Interface) { clientset := testclient.NewSimpleClientset() - client := kclient.CreateClient(clientset, stopCh, "/oapi", kclient.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + client := kclient.CreateClient(clientset, stopCh, "/oapi", kclient.MockAllPermitted(), &rest.Config{}, dynamicClient) return client, clientset } diff --git a/extdaemonset/daemonset_discovery_test.go b/extdaemonset/daemonset_discovery_test.go index ddac7934..d98923d6 100644 --- a/extdaemonset/daemonset_discovery_test.go +++ b/extdaemonset/daemonset_discovery_test.go @@ -6,9 +6,14 @@ package extdaemonset import ( "context" "fmt" + "sort" + "testing" + "time" + "github.com/steadybit/extension-kit/extutil" "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -18,9 +23,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" - "sort" - "testing" - "time" + "k8s.io/client-go/rest" ) func Test_daemonSetDiscovery(t *testing.T) { @@ -427,6 +430,7 @@ func testService(modifier func(service *v1.Service)) *v1.Service { func getTestClient(stopCh <-chan struct{}) (*client.Client, kubernetes.Interface) { clientset := testclient.NewSimpleClientset() - client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) return client, clientset } diff --git a/extdaemonset/daemonset_pod_count_check_test.go b/extdaemonset/daemonset_pod_count_check_test.go index 0206ceb3..3a726944 100644 --- a/extdaemonset/daemonset_pod_count_check_test.go +++ b/extdaemonset/daemonset_pod_count_check_test.go @@ -2,18 +2,21 @@ package extdaemonset import ( "context" + "testing" + "time" + "github.com/steadybit/action-kit/go/action_kit_api/v2" "github.com/steadybit/action-kit/go/action_kit_sdk" "github.com/steadybit/extension-kit/extutil" "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extcommon" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" testclient "k8s.io/client-go/kubernetes/fake" - "testing" - "time" + "k8s.io/client-go/rest" ) func TestPrepareCheckExtractsState(t *testing.T) { @@ -54,7 +57,8 @@ func TestPrepareCheckExtractsState(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) assert.Eventually(t, func() bool { return k8sclient.DaemonSetByNamespaceAndName("shop", "xyz") != nil }, time.Second, 100*time.Millisecond) @@ -98,7 +102,8 @@ func TestStatusCheckDaemonSetNotFound(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) action := NewDaemonSetPodCountCheckAction(k8sclient).(action_kit_sdk.ActionWithStatus[extcommon.PodCountCheckState]) diff --git a/extdeployment/deployment_discovery_test.go b/extdeployment/deployment_discovery_test.go index b14a87e3..7e3eeb43 100644 --- a/extdeployment/deployment_discovery_test.go +++ b/extdeployment/deployment_discovery_test.go @@ -13,6 +13,7 @@ import ( "github.com/steadybit/extension-kit/extutil" "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -23,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" ) func Test_deploymentDiscovery(t *testing.T) { @@ -693,6 +695,7 @@ func Test_getDiscoveredDeploymentsShouldNotIgnoreLabeledDeploymentsIfExcludesDis func getTestClient(stopCh <-chan struct{}) (*client.Client, kubernetes.Interface) { clientset := testclient.NewSimpleClientset() - client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) return client, clientset } diff --git a/extdeployment/deployment_pod_count_check_test.go b/extdeployment/deployment_pod_count_check_test.go index fa8031fb..470c3d1d 100644 --- a/extdeployment/deployment_pod_count_check_test.go +++ b/extdeployment/deployment_pod_count_check_test.go @@ -5,18 +5,21 @@ package extdeployment import ( "context" + "testing" + "time" + "github.com/steadybit/action-kit/go/action_kit_api/v2" "github.com/steadybit/action-kit/go/action_kit_sdk" "github.com/steadybit/extension-kit/extutil" "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extcommon" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" testclient "k8s.io/client-go/kubernetes/fake" - "testing" - "time" + "k8s.io/client-go/rest" ) func TestPrepareCheckExtractsState(t *testing.T) { @@ -59,7 +62,8 @@ func TestPrepareCheckExtractsState(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) assert.Eventually(t, func() bool { return k8sclient.DeploymentByNamespaceAndName("shop", "checkout") != nil }, time.Second, 100*time.Millisecond) @@ -103,7 +107,8 @@ func TestStatusCheckDeploymentNotFound(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) action := NewDeploymentPodCountCheckAction(k8sclient).(action_kit_sdk.ActionWithStatus[extcommon.PodCountCheckState]) diff --git a/extdeployment/pod_count_metrics_test.go b/extdeployment/pod_count_metrics_test.go index 93ead1f1..8101d4f9 100644 --- a/extdeployment/pod_count_metrics_test.go +++ b/extdeployment/pod_count_metrics_test.go @@ -5,15 +5,18 @@ package extdeployment import ( "context" + "testing" + "time" + "github.com/steadybit/action-kit/go/action_kit_api/v2" "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" testclient "k8s.io/client-go/kubernetes/fake" - "testing" - "time" + "k8s.io/client-go/rest" ) func TestPrepareMetricsExtractsState(t *testing.T) { @@ -76,7 +79,8 @@ func TestStatusReturnsMetrics(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) // When result := statusPodCountMetricsInternal(client, &state) diff --git a/extevents/events_test.go b/extevents/events_test.go index dab908fb..34704098 100644 --- a/extevents/events_test.go +++ b/extevents/events_test.go @@ -6,14 +6,17 @@ package extevents import ( "context" + "testing" + "time" + "github.com/steadybit/action-kit/go/action_kit_api/v2" "github.com/steadybit/extension-kubernetes/v2/client" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" testclient "k8s.io/client-go/kubernetes/fake" - "testing" - "time" + "k8s.io/client-go/rest" ) func TestPrepareExtractsState(t *testing.T) { @@ -40,7 +43,7 @@ func TestPrepareExtractsState(t *testing.T) { func TestStatusEventsFound(t *testing.T) { // Given stopCh := make(chan struct{}) - defer close(stopCh) + t.Cleanup(func() { close(stopCh) }) state, k8sClient := prepareTest(t, stopCh) @@ -92,5 +95,6 @@ func prepareTest(t *testing.T, stopCh chan struct{}) (*K8sEventsState, *client.C }, metav1.CreateOptions{}) require.NoError(t, err) - return &state, client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + return &state, client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) } diff --git a/extingress/ha_proxy_delay_traffic_test.go b/extingress/ha_proxy_delay_traffic_test.go index e7cc618e..764218a6 100644 --- a/extingress/ha_proxy_delay_traffic_test.go +++ b/extingress/ha_proxy_delay_traffic_test.go @@ -3,18 +3,21 @@ package extingress import ( "context" "fmt" + "testing" + "time" + "github.com/google/uuid" "github.com/steadybit/action-kit/go/action_kit_api/v2" "github.com/steadybit/extension-kit/extutil" "github.com/steadybit/extension-kubernetes/v2/client" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" - "testing" - "time" + "k8s.io/client-go/rest" ) func TestHAProxyDelayTrafficAction_Prepare(t *testing.T) { @@ -404,6 +407,7 @@ func TestHAProxyDelayTrafficAction_Prepare(t *testing.T) { func getTestClient(stopCh <-chan struct{}) (*client.Client, kubernetes.Interface) { clientset := testclient.NewSimpleClientset() - client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) return client, clientset } diff --git a/extingress/haproxy_ingress_discovery_test.go b/extingress/haproxy_ingress_discovery_test.go index c1bdb875..d0f30fec 100644 --- a/extingress/haproxy_ingress_discovery_test.go +++ b/extingress/haproxy_ingress_discovery_test.go @@ -13,9 +13,11 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/testutil" ) func strPtr(s string) *string { @@ -25,7 +27,8 @@ func strPtr(s string) *string { // newTestClient creates a fake client with provided initial objects. func newTestClient(stopCh <-chan struct{}, initObjs ...runtime.Object) (*client.Client, kubernetes.Interface) { cs := testclient.NewSimpleClientset(initObjs...) - cli := client.CreateClient(cs, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + cli := client.CreateClient(cs, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) return cli, cs } diff --git a/extingress/nginx_common_test.go b/extingress/nginx_common_test.go index fcb904a9..8e4f0510 100644 --- a/extingress/nginx_common_test.go +++ b/extingress/nginx_common_test.go @@ -17,14 +17,17 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" "github.com/steadybit/extension-kubernetes/v2/client" + "github.com/steadybit/extension-kubernetes/v2/testutil" ) // newNginxTestClient creates a fake client with provided initial objects. func newNginxTestClient(stopCh <-chan struct{}, initObjs ...runtime.Object) (*client.Client, kubernetes.Interface) { cs := testclient.NewSimpleClientset(initObjs...) - cli := client.CreateClient(cs, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + cli := client.CreateClient(cs, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) return cli, cs } @@ -33,7 +36,7 @@ func Test_findNginxControllerNamespace_Basic(t *testing.T) { defer close(stopCh) cli, cs := newNginxTestClient(stopCh) - + // Override the global client for testing originalClient := client.K8S client.K8S = cli @@ -42,7 +45,7 @@ func Test_findNginxControllerNamespace_Basic(t *testing.T) { // Test 1: Non-NGINX controller should return empty nonNginxClass := &networkingv1.IngressClass{ ObjectMeta: metav1.ObjectMeta{Name: "traefik"}, - Spec: networkingv1.IngressClassSpec{Controller: "traefik.io/ingress-controller"}, + Spec: networkingv1.IngressClassSpec{Controller: "traefik.io/ingress-controller"}, } _, err := cs.NetworkingV1().IngressClasses().Create(context.Background(), nonNginxClass, metav1.CreateOptions{}) require.NoError(t, err) @@ -50,7 +53,7 @@ func Test_findNginxControllerNamespace_Basic(t *testing.T) { // Test 2: Valid NGINX controller nginxClass := &networkingv1.IngressClass{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, - Spec: networkingv1.IngressClassSpec{Controller: "k8s.io/ingress-nginx"}, + Spec: networkingv1.IngressClassSpec{Controller: "k8s.io/ingress-nginx"}, } _, err = cs.NetworkingV1().IngressClasses().Create(context.Background(), nginxClass, metav1.CreateOptions{}) require.NoError(t, err) @@ -100,7 +103,7 @@ func Test_hasNginxControllerPods(t *testing.T) { defer close(stopCh) cli, _ := newNginxTestClient(stopCh) - + // Override the global client for testing originalClient := client.K8S client.K8S = cli @@ -120,7 +123,7 @@ func Test_findNginxControllerNamespace_WithAnnotations(t *testing.T) { defer close(stopCh) cli, cs := newNginxTestClient(stopCh) - + // Override the global client for testing originalClient := client.K8S client.K8S = cli @@ -163,7 +166,7 @@ func Test_findNginxControllerNamespace_WithAnnotations(t *testing.T) { // Will return empty since no pods exist, but that's expected in test environment assert.Equal(t, "", result, "UBI NGINX controller without pods should return empty") - // Test community NGINX - should try to look in ingress-nginx namespace + // Test community NGINX - should try to look in ingress-nginx namespace result = findNginxControllerNamespace("community-nginx") // Will return empty since no pods exist, but that's expected in test environment assert.Equal(t, "", result, "Community NGINX controller without pods should return empty") @@ -171,10 +174,10 @@ func Test_findNginxControllerNamespace_WithAnnotations(t *testing.T) { func Test_podServesIngressClass(t *testing.T) { tests := []struct { - name string - pod *corev1.Pod - ingressClass string - expected bool + name string + pod *corev1.Pod + ingressClass string + expected bool }{ { name: "pod with -ingress-class flag (separate args)", @@ -293,4 +296,4 @@ func Test_podServesIngressClass(t *testing.T) { assert.Equal(t, tt.expected, result) }) } -} \ No newline at end of file +} diff --git a/extingress/nginx_ingress_discovery.go b/extingress/nginx_ingress_discovery.go index 13c6b157..1d2a0ebd 100644 --- a/extingress/nginx_ingress_discovery.go +++ b/extingress/nginx_ingress_discovery.go @@ -176,4 +176,3 @@ func (d *nginxIngressDiscovery) isUsingNginxClass(className string, nginxClasses } return false } - diff --git a/extingress/nginx_ingress_discovery_test.go b/extingress/nginx_ingress_discovery_test.go index e26f5795..9d2a84a6 100644 --- a/extingress/nginx_ingress_discovery_test.go +++ b/extingress/nginx_ingress_discovery_test.go @@ -220,6 +220,3 @@ func Test_nginxIngressDiscovery_IncludeDisabledIfDisableDiscoveryExcludes(t *tes targets, _ := d.DiscoverTargets(context.Background()) assert.Equal(t, "test-cluster/default/included", targets[0].Id) } - - - diff --git a/extnamespace/namespace_util_test.go b/extnamespace/namespace_util_test.go index 9b812a6b..448fcfc4 100644 --- a/extnamespace/namespace_util_test.go +++ b/extnamespace/namespace_util_test.go @@ -5,17 +5,20 @@ package extnamespace import ( "context" + "sort" + "testing" + "time" + kclient "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" - "sort" - "testing" - "time" + "k8s.io/client-go/rest" ) func Test_namespaceDiscovery(t *testing.T) { @@ -105,6 +108,7 @@ func testNamespace(modifier func(namespace *v1.Namespace)) *v1.Namespace { func getTestClient(stopCh <-chan struct{}) (*kclient.Client, kubernetes.Interface) { clientset := testclient.NewSimpleClientset() - client := kclient.CreateClient(clientset, stopCh, "/oapi", kclient.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + client := kclient.CreateClient(clientset, stopCh, "/oapi", kclient.MockAllPermitted(), &rest.Config{}, dynamicClient) return client, clientset } diff --git a/extnode/node_count_check_test.go b/extnode/node_count_check_test.go index 7e25a3cf..3a6593c3 100644 --- a/extnode/node_count_check_test.go +++ b/extnode/node_count_check_test.go @@ -5,15 +5,18 @@ package extnode import ( "context" + "testing" + "time" + "github.com/steadybit/action-kit/go/action_kit_api/v2" "github.com/steadybit/extension-kit/extutil" "github.com/steadybit/extension-kubernetes/v2/client" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" testclient "k8s.io/client-go/kubernetes/fake" - "testing" - "time" + "k8s.io/client-go/rest" ) func TestPrepareCheckExtractsState(t *testing.T) { @@ -57,7 +60,8 @@ func TestPrepareCheckExtractsState(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) action := NewNodeCountCheckAction() state := action.NewEmptyState() @@ -131,7 +135,8 @@ func TestStatusCheckNodeCountAtLeastSuccess(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) // When result := statusNodeCountCheckInternal(k8sclient, &state) @@ -177,7 +182,8 @@ func TestStatusCheckNodeCountAtFail(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) // When result := statusNodeCountCheckInternal(k8sclient, &state) @@ -223,7 +229,8 @@ func TestStatusCheckNodeCountDecreasedBySuccess(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) // When result := statusNodeCountCheckInternal(k8sclient, &state) @@ -291,7 +298,8 @@ func TestStatusCheckNodeCountDecreasedByFail(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) // When result := statusNodeCountCheckInternal(k8sclient, &state) @@ -359,7 +367,8 @@ func TestStatusCheckNodeCountIncreasedBySuccess(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) // When result := statusNodeCountCheckInternal(k8sclient, &state) @@ -405,7 +414,8 @@ func TestStatusCheckNodeCountIncreasedByFail(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) // When result := statusNodeCountCheckInternal(k8sclient, &state) diff --git a/extnode/node_discovery_test.go b/extnode/node_discovery_test.go index 80538c42..c91b3619 100644 --- a/extnode/node_discovery_test.go +++ b/extnode/node_discovery_test.go @@ -5,8 +5,12 @@ package extnode import ( "context" + "testing" + "time" + "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -14,8 +18,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" - "testing" - "time" + "k8s.io/client-go/rest" ) func Test_nodeDiscovery(t *testing.T) { @@ -169,6 +172,7 @@ func Test_nodeDiscovery(t *testing.T) { func getTestClient(stopCh <-chan struct{}) (*client.Client, kubernetes.Interface) { clientset := testclient.NewSimpleClientset() - client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) return client, clientset } diff --git a/extpod/pod_discovery.go b/extpod/pod_discovery.go index e823252c..df34e487 100644 --- a/extpod/pod_discovery.go +++ b/extpod/pod_discovery.go @@ -6,6 +6,10 @@ package extpod import ( "context" "fmt" + "reflect" + "strings" + "time" + "github.com/steadybit/discovery-kit/go/discovery_kit_api" "github.com/steadybit/discovery-kit/go/discovery_kit_commons" "github.com/steadybit/discovery-kit/go/discovery_kit_sdk" @@ -17,9 +21,6 @@ import ( "github.com/steadybit/extension-kubernetes/v2/extnamespace" "golang.org/x/exp/slices" corev1 "k8s.io/api/core/v1" - "reflect" - "strings" - "time" ) type podDiscovery struct { @@ -60,7 +61,7 @@ func (*podDiscovery) DescribeTarget() discovery_kit_api.TargetDescription { {Attribute: "k8s.pod.name"}, {Attribute: "k8s.cluster-name"}, {Attribute: "k8s.namespace"}, - {Attribute: "k8s.deployment", FallbackAttributes: extutil.Ptr([]string{"k8s.statefulset", "k8s.daemonset"})}, + {Attribute: "k8s.deployment", FallbackAttributes: extutil.Ptr([]string{"k8s.statefulset", "k8s.daemonset", "k8s.argo-rollout"})}, }, OrderBy: []discovery_kit_api.OrderBy{ { diff --git a/extpod/pod_discovery_test.go b/extpod/pod_discovery_test.go index 4dc65c87..753f7bac 100644 --- a/extpod/pod_discovery_test.go +++ b/extpod/pod_discovery_test.go @@ -5,8 +5,12 @@ package extpod import ( "context" + "testing" + "time" + "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -14,8 +18,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" - "testing" - "time" + "k8s.io/client-go/rest" ) func Test_getDiscoveredPods(t *testing.T) { @@ -295,6 +298,7 @@ func Test_getDiscoveredPodsShouldIgnoreLabeledPods(t *testing.T) { func getTestClient(stopCh <-chan struct{}) (*client.Client, kubernetes.Interface) { clientset := testclient.NewSimpleClientset() - client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + client := client.CreateClient(clientset, stopCh, "/oapi", client.MockAllPermitted(), &rest.Config{}, dynamicClient) return client, clientset } diff --git a/extreplicaset/replicaset_discovery_test.go b/extreplicaset/replicaset_discovery_test.go index 560a92e6..69959ae5 100644 --- a/extreplicaset/replicaset_discovery_test.go +++ b/extreplicaset/replicaset_discovery_test.go @@ -13,6 +13,7 @@ import ( "github.com/steadybit/extension-kit/extutil" "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -21,6 +22,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" ) func Test_replicasetDiscovery(t *testing.T) { @@ -432,6 +434,7 @@ func Test_getDiscoveredReplicaSetsShouldNotIgnoreLabeledReplicaSetsIfExcludesDis func getTestClient(stopCh <-chan struct{}) (*client.Client, kubernetes.Interface) { clientset := testclient.NewClientset() - client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) return client, clientset } diff --git a/extreplicaset/replicaset_pod_count_check.go b/extreplicaset/replicaset_pod_count_check.go index 0e36233b..3ed8502d 100644 --- a/extreplicaset/replicaset_pod_count_check.go +++ b/extreplicaset/replicaset_pod_count_check.go @@ -37,4 +37,4 @@ func NewReplicaSetPodCountCheckAction(k8s *client.Client) action_kit_sdk.Action[ return rs.Spec.Replicas, rs.Status.ReadyReplicas, nil }, } -} \ No newline at end of file +} diff --git a/extreplicaset/replicaset_pod_count_check_test.go b/extreplicaset/replicaset_pod_count_check_test.go index 3d2b081f..273ad64b 100644 --- a/extreplicaset/replicaset_pod_count_check_test.go +++ b/extreplicaset/replicaset_pod_count_check_test.go @@ -5,18 +5,21 @@ package extreplicaset import ( "context" + "testing" + "time" + "github.com/steadybit/action-kit/go/action_kit_api/v2" "github.com/steadybit/action-kit/go/action_kit_sdk" "github.com/steadybit/extension-kit/extutil" "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extcommon" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" testclient "k8s.io/client-go/kubernetes/fake" - "testing" - "time" + "k8s.io/client-go/rest" ) func TestPrepareCheckExtractsState(t *testing.T) { @@ -59,7 +62,8 @@ func TestPrepareCheckExtractsState(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) assert.Eventually(t, func() bool { return k8sclient.ReplicaSetByNamespaceAndName("shop", "checkout") != nil }, time.Second, 100*time.Millisecond) @@ -103,7 +107,8 @@ func TestStatusCheckReplicaSetNotFound(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) action := NewReplicaSetPodCountCheckAction(k8sclient).(action_kit_sdk.ActionWithStatus[extcommon.PodCountCheckState]) @@ -113,4 +118,4 @@ func TestStatusCheckReplicaSetNotFound(t *testing.T) { // Then require.EqualError(t, err, "ReplicaSet checkout not found.") require.Nil(t, result) -} \ No newline at end of file +} diff --git a/extrollout/common.go b/extrollout/common.go new file mode 100644 index 00000000..81abe6be --- /dev/null +++ b/extrollout/common.go @@ -0,0 +1,5 @@ +package extrollout + +const ( + RolloutTargetType = "com.steadybit.extension_kubernetes.kubernetes-argo-rollout" +) diff --git a/extrollout/rollout_discovery.go b/extrollout/rollout_discovery.go new file mode 100644 index 00000000..d29aa2ff --- /dev/null +++ b/extrollout/rollout_discovery.go @@ -0,0 +1,216 @@ +package extrollout + +import ( + "context" + "fmt" + "reflect" + "slices" + "time" + + "github.com/steadybit/discovery-kit/go/discovery_kit_api" + "github.com/steadybit/discovery-kit/go/discovery_kit_commons" + "github.com/steadybit/discovery-kit/go/discovery_kit_sdk" + "github.com/steadybit/extension-kit/extbuild" + "github.com/steadybit/extension-kit/extutil" + "github.com/steadybit/extension-kubernetes/v2/client" + "github.com/steadybit/extension-kubernetes/v2/extcommon" + "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/extnamespace" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type rolloutDiscovery struct { + k8s *client.Client +} + +var ( + _ discovery_kit_sdk.TargetDescriber = (*rolloutDiscovery)(nil) + _ discovery_kit_sdk.EnrichmentRulesDescriber = (*rolloutDiscovery)(nil) +) + +func NewRolloutDiscovery(k8s *client.Client) discovery_kit_sdk.TargetDiscovery { + discovery := &rolloutDiscovery{k8s: k8s} + chRefresh := extcommon.TriggerOnKubernetesResourceChange(k8s, + reflect.TypeOf(corev1.Pod{}), + reflect.TypeOf(unstructured.Unstructured{}), + reflect.TypeOf(autoscalingv2.HorizontalPodAutoscaler{}), + reflect.TypeOf(corev1.Service{}), + ) + return discovery_kit_sdk.NewCachedTargetDiscovery(discovery, + discovery_kit_sdk.WithRefreshTargetsNow(), + discovery_kit_sdk.WithRefreshTargetsTrigger(context.Background(), chRefresh, 5*time.Second), + ) +} + +func (d *rolloutDiscovery) Describe() discovery_kit_api.DiscoveryDescription { + return discovery_kit_api.DiscoveryDescription{ + Id: RolloutTargetType, + Discover: discovery_kit_api.DescribingEndpointReferenceWithCallInterval{ + CallInterval: extutil.Ptr("30s"), + }, + } +} + +func (d *rolloutDiscovery) DescribeTarget() discovery_kit_api.TargetDescription { + return discovery_kit_api.TargetDescription{ + Id: RolloutTargetType, + Label: discovery_kit_api.PluralLabel{One: "Kubernetes Argo Rollout", Other: "Kubernetes Argo Rollouts"}, + Category: extutil.Ptr("Kubernetes"), + Version: extbuild.GetSemverVersionStringOrUnknown(), + Icon: extutil.Ptr("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M10.4478%202.65625C11.2739%202.24209%2012.2447%202.23174%2013.0794%202.62821L19.2871%205.57666C20.3333%206.07356%2021%207.12832%2021%208.28652V15.7134C21%2016.8717%2020.3333%2017.9264%2019.2871%2018.4233L13.0794%2021.3718C12.2447%2021.7682%2011.2739%2021.7579%2010.4478%2021.3437L4.65545%2018.4397L5.55182%2016.6518L11.3441%2019.5558C11.6195%2019.6939%2011.9431%2019.6973%2012.2214%2019.5652L18.429%2016.6167C18.7778%2016.4511%2019%2016.0995%2019%2015.7134V8.28652C19%207.90045%2018.7778%207.54887%2018.429%207.38323L12.2214%204.43479C11.9431%204.30263%2011.6195%204.30608%2011.3441%204.44413L5.55182%207.34814C5.21357%207.51773%205%207.8637%205%208.24208V15.7579C5%2016.1363%205.21357%2016.4822%205.55182%2016.6518L4.65545%2018.4397C3.6407%2017.931%203%2016.893%203%2015.7579V8.24208C3%207.10694%203.6407%206.06901%204.65545%205.56026L10.4478%202.65625Z%22%20fill%3D%22%231D2632%22%2F%3E%0A%3Cpath%20d%3D%22M11.1377%207.16465C11.5966%206.95033%2012.1359%206.94497%2012.5997%207.15014L16.0484%208.67595C16.6296%208.9331%2017%209.47893%2017%2010.0783V13.9217C17%2014.5211%2016.6296%2015.0669%2016.0484%2015.324L12.5997%2016.8499C12.1359%2017.055%2011.5966%2017.0497%2011.1377%2016.8353L7.9197%2015.3325C7.35594%2015.0693%207%2014.5321%207%2013.9447V10.0553C7%209.46787%207.35594%208.93074%207.9197%208.66747L11.1377%207.16465Z%22%20fill%3D%22%231D2632%22%2F%3E%0A%3C%2Fsvg%3E%0A"), + Table: discovery_kit_api.Table{ + Columns: []discovery_kit_api.Column{ + {Attribute: "k8s.argo-rollout"}, + {Attribute: "k8s.namespace"}, + {Attribute: "k8s.cluster-name"}, + }, + OrderBy: []discovery_kit_api.OrderBy{ + { + Attribute: "k8s.argo-rollout", + Direction: "ASC", + }, + }, + }, + } +} + +func (d *rolloutDiscovery) DiscoverTargets(_ context.Context) ([]discovery_kit_api.Target, error) { + rollouts := d.k8s.ArgoRollouts() + + filteredRollouts := make([]*unstructured.Unstructured, 0, len(rollouts)) + for _, rollout := range rollouts { + if client.IsExcludedFromDiscovery(metav1.ObjectMeta{ + Name: rollout.GetName(), + Namespace: rollout.GetNamespace(), + Annotations: rollout.GetAnnotations(), + Labels: rollout.GetLabels(), + }) { + continue + } + filteredRollouts = append(filteredRollouts, rollout) + } + + targets := make([]discovery_kit_api.Target, len(filteredRollouts)) + nodes := d.k8s.Nodes() + + for i, rollout := range filteredRollouts { + targetName := fmt.Sprintf("%s/%s/%s", extconfig.Config.ClusterName, rollout.GetNamespace(), rollout.GetName()) + attributes := map[string][]string{ + "k8s.namespace": {rollout.GetNamespace()}, + "k8s.argo-rollout": {rollout.GetName()}, + "k8s.workload-type": {"argo-rollout"}, + "k8s.workload-owner": {rollout.GetName()}, + "k8s.cluster-name": {extconfig.Config.ClusterName}, + "k8s.distribution": {d.k8s.Distribution}, + } + + // Get replicas from spec + if replicas, found, err := unstructured.NestedInt64(rollout.Object, "spec", "replicas"); err == nil && found { + attributes["k8s.specification.replicas"] = []string{fmt.Sprintf("%d", replicas)} + } + + // Get min-ready-seconds from spec + if minReadySeconds, found, err := unstructured.NestedInt64(rollout.Object, "spec", "minReadySeconds"); err == nil && found { + attributes["k8s.specification.min-ready-seconds"] = []string{fmt.Sprintf("%d", minReadySeconds)} + } + + // Add labels + for key, value := range rollout.GetLabels() { + if !slices.Contains(extconfig.Config.LabelFilter, key) { + attributes[fmt.Sprintf("k8s.argo-rollout.label.%v", key)] = []string{value} + attributes[fmt.Sprintf("k8s.label.%v", key)] = []string{value} + } + } + + extnamespace.AddNamespaceLabels(d.k8s, rollout.GetNamespace(), attributes) + + // Get pod template labels + if podTemplate, found, err := unstructured.NestedMap(rollout.Object, "spec", "template", "metadata", "labels"); err == nil && found { + // Convert map[string]interface{} to map[string]string + podTemplateLabels := make(map[string]string) + for k, v := range podTemplate { + if str, ok := v.(string); ok { + podTemplateLabels[k] = str + } + } + + // Get pods by label selector + pods := d.k8s.PodsByLabelSelector(&metav1.LabelSelector{ + MatchLabels: podTemplateLabels, + }, rollout.GetNamespace()) + + // Add pod-based attributes + for key, value := range extcommon.GetPodBasedAttributes("argo-rollout", metav1.ObjectMeta{ + Name: rollout.GetName(), + Namespace: rollout.GetNamespace(), + Annotations: rollout.GetAnnotations(), + Labels: rollout.GetLabels(), + }, pods, nodes) { + attributes[key] = value + } + + // Add service names + for key, value := range extcommon.GetServiceNames(d.k8s.ServicesMatchingToPodLabels(rollout.GetNamespace(), podTemplateLabels)) { + attributes[key] = value + } + } + + // TODO: Get HPA if exists + // var hpa *autoscalingv2.HorizontalPodAutoscaler + // if d.k8s.Permissions().CanReadHorizontalPodAutoscalers() { + // hpa = d.k8s.HorizontalPodAutoscalerByNamespaceAndDeployment(rollout.GetNamespace(), rollout.GetName()) + // } + + // TODO: Add kube-score attributes + // for key, value := range extcommon.GetKubeScoreForDeployment(nil, d.k8s.ServicesMatchingToPodLabels(rollout.GetNamespace(), rollout.GetLabels()), hpa) { + // attributes[key] = value + // } + + targets[i] = discovery_kit_api.Target{ + Id: targetName, + TargetType: RolloutTargetType, + Label: rollout.GetName(), + Attributes: attributes, + } + } + + return discovery_kit_commons.ApplyAttributeExcludes(targets, extconfig.Config.DiscoveryAttributesExcludesDeployment), nil +} + +func (d *rolloutDiscovery) DescribeEnrichmentRules() []discovery_kit_api.TargetEnrichmentRule { + return []discovery_kit_api.TargetEnrichmentRule{ + getRolloutToContainerEnrichmentRule(), + } +} + +func getRolloutToContainerEnrichmentRule() discovery_kit_api.TargetEnrichmentRule { + return discovery_kit_api.TargetEnrichmentRule{ + Id: "com.steadybit.extension_kubernetes.kubernetes-argo-rollout-to-container", + Version: extbuild.GetSemverVersionStringOrUnknown(), + Src: discovery_kit_api.SourceOrDestination{ + Type: RolloutTargetType, + Selector: map[string]string{ + "k8s.container.id.stripped": "${dest.container.id.stripped}", + }, + }, + Dest: discovery_kit_api.SourceOrDestination{ + Type: "com.steadybit.extension_container.container", + Selector: map[string]string{ + "container.id.stripped": "${src.k8s.container.id.stripped}", + }, + }, + Attributes: []discovery_kit_api.Attribute{ + { + Matcher: discovery_kit_api.StartsWith, + Name: "k8s.argo-rollout.label.", + }, + { + Matcher: discovery_kit_api.Regex, + Name: "^k8s\\.label\\.(?!topology).*", + }, + }, + } +} diff --git a/extrollout/rollout_discovery_test.go b/extrollout/rollout_discovery_test.go new file mode 100644 index 00000000..2460df0c --- /dev/null +++ b/extrollout/rollout_discovery_test.go @@ -0,0 +1,568 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2023 Steadybit GmbH + +package extrollout + +import ( + "context" + "fmt" + "sort" + "testing" + "time" + + "github.com/steadybit/discovery-kit/go/discovery_kit_api" + "github.com/steadybit/extension-kubernetes/v2/client" + "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + testclient "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" +) + +func Test_rolloutDiscovery_Describe(t *testing.T) { + // Setup test client + stopCh := make(chan struct{}) + defer close(stopCh) + k8sClient, _, _ := getTestClient(stopCh) + + discovery := &rolloutDiscovery{k8s: k8sClient} + + description := discovery.Describe() + + assert.Equal(t, RolloutTargetType, description.Id) + assert.NotNil(t, description.Discover) + + // Check that CallInterval is set to 30s + callInterval := description.Discover.CallInterval + assert.Equal(t, "30s", *callInterval) +} + +func Test_rolloutDiscovery_DescribeTarget(t *testing.T) { + k8sClient := &client.Client{} + discovery := &rolloutDiscovery{k8s: k8sClient} + + targetDescription := discovery.DescribeTarget() + + assert.Equal(t, RolloutTargetType, targetDescription.Id) + assert.Equal(t, "Kubernetes Argo Rollout", targetDescription.Label.One) + assert.Equal(t, "Kubernetes Argo Rollouts", targetDescription.Label.Other) + assert.Equal(t, "Kubernetes", *targetDescription.Category) + assert.NotEmpty(t, targetDescription.Version) + assert.NotNil(t, targetDescription.Icon) + + assert.Len(t, targetDescription.Table.Columns, 3) + assert.Equal(t, "k8s.argo-rollout", targetDescription.Table.Columns[0].Attribute) + assert.Equal(t, "k8s.namespace", targetDescription.Table.Columns[1].Attribute) + assert.Equal(t, "k8s.cluster-name", targetDescription.Table.Columns[2].Attribute) + + assert.Len(t, targetDescription.Table.OrderBy, 1) + assert.Equal(t, "k8s.argo-rollout", targetDescription.Table.OrderBy[0].Attribute) + assert.Equal(t, discovery_kit_api.OrderByDirection("ASC"), targetDescription.Table.OrderBy[0].Direction) +} + +func Test_rolloutDiscovery_DescribeEnrichmentRules(t *testing.T) { + k8sClient := &client.Client{} + discovery := &rolloutDiscovery{k8s: k8sClient} + + enrichmentRules := discovery.DescribeEnrichmentRules() + + require.Len(t, enrichmentRules, 1) + + rule := enrichmentRules[0] + assert.Equal(t, "com.steadybit.extension_kubernetes.kubernetes-argo-rollout-to-container", rule.Id) + assert.NotEmpty(t, rule.Version) + + assert.Equal(t, RolloutTargetType, rule.Src.Type) + assert.Equal(t, "${dest.container.id.stripped}", rule.Src.Selector["k8s.container.id.stripped"]) + + assert.Equal(t, "com.steadybit.extension_container.container", rule.Dest.Type) + assert.Equal(t, "${src.k8s.container.id.stripped}", rule.Dest.Selector["container.id.stripped"]) + + assert.Len(t, rule.Attributes, 2) + assert.Equal(t, "k8s.argo-rollout.label.", rule.Attributes[0].Name) + assert.Equal(t, discovery_kit_api.StartsWith, rule.Attributes[0].Matcher) + assert.Equal(t, "^k8s\\.label\\.(?!topology).*", rule.Attributes[1].Name) + assert.Equal(t, discovery_kit_api.Regex, rule.Attributes[1].Matcher) +} + +func Test_rolloutDiscovery(t *testing.T) { + tests := []struct { + name string + configModifier func(*extconfig.Specification) + pods []*v1.Pod + nodes []*v1.Node + rollout *unstructured.Unstructured + service *v1.Service + expectedAttributesExactly map[string][]string + expectedAttributes map[string][]string + }{ + { + name: "should discover basic attributes", + pods: []*v1.Pod{testPod("aaaaa", nil), testPod("bbbbb", func(pod *v1.Pod) { + pod.Spec.NodeName = "worker-2" + })}, + nodes: []*v1.Node{testNode("worker-1"), testNode("worker-2")}, + rollout: testRollout(nil), + expectedAttributesExactly: map[string][]string{ + "host.hostname": {"worker-1", "worker-2"}, + "host.domainname": {"worker-1.internal", "worker-2.internal"}, + "k8s.namespace": {"default"}, + "k8s.argo-rollout": {"shop"}, + "k8s.workload-type": {"argo-rollout"}, + "k8s.workload-owner": {"shop"}, + "k8s.argo-rollout.label.best-city": {"Kevelaer"}, + "k8s.label.best-city": {"Kevelaer"}, + "k8s.specification.min-ready-seconds": {"10"}, + "k8s.specification.replicas": {"3"}, + "k8s.cluster-name": {"development"}, + "k8s.pod.name": {"shop-pod-aaaaa", "shop-pod-bbbbb"}, + "k8s.container.id": {"crio://abcdef-aaaaa-nginx", "crio://abcdef-aaaaa-shop", "crio://abcdef-bbbbb-nginx", "crio://abcdef-bbbbb-shop"}, + "k8s.container.id.stripped": {"abcdef-aaaaa-nginx", "abcdef-aaaaa-shop", "abcdef-bbbbb-nginx", "abcdef-bbbbb-shop"}, + "k8s.distribution": {"kubernetes"}, + }, + }, + { + name: "hostnames should be unique and not duplicated", + nodes: []*v1.Node{testNode("worker-1")}, + pods: []*v1.Pod{testPod("aaaaa", nil), testPod("bbbbb", nil)}, + rollout: testRollout(nil), + expectedAttributes: map[string][]string{ + "host.hostname": {"worker-1"}, + "host.domainname": {"worker-1.internal"}, + }, + }, + { + name: "should add service name", + pods: []*v1.Pod{testPod("aaaaa", nil)}, + rollout: testRollout(nil), + service: testService(nil), + expectedAttributes: map[string][]string{ + "k8s.service.name": {"shop-kevelaer"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup config + extconfig.Config.ClusterName = "development" + extconfig.Config.LabelFilter = []string{"topology.kubernetes.io/zone"} + extconfig.Config.DiscoveryDisabledArgoRollout = false // Enable Argo Rollout discovery + extconfig.Config.DiscoveryMaxPodCount = 50 // Set pod count limit + if tt.configModifier != nil { + tt.configModifier(&extconfig.Config) + } + + // Setup test client + stopCh := make(chan struct{}) + defer close(stopCh) + k8sClient, clientset, dynamicClient := getTestClient(stopCh) + + // Setup test data + if tt.rollout != nil { + // Add rollout to the dynamic client that's used by the k8s client + _, err := dynamicClient.Resource(schema.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "rollouts", + }).Namespace(tt.rollout.GetNamespace()).Create(context.Background(), tt.rollout, metav1.CreateOptions{}) + require.NoError(t, err) + } + + if tt.pods != nil { + // Add pods to client + for _, pod := range tt.pods { + _, err := clientset.CoreV1().Pods(pod.Namespace).Create(context.Background(), pod, metav1.CreateOptions{}) + require.NoError(t, err) + } + } + + if tt.nodes != nil { + // Add nodes to client + for _, node := range tt.nodes { + _, err := clientset.CoreV1().Nodes().Create(context.Background(), node, metav1.CreateOptions{}) + require.NoError(t, err) + } + } + + if tt.service != nil { + // Add service to client + _, err := clientset.CoreV1().Services(tt.service.Namespace).Create(context.Background(), tt.service, metav1.CreateOptions{}) + require.NoError(t, err) + } + + // Create discovery + discovery := &rolloutDiscovery{k8s: k8sClient} + + // Discover targets and wait for informer to sync + assert.EventuallyWithT(t, func(c *assert.CollectT) { + targets, err := discovery.DiscoverTargets(context.Background()) + require.NoError(t, err) + + // Assertions + if len(tt.expectedAttributesExactly) > 0 { + require.Len(t, targets, 1) + target := targets[0] + + for _, v := range target.Attributes { + sort.Strings(v) + } + assert.Equal(t, tt.expectedAttributesExactly, target.Attributes) + } + + if len(tt.expectedAttributes) > 0 { + require.Len(t, targets, 1) + target := targets[0] + + for k, v := range tt.expectedAttributes { + attributeValues := target.Attributes[k] + sort.Strings(attributeValues) + assert.Equal(t, v, attributeValues, "Attribute %s should match", k) + } + } + }, 1*time.Second, 100*time.Millisecond) + }) + } +} + +func Test_getDiscoveredRolloutsShouldIgnoreLabeledRollouts(t *testing.T) { + // Setup config + extconfig.Config.ClusterName = "development" + extconfig.Config.DisableDiscoveryExcludes = false + extconfig.Config.DiscoveryDisabledArgoRollout = false // Enable Argo Rollout discovery + extconfig.Config.DiscoveryMaxPodCount = 50 // Set pod count limit + + // Setup test client + stopCh := make(chan struct{}) + defer close(stopCh) + k8sClient, _, dynamicClient := getTestClient(stopCh) + + // Create rollout with exclusion label + rollout := testRollout(func(rollout *unstructured.Unstructured) { + rollout.SetLabels(map[string]string{ + "steadybit.com/discovery-disabled": "true", + }) + }) + + // Add rollout to dynamic client + _, err := dynamicClient.Resource(schema.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "rollouts", + }).Namespace(rollout.GetNamespace()).Create(context.Background(), rollout, metav1.CreateOptions{}) + require.NoError(t, err) + + // Create discovery + discovery := &rolloutDiscovery{k8s: k8sClient} + + // Discover targets and wait for informer to sync + assert.EventuallyWithT(t, func(c *assert.CollectT) { + targets, err := discovery.DiscoverTargets(context.Background()) + require.NoError(t, err) + // Should be empty because rollout is excluded + assert.Len(c, targets, 0) + }, 1*time.Second, 100*time.Millisecond) +} + +func Test_getDiscoveredRolloutsShouldNotIgnoreLabeledRolloutsIfExcludesDisabled(t *testing.T) { + // Setup config + extconfig.Config.ClusterName = "development" + extconfig.Config.DisableDiscoveryExcludes = true + extconfig.Config.DiscoveryDisabledArgoRollout = false // Enable Argo Rollout discovery + extconfig.Config.DiscoveryMaxPodCount = 50 // Set pod count limit + + // Setup test client + stopCh := make(chan struct{}) + defer close(stopCh) + k8sClient, _, dynamicClient := getTestClient(stopCh) + + // Create rollout with exclusion label + rollout := testRollout(func(rollout *unstructured.Unstructured) { + rollout.SetLabels(map[string]string{ + "steadybit.com/discovery-disabled": "true", + }) + }) + + // Add rollout to dynamic client + _, err := dynamicClient.Resource(schema.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "rollouts", + }).Namespace(rollout.GetNamespace()).Create(context.Background(), rollout, metav1.CreateOptions{}) + require.NoError(t, err) + + // Create discovery + discovery := &rolloutDiscovery{k8s: k8sClient} + + // Discover targets and wait for informer to sync + assert.EventuallyWithT(t, func(c *assert.CollectT) { + targets, err := discovery.DiscoverTargets(context.Background()) + require.NoError(t, err) + // Should not be empty because excludes are disabled + assert.Len(c, targets, 1) + }, 1*time.Second, 100*time.Millisecond) +} + +func Test_rolloutDiscovery_Simple(t *testing.T) { + // Setup config + extconfig.Config.ClusterName = "development" + extconfig.Config.DiscoveryDisabledArgoRollout = false // Enable Argo Rollout discovery + extconfig.Config.DiscoveryMaxPodCount = 50 // Set pod count limit + + // Setup test client + stopCh := make(chan struct{}) + defer close(stopCh) + k8sClient, _, dynamicClient := getTestClient(stopCh) + + // Create a simple rollout + rollout := testRollout(nil) + + // Add rollout to dynamic client + _, err := dynamicClient.Resource(schema.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "rollouts", + }).Namespace(rollout.GetNamespace()).Create(context.Background(), rollout, metav1.CreateOptions{}) + require.NoError(t, err) + + // Create discovery + discovery := &rolloutDiscovery{k8s: k8sClient} + + // Discover targets and wait for informer to sync + assert.EventuallyWithT(t, func(c *assert.CollectT) { + targets, err := discovery.DiscoverTargets(context.Background()) + require.NoError(t, err) + // Should discover at least one rollout + assert.GreaterOrEqual(c, len(targets), 1) + if len(targets) > 0 { + target := targets[0] + assert.Equal(c, "development/default/shop", target.Id) + assert.Equal(c, "shop", target.Label) + assert.Equal(c, RolloutTargetType, target.TargetType) + } + }, 1*time.Second, 100*time.Millisecond) +} + +// Helper functions + +func testNode(name string) *v1.Node { + return &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + "kubernetes.io/hostname": name, + }, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeHostName, + Address: name, + }, + { + Type: v1.NodeInternalDNS, + Address: fmt.Sprintf("%s.internal", name), + }, + { + Type: v1.NodeInternalIP, + Address: "192.168.1.1", + }, + }, + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } +} + +func testRollout(modifier func(rollout *unstructured.Unstructured)) *unstructured.Unstructured { + rollout := &unstructured.Unstructured{} + rollout.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "argoproj.io", + Version: "v1alpha1", + Kind: "Rollout", + }) + rollout.SetName("shop") + rollout.SetNamespace("default") + rollout.SetLabels(map[string]string{ + "best-city": "Kevelaer", + }) + + // Set basic spec + spec := map[string]interface{}{ + "replicas": int64(3), + "minReadySeconds": int64(10), + "selector": map[string]interface{}{ + "matchLabels": map[string]interface{}{ + "app": "shop", + }, + }, + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "shop", + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx:1.19", + "ports": []interface{}{ + map[string]interface{}{ + "containerPort": int64(80), + }, + }, + "livenessProbe": map[string]interface{}{ + "httpGet": map[string]interface{}{ + "path": "/live", + "port": int64(80), + }, + }, + "readinessProbe": map[string]interface{}{ + "httpGet": map[string]interface{}{ + "path": "/ready", + "port": int64(80), + }, + }, + }, + map[string]interface{}{ + "name": "shop", + "image": "shop:1.0", + "ports": []interface{}{ + map[string]interface{}{ + "containerPort": int64(8080), + }, + }, + }, + }, + }, + }, + } + + if err := unstructured.SetNestedMap(rollout.Object, spec, "spec"); err != nil { + panic(fmt.Sprintf("failed to set nested map: %v", err)) + } + + if modifier != nil { + modifier(rollout) + } + return rollout +} + +func testPod(nameSuffix string, modifier func(*v1.Pod)) *v1.Pod { + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("shop-pod-%s", nameSuffix), + Namespace: "default", + Labels: map[string]string{ + "app": "shop", + }, + }, + Spec: v1.PodSpec{ + NodeName: "worker-1", + Containers: []v1.Container{ + { + Name: "nginx", + Image: "nginx:1.19", + Ports: []v1.ContainerPort{ + { + ContainerPort: 80, + }, + }, + LivenessProbe: &v1.Probe{ + ProbeHandler: v1.ProbeHandler{ + HTTPGet: &v1.HTTPGetAction{ + Path: "/live", + Port: intstr.FromInt32(80), + }, + }, + }, + ReadinessProbe: &v1.Probe{ + ProbeHandler: v1.ProbeHandler{ + HTTPGet: &v1.HTTPGetAction{ + Path: "/ready", + Port: intstr.FromInt32(80), + }, + }, + }, + }, + { + Name: "shop", + Image: "shop:1.0", + Ports: []v1.ContainerPort{ + { + ContainerPort: 8080, + }, + }, + }, + }, + }, + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "nginx", + ContainerID: fmt.Sprintf("crio://abcdef-%s-nginx", nameSuffix), + Ready: true, + }, + { + Name: "shop", + ContainerID: fmt.Sprintf("crio://abcdef-%s-shop", nameSuffix), + Ready: true, + }, + }, + }, + } + if modifier != nil { + modifier(pod) + } + return pod +} + +func testService(modifier func(service *v1.Service)) *v1.Service { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "shop-kevelaer", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "app": "shop", + }, + Ports: []v1.ServicePort{ + { + Port: 80, + TargetPort: intstr.FromInt32(80), + }, + }, + }, + } + if modifier != nil { + modifier(service) + } + return service +} + +func getTestClient(stopCh <-chan struct{}) (*client.Client, kubernetes.Interface, dynamic.Interface) { + clientset := testclient.NewSimpleClientset() + config := &rest.Config{} + permissions := &client.PermissionCheckResult{} + dynamicClient := testutil.NewFakeDynamicClient() + + k8sClient := client.CreateClient(clientset, stopCh, "", permissions, config, dynamicClient) + k8sClient.Distribution = "kubernetes" + + return k8sClient, clientset, dynamicClient +} diff --git a/extstatefulset/statefulset_discovery_test.go b/extstatefulset/statefulset_discovery_test.go index 096fe514..d6059e5e 100644 --- a/extstatefulset/statefulset_discovery_test.go +++ b/extstatefulset/statefulset_discovery_test.go @@ -6,9 +6,14 @@ package extstatefulset import ( "context" "fmt" + "sort" + "testing" + "time" + "github.com/steadybit/extension-kit/extutil" "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extconfig" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -18,9 +23,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" - "sort" - "testing" - "time" + "k8s.io/client-go/rest" ) func Test_statefulSetDiscovery(t *testing.T) { @@ -456,6 +459,7 @@ func testService(modifier func(service *v1.Service)) *v1.Service { func getTestClient(stopCh <-chan struct{}) (*client.Client, kubernetes.Interface) { clientset := testclient.NewSimpleClientset() - client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + client := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) return client, clientset } diff --git a/extstatefulset/statefulset_pod_count_check_test.go b/extstatefulset/statefulset_pod_count_check_test.go index 605f143d..52bb64ad 100644 --- a/extstatefulset/statefulset_pod_count_check_test.go +++ b/extstatefulset/statefulset_pod_count_check_test.go @@ -2,18 +2,21 @@ package extstatefulset import ( "context" + "testing" + "time" + "github.com/steadybit/action-kit/go/action_kit_api/v2" "github.com/steadybit/action-kit/go/action_kit_sdk" "github.com/steadybit/extension-kit/extutil" "github.com/steadybit/extension-kubernetes/v2/client" "github.com/steadybit/extension-kubernetes/v2/extcommon" + "github.com/steadybit/extension-kubernetes/v2/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" testclient "k8s.io/client-go/kubernetes/fake" - "testing" - "time" + "k8s.io/client-go/rest" ) func TestPrepareCheckExtractsState(t *testing.T) { @@ -56,7 +59,8 @@ func TestPrepareCheckExtractsState(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) assert.Eventually(t, func() bool { return k8sclient.StatefulSetByNamespaceAndName("shop", "xyz") != nil }, time.Second, 100*time.Millisecond) @@ -100,7 +104,8 @@ func TestStatusCheckStatefulSetNotFound(t *testing.T) { stopCh := make(chan struct{}) defer close(stopCh) - k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted()) + dynamicClient := testutil.NewFakeDynamicClient() + k8sclient := client.CreateClient(clientset, stopCh, "", client.MockAllPermitted(), &rest.Config{}, dynamicClient) action := NewStatefulSetPodCountCheckAction(k8sclient).(action_kit_sdk.ActionWithStatus[extcommon.PodCountCheckState]) diff --git a/main.go b/main.go index 25802537..72b1a993 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,6 @@ package main import ( "context" - "github.com/steadybit/extension-kubernetes/v2/extingress" _ "github.com/KimMachineGun/automemlimit" // By default, it sets `GOMEMLIMIT` to 90% of cgroup's memory limit. "github.com/rs/zerolog" @@ -41,9 +40,11 @@ import ( "github.com/steadybit/extension-kubernetes/v2/extdaemonset" "github.com/steadybit/extension-kubernetes/v2/extdeployment" "github.com/steadybit/extension-kubernetes/v2/extevents" + "github.com/steadybit/extension-kubernetes/v2/extingress" "github.com/steadybit/extension-kubernetes/v2/extnode" "github.com/steadybit/extension-kubernetes/v2/extpod" "github.com/steadybit/extension-kubernetes/v2/extreplicaset" + "github.com/steadybit/extension-kubernetes/v2/extrollout" "github.com/steadybit/extension-kubernetes/v2/extstatefulset" _ "go.uber.org/automaxprocs" // Importing automaxprocs automatically adjusts GOMAXPROCS. ) @@ -65,6 +66,10 @@ func main() { client.PrepareClient(stopCh) + if !extconfig.Config.DiscoveryDisabledArgoRollout { + discovery_kit_sdk.Register(extrollout.NewRolloutDiscovery(client.K8S)) + } + if !extconfig.Config.DiscoveryDisabledDeployment { discovery_kit_sdk.Register(extdeployment.NewDeploymentDiscovery(client.K8S)) action_kit_sdk.RegisterAction(extdeployment.NewCheckDeploymentRolloutStatusAction()) diff --git a/testutil/dynamic_client.go b/testutil/dynamic_client.go new file mode 100644 index 00000000..d7828446 --- /dev/null +++ b/testutil/dynamic_client.go @@ -0,0 +1,25 @@ +package testutil + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic/fake" +) + +// NewFakeDynamicClient creates a fake dynamic client with additional custom types (i.e. Argo Rollouts) +func NewFakeDynamicClient() *fake.FakeDynamicClient { + scheme := runtime.NewScheme() + gvk := schema.GroupVersionKind{ + Group: "argoproj.io", + Version: "v1alpha1", + Kind: "Rollout", + } + scheme.AddKnownTypeWithName(gvk, &unstructured.Unstructured{}) + scheme.AddKnownTypeWithName(schema.GroupVersionKind{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind + "List", + }, &unstructured.UnstructuredList{}) + return fake.NewSimpleDynamicClient(scheme) +}