diff --git a/modules/api/pkg/handler/apihandler.go b/modules/api/pkg/handler/apihandler.go index f863babaffd7..e383fa73edf8 100644 --- a/modules/api/pkg/handler/apihandler.go +++ b/modules/api/pkg/handler/apihandler.go @@ -454,6 +454,15 @@ func CreateHTTPAPIHandler(iManager integration.Manager) (*restful.Container, err Param(apiV1Ws.PathParameter("daemonSet", "name of the DaemonSet")). Writes(common.EventList{}). Returns(http.StatusOK, "OK", common.EventList{})) + apiV1Ws.Route( + apiV1Ws.PUT("/daemonset/{namespace}/{daemonSet}/restart").To(apiHandler.handleDaemonSetRestart). + // docs + Doc("rollout restart of the Daemon Set"). + Param(apiV1Ws.PathParameter("namespace", "namespace of the Daemon Set")). + Param(apiV1Ws.PathParameter("daemonSet", "name of the Daemon Set")). + Writes(deployment.RolloutSpec{}). + Returns(http.StatusOK, "OK", daemonset.DaemonSetDetail{}), + ) // HorizontalPodAutoscaler apiV1Ws.Route( @@ -846,6 +855,15 @@ func CreateHTTPAPIHandler(iManager integration.Manager) (*restful.Container, err Param(apiV1Ws.PathParameter("statefulset", "name of the StatefulSet")). Writes(common.EventList{}). Returns(http.StatusOK, "OK", common.EventList{})) + apiV1Ws.Route( + apiV1Ws.PUT("/statefulset/{namespace}/{statefulset}/restart").To(apiHandler.handleStatefulSetRestart). + // docs + Doc("rollout restart of the Daemon Set"). + Param(apiV1Ws.PathParameter("namespace", "namespace of the StatefulSet")). + Param(apiV1Ws.PathParameter("statefulset", "name of the StatefulSet")). + Writes(deployment.RolloutSpec{}). + Returns(http.StatusOK, "OK", statefulset.StatefulSetDetail{}), + ) // Node apiV1Ws.Route( @@ -2801,6 +2819,40 @@ func (apiHandler *APIHandler) handleGetDaemonSetEvents(request *restful.Request, _ = response.WriteHeaderAndEntity(http.StatusOK, result) } +func (apiHandle *APIHandler) handleDaemonSetRestart(request *restful.Request, response *restful.Response) { + k8sClient, err := client.Client(request.Request) + if err != nil { + errors.HandleInternalError(response, err) + return + } + + namespace := request.PathParameter("namespace") + name := request.PathParameter("daemonSet") + result, err := daemonset.RestartDaemonSet(k8sClient, namespace, name) + if err != nil { + errors.HandleInternalError(response, err) + return + } + _ = response.WriteHeaderAndEntity(http.StatusOK, result) +} + +func (apiHandle *APIHandler) handleStatefulSetRestart(request *restful.Request, response *restful.Response) { + k8sClient, err := client.Client(request.Request) + if err != nil { + errors.HandleInternalError(response, err) + return + } + + namespace := request.PathParameter("namespace") + name := request.PathParameter("daemonSet") + result, err := statefulset.RestartStatefulSet(k8sClient, namespace, name) + if err != nil { + errors.HandleInternalError(response, err) + return + } + _ = response.WriteHeaderAndEntity(http.StatusOK, result) +} + func (apiHandler *APIHandler) handleGetHorizontalPodAutoscalerList(request *restful.Request, response *restful.Response) { k8sClient, err := client.Client(request.Request) diff --git a/modules/api/pkg/resource/daemonset/list_test.go b/modules/api/pkg/resource/daemonset/list_test.go index d5936a82fb3d..29c0c13d2a3d 100644 --- a/modules/api/pkg/resource/daemonset/list_test.go +++ b/modules/api/pkg/resource/daemonset/list_test.go @@ -106,7 +106,7 @@ func TestGetDaemonSetListFromChannels(t *testing.T) { Labels: map[string]string{"key": "value"}, CreationTimestamp: metaV1.Unix(111, 222), }, - TypeMeta: types.TypeMeta{Kind: types.ResourceKindDaemonSet}, + TypeMeta: types.TypeMeta{Kind: types.ResourceKindDaemonSet, Restartable: true}, Pods: common.PodInfo{ Current: 0, Failed: 0, @@ -351,7 +351,7 @@ func TestToDaemonSetList(t *testing.T) { Namespace: "namespace-1", UID: "uid-1", }, - TypeMeta: types.TypeMeta{Kind: types.ResourceKindDaemonSet}, + TypeMeta: types.TypeMeta{Kind: types.ResourceKindDaemonSet, Restartable: true}, ContainerImages: []string{"my-container-image-1"}, InitContainerImages: []string{"my-init-container-image-1"}, Pods: common.PodInfo{ @@ -367,7 +367,7 @@ func TestToDaemonSetList(t *testing.T) { Name: "my-app-2", Namespace: "namespace-2", }, - TypeMeta: types.TypeMeta{Kind: types.ResourceKindDaemonSet}, + TypeMeta: types.TypeMeta{Kind: types.ResourceKindDaemonSet, Restartable: true}, ContainerImages: []string{"my-container-image-2"}, InitContainerImages: []string{"my-init-container-image-2"}, Pods: common.PodInfo{ @@ -379,7 +379,7 @@ func TestToDaemonSetList(t *testing.T) { Name: "my-app-3", Namespace: "namespace-3", }, - TypeMeta: types.TypeMeta{Kind: types.ResourceKindDaemonSet}, + TypeMeta: types.TypeMeta{Kind: types.ResourceKindDaemonSet, Restartable: true}, ContainerImages: []string{"my-container-image-3"}, InitContainerImages: []string{"my-init-container-image-3"}, Pods: common.PodInfo{ diff --git a/modules/api/pkg/resource/daemonset/restart.go b/modules/api/pkg/resource/daemonset/restart.go new file mode 100644 index 000000000000..e306e90e57b9 --- /dev/null +++ b/modules/api/pkg/resource/daemonset/restart.go @@ -0,0 +1,43 @@ +// Copyright 2017 The Kubernetes Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package daemonset + +import ( + "context" + "time" + + v1 "k8s.io/api/apps/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + client "k8s.io/client-go/kubernetes" +) + +const ( + // RestartedAtAnnotationKey is an annotation key for rollout restart + RestartedAtAnnotationKey = "kubectl.kubernetes.io/restartedAt" +) + +// RestartDaemonSet restarts a daemon set in the manner of `kubectl rollout restart`. +func RestartDaemonSet(client client.Interface, namespace, name string) (*v1.DaemonSet, error) { + daemonSet, err := client.AppsV1().DaemonSets(namespace).Get(context.TODO(), name, metaV1.GetOptions{}) + if err != nil { + return nil, err + } + + if daemonSet.Spec.Template.ObjectMeta.Annotations == nil { + daemonSet.Spec.Template.ObjectMeta.Annotations = map[string]string{} + } + daemonSet.Spec.Template.ObjectMeta.Annotations[RestartedAtAnnotationKey] = time.Now().Format(time.RFC3339) + return client.AppsV1().DaemonSets(namespace).Update(context.TODO(), daemonSet, metaV1.UpdateOptions{}) +} diff --git a/modules/api/pkg/resource/statefulset/list_test.go b/modules/api/pkg/resource/statefulset/list_test.go index df393da3b2df..0582df97a7f2 100644 --- a/modules/api/pkg/resource/statefulset/list_test.go +++ b/modules/api/pkg/resource/statefulset/list_test.go @@ -148,8 +148,9 @@ func TestGetStatefulSetListFromChannels(t *testing.T) { CreationTimestamp: metaV1.Unix(111, 222), }, TypeMeta: types.TypeMeta{ - Kind: types.ResourceKindStatefulSet, - Scalable: true, + Kind: types.ResourceKindStatefulSet, + Scalable: true, + Restartable: true, }, Pods: common.PodInfo{ Current: 7, diff --git a/modules/api/pkg/resource/statefulset/restart.go b/modules/api/pkg/resource/statefulset/restart.go new file mode 100644 index 000000000000..a98574d68f10 --- /dev/null +++ b/modules/api/pkg/resource/statefulset/restart.go @@ -0,0 +1,43 @@ +// Copyright 2017 The Kubernetes Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package statefulset + +import ( + "context" + "time" + + v1 "k8s.io/api/apps/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + client "k8s.io/client-go/kubernetes" +) + +const ( + // RestartedAtAnnotationKey is an annotation key for rollout restart + RestartedAtAnnotationKey = "kubectl.kubernetes.io/restartedAt" +) + +// RestartStatefulSet restarts a daemon set in the manner of `kubectl rollout restart`. +func RestartStatefulSet(client client.Interface, namespace, name string) (*v1.StatefulSet, error) { + statefulSet, err := client.AppsV1().StatefulSets(namespace).Get(context.TODO(), name, metaV1.GetOptions{}) + if err != nil { + return nil, err + } + + if statefulSet.Spec.Template.ObjectMeta.Annotations == nil { + statefulSet.Spec.Template.ObjectMeta.Annotations = map[string]string{} + } + statefulSet.Spec.Template.ObjectMeta.Annotations[RestartedAtAnnotationKey] = time.Now().Format(time.RFC3339) + return client.AppsV1().StatefulSets(namespace).Update(context.TODO(), statefulSet, metaV1.UpdateOptions{}) +} diff --git a/modules/common/types/resourcekind.go b/modules/common/types/resourcekind.go index 68946c62818d..39ca1e1ef802 100644 --- a/modules/common/types/resourcekind.go +++ b/modules/common/types/resourcekind.go @@ -75,6 +75,8 @@ func (k ResourceKind) Scalable() bool { func (k ResourceKind) Restartable() bool { restartable := []ResourceKind{ ResourceKindDeployment, + ResourceKindDaemonSet, + ResourceKindStatefulSet, } for _, kind := range restartable {