Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show current and ready replicas of revisions in get output #42

Merged
merged 9 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
e2e-kind:
runs-on: ubuntu-latest
env:
ARTIFACTS: artifacts
ARTIFACTS: ${{github.workspace}}/artifacts

steps:
- uses: actions/checkout@v4
Expand All @@ -26,5 +26,5 @@ jobs:
if: always()
with:
name: e2e-artifacts
path: artifacts
path: ${{env.ARTIFACTS}}
if-no-files-found: error
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ kind-up kind-down: export KUBECONFIG = $(KIND_KUBECONFIG)

.PHONY: kind-up
kind-up: $(KIND) $(KUBECTL) ## Launch a kind cluster for local development and testing.
$(KIND) create cluster --name revisions --image kindest/node:$(KIND_KUBERNETES_VERSION)
$(KIND) create cluster --name revisions --config hack/kind-config.yaml --image kindest/node:$(KIND_KUBERNETES_VERSION)
# workaround https://kind.sigs.k8s.io/docs/user/known-issues/#pod-errors-due-to-too-many-open-files
$(KUBECTL) get nodes -o name | cut -d/ -f2 | xargs -I {} docker exec {} sh -c "sysctl fs.inotify.max_user_instances=8192"
# run `export KUBECONFIG=$$PWD/hack/kind_kubeconfig.yaml` to target the created kind cluster.
Expand Down
Binary file modified docs/assets/diff-dyff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/diff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/get.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions hack/kind-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
# create a cluster with 3 worker nodes so that we can test DaemonSets with multiple replicas easily.
nodes:
- role: control-plane
- role: worker
- role: worker
- role: worker
13 changes: 13 additions & 0 deletions pkg/helper/helper_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package helper_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestHelper(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Helper Suite")
}
37 changes: 37 additions & 0 deletions pkg/helper/pod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package helper

import (
corev1 "k8s.io/api/core/v1"
)

// IsPodReady returns true if a pod is ready.
func IsPodReady(pod *corev1.Pod) bool {
condition := GetPodCondition(pod.Status.Conditions, corev1.PodReady)
return condition != nil && condition.Status == corev1.ConditionTrue
}

// GetPodCondition extracts the provided condition from the given conditions list.
// Returns nil if the condition is not present.
func GetPodCondition(conditions []corev1.PodCondition, conditionType corev1.PodConditionType) *corev1.PodCondition {
for i := range conditions {
if conditions[i].Type == conditionType {
return &conditions[i]
}
}
return nil
}

// SetPodCondition sets the provided condition in the Pod to the given status or adds the condition if it is missing.
func SetPodCondition(pod *corev1.Pod, conditionType corev1.PodConditionType, status corev1.ConditionStatus) {
condition := GetPodCondition(pod.Status.Conditions, conditionType)
if condition != nil {
condition.Status = status
return
}

condition = &corev1.PodCondition{
Type: conditionType,
Status: status,
}
pod.Status.Conditions = append(pod.Status.Conditions, *condition)
}
79 changes: 79 additions & 0 deletions pkg/helper/pod_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package helper_test

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"

. "github.com/timebertt/kubectl-revisions/pkg/helper"
)

