/*
Copyright 2016 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"
	"fmt"
	"reflect"
	"time"

	apps "k8s.io/api/apps/v1"
	"k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/kubernetes/pkg/controller/deployment/util"
)

// syncRolloutStatus updates the status of a deployment during a rollout. There are
// cases this helper will run that cannot be prevented from the scaling detection,
// for example a resync of the deployment after it was scaled up. In those cases,
// we shouldn't try to estimate any progress.
func (dc *DeploymentController) syncRolloutStatus(ctx context.Context, allRSs []*apps.ReplicaSet, newRS *apps.ReplicaSet, d *apps.Deployment) error {
	newStatus := calculateStatus(allRSs, newRS, d)

	// If there is no progressDeadlineSeconds set, remove any Progressing condition.
	if !util.HasProgressDeadline(d) {
		util.RemoveDeploymentCondition(&newStatus, apps.DeploymentProgressing)
	}
	//该函数用于更新部署期间的部署状态。在某些情况下,无法从缩放检测中防止此帮助程序运行,例如在部署缩放后进行同步。
	//在这些情况下,不应尝试估计任何进度。
	//函数首先计算所有复制集、新复制集和部署的状态,然后如果未设置进度截止时间,则删除任何正在进行的条件。

	// If there is only one replica set that is active then that means we are not running
	// a new rollout and this is a resync where we don't need to estimate any progress.
	// In such a case, we should simply not estimate any progress for this deployment.
	currentCond := util.GetDeploymentCondition(d.Status, apps.DeploymentProgressing)
	isCompleteDeployment := newStatus.Replicas == newStatus.UpdatedReplicas && currentCond != nil && currentCond.Reason == util.NewRSAvailableReason
	// Check for progress only if there is a progress deadline set and the latest rollout
	// hasn't completed yet.
	if util.HasProgressDeadline(d) && !isCompleteDeployment {
		switch {
		case util.DeploymentComplete(d, &newStatus):
			// Update the deployment conditions with a message for the new replica set that
			// was successfully deployed. If the condition already exists, we ignore this update.
			msg := fmt.Sprintf("Deployment %q has successfully progressed.", d.Name)
			if newRS != nil {
				msg = fmt.Sprintf("ReplicaSet %q has successfully progressed.", newRS.Name)
			}
			condition := util.NewDeploymentCondition(apps.DeploymentProgressing, v1.ConditionTrue, util.NewRSAvailableReason, msg)
			util.SetDeploymentCondition(&newStatus, *condition)

		case util.DeploymentProgressing(d, &newStatus):
			// If there is any progress made, continue by not checking if the deployment failed. This
			// behavior emulates the rolling updater progressDeadline check.
			msg := fmt.Sprintf("Deployment %q is progressing.", d.Name)
			if newRS != nil {
				msg = fmt.Sprintf("ReplicaSet %q is progressing.", newRS.Name)
			}
			condition := util.NewDeploymentCondition(apps.DeploymentProgressing, v1.ConditionTrue, util.ReplicaSetUpdatedReason, msg)
			//这段Go代码是用于检查和更新Kubernetes部署(Deployment)的进度状态的。具体来说,它根据以下条件来判断是否需要更新部署的进度状态:
			//1. 如果当前只有一个活动的副本集(replica set),则认为这不是一个新的滚动发布(rollout),而是一个重新同步(resync),
			//在这种情况下,不需要估计任何进度,因此直接不估计该部署的进度。
			//2. 如果设置了进度截止时间(progress deadline)并且最新滚动发布尚未完成,则检查部署的进度。
			//- 如果部署已完成,则更新部署状态,设置进度状态为成功,并添加相应的消息。
			//- 如果部署正在进展中,则更新部署状态,设置进度状态为正在进展,并添加相应的消息。
			//这段代码通过调用util包中的一系列函数来实现对部署进度的检查和更新,其中涉及到对部署条件(condition)的获取、设置和更新等操作。

			// Update the current Progressing condition or add a new one if it doesn't exist.
			// If a Progressing condition with status=true already exists, we should update
			// everything but lastTransitionTime. SetDeploymentCondition already does that but
			// it also is not updating conditions when the reason of the new condition is the
			// same as the old. The Progressing condition is a special case because we want to
			// update with the same reason and change just lastUpdateTime iff we notice any
			// progress. That's why we handle it here.
			if currentCond != nil {
				if currentCond.Status == v1.ConditionTrue {
					condition.LastTransitionTime = currentCond.LastTransitionTime
				}
				util.RemoveDeploymentCondition(&newStatus, apps.DeploymentProgressing)
			}
			util.SetDeploymentCondition(&newStatus, *condition)

		case util.DeploymentTimedOut(ctx, d, &newStatus):
			// Update the deployment with a timeout condition. If the condition already exists,
			// we ignore this update.
			msg := fmt.Sprintf("Deployment %q has timed out progressing.", d.Name)
			if newRS != nil {
				msg = fmt.Sprintf("ReplicaSet %q has timed out progressing.", newRS.Name)
			}
			condition := util.NewDeploymentCondition(apps.DeploymentProgressing, v1.ConditionFalse, util.TimedOutReason, msg)
			util.SetDeploymentCondition(&newStatus, *condition)
		}
		//该代码片段是Go语言编写的,它包含两个分支,分别处理不同的条件。
		//第一个分支更新或添加一个正在进行的条件。如果已经存在一个状态为true的正在进行的条件,那么只更新除lastTransitionTime之外的所有内容。
		//这通过调用util.RemoveDeploymentCondition和util.SetDeploymentCondition函数来实现。
		//第二个分支处理超时条件。如果部署或新ReplicaSet超时进行,则更新部署的条件为超时状态,并忽略已经存在的超时条件。
		//这通过调用util.NewDeploymentCondition和util.SetDeploymentCondition函数来实现。

	}

	// Move failure conditions of all replica sets in deployment conditions. For now,
	// only one failure condition is returned from getReplicaFailures.
	if replicaFailureCond := dc.getReplicaFailures(allRSs, newRS); len(replicaFailureCond) > 0 {
		// There will be only one ReplicaFailure condition on the replica set.
		util.SetDeploymentCondition(&newStatus, replicaFailureCond[0])
	} else {
		util.RemoveDeploymentCondition(&newStatus, apps.DeploymentReplicaFailure)
	}

	// Do not update if there is nothing new to add.
	if reflect.DeepEqual(d.Status, newStatus) {
		// Requeue the deployment if required.
		dc.requeueStuckDeployment(ctx, d, newStatus)
		return nil
	}

	newDeployment := d
	newDeployment.Status = newStatus
	_, err := dc.client.AppsV1().Deployments(newDeployment.Namespace).UpdateStatus(ctx, newDeployment, metav1.UpdateOptions{})
	return err
}

//该函数是用于更新部署(deployment)在滚动更新过程中的状态。
//它根据给定的所有复制集(replica set)和新的复制集的信息,计算出新的部署状态。
//然后,根据是否有进度截止时间,以及部署是否已完成,来检查和更新部署的进度状态。
//此外,它还会将复制集的失败条件转移到部署的条件中。
//最后,如果状态有更新,它会将新的部署状态更新到Kubernetes API中。

// getReplicaFailures will convert replica failure conditions from replica sets
// to deployment conditions.
func (dc *DeploymentController) getReplicaFailures(allRSs []*apps.ReplicaSet, newRS *apps.ReplicaSet) []apps.DeploymentCondition {
	var conditions []apps.DeploymentCondition
	if newRS != nil {
		for _, c := range newRS.Status.Conditions {
			if c.Type != apps.ReplicaSetReplicaFailure {
				continue
			}
			conditions = append(conditions, util.ReplicaSetToDeploymentCondition(c))
		}
	}

	// Return failures for the new replica set over failures from old replica sets.
	if len(conditions) > 0 {
		return conditions
	}

	for i := range allRSs {
		rs := allRSs[i]
		if rs == nil {
			continue
		}

		for _, c := range rs.Status.Conditions {
			if c.Type != apps.ReplicaSetReplicaFailure {
				continue
			}
			conditions = append(conditions, util.ReplicaSetToDeploymentCondition(c))
		}
	}
	return conditions
}

//该函数是用于将副本集的副本失败条件转换为部署条件的。
//函数首先检查新副本集(newRS)的状态条件,将除ReplicaSetReplicaFailure类型外的条件添加到conditions切片中。
//如果conditions切片不为空,则直接返回。
//否则,遍历所有旧副本集(allRSs),将除ReplicaSetReplicaFailure类型外的条件添加到conditions切片中。
//最后返回conditions切片。

// used for unit testing
var nowFn = func() time.Time { return time.Now() }

// requeueStuckDeployment checks whether the provided deployment needs to be synced for a progress
// check. It returns the time after the deployment will be requeued for the progress check, 0 if it
// will be requeued now, or -1 if it does not need to be requeued.
func (dc *DeploymentController) requeueStuckDeployment(ctx context.Context, d *apps.Deployment, newStatus apps.DeploymentStatus) time.Duration {
	logger := klog.FromContext(ctx)
	currentCond := util.GetDeploymentCondition(d.Status, apps.DeploymentProgressing)
	// Can't estimate progress if there is no deadline in the spec or progressing condition in the current status.
	if !util.HasProgressDeadline(d) || currentCond == nil {
		return time.Duration(-1)
	}
	// No need to estimate progress if the rollout is complete or already timed out.
	if util.DeploymentComplete(d, &newStatus) || currentCond.Reason == util.TimedOutReason {
		return time.Duration(-1)
	}
	// If there is no sign of progress at this point then there is a high chance that the
	// deployment is stuck. We should resync this deployment at some point in the future[1]
	// and check whether it has timed out. We definitely need this, otherwise we depend on the
	// controller resync interval. See https://github.com/kubernetes/kubernetes/issues/34458.
	//
	// [1] ProgressingCondition.LastUpdatedTime + progressDeadlineSeconds - time.Now()
	//
	// For example, if a Deployment updated its Progressing condition 3 minutes ago and has a
	// deadline of 10 minutes, it would need to be resynced for a progress check after 7 minutes.
	//
	// lastUpdated: 			00:00:00
	// now: 					00:03:00
	// progressDeadlineSeconds: 600 (10 minutes)
	//
	// lastUpdated + progressDeadlineSeconds - now => 00:00:00 + 00:10:00 - 00:03:00 => 07:00
	after := currentCond.LastUpdateTime.Time.Add(time.Duration(*d.Spec.ProgressDeadlineSeconds) * time.Second).Sub(nowFn())
	// If the remaining time is less than a second, then requeue the deployment immediately.
	// Make it ratelimited so we stay on the safe side, eventually the Deployment should
	// transition either to a Complete or to a TimedOut condition.
	if after < time.Second {
		logger.V(4).Info("Queueing up deployment for a progress check now", "deployment", klog.KObj(d))
		dc.enqueueRateLimited(d)
		return time.Duration(0)
	}
	logger.V(4).Info("Queueing up deployment for a progress check", "deployment", klog.KObj(d), "queueAfter", int(after.Seconds()))
	// Add a second to avoid milliseconds skew in AddAfter.
	// See https://github.com/kubernetes/kubernetes/issues/39785#issuecomment-279959133 for more info.
	dc.enqueueAfter(d, after+time.Second)
	return after
}

//该函数用于检查提供的部署是否需要同步进行进度检查。
//它返回部署重新排队进行进度检查的时间,如果需要立即重新排队则返回0,如果不需要重新排队则返回-1。
//函数首先从上下文中获取日志记录器,然后获取部署的当前状态和新的状态。
//如果部署的规范中没有截止日期或当前状态中没有进行中的条件,则无法估计进度,函数将返回-1。
//如果部署已经完成或已经超时,则无需估计进度,函数将返回-1。
//如果此时没有进度的迹象,则部署可能卡住了。函数将在未来的某个时间点重新同步此部署,并检查是否超时。
//这需要通过调用enqueueRateLimited方法将部署加入到队列中,并立即返回0。
//否则,函数将计算重新同步部署的时间,并通过调用enqueueAfter方法将部署加入到队列中,并返回计算的时间。
//如果计算的时间小于1秒,则函数将立即重新排队,并通过调用enqueueRateLimited方法将部署加入到队列中,并返回0。
//最后,函数将相关的日志信息记录下来。