Skip to content

Commit

Permalink
Implements rollout APIs for deployment (kubernetes#5917)
Browse files Browse the repository at this point in the history
* Implements rollout APIs for deployment

This implements REST APIs for `kubectl rollout deployment` command.
Also, implements frontend for `kubectl rollout restart deployment` command.

Co-authored-by: ZeHuaiWang <[email protected]>

* Implements REST APIs for pause and resume

Also, refactor followings:
- Change http method for rollout actions to PUT, following other actions.
- Define annotation keys as const.

* Remove MatDialogRef

Co-authored-by: ZeHuaiWang <[email protected]>
  • Loading branch information
shu-mutou and zehuaiWANG authored Apr 2, 2021
1 parent 421557f commit a64a975
Show file tree
Hide file tree
Showing 16 changed files with 2,838 additions and 2,142 deletions.
566 changes: 305 additions & 261 deletions i18n/de/messages.de.xlf

Large diffs are not rendered by default.

566 changes: 305 additions & 261 deletions i18n/fr/messages.fr.xlf

Large diffs are not rendered by default.

566 changes: 305 additions & 261 deletions i18n/ja/messages.ja.xlf

Large diffs are not rendered by default.

566 changes: 305 additions & 261 deletions i18n/ko/messages.ko.xlf

Large diffs are not rendered by default.

665 changes: 352 additions & 313 deletions i18n/messages.xlf

Large diffs are not rendered by default.

566 changes: 305 additions & 261 deletions i18n/zh-Hans/messages.zh-Hans.xlf

Large diffs are not rendered by default.

566 changes: 305 additions & 261 deletions i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf

Large diffs are not rendered by default.

566 changes: 305 additions & 261 deletions i18n/zh-Hant/messages.zh-Hant.xlf

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions src/app/backend/handler/apihandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,23 @@ func CreateHTTPAPIHandler(iManager integration.IntegrationManager, cManager clie
apiV1Ws.GET("/deployment/{namespace}/{deployment}/newreplicaset").
To(apiHandler.handleGetDeploymentNewReplicaSet).
Writes(replicaset.ReplicaSet{}))
apiV1Ws.Route(
apiV1Ws.PUT("/{kind}/{namespace}/{deployment}/pause").
To(apiHandler.handleDeploymentPause).
Writes(deployment.DeploymentDetail{}))
apiV1Ws.Route(
apiV1Ws.PUT("/{kind}/{namespace}/{deployment}/rollback").
To(apiHandler.handleDeploymentRollback).
Reads(deployment.RolloutSpec{}).
Writes(deployment.RolloutSpec{}))
apiV1Ws.Route(
apiV1Ws.PUT("/{kind}/{namespace}/{deployment}/restart").
To(apiHandler.handleDeploymentRestart).
Writes(deployment.RolloutSpec{}))
apiV1Ws.Route(
apiV1Ws.PUT("/{kind}/{namespace}/{deployment}/resume").
To(apiHandler.handleDeploymentResume).
Writes(deployment.DeploymentDetail{}))

apiV1Ws.Route(
apiV1Ws.PUT("/scale/{kind}/{namespace}/{name}/").
Expand Down Expand Up @@ -1253,6 +1270,79 @@ func (apiHandler *APIHandler) handleDeployFromFile(request *restful.Request, res
})
}

func (apiHandler *APIHandler) handleDeploymentPause(request *restful.Request, response *restful.Response) {
k8sClient, err := apiHandler.cManager.Client(request)
if err != nil {
errors.HandleInternalError(response, err)
return
}

namespace := request.PathParameter("namespace")
name := request.PathParameter("deployment")
deploymentSpec, err := deployment.PauseDeployment(k8sClient, namespace, name)
if err != nil {
errors.HandleInternalError(response, err)
return
}
response.WriteHeaderAndEntity(http.StatusOK, deploymentSpec)
}

func (apiHandler *APIHandler) handleDeploymentRollback(request *restful.Request, response *restful.Response) {
k8sClient, err := apiHandler.cManager.Client(request)
if err != nil {
errors.HandleInternalError(response, err)
return
}

rolloutSpec := new(deployment.RolloutSpec)
if err := request.ReadEntity(rolloutSpec); err != nil {
errors.HandleInternalError(response, err)
return
}
namespace := request.PathParameter("namespace")
name := request.PathParameter("deployment")
rolloutSpec, err = deployment.RollbackDeployment(k8sClient, rolloutSpec, namespace, name)
if err != nil {
errors.HandleInternalError(response, err)
return
}
response.WriteHeaderAndEntity(http.StatusOK, rolloutSpec)
}