var _ = Describe("Pod helpers", func() {
var pod *corev1.Pod

BeforeEach(func() {
pod = &corev1.Pod{}
})

Describe("IsPodReady", func() {
It("should return false if the Ready condition is missing", func() {
SetPodCondition(pod, corev1.PodInitialized, corev1.ConditionTrue)
Expect(IsPodReady(pod)).To(BeFalse())
})

It("should return false if the Ready condition is not true", func() {
SetPodCondition(pod, corev1.PodReady, corev1.ConditionUnknown)
Expect(IsPodReady(pod)).To(BeFalse())
SetPodCondition(pod, corev1.PodReady, corev1.ConditionFalse)
Expect(IsPodReady(pod)).To(BeFalse())
})
})

Describe("GetPodCondition", func() {
It("should return the condition if it is present", func() {
SetPodCondition(pod, corev1.PodInitialized, corev1.ConditionTrue)
SetPodCondition(pod, corev1.PodReady, corev1.ConditionTrue)
SetPodCondition(pod, corev1.ContainersReady, corev1.ConditionTrue)
Expect(GetPodCondition(pod.Status.Conditions, corev1.PodReady)).To(BeIdenticalTo(&pod.Status.Conditions[1]))
})

It("should return nil if the condition is missing", func() {
Expect(GetPodCondition(pod.Status.Conditions, corev1.PodReady)).To(BeNil())

SetPodCondition(pod, corev1.PodInitialized, corev1.ConditionTrue)
SetPodCondition(pod, corev1.ContainersReady, corev1.ConditionTrue)
Expect(GetPodCondition(pod.Status.Conditions, corev1.PodReady)).To(BeNil())
})
})

Describe("SetPodCondition", func() {
It("should set the condition status if it is present", func() {
pod.Status.Conditions = []corev1.PodCondition{
{Type: corev1.PodReady, Status: corev1.ConditionFalse},
{Type: corev1.ContainersReady, Status: corev1.ConditionFalse},
}

SetPodCondition(pod, corev1.PodReady, corev1.ConditionTrue)

Expect(pod.Status.Conditions).To(ConsistOf(
corev1.PodCondition{Type: corev1.PodReady, Status: corev1.ConditionTrue},
corev1.PodCondition{Type: corev1.ContainersReady, Status: corev1.ConditionFalse},
))
})

It("should add the condition status if it is present", func() {
pod.Status.Conditions = []corev1.PodCondition{
{Type: corev1.PodInitialized, Status: corev1.ConditionFalse},
{Type: corev1.ContainersReady, Status: corev1.ConditionFalse},
}

SetPodCondition(pod, corev1.PodReady, corev1.ConditionTrue)

Expect(pod.Status.Conditions).To(ConsistOf(
corev1.PodCondition{Type: corev1.PodInitialized, Status: corev1.ConditionFalse},
corev1.PodCondition{Type: corev1.ContainersReady, Status: corev1.ConditionFalse},
corev1.PodCondition{Type: corev1.PodReady, Status: corev1.ConditionTrue},
))
})
})
})
31 changes: 31 additions & 0 deletions pkg/history/controllerrevision.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package history

import (
"context"
"fmt"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -15,6 +18,8 @@ var _ Revision = &ControllerRevision{}
type ControllerRevision struct {
ControllerRevision *appsv1.ControllerRevision
Template *corev1.Pod

Replicas
}

// GetObjectKind implements runtime.Object.
Expand Down Expand Up @@ -56,3 +61,29 @@ func (c *ControllerRevision) Object() client.Object {
func (c *ControllerRevision) PodTemplate() *corev1.Pod {
return c.Template
}

// ListControllerRevisionsAndPods is a helper for a ControllerRevision-based History implementation that needs to find
// all ControllerRevisions and Pods belonging to a given workload object.
func ListControllerRevisionsAndPods(ctx context.Context, r client.Reader, namespace string, selector *metav1.LabelSelector) (*appsv1.ControllerRevisionList, *corev1.PodList, error) {
listOptions := &client.ListOptions{
Namespace: namespace,
}

var err error
listOptions.LabelSelector, err = metav1.LabelSelectorAsSelector(selector)
if err != nil {
return nil, nil, fmt.Errorf("error parsing selector: %w", err)
}

controllerRevisionList := &appsv1.ControllerRevisionList{}
if err := r.List(ctx, controllerRevisionList, listOptions); err != nil {
return nil, nil, fmt.Errorf("error listing ControllerRevisions: %w", err)
}

podList := &corev1.PodList{}
if err := r.List(ctx, podList, listOptions); err != nil {
return nil, nil, fmt.Errorf("error listing Pods: %w", err)
}

return controllerRevisionList, podList, nil
}
83 changes: 83 additions & 0 deletions pkg/history/controllerrevision_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package history_test

import (
"context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"

. "github.com/timebertt/kubectl-revisions/pkg/history"
)
Expand Down Expand Up @@ -43,6 +47,10 @@ var _ = Describe("ControllerRevision", func() {
rev = &ControllerRevision{
ControllerRevision: controllerRevision,
Template: template,
Replicas: Replicas{
Current: 2,
Ready: 1,
},
}
})

Expand Down Expand Up @@ -100,4 +108,79 @@ var _ = Describe("ControllerRevision", func() {
Expect(rev.PodTemplate()).To(Equal(rev.Template))
})
})

