未验证 提交 22d53bd5 编写于 作者: M M-Cosmosss 提交者: GitHub

feat: Blue Green Release v2 (#2916)

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* fix
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* fix
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* fix
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* fix
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* fix
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* wip
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* clear
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

* clear
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>

---------
Signed-off-by: NM-Cosmosss <yuzhou@koderover.com>
上级 8add903a
......@@ -372,6 +372,7 @@ const (
BlueGreenVerionLabelName = "zadig-blue-green-version"
BlueServiceNameSuffix = "-zadig-blue"
OriginVersion = "origin"
BlueVersion = "blue"
)
// for custom gray release job
......@@ -483,9 +484,3 @@ const (
const (
CUSTOME_THEME = "custom"
)
type ZadigXReleaseType string
var (
ZadigXMseGrayRelease ZadigXReleaseType = "mse-gray"
)
......@@ -267,6 +267,14 @@ type JobTaskBlueGreenDeploySpec struct {
Events *Events `bson:"events" json:"events" yaml:"events"`
}
type JobTaskBlueGreenDeployV2Spec struct {
Production bool `bson:"production" json:"production" yaml:"production"`
Env string `bson:"env" json:"env" yaml:"env"`
Service *BlueGreenDeployV2Service `bson:"service" json:"service" yaml:"service"`
Events *Events `bson:"events" json:"events" yaml:"events"`
DeployTimeout int `bson:"deploy_timeout" json:"deploy_timeout" yaml:"deploy_timeout"`
}
type JobTaskBlueGreenReleaseSpec struct {
ClusterID string `bson:"cluster_id" json:"cluster_id" yaml:"cluster_id"`
Namespace string `bson:"namespace" json:"namespace" yaml:"namespace"`
......@@ -281,6 +289,15 @@ type JobTaskBlueGreenReleaseSpec struct {
Events *Events `bson:"events" json:"events" yaml:"events"`
}
type JobTaskBlueGreenReleaseV2Spec struct {
Production bool `bson:"production" json:"production" yaml:"production"`
Env string `bson:"env" json:"env" yaml:"env"`
Namespace string `bson:"namespace" json:"namespace" yaml:"namespace"`
Service *BlueGreenDeployV2Service `bson:"service" json:"service" yaml:"service"`
Events *Events `bson:"events" json:"events" yaml:"events"`
DeployTimeout int `bson:"deploy_timeout" json:"deploy_timeout" yaml:"deploy_timeout"`
}
type JobTaskCanaryDeploySpec struct {
ClusterID string `bson:"cluster_id" json:"cluster_id" yaml:"cluster_id"`
Namespace string `bson:"namespace" json:"namespace" yaml:"namespace"`
......
......@@ -404,10 +404,44 @@ type BlueGreenDeployJobSpec struct {
Targets []*BlueGreenTarget `bson:"targets" json:"targets" yaml:"targets"`
}
type BlueGreenDeployV2JobSpec struct {
Version string `bson:"version" json:"version" yaml:"version"`
Production bool `bson:"production" json:"production" yaml:"production"`
Env string `bson:"env" json:"env" yaml:"env"`
Source string `bson:"source" json:"source" yaml:"source"`
Services []*BlueGreenDeployV2Service `bson:"services" json:"services" yaml:"services"`
}
type BlueGreenDeployV2ServiceModuleAndImage struct {
ServiceModule string `bson:"service_module" json:"service_module" yaml:"service_module"`
Image string `bson:"image" json:"image" yaml:"image"`
// Following fields only save for frontend
ImageName string `bson:"image_name" json:"image_name" yaml:"image_name"`
Name string `bson:"name" json:"name" yaml:"name"`
ServiceName string `bson:"service_name" json:"service_name" yaml:"service_name"`
Value string `bson:"value" json:"value" yaml:"value"`
}
type BlueGreenDeployV2Service struct {
// ServiceName is zadig service name
ServiceName string `bson:"service_name" json:"service_name" yaml:"service_name"`
BlueServiceYaml string `bson:"blue_service_yaml" json:"blue_service_yaml" yaml:"blue_service_yaml"`
BlueServiceName string `bson:"blue_service_name,omitempty" json:"blue_service_name,omitempty" yaml:"blue_service_name,omitempty"`
BlueDeploymentYaml string `bson:"blue_deployment_yaml,omitempty" json:"blue_deployment_yaml,omitempty" yaml:"blue_deployment_yaml,omitempty"`
BlueDeploymentName string `bson:"blue_deployment_name,omitempty" json:"blue_deployment_name,omitempty" yaml:"blue_deployment_name,omitempty"`
GreenDeploymentName string `bson:"green_deployment_name,omitempty" json:"green_deployment_name,omitempty" yaml:"green_deployment_name,omitempty"`
GreenServiceName string `bson:"green_service_name,omitempty" json:"green_service_name,omitempty" yaml:"green_service_name,omitempty"`
ServiceAndImage []*BlueGreenDeployV2ServiceModuleAndImage `bson:"service_and_image" json:"service_and_image" yaml:"service_and_image"`
}
type BlueGreenReleaseJobSpec struct {
FromJob string `bson:"from_job" json:"from_job" yaml:"from_job"`
}
type BlueGreenReleaseV2JobSpec struct {
FromJob string `bson:"from_job" json:"from_job" yaml:"from_job"`
}
type BlueGreenTarget struct {
K8sServiceName string `bson:"k8s_service_name" json:"k8s_service_name" yaml:"k8s_service_name"`
BlueK8sServiceName string `bson:"blue_k8s_service_name" json:"blue_k8s_service_name" yaml:"-"`
......
......@@ -98,8 +98,8 @@ type ServiceResp struct {
DeployStrategy string `json:"deploy_strategy"`
// ZadigXReleaseType represents the service contain created by zadigx release workflow
// frontend should limit some operations on these services
ZadigXReleaseType config.ZadigXReleaseType `json:"zadigx_release_type"`
ZadigXReleaseTag string `json:"zadigx_release_tag"`
ZadigXReleaseType string `json:"zadigx_release_type"`
ZadigXReleaseTag string `json:"zadigx_release_tag"`
}
type IngressInfo struct {
......
......@@ -63,9 +63,9 @@ func initJobCtl(job *commonmodels.JobTask, workflowCtx *commonmodels.WorkflowTas
case string(config.JobK8sCanaryRelease):
jobCtl = NewCanaryReleaseJobCtl(job, workflowCtx, ack, logger)
case string(config.JobK8sBlueGreenDeploy):
jobCtl = NewBlueGreenDeployJobCtl(job, workflowCtx, ack, logger)
jobCtl = NewBlueGreenDeployV2JobCtl(job, workflowCtx, ack, logger)
case string(config.JobK8sBlueGreenRelease):
jobCtl = NewBlueGreenReleaseJobCtl(job, workflowCtx, ack, logger)
jobCtl = NewBlueGreenReleaseV2JobCtl(job, workflowCtx, ack, logger)
case string(config.JobK8sGrayRelease):
jobCtl = NewGrayReleaseJobCtl(job, workflowCtx, ack, logger)
case string(config.JobK8sGrayRollback):
......
/*
Copyright 2023 The KodeRover 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 jobcontroller
import (
"context"
"fmt"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes/scheme"
crClient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/koderover/zadig/pkg/microservice/aslan/config"
commonmodels "github.com/koderover/zadig/pkg/microservice/aslan/core/common/repository/models"
"github.com/koderover/zadig/pkg/microservice/aslan/core/common/repository/mongodb"
"github.com/koderover/zadig/pkg/setting"
kubeclient "github.com/koderover/zadig/pkg/shared/kube/client"
"github.com/koderover/zadig/pkg/shared/kube/wrapper"
"github.com/koderover/zadig/pkg/tool/kube/getter"
"github.com/koderover/zadig/pkg/tool/kube/updater"
)
type BlueGreenDeployV2JobCtl struct {
job *commonmodels.JobTask
workflowCtx *commonmodels.WorkflowTaskCtx
logger *zap.SugaredLogger
kubeClient crClient.Client
namespace string
jobTaskSpec *commonmodels.JobTaskBlueGreenDeployV2Spec
ack func()
}
func NewBlueGreenDeployV2JobCtl(job *commonmodels.JobTask, workflowCtx *commonmodels.WorkflowTaskCtx, ack func(), logger *zap.SugaredLogger) *BlueGreenDeployV2JobCtl {
jobTaskSpec := &commonmodels.JobTaskBlueGreenDeployV2Spec{}
if err := commonmodels.IToi(job.Spec, jobTaskSpec); err != nil {
logger.Error(err)
}
if jobTaskSpec.Events == nil {
jobTaskSpec.Events = &commonmodels.Events{}
}
job.Spec = jobTaskSpec
return &BlueGreenDeployV2JobCtl{
job: job,
workflowCtx: workflowCtx,
logger: logger,
ack: ack,
jobTaskSpec: jobTaskSpec,
}
}
func (c *BlueGreenDeployV2JobCtl) Clean(ctx context.Context) {}
func (c *BlueGreenDeployV2JobCtl) Run(ctx context.Context) {
c.job.Status = config.StatusRunning
c.ack()
if err := c.run(ctx); err != nil {
return
}
c.wait(ctx)
}
func (c *BlueGreenDeployV2JobCtl) run(ctx context.Context) error {
var err error
env, err := mongodb.NewProductColl().Find(&mongodb.ProductFindOptions{
Name: c.workflowCtx.ProjectName,
EnvName: c.jobTaskSpec.Env,
})
if err != nil {
msg := fmt.Sprintf("find project error: %v", err)
logError(c.job, msg, c.logger)
return errors.New(msg)
}
c.namespace = env.Namespace
clusterID := env.ClusterID
c.kubeClient, err = kubeclient.GetKubeClient(config.HubServerAddress(), clusterID)
if err != nil {
msg := fmt.Sprintf("can't init k8s client: %v", err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
// get raw green
greenDeployment, found, err := getter.GetDeployment(c.namespace, c.jobTaskSpec.Service.GreenDeploymentName, c.kubeClient)
if err != nil || !found {
msg := fmt.Sprintf("get green deployment: %s error: %v", c.jobTaskSpec.Service.GreenDeploymentName, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
greenService, found, err := getter.GetService(c.namespace, c.jobTaskSpec.Service.GreenServiceName, c.kubeClient)
if err != nil || !found {
msg := fmt.Sprintf("get green service: %s error: %v", c.jobTaskSpec.Service.GreenServiceName, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
if greenService.Spec.Selector == nil {
msg := fmt.Sprintf("blue service %s selector is nil", c.jobTaskSpec.Service.GreenServiceName)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
// raw pods list add original version label
pods, err := getter.ListPods(c.namespace, labels.Set(greenDeployment.Spec.Selector.MatchLabels).AsSelector(), c.kubeClient)
if err != nil {
msg := fmt.Sprintf("list green deployment %s pods error: %v", c.jobTaskSpec.Service.GreenDeploymentName, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
for _, pod := range pods {
addlabelPatch := fmt.Sprintf(`{"metadata":{"labels":{"%s":"%s"}}}`, config.BlueGreenVerionLabelName, config.OriginVersion)
if err := updater.PatchPod(c.namespace, pod.Name, []byte(addlabelPatch), c.kubeClient); err != nil {
msg := fmt.Sprintf("add origin label to pod error: %v", err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
}
c.jobTaskSpec.Events.Info(fmt.Sprintf("add origin label to %d pods", len(pods)))
c.ack()
// green service selector add original version label
greenService.Spec.Selector[config.BlueGreenVerionLabelName] = config.OriginVersion
if err := updater.CreateOrPatchService(greenService, c.kubeClient); err != nil {
msg := fmt.Sprintf("add origin label selector to green serivce: %s error: %v", greenService.Name, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
c.jobTaskSpec.Events.Info(fmt.Sprintf("add origin label selector to service: %s", c.jobTaskSpec.Service.ServiceName))
c.ack()
decoder := serializer.NewCodecFactory(scheme.Scheme).UniversalDecoder()
// create blue service
service := &corev1.Service{}
err = runtime.DecodeInto(decoder, []byte(c.jobTaskSpec.Service.BlueServiceYaml), service)
if err != nil {
return errors.Errorf("failed to decode %s k8s service yaml, err: %s", c.jobTaskSpec.Service.ServiceName, err)
}
service.Namespace = c.namespace
if err := c.kubeClient.Create(ctx, service); err != nil {
msg := fmt.Sprintf("create blue serivce: %s error: %v", c.jobTaskSpec.Service.ServiceName, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
c.jobTaskSpec.Events.Info(fmt.Sprintf("create blue serivce: %s success", service.Name))
// create blue deployment
deployment := &v1.Deployment{}
err = runtime.DecodeInto(decoder, []byte(c.jobTaskSpec.Service.BlueDeploymentYaml), deployment)
if err != nil {
return errors.Errorf("failed to decode %s k8s deployment yaml, err: %s", c.jobTaskSpec.Service.ServiceName, err)
}
deployment.Namespace = c.namespace
if err := c.kubeClient.Create(ctx, deployment); err != nil {
msg := fmt.Sprintf("create blue deployment: %s error: %v", c.jobTaskSpec.Service.ServiceName, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
c.jobTaskSpec.Events.Info(fmt.Sprintf("create blue deployment: %s success", deployment.Name))
return nil
}
func (c *BlueGreenDeployV2JobCtl) wait(ctx context.Context) {
timeout := time.After(time.Duration(c.timeout()) * time.Second)
for {
select {
case <-ctx.Done():
c.job.Status = config.StatusCancelled
return
case <-timeout:
c.job.Status = config.StatusTimeout
msg := fmt.Sprintf("timeout waiting for the blue deployment: %s to run", c.jobTaskSpec.Service.BlueDeploymentName)
c.jobTaskSpec.Events.Info(msg)
return
default:
time.Sleep(time.Second * 2)
d, found, err := getter.GetDeployment(c.namespace, c.jobTaskSpec.Service.BlueDeploymentName, c.kubeClient)
if err != nil || !found {
c.logger.Errorf(
"failed to check deployment ready status %s/%s - %v",
c.namespace,
c.jobTaskSpec.Service.BlueDeploymentName,
err,
)
} else {
if wrapper.Deployment(d).Ready() {
c.job.Status = config.StatusPassed
msg := fmt.Sprintf("blue-green deployment: %s create successfully", c.jobTaskSpec.Service.BlueDeploymentName)
c.jobTaskSpec.Events.Info(msg)
return
}
}
}
}
}
func (c *BlueGreenDeployV2JobCtl) timeout() int {
if c.jobTaskSpec.DeployTimeout == 0 {
c.jobTaskSpec.DeployTimeout = setting.DeployTimeout
}
return c.jobTaskSpec.DeployTimeout
}
func (c *BlueGreenDeployV2JobCtl) SaveInfo(ctx context.Context) error {
return mongodb.NewJobInfoColl().Create(context.TODO(), &commonmodels.JobInfo{
Type: c.job.JobType,
WorkflowName: c.workflowCtx.WorkflowName,
WorkflowDisplayName: c.workflowCtx.WorkflowDisplayName,
TaskID: c.workflowCtx.TaskID,
ProductName: c.workflowCtx.ProjectName,
StartTime: c.job.StartTime,
EndTime: c.job.EndTime,
Duration: c.job.EndTime - c.job.StartTime,
Status: string(c.job.Status),
})
}
/*
Copyright 2023 The KodeRover 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 jobcontroller
import (
"context"
"fmt"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/labels"
crClient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/koderover/zadig/pkg/microservice/aslan/config"
commonmodels "github.com/koderover/zadig/pkg/microservice/aslan/core/common/repository/models"
"github.com/koderover/zadig/pkg/microservice/aslan/core/common/repository/mongodb"
"github.com/koderover/zadig/pkg/setting"
kubeclient "github.com/koderover/zadig/pkg/shared/kube/client"
"github.com/koderover/zadig/pkg/shared/kube/wrapper"
"github.com/koderover/zadig/pkg/tool/kube/getter"
"github.com/koderover/zadig/pkg/tool/kube/updater"
)
type BlueGreenReleaseV2JobCtl struct {
job *commonmodels.JobTask
workflowCtx *commonmodels.WorkflowTaskCtx
logger *zap.SugaredLogger
kubeClient crClient.Client
namespace string
jobTaskSpec *commonmodels.JobTaskBlueGreenReleaseV2Spec
ack func()
}
func NewBlueGreenReleaseV2JobCtl(job *commonmodels.JobTask, workflowCtx *commonmodels.WorkflowTaskCtx, ack func(), logger *zap.SugaredLogger) *BlueGreenReleaseV2JobCtl {
jobTaskSpec := &commonmodels.JobTaskBlueGreenReleaseV2Spec{}
if err := commonmodels.IToi(job.Spec, jobTaskSpec); err != nil {
logger.Error(err)
}
if jobTaskSpec.Events == nil {
jobTaskSpec.Events = &commonmodels.Events{}
}
job.Spec = jobTaskSpec
return &BlueGreenReleaseV2JobCtl{
job: job,
workflowCtx: workflowCtx,
logger: logger.With("ctl", "BlueGreenReleaseV2"),
ack: ack,
jobTaskSpec: jobTaskSpec,
}
}
func (c *BlueGreenReleaseV2JobCtl) Clean(ctx context.Context) {
env, err := mongodb.NewProductColl().Find(&mongodb.ProductFindOptions{
Name: c.workflowCtx.ProjectName,
EnvName: c.jobTaskSpec.Env,
})
if err != nil {
c.logger.Errorf("find project error: %v", err)
return
}
c.namespace = env.Namespace
clusterID := env.ClusterID
c.kubeClient, err = kubeclient.GetKubeClient(config.HubServerAddress(), clusterID)
if err != nil {
c.logger.Errorf("can't init k8s client: %v", err)
return
}
// ensure delete blue deployment and service
err = updater.DeleteDeploymentAndWait(c.namespace, c.jobTaskSpec.Service.BlueDeploymentName, c.kubeClient)
if err != nil {
c.logger.Warnf("can't delete blue deployment %s, err: %v", c.jobTaskSpec.Service.BlueDeploymentName, err)
}
err = updater.DeleteService(c.namespace, c.jobTaskSpec.Service.BlueServiceName, c.kubeClient)
if err != nil {
c.logger.Warnf("can't delete blue service %s, err: %v", c.jobTaskSpec.Service.BlueServiceName, err)
}
// ensure green service and pods not contain release label
greenDeployment, found, err := getter.GetDeployment(c.namespace, c.jobTaskSpec.Service.GreenDeploymentName, c.kubeClient)
if err != nil || !found {
c.logger.Errorf("get green deployment: %s error: %v", c.jobTaskSpec.Service.GreenDeploymentName, err)
return
}
greenService, found, err := getter.GetService(c.namespace, c.jobTaskSpec.Service.GreenServiceName, c.kubeClient)
if err != nil || !found {
c.logger.Errorf("get green service: %s error: %v", c.jobTaskSpec.Service.GreenServiceName, err)
return
}
if greenService.Spec.Selector == nil {
c.logger.Errorf("blue service %s selector is nil", c.jobTaskSpec.Service.GreenServiceName)
return
}
// must remove service selector before remove pods labels
if _, ok := greenService.Spec.Selector[config.BlueGreenVerionLabelName]; ok {
delete(greenService.Spec.Selector, config.BlueGreenVerionLabelName)
if err := updater.CreateOrPatchService(greenService, c.kubeClient); err != nil {
c.logger.Errorf("delete origin label for service error: %v", err)
return
}
}
pods, err := getter.ListPods(c.namespace, labels.Set(greenDeployment.Spec.Selector.MatchLabels).AsSelector(), c.kubeClient)
if err != nil {
c.logger.Errorf("list green deployment %s pods error: %v", c.jobTaskSpec.Service.GreenDeploymentName, err)
return
}
for _, pod := range pods {
if pod.Labels == nil {
continue
}
if _, ok := pod.Labels[config.BlueGreenVerionLabelName]; ok {
removeLabelPatch := fmt.Sprintf(`{"metadata":{"labels":{"%s":null}}}`, config.BlueGreenVerionLabelName)
if err := updater.PatchPod(c.namespace, pod.Name, []byte(removeLabelPatch), c.kubeClient); err != nil {
c.logger.Errorf("remove origin label to pod error: %v", err)
continue
}
}
}
return
}
func (c *BlueGreenReleaseV2JobCtl) Run(ctx context.Context) {
c.job.Status = config.StatusRunning
c.ack()
if err := c.run(ctx); err != nil {
return
}
c.wait(ctx)
}
func (c *BlueGreenReleaseV2JobCtl) run(ctx context.Context) error {
var err error
env, err := mongodb.NewProductColl().Find(&mongodb.ProductFindOptions{
Name: c.workflowCtx.ProjectName,
EnvName: c.jobTaskSpec.Env,
})
if err != nil {
msg := fmt.Sprintf("find project error: %v", err)
logError(c.job, msg, c.logger)
return errors.New(msg)
}
c.namespace = env.Namespace
c.jobTaskSpec.Namespace = env.Namespace
clusterID := env.ClusterID
c.kubeClient, err = kubeclient.GetKubeClient(config.HubServerAddress(), clusterID)
if err != nil {
msg := fmt.Sprintf("can't init k8s client: %v", err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
// offline blue service and deployment first
c.jobTaskSpec.Events.Info(fmt.Sprintf("wait for blue deployment %s be deleted", c.jobTaskSpec.Service.BlueDeploymentName))
c.ack()
err = updater.DeleteDeploymentAndWait(c.namespace, c.jobTaskSpec.Service.BlueDeploymentName, c.kubeClient)
if err != nil {
msg := fmt.Sprintf("can't delete blue deployment %s, err: %v", c.jobTaskSpec.Service.BlueDeploymentName, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
c.jobTaskSpec.Events.Info(fmt.Sprintf("delete blue deployment %s success", c.jobTaskSpec.Service.BlueDeploymentName))
c.ack()
err = updater.DeleteService(c.namespace, c.jobTaskSpec.Service.BlueServiceName, c.kubeClient)
if err != nil {
msg := fmt.Sprintf("can't delete blue service %s, err: %v", c.jobTaskSpec.Service.BlueServiceName, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
c.jobTaskSpec.Events.Info(fmt.Sprintf("delete blue service %s success", c.jobTaskSpec.Service.BlueServiceName))
c.ack()
// rollback green service selector
greenService, found, err := getter.GetService(c.namespace, c.jobTaskSpec.Service.GreenServiceName, c.kubeClient)
if err != nil || !found {
msg := fmt.Sprintf("can't get green service %s, err: %v", c.jobTaskSpec.Service.GreenServiceName, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
delete(greenService.Spec.Selector, config.BlueGreenVerionLabelName)
err = updater.CreateOrPatchService(greenService, c.kubeClient)
if err != nil {
msg := fmt.Sprintf("can't update green service %s selector, err: %v", c.jobTaskSpec.Service.GreenServiceName, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
c.jobTaskSpec.Events.Info(fmt.Sprintf("update green service %s selector success", c.jobTaskSpec.Service.GreenServiceName))
c.ack()
// update green deployment image
for _, v := range c.jobTaskSpec.Service.ServiceAndImage {
err := updater.UpdateDeploymentImage(c.namespace, c.jobTaskSpec.Service.GreenDeploymentName, v.ServiceModule, v.Image, c.kubeClient)
if err != nil {
msg := fmt.Sprintf("can't update deployment %s container %s image %s, err: %v",
c.jobTaskSpec.Service.GreenDeploymentName, v.ServiceModule, v.Image, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
if err := updateProductImageByNs(c.jobTaskSpec.Env, c.workflowCtx.ProjectName, c.jobTaskSpec.Service.ServiceName, map[string]string{v.ServiceModule: v.Image}, c.logger); err != nil {
msg := fmt.Sprintf("update product image service %s service module %s image %s error: %v", c.jobTaskSpec.Service.ServiceName, v.ServiceModule, v.Image, err)
logError(c.job, msg, c.logger)
c.jobTaskSpec.Events.Error(msg)
return errors.New(msg)
}
}
c.jobTaskSpec.Events.Info(fmt.Sprintf("update deployment %s image success", c.jobTaskSpec.Service.GreenDeploymentName))
return nil
}
func (c *BlueGreenReleaseV2JobCtl) wait(ctx context.Context) {
c.jobTaskSpec.Events.Info(fmt.Sprintf("wait for deployment %s ready", c.jobTaskSpec.Service.GreenDeploymentName))
timeout := time.After(time.Duration(c.timeout()) * time.Second)
for {
select {
case <-ctx.Done():
c.job.Status = config.StatusCancelled
return
case <-timeout:
c.job.Status = config.StatusTimeout
msg := fmt.Sprintf("timeout waiting for deployment %s ready", c.jobTaskSpec.Service.GreenDeploymentName)
c.jobTaskSpec.Events.Info(msg)
return
default:
time.Sleep(time.Second * 2)
d, found, err := getter.GetDeployment(c.namespace, c.jobTaskSpec.Service.GreenDeploymentName, c.kubeClient)
if err != nil || !found {
c.logger.Errorf(
"failed to check deployment ready status %s/%s - %v",
c.namespace,
c.jobTaskSpec.Service.GreenDeploymentName,
err,
)
} else {
if wrapper.Deployment(d).Ready() {
c.job.Status = config.StatusPassed
msg := fmt.Sprintf("blue-green deployment: %s release successfully", c.jobTaskSpec.Service.GreenDeploymentName)
c.jobTaskSpec.Events.Info(msg)
return
}
}
}
}
}
func (c *BlueGreenReleaseV2JobCtl) timeout() int {
if c.jobTaskSpec.DeployTimeout == 0 {
c.jobTaskSpec.DeployTimeout = setting.DeployTimeout
}
return c.jobTaskSpec.DeployTimeout
}
func (c *BlueGreenReleaseV2JobCtl) SaveInfo(ctx context.Context) error {
return mongodb.NewJobInfoColl().Create(context.TODO(), &commonmodels.JobInfo{
Type: c.job.JobType,
WorkflowName: c.workflowCtx.WorkflowName,
WorkflowDisplayName: c.workflowCtx.WorkflowDisplayName,
TaskID: c.workflowCtx.TaskID,
ProductName: c.workflowCtx.ProjectName,
StartTime: c.job.StartTime,
EndTime: c.job.EndTime,
Duration: c.job.EndTime - c.job.StartTime,
Status: string(c.job.Status),
})
}
......@@ -159,26 +159,28 @@ func ListGroups(serviceName, envName, productName string, perPage, page int, pro
respMap[serviceResp.ServiceName] = serviceResp
}
if projectType == setting.K8SDeployType {
mseService, err := listZadigXMseReleaseServices(productInfo.Namespace, productName, envName, kubeClient)
if err != nil {
return resp, count, e.ErrListGroups.AddErr(errors.Wrap(err, "list mse release services"))
}
log.Debugf("mse release services num: %d", len(mseService))
for _, serviceResp := range mseService {
if svc, ok := respMap[serviceResp.ServiceName]; !ok {
resp = append(resp, serviceResp)
} else {
// when zadigx release resource exists, should set ZadigXReleaseType field too
svc.ZadigXReleaseType = serviceResp.ZadigXReleaseType
svc.ZadigXReleaseTag = serviceResp.ZadigXReleaseTag
for _, releaseType := range types.ZadigReleaseTypeList {
releaseService, err := listZadigXReleaseServices(releaseType, productInfo.Namespace, productName, envName, kubeClient)
if err != nil {
return resp, count, e.ErrListGroups.AddErr(errors.Wrapf(err, "list zadigx %s release services", releaseType))
}
log.Debugf("%s release services num: %d", releaseType, len(releaseService))
for _, serviceResp := range releaseService {
if svc, ok := respMap[serviceResp.ServiceName]; !ok {
resp = append(resp, serviceResp)
} else {
// when zadigx release resource exists, should set ZadigXReleaseType field too
svc.ZadigXReleaseType = serviceResp.ZadigXReleaseType
svc.ZadigXReleaseTag = serviceResp.ZadigXReleaseTag
}
}
}
}
return resp, count, nil
}
func listZadigXMseReleaseServices(namespace, productName, envName string, client client.Client) ([]*commonservice.ServiceResp, error) {
selector := labels.Set{types.ZadigReleaseTypeLabelKey: types.ZadigReleaseTypeMseGray}.AsSelector()
func listZadigXReleaseServices(releaseType, namespace, productName, envName string, client client.Client) ([]*commonservice.ServiceResp, error) {
selector := labels.Set{types.ZadigReleaseTypeLabelKey: releaseType}.AsSelector()
deployments, err := getter.ListDeployments(namespace, selector, client)
if err != nil {
return nil, err
......@@ -188,7 +190,7 @@ func listZadigXMseReleaseServices(namespace, productName, envName string, client
for _, deployment := range deployments {
serviceName := deployment.GetLabels()[types.ZadigReleaseServiceNameLabelKey]
if serviceName == "" {
log.Warnf("listZadigXMseReleaseServices: deployment %s/%s has no service name label", deployment.Namespace, deployment.Name)
log.Warnf("listZadigXReleaseServices: deployment %s/%s has no service name label", deployment.Namespace, deployment.Name)
continue
}
if resp, ok := serviceSets[serviceName]; ok {
......@@ -215,7 +217,7 @@ func listZadigXMseReleaseServices(namespace, productName, envName string, client
ProductName: productName,
EnvName: envName,
DeployStrategy: "deploy",
ZadigXReleaseType: config.ZadigXMseGrayRelease,
ZadigXReleaseType: releaseType,
ZadigXReleaseTag: deployment.GetLabels()[types.ZadigReleaseVersionLabelKey],
}
serviceSets[serviceName] = svcResp
......
......@@ -212,15 +212,17 @@ func GetService(envName, productName, serviceName string, production bool, workL
}
}
if env.Source == setting.SourceFromZadig {
mseResp, err := GetMseServiceImpl(serviceName, workLoadType, env, kubeClient, clientset, inf, log)
if err != nil {
return nil, e.ErrGetService.AddErr(errors.Wrap(err, "failed to get mse service"))
for _, releaseType := range types.ZadigReleaseTypeList {
releaseService, err := GetZadigReleaseServiceImpl(releaseType, serviceName, workLoadType, env, kubeClient, clientset, inf, log)
if err != nil {
return nil, e.ErrGetService.AddErr(errors.Wrapf(err, "failed to get %s release service", releaseType))
}
ret.Scales = append(ret.Scales, releaseService.Scales...)
ret.Services = append(ret.Services, releaseService.Services...)
ret.Workloads = nil
ret.Namespace = env.Namespace
}
ret.Scales = append(ret.Scales, mseResp.Scales...)
ret.Services = append(ret.Services, mseResp.Services...)
}
ret.Workloads = nil
ret.Namespace = env.Namespace
} else {
ret, err = GetServiceImpl(serviceName, serviceTmpl, workLoadType, env, clientset, inf, log)
if err != nil {
......@@ -505,7 +507,7 @@ func GetServiceImpl(serviceName string, serviceTmpl *commonmodels.Service, workL
return
}
func GetMseServiceImpl(serviceName string, workLoadType string, env *commonmodels.Product, kubeClient client.Client, clientset *kubernetes.Clientset, inf informers.SharedInformerFactory, log *zap.SugaredLogger) (ret *SvcResp, err error) {
func GetZadigReleaseServiceImpl(releaseType, serviceName string, workLoadType string, env *commonmodels.Product, kubeClient client.Client, clientset *kubernetes.Clientset, inf informers.SharedInformerFactory, log *zap.SugaredLogger) (ret *SvcResp, err error) {
envName, productName := env.EnvName, env.ProductName
ret = &SvcResp{
ServiceName: serviceName,
......@@ -525,7 +527,7 @@ func GetMseServiceImpl(serviceName string, workLoadType string, env *commonmodel
default:
selector := labels.SelectorFromSet(map[string]string{
types.ZadigReleaseServiceNameLabelKey: serviceName,
types.ZadigReleaseTypeLabelKey: types.ZadigReleaseTypeMseGray,
types.ZadigReleaseTypeLabelKey: releaseType,
})
deployments, err := getter.ListDeployments(namespace, selector, kubeClient)
if err != nil {
......@@ -533,7 +535,7 @@ func GetMseServiceImpl(serviceName string, workLoadType string, env *commonmodel
}
for _, deployment := range deployments {
ret.Scales = append(ret.Scales, getDeploymentWorkloadResource(deployment, inf, log))
ret.Scales[len(ret.Scales)-1].ZadigXReleaseType = config.ZadigXMseGrayRelease
ret.Scales[len(ret.Scales)-1].ZadigXReleaseType = releaseType
ret.Scales[len(ret.Scales)-1].ZadigXReleaseTag = deployment.Labels[types.ZadigReleaseVersionLabelKey]
ret.Workloads = append(ret.Workloads, toDeploymentWorkload(deployment))
}
......
......@@ -699,7 +699,10 @@ func InitWorkflowTemplateInfos() []*commonmodels.WorkflowV4Template {
{
Name: "blue-green-deploy",
JobType: config.JobK8sBlueGreenDeploy,
Spec: commonmodels.BlueGreenDeployJobSpec{},
Spec: commonmodels.BlueGreenDeployV2JobSpec{
Version: "v2",
Production: true,
},
},
},
},
......@@ -719,7 +722,7 @@ func InitWorkflowTemplateInfos() []*commonmodels.WorkflowV4Template {
{
Name: "blue-green-release",
JobType: config.JobK8sBlueGreenRelease,
Spec: commonmodels.BlueGreenReleaseJobSpec{
Spec: commonmodels.BlueGreenReleaseV2JobSpec{
FromJob: "blue-green-deploy",
},
},
......
......@@ -215,6 +215,7 @@ func (*Router) Inject(router *gin.RouterGroup) {
workflowV4.POST("/mse/render", RenderMseServiceYaml)
workflowV4.GET("/mse/offline", GetMseOfflineResources)
workflowV4.GET("/mse/:envName/tag", GetMseTagsInEnv)
workflowV4.GET("/bluegreen/:envName/:serviceName", GetBlueGreenServiceK8sServiceYaml)
}
// ---------------------------------------------------------------------------------------
......
......@@ -639,7 +639,7 @@ func CompareHelmServiceYamlInEnv(c *gin.Context) {
ctx.Resp, ctx.Err = workflow.CompareHelmServiceYamlInEnv(req.ServiceName, req.VariableYaml, req.EnvName, projectName, images, req.IsProduction, req.UpdateServiceRevision, req.IsHelmChartDeploy, ctx.Logger)
}
type MseResponse struct {
type YamlResponse struct {
Yaml string `json:"yaml"`
}
......@@ -666,7 +666,7 @@ func RenderMseServiceYaml(c *gin.Context) {
ctx.Err = err
return
}
ctx.Resp = MseResponse{Yaml: mseServiceYaml}
ctx.Resp = YamlResponse{Yaml: mseServiceYaml}
return
}
......@@ -675,7 +675,7 @@ func RenderMseServiceYaml(c *gin.Context) {
ctx.Err = err
return
}
ctx.Resp = MseResponse{Yaml: mseServiceYaml}
ctx.Resp = YamlResponse{Yaml: mseServiceYaml}
}
func GetMseOfflineResources(c *gin.Context) {
......@@ -694,6 +694,18 @@ func GetMseOfflineResources(c *gin.Context) {
}
}
func GetBlueGreenServiceK8sServiceYaml(c *gin.Context) {
ctx := internalhandler.NewContext(c)
defer func() { internalhandler.JSONResponse(c, ctx) }()
blueGreenServiceYaml, err := workflow.GetBlueGreenServiceK8sServiceYaml(c.Query("projectName"), c.Param("envName"), c.Param("serviceName"))
if err != nil {
ctx.Err = err
return
}
ctx.Resp = YamlResponse{Yaml: blueGreenServiceYaml}
}
func GetMseTagsInEnv(c *gin.Context) {
ctx := internalhandler.NewContext(c)
defer func() { internalhandler.JSONResponse(c, ctx) }()
......
......@@ -70,9 +70,9 @@ func InitJobCtl(job *commonmodels.Job, workflow *commonmodels.WorkflowV4) (JobCt
case config.JobCustomDeploy:
resp = &CustomDeployJob{job: job, workflow: workflow}
case config.JobK8sBlueGreenDeploy:
resp = &BlueGreenDeployJob{job: job, workflow: workflow}
resp = &BlueGreenDeployV2Job{job: job, workflow: workflow}
case config.JobK8sBlueGreenRelease:
resp = &BlueGreenReleaseJob{job: job, workflow: workflow}
resp = &BlueGreenReleaseV2Job{job: job, workflow: workflow}
case config.JobK8sCanaryDeploy:
resp = &CanaryDeployJob{job: job, workflow: workflow}
case config.JobK8sCanaryRelease:
......
/*
* Copyright 2023 The KodeRover 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 job
import (
"bytes"
"fmt"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/releaseutil"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/client-go/kubernetes/scheme"
"github.com/koderover/zadig/pkg/microservice/aslan/config"
commonmodels "github.com/koderover/zadig/pkg/microservice/aslan/core/common/repository/models"
commonrepo "github.com/koderover/zadig/pkg/microservice/aslan/core/common/repository/mongodb"
templaterepo "github.com/koderover/zadig/pkg/microservice/aslan/core/common/repository/mongodb/template"
"github.com/koderover/zadig/pkg/microservice/aslan/core/common/service/kube"
"github.com/koderover/zadig/pkg/setting"
serializer2 "github.com/koderover/zadig/pkg/tool/kube/serializer"
"github.com/koderover/zadig/pkg/types"
"github.com/koderover/zadig/pkg/util"
)
type BlueGreenDeployV2Job struct {
job *commonmodels.Job
workflow *commonmodels.WorkflowV4
spec *commonmodels.BlueGreenDeployV2JobSpec
}
func (j *BlueGreenDeployV2Job) Instantiate() error {
j.spec = &commonmodels.BlueGreenDeployV2JobSpec{}
if err := commonmodels.IToiYaml(j.job.Spec, j.spec); err != nil {
return err
}
j.job.Spec = j.spec
return nil
}
func (j *BlueGreenDeployV2Job) SetPreset() error {
j.spec = &commonmodels.BlueGreenDeployV2JobSpec{}
if err := commonmodels.IToi(j.job.Spec, j.spec); err != nil {
return err
}
product, err := commonrepo.NewProductColl().Find(&commonrepo.ProductFindOptions{EnvName: j.spec.Env, Name: j.workflow.Project, Production: util.GetBoolPointer(j.spec.Production)})
if err != nil {
return errors.Errorf("failed to find product %s, env %s, err: %s", j.workflow.Project, j.spec.Env, err)
}
for _, target := range j.spec.Services {
L:
for _, services := range product.Services {
for _, productService := range services {
if productService.ServiceName == target.ServiceName {
for _, container := range productService.Containers {
target.ServiceAndImage = append(target.ServiceAndImage, &commonmodels.BlueGreenDeployV2ServiceModuleAndImage{
ServiceModule: container.Name,
Image: container.Image,
ImageName: container.ImageName,
Name: container.Name,
ServiceName: target.ServiceName,
})
}
break L
}
}
}
if target.BlueServiceYaml == "" {
yamlContent, _, err := kube.FetchCurrentAppliedYaml(&kube.GeneSvcYamlOption{
ProductName: j.workflow.Project,
EnvName: j.spec.Env,
ServiceName: target.ServiceName,
})
if err != nil {
return errors.Errorf("failed to fetch %s current applied yaml, err: %s", target.ServiceName, err)
}
resources := make([]*unstructured.Unstructured, 0)
manifests := releaseutil.SplitManifests(yamlContent)
for _, item := range manifests {
u, err := serializer2.NewDecoder().YamlToUnstructured([]byte(item))
if err != nil {
return errors.Errorf("failed to decode service %s yaml to unstructured: %v", target.ServiceName, err)
}
resources = append(resources, u)
}
serviceNum := 0
for _, resource := range resources {
switch resource.GetKind() {
case setting.Service:
if serviceNum > 0 {
return errors.Errorf("service %s has more than one service", target.ServiceName)
}
serviceNum++
service := &corev1.Service{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(resource.Object, service)
if err != nil {
return errors.Errorf("failed to convert service %s service to service object: %v", target.ServiceName, err)
}
service.Name = service.Name + "-blue"
if service.Spec.Selector == nil {
service.Spec.Selector = make(map[string]string)
}
service.Spec.Selector[config.BlueGreenVerionLabelName] = config.BlueVersion
target.BlueServiceYaml, err = toYaml(service)
if err != nil {
return errors.Errorf("failed to marshal service %s service object: %v", target.ServiceName, err)
}
}
}
if serviceNum == 0 {
return errors.Errorf("service %s has no service", target.ServiceName)
}
}
}
j.job.Spec = j.spec
return nil
}
func (j *BlueGreenDeployV2Job) MergeArgs(args *commonmodels.Job) error {
if j.job.Name == args.Name && j.job.JobType == args.JobType {
j.spec = &commonmodels.BlueGreenDeployV2JobSpec{}
if err := commonmodels.IToi(j.job.Spec, j.spec); err != nil {
return err
}
j.job.Spec = j.spec
argsSpec := &commonmodels.BlueGreenDeployV2JobSpec{}
if err := commonmodels.IToi(args.Spec, argsSpec); err != nil {
return err
}
j.spec.Services = argsSpec.Services
j.job.Spec = j.spec
}
return nil
}
func (j *BlueGreenDeployV2Job) ToJobs(taskID int64) ([]*commonmodels.JobTask, error) {
resp := []*commonmodels.JobTask{}
j.spec = &commonmodels.BlueGreenDeployV2JobSpec{}
if err := commonmodels.IToi(j.job.Spec, j.spec); err != nil {
return resp, err
}
templateProduct, err := templaterepo.NewProductColl().Find(j.workflow.Project)
if err != nil {
return resp, fmt.Errorf("cannot find product %s: %w", j.workflow.Project, err)
}
timeout := templateProduct.Timeout * 60
if len(j.spec.Services) == 0 {
return resp, errors.Errorf("target services is empty")
}
for _, target := range j.spec.Services {
var (
deployment *v1.Deployment
deploymentYaml string
greenDeploymentSelector map[string]string
service *corev1.Service
greenService *corev1.Service
serviceYaml string
greenDeploymentName string
)
if target.BlueServiceYaml != "" {
service = &corev1.Service{}
decoder := serializer.NewCodecFactory(scheme.Scheme).UniversalDecoder()
err := runtime.DecodeInto(decoder, []byte(target.BlueServiceYaml), service)
if err != nil {
return resp, errors.Errorf("failed to decode %s k8s service yaml, err: %s", target.ServiceName, err)
}
serviceYaml = target.BlueServiceYaml
} else {
return resp, errors.Errorf("service %s blue service yaml is empty", target.ServiceName)
}
yamlContent, _, err := kube.FetchCurrentAppliedYaml(&kube.GeneSvcYamlOption{
ProductName: j.workflow.Project,
EnvName: j.spec.Env,
ServiceName: target.ServiceName,
})
if err != nil {
return resp, errors.Errorf("failed to fetch %s current applied yaml, err: %s", target.ServiceName, err)
}
resources := make([]*unstructured.Unstructured, 0)
manifests := releaseutil.SplitManifests(yamlContent)
for _, item := range manifests {
u, err := serializer2.NewDecoder().YamlToUnstructured([]byte(item))
if err != nil {
return resp, errors.Errorf("failed to decode service %s yaml to unstructured: %v", target.ServiceName, err)
}
resources = append(resources, u)
}
for _, resource := range resources {
switch resource.GetKind() {
case setting.Deployment:
if deployment != nil {
return resp, errors.Errorf("service %s has more than one deployment", target.ServiceName)
}
deployment = &v1.Deployment{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(resource.Object, deployment)
if err != nil {
return resp, errors.Errorf("failed to convert service %s deployment to deployment object: %v", target.ServiceName, err)
}
greenDeploymentName = deployment.Name
if deployment.Spec.Selector == nil {
return resp, errors.Errorf("service %s deployment selector is empty", target.ServiceName)
}
greenDeploymentSelector = deployment.Spec.Selector.MatchLabels
deployment.Name = deployment.Name + "-blue"
deployment.Labels = addLabels(deployment.Labels, map[string]string{
types.ZadigReleaseTypeLabelKey: types.ZadigReleaseTypeBlueGreen,
types.ZadigReleaseServiceNameLabelKey: target.ServiceName,
})
deployment.Spec.Selector.MatchLabels = addLabels(deployment.Spec.Selector.MatchLabels, map[string]string{
config.BlueGreenVerionLabelName: config.BlueVersion,
})
deployment.Spec.Template.Labels = addLabels(deployment.Spec.Template.Labels, map[string]string{
config.BlueGreenVerionLabelName: config.BlueVersion,
})
deploymentYaml, err = toYaml(deployment)
if err != nil {
return resp, errors.Errorf("failed to marshal service %s deployment object: %v", target.ServiceName, err)
}
var newImages []*commonmodels.Container
for _, image := range target.ServiceAndImage {
newImages = append(newImages, &commonmodels.Container{
Name: image.ServiceModule,
Image: image.Image,
})
}
deploymentYaml, _, err = kube.ReplaceWorkloadImages(deploymentYaml, newImages)
if err != nil {
return resp, errors.Errorf("failed to replace service %s deployment image: %v", target.ServiceName, err)
}
case setting.Service:
greenService = &corev1.Service{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(resource.Object, greenService)
if err != nil {
return resp, errors.Errorf("failed to convert service %s service to service object: %v", target.ServiceName, err)
}
target.GreenServiceName = greenService.Name
}
}
if deployment == nil || service == nil {
return resp, errors.Errorf("service %s has no deployment or service", target.ServiceName)
}
if service.Spec.Selector == nil || deployment.Spec.Template.Labels == nil {
return resp, errors.Errorf("service %s has no service selector or deployment has no labels", target.ServiceName)
}
selector, err := metav1.LabelSelectorAsSelector(metav1.SetAsLabelSelector(service.Spec.Selector))
if err != nil {
return resp, errors.Errorf("service %s k8s service convert to selector err: %v", target.ServiceName, err)
}
if !selector.Matches(labels.Set(deployment.Spec.Template.Labels)) {
return resp, errors.Errorf("service %s k8s service selector not match deployment.spec.template labels", target.ServiceName)
}
if greenService == nil {
return resp, errors.Errorf("service %s has no k8s service", target.ServiceName)
}
greenSelector, err := metav1.LabelSelectorAsSelector(metav1.SetAsLabelSelector(greenService.Spec.Selector))
if err != nil {
return resp, errors.Errorf("service %s k8s green service convert to selector err: %v", target.ServiceName, err)
}
if !greenSelector.Matches(labels.Set(greenDeploymentSelector)) {
return resp, errors.Errorf("service %s k8s green service selector not match deployment.spec.template labels", target.ServiceName)
}
// set target value for blue_green_release ToJobs get these
target.BlueDeploymentName = deployment.Name
target.BlueServiceName = service.Name
target.GreenDeploymentName = greenDeploymentName
task := &commonmodels.JobTask{
Name: jobNameFormat(j.job.Name + "-" + target.ServiceName),
Key: strings.Join([]string{j.job.Name, target.ServiceName}, "."),
JobInfo: map[string]string{
JobNameKey: j.job.Name,
"service_name": target.ServiceName,
},
JobType: string(config.JobK8sBlueGreenDeploy),
Spec: &commonmodels.JobTaskBlueGreenDeployV2Spec{
Production: j.spec.Production,
Env: j.spec.Env,
Service: &commonmodels.BlueGreenDeployV2Service{
ServiceName: target.ServiceName,
BlueServiceYaml: serviceYaml,
BlueServiceName: service.Name,
BlueDeploymentYaml: deploymentYaml,
BlueDeploymentName: deployment.Name,
GreenServiceName: target.GreenServiceName,
GreenDeploymentName: greenDeploymentName,
ServiceAndImage: target.ServiceAndImage,
},
DeployTimeout: timeout,
},
}
resp = append(resp, task)
}
j.job.Spec = j.spec
return resp, nil
}
func (j *BlueGreenDeployV2Job) LintJob() error {
j.spec = &commonmodels.BlueGreenDeployV2JobSpec{}
if err := commonmodels.IToiYaml(j.job.Spec, j.spec); err != nil {
return err
}
if j.spec.Version == "" {
return errors.Errorf("job %s version is too old and not supported, please remove it and create a new one", j.job.Name)
}
quoteJobs := []*commonmodels.Job{}
for _, stage := range j.workflow.Stages {
for _, job := range stage.Jobs {
if job.JobType != config.JobK8sBlueGreenRelease {
continue
}
releaseJobSpec := &commonmodels.BlueGreenReleaseV2JobSpec{}
if err := commonmodels.IToiYaml(job.Spec, releaseJobSpec); err != nil {
return err
}
if releaseJobSpec.FromJob == j.job.Name {
quoteJobs = append(quoteJobs, job)
}
}
}
if len(quoteJobs) == 0 {
return errors.Errorf("no blue-green release job quote blue-green deploy job %s", j.job.Name)
}
if len(quoteJobs) > 1 {
return errors.Errorf("more than one blue-green release job quote blue-green deploy job %s", j.job.Name)
}
jobRankmap := getJobRankMap(j.workflow.Stages)
if jobRankmap[j.job.Name] >= jobRankmap[quoteJobs[0].Name] {
return errors.Errorf("blue-green release job %s should run before blue-green deploy job %s", quoteJobs[0].Name, j.job.Name)
}
return nil
}
func toYaml(obj runtime.Object) (string, error) {
y := printers.YAMLPrinter{}
writer := bytes.NewBuffer(nil)
err := y.PrintObj(obj, writer)
if err != nil {
return "", errors.Wrapf(err, "failed to marshal object to yaml")
}
return writer.String(), nil
}
func addLabels(labels, newLabels map[string]string) map[string]string {
if labels == nil {
labels = make(map[string]string)
}
for k, v := range newLabels {
labels[k] = v
}
return labels
}
/*
Copyright 2023 The KodeRover 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 job
import (
"fmt"
"strings"
"github.com/koderover/zadig/pkg/microservice/aslan/config"
commonmodels "github.com/koderover/zadig/pkg/microservice/aslan/core/common/repository/models"
templaterepo "github.com/koderover/zadig/pkg/microservice/aslan/core/common/repository/mongodb/template"
)
type BlueGreenReleaseV2Job struct {
job *commonmodels.Job
workflow *commonmodels.WorkflowV4
spec *commonmodels.BlueGreenReleaseV2JobSpec
}
func (j *BlueGreenReleaseV2Job) Instantiate() error {
j.spec = &commonmodels.BlueGreenReleaseV2JobSpec{}
if err := commonmodels.IToiYaml(j.job.Spec, j.spec); err != nil {
return err
}
j.job.Spec = j.spec
return nil
}
func (j *BlueGreenReleaseV2Job) SetPreset() error {
j.spec = &commonmodels.BlueGreenReleaseV2JobSpec{}
if err := commonmodels.IToi(j.job.Spec, j.spec); err != nil {
return err
}
j.job.Spec = j.spec
return nil
}
func (j *BlueGreenReleaseV2Job) MergeArgs(args *commonmodels.Job) error {
return nil
}
func (j *BlueGreenReleaseV2Job) ToJobs(taskID int64) ([]*commonmodels.JobTask, error) {
resp := []*commonmodels.JobTask{}
j.spec = &commonmodels.BlueGreenReleaseV2JobSpec{}
if err := commonmodels.IToi(j.job.Spec, j.spec); err != nil {
return resp, err
}
deployJobSpec := &commonmodels.BlueGreenDeployV2JobSpec{}
found := false
for _, stage := range j.workflow.Stages {
for _, job := range stage.Jobs {
if job.JobType != config.JobK8sBlueGreenDeploy || job.Name != j.spec.FromJob {
continue
}
if err := commonmodels.IToi(job.Spec, deployJobSpec); err != nil {
return resp, err
}
found = true
break
}
}
if !found {
return resp, fmt.Errorf("no blue-green release job: %s found, please check workflow configuration", j.spec.FromJob)
}
templateProduct, err := templaterepo.NewProductColl().Find(j.workflow.Project)
if err != nil {
return resp, fmt.Errorf("cannot find product %s: %w", j.workflow.Project, err)
}
timeout := templateProduct.Timeout * 60
for _, target := range deployJobSpec.Services {
task := &commonmodels.JobTask{
Name: jobNameFormat(j.job.Name + "-" + target.ServiceName),
Key: strings.Join([]string{j.job.Name, target.ServiceName}, "."),
JobInfo: map[string]string{
JobNameKey: j.job.Name,
"service_name": target.ServiceName,
},
JobType: string(config.JobK8sBlueGreenRelease),
Spec: &commonmodels.JobTaskBlueGreenReleaseV2Spec{
Production: deployJobSpec.Production,
Env: deployJobSpec.Env,
Service: target,
DeployTimeout: timeout,
},
}
resp = append(resp, task)
}
j.job.Spec = j.spec
return resp, nil
}
func (j *BlueGreenReleaseV2Job) LintJob() error {
j.spec = &commonmodels.BlueGreenReleaseV2JobSpec{}
if err := commonmodels.IToiYaml(j.job.Spec, j.spec); err != nil {
return err
}
jobRankMap := getJobRankMap(j.workflow.Stages)
buildJobRank, ok := jobRankMap[j.spec.FromJob]
if !ok || buildJobRank >= jobRankMap[j.job.Name] {
return fmt.Errorf("can not quote job %s in job %s", j.spec.FromJob, j.job.Name)
}
return nil
}
......@@ -1500,44 +1500,6 @@ func jobsToJobPreviews(jobs []*commonmodels.JobTask, context map[string]string,
sepc.ClusterName = cluster.Name
}
jobPreview.Spec = sepc
case string(config.JobK8sBlueGreenDeploy):
taskJobSpec := &commonmodels.JobTaskBlueGreenDeploySpec{}
if err := commonmodels.IToi(job.Spec, taskJobSpec); err != nil {
continue
}
sepc := K8sBlueGreenDeployJobSpec{
Image: taskJobSpec.Image,
K8sServiceName: taskJobSpec.K8sServiceName,
Namespace: taskJobSpec.Namespace,
ContainerName: taskJobSpec.ContainerName,
Events: taskJobSpec.Events,
}
cluster, err := commonrepo.NewK8SClusterColl().Get(taskJobSpec.ClusterID)
if err != nil {
log.Errorf("cluster id: %s not found", taskJobSpec.ClusterID)
} else {
sepc.ClusterName = cluster.Name
}
jobPreview.Spec = sepc
case string(config.JobK8sBlueGreenRelease):
taskJobSpec := &commonmodels.JobTaskBlueGreenReleaseSpec{}
if err := commonmodels.IToi(job.Spec, taskJobSpec); err != nil {
continue
}
sepc := K8sBlueGreenReleaseJobSpec{
Image: taskJobSpec.Image,
K8sServiceName: taskJobSpec.K8sServiceName,
Namespace: taskJobSpec.Namespace,
ContainerName: taskJobSpec.ContainerName,
Events: taskJobSpec.Events,
}
cluster, err := commonrepo.NewK8SClusterColl().Get(taskJobSpec.ClusterID)
if err != nil {
log.Errorf("cluster id: %s not found", taskJobSpec.ClusterID)
} else {
sepc.ClusterName = cluster.Name
}
jobPreview.Spec = sepc
default:
jobPreview.Spec = job.Spec
}
......
......@@ -2407,6 +2407,54 @@ func GetMseOfflineResources(grayTag, envName, projectName string) ([]string, err
return services, nil
}
func GetBlueGreenServiceK8sServiceYaml(projectName, envName, serviceName string) (string, error) {
yamlContent, _, err := kube.FetchCurrentAppliedYaml(&kube.GeneSvcYamlOption{
ProductName: projectName,
EnvName: envName,
ServiceName: serviceName,
})
if err != nil {
return "", errors.Errorf("failed to fetch %s current applied yaml, err: %s", serviceName, err)
}
resources := make([]*unstructured.Unstructured, 0)
manifests := releaseutil.SplitManifests(yamlContent)
for _, item := range manifests {
u, err := serializer.NewDecoder().YamlToUnstructured([]byte(item))
if err != nil {
return "", errors.Errorf("failed to decode service %s yaml to unstructured: %v", serviceName, err)
}
resources = append(resources, u)
}
var (
service *corev1.Service
serviceYaml string
)
for _, resource := range resources {
switch resource.GetKind() {
case setting.Service:
if service != nil {
return "", errors.Errorf("service %s has more than one service", serviceName)
}
service = &corev1.Service{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(resource.Object, service)
if err != nil {
log.Errorf("failed to convert service %s service to service object: %v\nyaml: %s", serviceName, err, yamlContent)
return "", errors.Errorf("failed to convert service %s service to service object: %v", serviceName, err)
}
service.Name = service.Name + "-blue"
if service.Spec.Selector == nil {
service.Spec.Selector = make(map[string]string)
}
service.Spec.Selector[config.BlueGreenVerionLabelName] = config.BlueVersion
serviceYaml, err = toYaml(service)
if err != nil {
return "", errors.Errorf("failed to marshal service %s service object: %v", serviceName, err)
}
}
}
return serviceYaml, nil
}
func GetMseTagsInEnv(envName, projectName string) ([]string, error) {
prod, err := commonrepo.NewProductColl().Find(&commonrepo.ProductFindOptions{
Name: projectName,
......
......@@ -16,8 +16,6 @@ limitations under the License.
package resource
import "github.com/koderover/zadig/pkg/microservice/aslan/config"
type Workload struct {
Name string `json:"name"`
Type string `json:"type"`
......@@ -26,8 +24,8 @@ type Workload struct {
Replicas int32 `json:"replicas"`
// ZadigXReleaseType represent the release type of workload created by zadigx when it is not empty
// frontend should limit or allow some operations on these workloads
ZadigXReleaseType config.ZadigXReleaseType `json:"zadigx_release_type"`
ZadigXReleaseTag string `json:"zadigx_release_tag"`
ZadigXReleaseType string `json:"zadigx_release_type"`
ZadigXReleaseTag string `json:"zadigx_release_tag"`
}
type ContainerImage struct {
......
......@@ -37,5 +37,12 @@ const (
const (
ZadigReleaseVersionOriginal = "original"
ZadigReleaseTypeMseGray = "mse-gray"
ZadigReleaseTypeMseGray = "mse-gray"
ZadigReleaseTypeBlueGreen = "blue-green"
)
var ZadigReleaseTypeList = []string{
ZadigReleaseTypeMseGray,
ZadigReleaseTypeBlueGreen,
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册