func (apiHandler *APIHandler) handleDeploymentRestart(request *restful.Request, response *restful.Response) {
k8sClient, err := apiHandler.cManager.Client(request)
if err != nil {
errors.HandleInternalError(response, err)
return
}

namespace := request.PathParameter("namespace")
name := request.PathParameter("deployment")
rolloutSpec, err := deployment.RestartDeployment(k8sClient, namespace, name)
if err != nil {
errors.HandleInternalError(response, err)
return
}
response.WriteHeaderAndEntity(http.StatusOK, rolloutSpec)
}

func (apiHandler *APIHandler) handleDeploymentResume(request *restful.Request, response *restful.Response) {
k8sClient, err := apiHandler.cManager.Client(request)
if err != nil {
errors.HandleInternalError(response, err)
return
}

namespace := request.PathParameter("namespace")
name := request.PathParameter("deployment")
deploymentSpec, err := deployment.ResumeDeployment(k8sClient, namespace, name)
if err != nil {
errors.HandleInternalError(response, err)
return
}
response.WriteHeaderAndEntity(http.StatusOK, deploymentSpec)
}

func (apiHandler *APIHandler) handleNameValidity(request *restful.Request, response *restful.Response) {
k8sClient, err := apiHandler.cManager.Client(request)
if err != nil {
Expand Down
149 changes: 149 additions & 0 deletions src/app/backend/resource/deployment/rollout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// 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 deployment

import (
"context"
"errors"
"time"

v1 "k8s.io/api/apps/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
client "k8s.io/client-go/kubernetes"
)

const (
// FirstRevision is a first revision number
FirstRevision = "1"
// RestartedAtAnnotationKey is an annotation key for rollout restart
RestartedAtAnnotationKey = "kubectl.kubernetes.io/restartedAt"
// RevisionAnnotationKey is an annotation key for rollout targeted or resulted revision
RevisionAnnotationKey = "deployment.kubernetes.io/revision"
)

// RolloutSpec is a specification for deployment rollout
type RolloutSpec struct {
// Revision is the requested/resulted revision number of the ReplicateSet to rollback.
Revision string `json:"revision"`
}

// RollbackDeployment rollback to a specific ReplicaSet revision
func RollbackDeployment(client client.Interface, rolloutSpec *RolloutSpec, namespace, name string) (*RolloutSpec, error) {
deployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), name, metaV1.GetOptions{})
if err != nil {
return nil, err
}
currRevision := deployment.Annotations[RevisionAnnotationKey]
if currRevision == FirstRevision {
return nil, errors.New("No revision for rolling back ")
}
matchRS, err := GetReplicaSetFromDeployment(client, namespace, name)
if err != nil {
return nil, err
}
for _, rs := range matchRS {
if rs.Annotations[RevisionAnnotationKey] == rolloutSpec.Revision {
updateDeployment := deployment.DeepCopy()
updateDeployment.Spec.Template.Spec = rs.Spec.Template.Spec
res, err := client.AppsV1().Deployments(namespace).Update(context.TODO(), updateDeployment, metaV1.UpdateOptions{})
if err != nil {
return nil, err
}
return &RolloutSpec{
Revision: res.Annotations[RevisionAnnotationKey],
}, nil
}
}
return nil, errors.New("There is no ReplicaSet that has the requested revision for the Deployment.")
}

// PauseDeployment is used to pause a deployment
func PauseDeployment(client client.Interface, namespace, name string) (*v1.Deployment, error) {
deployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), name, metaV1.GetOptions{})
if err != nil {
return nil, err
}
if !deployment.Spec.Paused {
deployment.Spec.Paused = true
_, err = client.AppsV1().Deployments(namespace).Update(context.TODO(), deployment, metaV1.UpdateOptions{})
if err != nil {
return nil, err
}
return deployment, nil
}
return nil, errors.New("The Deployment is already paused.")
}

// ResumeDeployment is used to resume a deployment
func ResumeDeployment(client client.Interface, namespace, name string) (*v1.Deployment, error) {
deployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), name, metaV1.GetOptions{})
if err != nil {
return nil, err
}
if deployment.Spec.Paused {
deployment.Spec.Paused = false
_, err = client.AppsV1().Deployments(namespace).Update(context.TODO(), deployment, metaV1.UpdateOptions{})
if err != nil {
return nil, err
}
return deployment, nil
}
return nil, errors.New("The deployment is already resumed.")
}

// RestartDeployment restarts a deployment in the manner of `kubectl rollout restart`.
func RestartDeployment(client client.Interface, namespace, name string) (*RolloutSpec, error) {
deployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), name, metaV1.GetOptions{})
if err != nil {
return nil, err
}