Describe("CurrentReplicas", func() {
It("should return the value of the Replicas.Current field", func() {
Expect(rev.CurrentReplicas()).To(Equal(rev.Replicas.Current))
})
})

Describe("ReadyReplicas", func() {
It("should return the value of the Replicas.Ready field", func() {
Expect(rev.ReadyReplicas()).To(Equal(rev.Replicas.Ready))
})
})
})

var _ = Describe("ListControllerRevisionsAndPods", func() {
var (
fakeClient client.Client

namespace string
selector *metav1.LabelSelector

revision *appsv1.ControllerRevision
pod *corev1.Pod
)

BeforeEach(func() {
namespace = "default"
labels := map[string]string{"app": "test"}
selector = &metav1.LabelSelector{MatchLabels: labels}

revision = &appsv1.ControllerRevision{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: namespace,
Labels: labels,
},
}

revisionOtherNamespace := revision.DeepCopy()
revisionOtherNamespace.Name += "-other"
revisionOtherNamespace.Namespace += "-other"

revisionUnrelated := revision.DeepCopy()
revisionUnrelated.Name += "-not-matching"
revisionUnrelated.Labels["app"] = "other"

pod = &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-0",
Namespace: namespace,
Labels: labels,
},
}

podOtherNamespace := pod.DeepCopy()
podOtherNamespace.Name += "-other"
podOtherNamespace.Namespace += "-other"

podUnrelated := pod.DeepCopy()
podUnrelated.Name += "-not-matching"
podUnrelated.Labels["app"] = "other"

fakeClient = fakeclient.NewFakeClient(
revision, revisionOtherNamespace, revisionUnrelated,
pod, podOtherNamespace, podUnrelated,
)
})

It("should return matching objects in the same namespace", func() {
controllerRevisionList, podList, err := ListControllerRevisionsAndPods(context.Background(), fakeClient, namespace, selector)
Expect(err).NotTo(HaveOccurred())

Expect(controllerRevisionList.Items).To(ConsistOf(*revision))
Expect(podList.Items).To(ConsistOf(*pod))
})
})
17 changes: 10 additions & 7 deletions pkg/history/daemonset.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,9 @@ func (d DaemonSetHistory) ListRevisions(ctx context.Context, key client.ObjectKe
return nil, err
}

selector, err := metav1.LabelSelectorAsSelector(daemonSet.Spec.Selector)
controllerRevisionList, podList, err := ListControllerRevisionsAndPods(ctx, d.Client, daemonSet.Namespace, daemonSet.Spec.Selector)
if err != nil {
return nil, fmt.Errorf("error parsing DaemonSet selector: %w", err)
}

controllerRevisionList := &appsv1.ControllerRevisionList{}
if err := d.Client.List(ctx, controllerRevisionList, client.InNamespace(daemonSet.Namespace), client.MatchingLabelsSelector{Selector: selector}); err != nil {
return nil, fmt.Errorf("error listing ControllerRevisions: %w", err)
return nil, err
}

var revs Revisions
Expand All @@ -47,6 +42,8 @@ func (d DaemonSetHistory) ListRevisions(ctx context.Context, key client.ObjectKe
return nil, fmt.Errorf("error converting ControllerRevision %s: %w", controllerRevision.Name, err)
}

revision.Replicas = CountReplicas(podList, PodBelongsToDaemonSetRevision(&controllerRevision))

revs = append(revs, revision)
}

Expand Down Expand Up @@ -80,3 +77,9 @@ func NewControllerRevisionForDaemonSet(controllerRevision *appsv1.ControllerRevi

return revision, nil
}

func PodBelongsToDaemonSetRevision(revision *appsv1.ControllerRevision) PodPredicate {
return func(pod *corev1.Pod) bool {
return pod.Labels[appsv1.DefaultDaemonSetUniqueLabelKey] == revision.Labels[appsv1.DefaultDaemonSetUniqueLabelKey]
}
}
Loading
Loading