Skip to content

Commit 1fac58a

Browse files
authored
feat: Pod Disruption Budget API (#10009)
* start adding pod disruption budget api * add resource channel * update common funcs * update get and list * update handlers * add status fields * update tests * fix tests * update schema * update schema * make fix
1 parent abba0ef commit 1fac58a

19 files changed

+1442
-301
lines changed

modules/api/pkg/handler/apihandler.go

+61
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
v1 "k8s.io/api/core/v1"
2424
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
25+
"k8s.io/dashboard/api/pkg/resource/poddisruptionbudget"
2526
"k8s.io/klog/v2"
2627

2728
"k8s.io/dashboard/api/pkg/resource/networkpolicy"
@@ -1120,6 +1121,32 @@ func CreateHTTPAPIHandler(iManager integration.Manager) (*restful.Container, err
11201121
Writes(persistentvolumeclaim.PersistentVolumeClaimDetail{}).
11211122
Returns(http.StatusOK, "OK", persistentvolumeclaim.PersistentVolumeClaimDetail{}))
11221123

1124+
// PodDisruptionBudget
1125+
apiV1Ws.Route(
1126+
apiV1Ws.GET("/poddisruptionbudget/").
1127+
To(apiHandler.handleGetPodDisruptionBudgetList).
1128+
// docs
1129+
Doc("returns a list of PodDisruptionBudget").
1130+
Writes(poddisruptionbudget.PodDisruptionBudgetList{}).
1131+
Returns(http.StatusOK, "OK", poddisruptionbudget.PodDisruptionBudgetList{}))
1132+
apiV1Ws.Route(
1133+
apiV1Ws.GET("/poddisruptionbudget/{namespace}").
1134+
To(apiHandler.handleGetPodDisruptionBudgetList).
1135+
// docs
1136+
Doc("returns a list of PodDisruptionBudget from specified namespace").
1137+
Param(apiV1Ws.PathParameter("namespace", "namespace of the PodDisruptionBudget")).
1138+
Writes(poddisruptionbudget.PodDisruptionBudgetList{}).
1139+
Returns(http.StatusOK, "OK", poddisruptionbudget.PodDisruptionBudgetList{}))
1140+
apiV1Ws.Route(
1141+
apiV1Ws.GET("/poddisruptionbudget/{namespace}/{name}").
1142+
To(apiHandler.handleGetPodDisruptionBudgetDetail).
1143+
// docs
1144+
Doc("returns detailed information about PodDisruptionBudget").
1145+
Param(apiV1Ws.PathParameter("name", "name of the PodDisruptionBudget")).
1146+
Param(apiV1Ws.PathParameter("namespace", "namespace of the PodDisruptionBudget")).
1147+
Writes(poddisruptionbudget.PodDisruptionBudgetDetail{}).
1148+
Returns(http.StatusOK, "OK", poddisruptionbudget.PodDisruptionBudgetDetail{}))
1149+
11231150
// CRD
11241151
apiV1Ws.Route(
11251152
apiV1Ws.GET("/crd").
@@ -2674,6 +2701,40 @@ func (apiHandler *APIHandler) handleGetPersistentVolumeClaimDetail(request *rest
26742701
_ = response.WriteHeaderAndEntity(http.StatusOK, result)
26752702
}
26762703

2704+
func (apiHandler *APIHandler) handleGetPodDisruptionBudgetList(request *restful.Request, response *restful.Response) {
2705+
k8sClient, err := client.Client(request.Request)
2706+
if err != nil {
2707+
errors.HandleInternalError(response, err)
2708+
return
2709+
}
2710+
2711+
namespace := parseNamespacePathParameter(request)
2712+
dataSelect := parser.ParseDataSelectPathParameter(request)
2713+
result, err := poddisruptionbudget.List(k8sClient, namespace, dataSelect)
2714+
if err != nil {
2715+
errors.HandleInternalError(response, err)
2716+
return
2717+
}
2718+
_ = response.WriteHeaderAndEntity(http.StatusOK, result)
2719+
}
2720+
2721+
func (apiHandler *APIHandler) handleGetPodDisruptionBudgetDetail(request *restful.Request, response *restful.Response) {
2722+
k8sClient, err := client.Client(request.Request)
2723+
if err != nil {
2724+
errors.HandleInternalError(response, err)
2725+
return
2726+
}
2727+
2728+
namespace := request.PathParameter("namespace")
2729+
name := request.PathParameter("name")
2730+
result, err := poddisruptionbudget.Get(k8sClient, namespace, name)
2731+
if err != nil {
2732+
errors.HandleInternalError(response, err)
2733+
return
2734+
}
2735+
_ = response.WriteHeaderAndEntity(http.StatusOK, result)
2736+
}
2737+
26772738
func (apiHandler *APIHandler) handleGetPodContainers(request *restful.Request, response *restful.Response) {
26782739
k8sClient, err := client.Client(request.Request)
26792740
if err != nil {

modules/api/pkg/resource/common/resourcechannels.go

+27
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
batch "k8s.io/api/batch/v1"
2323
v1 "k8s.io/api/core/v1"
2424
networkingv1 "k8s.io/api/networking/v1"
25+
policyv1 "k8s.io/api/policy/v1"
2526
rbac "k8s.io/api/rbac/v1"
2627
storage "k8s.io/api/storage/v1"
2728
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -124,6 +125,8 @@ type ResourceChannels struct {
124125

125126
// List and error channels to ClusterRoleBindings
126127
ClusterRoleBindingList ClusterRoleBindingListChannel
128+
129+
PodDisruptionBudget PodDisruptionBudgetListChannel
127130
}
128131

129132
// ServiceListChannel is a list and error channels to Services.
@@ -841,6 +844,30 @@ func GetPersistentVolumeClaimListChannel(client client.Interface, nsQuery *Names
841844
return channel
842845
}
843846

847+
type PodDisruptionBudgetListChannel struct {
848+
List chan *policyv1.PodDisruptionBudgetList
849+
Error chan error
850+
}
851+
852+
func GetPodDisruptionBudgetListChannel(client client.Interface, nsQuery *NamespaceQuery,
853+
numReads int) PodDisruptionBudgetListChannel {
854+
855+
channel := PodDisruptionBudgetListChannel{
856+
List: make(chan *policyv1.PodDisruptionBudgetList, numReads),
857+
Error: make(chan error, numReads),
858+
}
859+
860+
go func() {
861+
list, err := client.PolicyV1().PodDisruptionBudgets(nsQuery.ToRequestParam()).List(context.TODO(), helpers.ListEverything)
862+
for i := 0; i < numReads; i++ {
863+
channel.List <- list
864+
channel.Error <- err
865+
}
866+
}()
867+
868+
return channel
869+
}
870+
844871
// CustomResourceDefinitionChannelV1 is a list and error channels to CustomResourceDefinition.
845872
type CustomResourceDefinitionChannelV1 struct {
846873
List chan *apiextensions.CustomResourceDefinitionList
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2017 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package poddisruptionbudget
16+
17+
import (
18+
policyv1 "k8s.io/api/policy/v1"
19+
20+
"k8s.io/dashboard/api/pkg/resource/dataselect"
21+
)
22+
23+
type PodDisruptionBudgetCell policyv1.PodDisruptionBudget
24+
25+
func (self PodDisruptionBudgetCell) GetProperty(name dataselect.PropertyName) dataselect.ComparableValue {
26+
switch name {
27+
case dataselect.NameProperty:
28+
return dataselect.StdComparableString(self.ObjectMeta.Name)
29+
case dataselect.CreationTimestampProperty:
30+
return dataselect.StdComparableTime(self.ObjectMeta.CreationTimestamp.Time)
31+
case dataselect.NamespaceProperty:
32+
return dataselect.StdComparableString(self.ObjectMeta.Namespace)
33+
default:
34+
// if name is not supported then just return a constant dummy value, sort will have no effect.
35+
return nil
36+
}
37+
}
38+
39+
func toCells(std []policyv1.PodDisruptionBudget) []dataselect.DataCell {
40+
cells := make([]dataselect.DataCell, len(std))
41+
for i := range std {
42+
cells[i] = PodDisruptionBudgetCell(std[i])
43+
}
44+
return cells
45+
}
46+
47+
func fromCells(cells []dataselect.DataCell) []policyv1.PodDisruptionBudget {
48+
std := make([]policyv1.PodDisruptionBudget, len(cells))
49+
for i := range std {
50+
std[i] = policyv1.PodDisruptionBudget(cells[i].(PodDisruptionBudgetCell))
51+
}
52+
return std
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2017 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package poddisruptionbudget
16+
17+
import (
18+
"context"
19+
20+
policyv1 "k8s.io/api/policy/v1"
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
23+
"k8s.io/client-go/kubernetes"
24+
)
25+
26+
type PodDisruptionBudgetDetail struct {
27+
PodDisruptionBudget `json:",inline"`
28+
DisruptedPods map[string]metav1.Time `json:"disruptedPods"`
29+
}
30+
31+
func Get(client kubernetes.Interface, namespace string, name string) (*PodDisruptionBudgetDetail, error) {
32+
pdb, err := client.PolicyV1().PodDisruptionBudgets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
return toDetails(*pdb), nil
38+
}
39+
40+
func toDetails(pdb policyv1.PodDisruptionBudget) *PodDisruptionBudgetDetail {
41+
return &PodDisruptionBudgetDetail{
42+
PodDisruptionBudget: toListItem(pdb),
43+
DisruptedPods: pdb.Status.DisruptedPods,
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2017 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package poddisruptionbudget
16+
17+
import (
18+
"reflect"
19+
"testing"
20+
21+
"github.com/samber/lo"
22+
policyv1 "k8s.io/api/policy/v1"
23+
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/util/intstr"
25+
"k8s.io/dashboard/types"
26+
)
27+
28+
func TestToDetails(t *testing.T) {
29+
cases := []struct {
30+
resource *policyv1.PodDisruptionBudget
31+
expected *PodDisruptionBudgetDetail
32+
}{
33+
{
34+
&policyv1.PodDisruptionBudget{
35+
ObjectMeta: metaV1.ObjectMeta{Name: "foo", Namespace: "bar"},
36+
TypeMeta: metaV1.TypeMeta{Kind: types.ResourceKindPodDisruptionBudget},
37+
Spec: policyv1.PodDisruptionBudgetSpec{
38+
MinAvailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1},
39+
MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 3},
40+
UnhealthyPodEvictionPolicy: lo.ToPtr(policyv1.IfHealthyBudget),
41+
},
42+
Status: policyv1.PodDisruptionBudgetStatus{
43+
CurrentHealthy: 10,
44+
DesiredHealthy: 10,
45+
ExpectedPods: 10,
46+
DisruptedPods: make(map[string]metaV1.Time),
47+
DisruptionsAllowed: 0,
48+
},
49+
},
50+
&PodDisruptionBudgetDetail{
51+
PodDisruptionBudget: PodDisruptionBudget{
52+
ObjectMeta: types.ObjectMeta{Name: "foo", Namespace: "bar"},
53+
TypeMeta: types.TypeMeta{Kind: types.ResourceKindPodDisruptionBudget},
54+
MinAvailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1},
55+
MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 3},
56+
UnhealthyPodEvictionPolicy: lo.ToPtr(policyv1.IfHealthyBudget),
57+
CurrentHealthy: 10,
58+
DesiredHealthy: 10,
59+
ExpectedPods: 10,
60+
DisruptionsAllowed: 0,
61+
},
62+
DisruptedPods: make(map[string]metaV1.Time),
63+
},
64+
},
65+
}
66+
for _, c := range cases {
67+
actual := toDetails(*c.resource)
68+
if !reflect.DeepEqual(actual, c.expected) {
69+
t.Errorf("toDetails(%#v) == \n%#v\nexpected \n%#v\n",
70+
c.resource, actual, c.expected)
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)