从0到1开发自动化运维平台-Kubernetes集群和应用管理 Day3

今天还是一些CRUD操作,继续分享k8s集群和应用的管理。

Kubernetes和应用模型

class KubernetesCluster(TimeAbstract):
    """
    K8s集群配置
    """
    name = models.CharField(max_length=100, unique=True, verbose_name='集群名称')
    version = models.JSONField(default=dict, verbose_name='版本',
                               help_text='{"core": "1.14", "apiversion": "apps/v1"}
core: 集群版本
apiversion: API版本')
    desc = models.TextField(null=True, blank=True, verbose_name='集群描述')
    config = models.JSONField(default=dict, verbose_name='集群配置')
    environment = models.ManyToManyField(Environment, related_name='env_k8s', blank=True, verbose_name='环境')
    product = models.ManyToManyField(Product, related_name='product_k8s', blank=True, verbose_name='产品')
    idc = models.ForeignKey(Idc, blank=True, null=True, on_delete=models.PROTECT, verbose_name='IDC')

    def __str__(self):
        return self.name

    class ExtMeta:
        related = True
        dashboard = True
        icon = 'k8s'

    class Meta:
        default_permissions = ()
        ordering = ['-id']
        verbose_name = 'K8s集群'
        verbose_name_plural = verbose_name + '管理'


def get_default_value():
    return {
        'key': 'default', 'value': 'default'
    }


# 应用部署方式
G_DEPLOY_TYPE = (
    ('nonk8s', '非Kubernetes部署'),
    ('docker', 'Docker部署'),
    ('k8s', 'Kubernetes部署')
)


G_ONLINE_CHOICE = (
    (0, '未上线'),
    (1, '已上线'),
    (2, '部署中'),
    (3, '部署异常'),
    (9, '已申请上线')
)


class MicroApp(TimeAbstract):
    # product.project.microapp
    appid = models.CharField(max_length=250, db_index=True, unique=True, verbose_name='应用ID',
                             help_text='应用唯一标识,无需填写')
    name = models.CharField(max_length=128, verbose_name='应用')
    alias = models.CharField(max_length=128, blank=True, verbose_name='别名')
    project = models.ForeignKey(Project, on_delete=models.PROTECT, null=True, blank=True, verbose_name='项目')
    creator = models.ForeignKey(User, on_delete=models.PROTECT, null=True, blank=True, verbose_name='创建者',
                                help_text='前端不需要传递')
    repo = models.JSONField(default=dict, verbose_name='仓库地址',
                            help_text='{"name": name, "description": "", "path_with_namespace": "", "http_url_to_repo": url}')
    target = models.JSONField(default=get_default_value, verbose_name='JAR包配置',
                              help_text='默认:default, {"default": "default", "custom": "xxx/a.war"}')
    extra_members = models.JSONField(default=get_default_extra_members,
                                     verbose_name="额外成员组", help_text='{"name": "自定义成员组1", members: [1,2,3]}')
    category = models.CharField(max_length=128, blank=True, null=True, verbose_name='应用分类')
    template = models.JSONField(default=dict, verbose_name='K8sDeployment模板',
                                help_text='Kubernetes Deployment部署模板配置')
    language = models.CharField(max_length=32, default='java', verbose_name='开发语言')
    multiple_app = models.BooleanField(default=False, blank=True, verbose_name='多应用标志')
    multiple_ids = models.JSONField(default=list, verbose_name='多应用关联ID列表')
    dockerfile = models.JSONField(default=get_default_value, verbose_name='Dockerfile配置',
                                  help_text='默认:{default: null}, 可选: {"default|默认": null, "project|使用项目Dockerfile": "project", "custom|自定义Dockerfile": ""}')
    online = models.BooleanField(default=True, blank=True, verbose_name='上线下线',
                                 help_text='应用上线/下线状态标记, 下线状态的应用禁止发布.')
    desc = models.TextField(verbose_name='描述', null=True, blank=True)
    notify = models.JSONField(default=dict, verbose_name='消息通知')
    can_edit = models.JSONField(default=list, verbose_name='管理人员',
                                help_text='有权限编辑该应用的人员ID
格式为数组, 如[1,2]')
    is_k8s = models.CharField(max_length=8, default='k8s', choices=G_DEPLOY_TYPE, verbose_name='部署方式',
                              help_text=f'默认k8s, 可选: {dict(G_DEPLOY_TYPE)}')
    modules = models.JSONField(default=list, verbose_name='工程模块')

    def __str__(self):
        return '[%s]%s' % (self.name, self.alias)

    class ExtMeta:
        related = True
        dashboard = True
        icon = 'component'

    class Meta:
        default_permissions = ()
        ordering = ['-created_time']
        verbose_name = '应用'
        verbose_name_plural = verbose_name + '管理'


class AppInfo(TimeAbstract):
    # uniq_tag: product.project.microapp.env
    uniq_tag = models.CharField(max_length=128, unique=True, verbose_name='唯一标识', help_text='前端留空,无需传值')
    app = models.ForeignKey(MicroApp, blank=True, null=True, on_delete=models.PROTECT, verbose_name='应用')
    environment = models.ForeignKey(Environment, on_delete=models.PROTECT, null=True, verbose_name='环境')
    branch = models.CharField(max_length=64, blank=True, null=True, verbose_name="默认构建分支")
    allow_ci_branch = models.JSONField(default=list, verbose_name='允许构建的分支',
                                       help_text="存储数组格式,具体的分支名; 默认['*'], 表示允许所有分支.")
    allow_cd_branch = models.JSONField(default=list, verbose_name='允许发布的分支',
                                       help_text="存储数组格式,具体的分支名; 默认['*'], 表示允许所有分支.")
    build_command = models.CharField(max_length=250, blank=True, null=True, verbose_name='构建命令',
                                     help_text='根据应用开发语言, 从getKey("LANGUAGE")获取数据, 取出extra字段的build值')
    kubernetes = models.ManyToManyField(KubernetesCluster, related_name='k8s_app', through='KubernetesDeploy',
                                        verbose_name='K8s集群')
    hosts = models.JSONField(default=list, verbose_name='部署主机', help_text='部署主机, 格式: []')
    template = models.JSONField(default=dict, verbose_name='K8sDeployment模板',
                                help_text='继承自当前应用的template字段,数据格式为对象
字段说明:
type: 0|1, 0表示继承应用模板,template为空字典;1表示自定义模板
示例: {"type": 0, "template": {}}')
    # {0: 禁用, 1: 启用}
    is_enable = models.SmallIntegerField(default=1, verbose_name='启用', help_text='状态 {0: 禁用, 1: 启用},默认值为1')
    desc = models.TextField(verbose_name='描述', null=True, blank=True)
    can_edit = models.JSONField(default=list, verbose_name='管理人员',
                                help_text='有权限编辑该应用的人员ID
格式为数组, 如[1,2]')
    online = models.SmallIntegerField(default=0, choices=G_ONLINE_CHOICE, verbose_name='是否上线',
                                      help_text=f'默认为0,即未上线
可选项: {G_ONLINE_CHOICE}')

    def __str__(self):
        return self.uniq_tag

    @property
    def namespace(self):
        return f'{self.environment.name.replace("_", "-")}-{self.app.project.name.replace("_", "-")}'.lower()

    @property
    def jenkins_jobname(self):
        try:
            job_name = f'{self.environment.name}-{self.app.category.split(".")[-1]}-{self.app.project.name}-{self.app.name.split(".")[-1]}'.lower()
        except AppInfo.DoesNotExist:
            job_name = ''
        return job_name

    class ExtMeta:
        related = True
        dashboard = True

    class Meta:
        default_permissions = ()
        ordering = ['-update_time', '-id']
        verbose_name = '应用模块'
        verbose_name_plural = verbose_name + '管理'


class KubernetesDeploy(TimeAbstract):
    appinfo = models.ForeignKey(AppInfo, related_name='app_info', null=True, on_delete=models.CASCADE)
    kubernetes = models.ForeignKey(KubernetesCluster, related_name='app_k8s', null=True, on_delete=models.CASCADE)
    online = models.SmallIntegerField(default=0, choices=G_ONLINE_CHOICE, verbose_name='是否上线',
                                      help_text=f'默认为0,即未上线
可选项: {G_ONLINE_CHOICE}')
    version = models.CharField(max_length=250, blank=True, null=True, verbose_name='当前版本')

    def __str__(self):
        return '%s-%s' % (self.appinfo.app.appid, self.kubernetes.name)

    class Meta:
        default_permissions = ()


class DataDict(CommonParent):
    key = models.CharField(max_length=80, unique=True, verbose_name='键')
    value = models.CharField(max_length=80, verbose_name='值')
    extra = models.TextField(null=True, blank=True, default='', verbose_name='额外参数')
    desc = models.CharField(max_length=255, blank=True, null=True, verbose_name='备注')

    def __str__(self):
        return self.value

    class Meta:
        default_permissions = ()
        verbose_name = '字典'
        verbose_name_plural = verbose_name + '管理'

编写序列化器

import json
from typing import List
from rest_framework import serializers

from django.db import transaction
from django.contrib.auth.models import User

from cmdb.models import Product, Project, Environment, KubernetesCluster, MicroApp, AppInfo, KubernetesDeploy


class KubernetesClusterListSerializers(serializers.ModelSerializer):
    config = serializers.SerializerMethodField()

    def get_config(self, instance):
        return json.loads(instance.config)

    class Meta:
        model = KubernetesCluster
        fields = '__all__'


class KubernetesClusterSerializers(serializers.ModelSerializer):
    class Meta:
        model = KubernetesCluster
        fields = '__all__'


class MicroAppListSerializers(serializers.ModelSerializer):
    project_info = serializers.SerializerMethodField()
    appinfo = serializers.SerializerMethodField()
    creator_info = serializers.SerializerMethodField()
    extra_team_info = serializers.SerializerMethodField()

    def get_project_info(self, instance):
        project = instance.project
        return {'project': {'id': project.id, 'alias': project.alias}, 
                'product': {'id': project.product.id, 'alias': project.product.alias}}

    def get_appinfo(self, instance):
        return [
            {'id': i.id, 'env_alias': i.environment.alias, 'env': {'name': i.environment.name, 'id': i.environment.id},
             'online': i.online} for i in instance.appinfo_set.all()]

    def get_creator_info(self, instance):
        try:
            return {'id': instance.creator.id, 'first_name': instance.creator.first_name,
                    'username': instance.creator.username}
        except BaseException as e:
            return {'id': '', 'first_name': '', 'username': ''}

    def get_extra_team_info(self, instance):
        data = {}
        for k, v in instance.extra_members.items():
            data[k] = [
                {'id': i.id, 'name': i.name, 'first_name': i.first_name, 'username': i.username}
                for i in User.objects.filter(id__in=v)
            ]
        return data

    class Meta:
        model = MicroApp
        fields = '__all__'


class MicroAppSerializers(serializers.ModelSerializer):
    class Meta:
        model = MicroApp
        fields = '__all__'
        read_only_fields = ('appid',)

    @staticmethod
    def perform_extend_save(validated_data):
        def default_value(fields: List):
            for field in fields:
                if validated_data.get(field):
                    if validated_data[field].get('key') != 'custom':
                        validated_data[field]['value'] = validated_data[field]['key']
            return validated_data

        validated_data = default_value(['dockerfile', 'target'])
        validated_data[
            'appid'] = f"{validated_data['project'].product.name}.{validated_data['project'].name}.{validated_data['name']}"
        return validated_data

    def create(self, validated_data):
        instance = MicroApp.objects.create(can_edit=[validated_data['creator'].id],
                                           **self.perform_extend_save(validated_data))
        return instance

    def update(self, instance, validated_data):
        return super().update(instance, self.perform_extend_save(validated_data))


class AppInfoListSerializers(serializers.ModelSerializer):
    app = MicroAppSerializers()
    kubernetes_info = serializers.SerializerMethodField()

    def get_kubernetes_info(self, instance):
        serializer = KubernetesDeploySerializers(data=KubernetesDeploy.objects.filter(appinfo=instance.id), many=True)
        serializer.is_valid()
        return serializer.data

    class Meta:
        model = AppInfo
        fields = '__all__'


class AppInfoSerializers(serializers.ModelSerializer):
    class Meta:
        model = AppInfo
        fields = '__all__'

    def perform_extend_save(self, validated_data, *args, **kwargs):
        if validated_data.get('app', None) and validated_data.get('environment', None):
            validated_data[
                'uniq_tag'] = f"{validated_data['app'].appid}.{validated_data['environment'].name.split('_')[-1].lower()}"

        if kwargs.get('instance', None):
            kubernetes = self.initial_data.get('kubernetes')
            _bulk = []
            for kid in kubernetes:
                _ks = KubernetesCluster.objects.get(id=kid)
                _bulk.append(KubernetesDeploy(appinfo=kwargs['instance'], kubernetes=_ks))
            KubernetesDeploy.objects.bulk_create(_bulk, ignore_conflicts=True)
        return validated_data

    @transaction.atomic
    def create(self, validated_data):
        instance = AppInfo.objects.create(**self.perform_extend_save(validated_data))
        if 'kubernetes' in self.initial_data:
            self.perform_extend_save(validated_data, **{'instance': instance})
        return instance

    @transaction.atomic
    def update(self, instance, validated_data):
        KubernetesDeploy.objects.filter(appinfo=instance).delete()
        instance.__dict__.update(**self.perform_extend_save(validated_data, **{'instance': instance}))
        instance.save()
        return instance

编写视图

from rest_framework.response import Response
from rest_framework.decorators import action
from django.db import transaction

from cmdb.models import Product, Project, Environment, KubernetesCluster, MicroApp, AppInfo
from cmdb.serializers import AppInfoListSerializers, AppInfoSerializers, KubernetesClusterListSerializers, KubernetesClusterSerializers, MicroAppListSerializers, MicroAppSerializers
from common.extends.decorators import cmdb_app_unique_check


class KubernetesClusterViewSet(viewsets.ModelViewSet):
    """
    Kubernetes集群视图

    ### Kubernetes集群权限
        {'*': ('k8scluster_all', 'k8s集群管理')},
        {'get': ('k8scluster_list', '查看k8s集群')},
        {'post': ('k8scluster_create', '创建k8s集群')},
        {'put': ('k8scluster_edit', '编辑k8s集群')},
        {'patch': ('k8scluster_edit', '编辑k8s集群')},
        {'delete': ('k8scluster_delete', '删除k8s集群')}
    """
    perms_map = (
        {'*': ('admin', '管理员')},
        {'*': ('k8scluster_all', 'k8s集群管理')},
        {'get': ('k8scluster_list', '查看k8s集群')},
        {'post': ('k8scluster_create', '创建k8s集群')},
        {'put': ('k8scluster_edit', '编辑k8s集群')},
        {'patch': ('k8scluster_edit', '编辑k8s集群')},
        {'delete': ('k8scluster_delete', '删除k8s集群')}
    )
    queryset = KubernetesCluster.objects.all()
    serializer_class = KubernetesClusterSerializers

    def get_serializer_class(self):
        if self.action in ['list', 'retrieve']:
            return KubernetesClusterListSerializers
        return KubernetesClusterSerializers
    

class MicroAppViewSet(viewsets.ModelViewSet):
    """
    项目应用视图

    ### 项目应用权限
        {'*': ('microapp_all', '应用管理')},
        {'get': ('microapp_list', '查看应用')},
        {'post': ('microapp_create', '创建应用')},
        {'put': ('microapp_edit', '编辑应用')},
        {'patch': ('microapp_edit', '编辑应用')},
        {'delete': ('microapp_delete', '删除应用')}
    """
    perms_map = (
        {'*': ('admin', '管理员')},
        {'*': ('microapp_all', '应用管理')},
        {'get': ('microapp_list', '查看应用')},
        {'post': ('microapp_create', '创建应用')},
        {'put': ('microapp_edit', '编辑应用')},
        {'patch': ('microapp_edit', '编辑应用')},
        {'delete': ('microapp_delete', '删除应用')}
    )
    queryset = MicroApp.objects.all()
    serializer_class = MicroAppSerializers

    def get_serializer_class(self):
        if self.action in ['list', 'retrieve']:
            return MicroAppListSerializers
        return MicroAppSerializers
    
    @cmdb_app_unique_check()
    def create(self, request, *args, **kwargs):
        """
        创建应用

        提交参数
        创建:{}
        """
        try:
            request.data['name'] = request.data['name'].strip(' ').replace(' ', '-')
        except BaseException as e:
            logger.error('exception ', str(e))
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response({'code': 40000, 'status': 'failed', 'message': serializer.errors})
        try:
            self.perform_create(serializer)
        except BaseException as e:
            logger.error(e)
            return Response({'code': 50000, 'status': 'failed', 'message': str(e)})
        data = {'data': serializer.data, 'status': 'success', 'code': 20000}
        return Response(data)


    @transaction.atomic
    def perform_create(self, serializer):
        serializer.save(creator=self.request.user)

    @action(methods=['POST'], url_path='related', detail=False)
    def app_related(self, request):
        """
        应用关联

        ### 传递参数:
            ids: 待关联应用id数组
            target: 目标应用id
        """
        try:
            target = request.data.get('target', None)
            ids = request.data.get('ids', None)
            if target:
                instance = self.queryset.get(id=target)
                ids.extend(instance.multiple_ids)
            self.queryset.filter(id__in=list(set(ids))).update(multiple_app=True, multiple_ids=list(set(ids)))
            return Response({'code': 20000, 'data': '应用关联成功.'})
        except BaseException as e:
            logger.error('err', e)
            return Response({'code': 50000, 'data': '关联应用异常,请联系管理员!'})

    @action(methods=['POST'], url_path='unrelated', detail=False)
    def app_unrelated(self, request):
        """
        取消应用关联

        ### 传递参数:
            id: 应用id
        """
        try:
            instance = self.queryset.filter(id=request.data.get('id'))
            # 获取关联应用ID列表
            ids = instance[0].multiple_ids
            ids.remove(instance[0].id)
            if len(ids) == 1:
                # 如果关联应用只剩下一个,则一起取消关联
                self.queryset.filter(id__in=instance[0].multiple_ids).update(multiple_app=False, multiple_ids=[])
            else:
                # 更新其它应用的关联应用ID
                self.queryset.filter(id__in=ids).update(multiple_ids=ids)
                # 取消当前实例应用关联
                instance.update(multiple_app=False, multiple_ids=[])
            return Response({'code': 20000, 'data': '应用取消关联成功.'})
        except BaseException as e:
            return Response({'code': 50000, 'data': '关联应用异常,请联系管理员!'})


class AppInfoViewSet(viewsets.ModelViewSet):
    """
    项目应用服务

    * 服务对应着应用的不同环境,即应用每个环境创建一个对应的服务

    ### 项目应用服务权限
        {'*': ('microapp_all', '应用管理')},
        {'get': ('microapp_list', '查看应用')},
        {'post': ('microapp_create', '创建应用')},
        {'put': ('microapp_edit', '编辑应用')},
        {'patch': ('microapp_edit', '编辑应用')},
        {'delete': ('microapp_delete', '删除应用')}
    """
    perms_map = (
        {'*': ('admin', '管理员')},
        {'*': ('microapp_all', '应用管理')},
        {'get': ('microapp_list', '查看应用')},
        {'post': ('microapp_create', '创建应用')},
        {'put': ('microapp_edit', '编辑应用')},
        {'patch': ('microapp_edit', '编辑应用')},
        {'delete': ('microapp_delete', '删除应用')}
    )
    queryset = AppInfo.objects.all()
    serializer_class = AppInfoSerializers

    def get_serializer_class(self):
        if self.action in ['list', 'retrieve']:
            return AppInfoListSerializers
        return AppInfoSerializers

    def create(self, request, *args, **kwargs):
        request.data['uniq_tag'] = 'default'
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        data = serializer.data

        data = {'code': 20000, 'status': 200, 'data': data}
        return Response(data)

添加装饰器

细心的朋友应该可以发现,在视图里文件里导入一个自己编写的装饰器

from common.extends.decorators import cmdb_app_unique_check

我们先在项目根下创建common目录

(venv)    ydevops-backend mkdir -p common/extends
(venv)    ydevops-backend touch common/extends/decorators.py

然后编写装饰器cmdb_app_unique_check

from functools import wraps
from rest_framework.response import Response
from cmdb.models import MicroApp


def cmdb_app_unique_check():
    """
    应用唯一性检查
    appid: {product.name}.{app.name}
    """

    def check_app(product, name):
        try:
            if MicroApp.objects.filter(project__product__id=product, name=name).exists():
                # 存在应用
                return True
        except BaseException as e:
            pass
        return False

    def decorator(func):
        @wraps(func)
        def wrapper(self, request, *args, **kwargs):
            if check_app(request.data['product'], request.data['name']):
                return Response({'code': 50000, 'message': f'该产品下已存在[{request.data["name"]}]同名应用.'})
            return func(self, request, *args, **kwargs)

        return wrapper

    return decorator

添加路由

from django.contrib import admin
from django.urls import path, include
from cmdb.view.view_cmdb import AppInfoViewSet, KubernetesClusterViewSet, MicroAppViewSet

from rest_framework.routers import DefaultRouter

from cmdb.views import RegionViewSet, IdcViewSet, ProductViewSet, ProjectViewSet, EnvironmentViewSet


router = DefaultRouter()

router.register('region', RegionViewSet)
router.register('asset/idc', IdcViewSet)
router.register('product', ProductViewSet)
router.register('project', ProjectViewSet)
router.register('environment', EnvironmentViewSet)
router.register('app/service', AppInfoViewSet)
router.register('app', MicroAppViewSet)
router.register('kubernetes', KubernetesClusterViewSet)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),
]

迁移数据表

(venv)    ydevops-backend python manage.py makemigrations
Migrations for 'cmdb':
  apps/cmdb/migrations/0003_appinfo_kubernetescluster_microapp_kubernetesdeploy_and_more.py
    - Create model AppInfo
    - Create model KubernetesCluster
    - Create model MicroApp
    - Create model KubernetesDeploy
    - Add field app to appinfo
    - Add field environment to appinfo
    - Add field kubernetes to appinfo
(venv)    ydevops-backend python manage.py makemigrations
Migrations for 'cmdb':
  apps/cmdb/migrations/0004_datadict.py
    - Create model DataDict
(venv)    ydevops-backend python manage.py migrate       
Operations to perform:
  Apply all migrations: admin, auth, cmdb, contenttypes, sessions
Running migrations:
  Applying cmdb.0003_appinfo_kubernetescluster_microapp_kubernetesdeploy_and_more... OK
  Applying cmdb.0004_datadict... OK

运行项目

(venv)    ydevops-backend python manage.py runserver 0.0.0.0:9000
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
March 23, 2023 - 22:13:47
Django version 4.1.7, using settings 'devops_backend.settings'
Starting development server at http://0.0.0.0:9000/
Quit the server with CONTROL-C.

打开浏览器访问http://localhost:9000/api/就可以看到目前已完成的接口了

展开阅读全文

页面更新:2024-03-13

标签:集群   数组   视图   分支   上线   权限   管理员   模板   编辑   项目   平台

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top