if deployment.Spec.Template.ObjectMeta.Annotations == nil {
deployment.Spec.Template.ObjectMeta.Annotations = map[string]string{}
}
deployment.Spec.Template.ObjectMeta.Annotations[RestartedAtAnnotationKey] = time.Now().Format(time.RFC3339)
res, err := client.AppsV1().Deployments(namespace).Update(context.TODO(), deployment, metaV1.UpdateOptions{})
if err != nil {
return nil, err
}
return &RolloutSpec{
Revision: res.Annotations[RevisionAnnotationKey],
}, nil
}

// GetReplicaSetFromDeployment return all replicaSet which is belong to the deployment
func GetReplicaSetFromDeployment(client client.Interface, namespace, name string) ([]v1.ReplicaSet, error) {
deployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), name, metaV1.GetOptions{})
if err != nil {
return nil, err
}

selector, err := metaV1.LabelSelectorAsSelector(deployment.Spec.Selector)
if err != nil {
return nil, err
}
options := metaV1.ListOptions{LabelSelector: selector.String()}
allRS, err := client.AppsV1().ReplicaSets(namespace).List(context.TODO(), options)
if err != nil {
return nil, err
}
var result []v1.ReplicaSet
for _, rs := range allRS.Items {
if metaV1.IsControlledBy(&rs, deployment) {
result = append(result, rs)
}
}
return result, nil
}
11 changes: 9 additions & 2 deletions src/app/frontend/common/components/list/column/menu/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ const loggableResources: string[] = [
];

const pinnableResources: string[] = [Resource.crdFull];

const executableResources: string[] = [Resource.pod];

const triggerableResources: string[] = [Resource.cronJob];
const restartableResources: string[] = [Resource.deployment];

@Component({
selector: 'kd-resource-context-menu',
Expand Down Expand Up @@ -127,6 +126,14 @@ export class MenuComponent implements ActionColumn {
this.verber_.showEditDialog(this.typeMeta.kind, this.typeMeta, this.objectMeta);
}

isRestartEnabled(): boolean {
return restartableResources.includes(this.typeMeta.kind);
}

onRestart(): void {
this.verber_.showRestartDialog(this.typeMeta.kind, this.typeMeta, this.objectMeta);
}

onDelete(): void {
this.verber_.showDeleteDialog(this.typeMeta.kind, this.typeMeta, this.objectMeta);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
id="edit"
(click)="onEdit()"
i18n>Edit</button>
<button mat-menu-item
*ngIf="isRestartEnabled()"
(click)="onRestart()"
i18n>Restart</button>
<button mat-menu-item
id="delete"
(click)="onDelete()"
Expand Down
4 changes: 4 additions & 0 deletions src/app/frontend/common/dialogs/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {AlertDialog} from './alert/dialog';
import {DeleteResourceDialog} from './deleteresource/dialog';
import {LogsDownloadDialog} from './download/dialog';
import {EditResourceDialog} from './editresource/dialog';
import {RestartResourceDialog} from './restartresource/dialog';
import {ScaleResourceDialog} from './scaleresource/dialog';
import {TriggerResourceDialog} from './triggerresource/dialog';

Expand All @@ -31,6 +32,7 @@ import {TriggerResourceDialog} from './triggerresource/dialog';
EditResourceDialog,
DeleteResourceDialog,
LogsDownloadDialog,
RestartResourceDialog,
ScaleResourceDialog,
TriggerResourceDialog,
],
Expand All @@ -39,6 +41,7 @@ import {TriggerResourceDialog} from './triggerresource/dialog';
EditResourceDialog,
DeleteResourceDialog,
LogsDownloadDialog,
RestartResourceDialog,
ScaleResourceDialog,
TriggerResourceDialog,
],
Expand All @@ -47,6 +50,7 @@ import {TriggerResourceDialog} from './triggerresource/dialog';
EditResourceDialog,
DeleteResourceDialog,
LogsDownloadDialog,
RestartResourceDialog,
ScaleResourceDialog,
TriggerResourceDialog,
],
Expand Down
25 changes: 25 additions & 0 deletions src/app/frontend/common/dialogs/restartresource/dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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.

import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA} from '@angular/material/dialog';
import {ResourceMeta} from '../../services/global/actionbar';

@Component({
selector: 'kd-restart-resource-dialog',
templateUrl: 'template.html',
})
export class RestartResourceDialog {
constructor(@Inject(MAT_DIALOG_DATA) public data: ResourceMeta) {}
}
Loading

0 comments on commit a64a975

Please sign in to comment.