├── web ├── src │ ├── vite-env.d.ts │ ├── favicon.ico │ ├── pages │ │ ├── login │ │ │ ├── data.d.ts │ │ │ ├── service.tsx │ │ │ ├── Login.less │ │ │ └── Login.tsx │ │ ├── settings │ │ │ ├── role │ │ │ │ ├── data.d.ts │ │ │ │ ├── service.tsx │ │ │ │ └── components │ │ │ │ │ ├── CreateForm.tsx │ │ │ │ │ └── UpdateForm.tsx │ │ │ ├── menu │ │ │ │ ├── service.tsx │ │ │ │ └── data.d.ts │ │ │ ├── group │ │ │ │ ├── data.d.ts │ │ │ │ ├── service.tsx │ │ │ │ └── components │ │ │ │ │ ├── UpdateForm.tsx │ │ │ │ │ └── CreateForm.tsx │ │ │ └── user │ │ │ │ ├── data.d.ts │ │ │ │ ├── service.tsx │ │ │ │ └── components │ │ │ │ └── ResetPasswordForm.tsx │ │ ├── ops │ │ │ ├── label │ │ │ │ ├── data.d.ts │ │ │ │ ├── service.tsx │ │ │ │ └── components │ │ │ │ │ ├── UpdateForm.tsx │ │ │ │ │ └── CreateForm.tsx │ │ │ ├── env │ │ │ │ ├── data.d.ts │ │ │ │ ├── service.tsx │ │ │ │ └── components │ │ │ │ │ └── UpdateForm.tsx │ │ │ ├── cluster │ │ │ │ ├── data.d.ts │ │ │ │ ├── service.tsx │ │ │ │ └── components │ │ │ │ │ ├── UpdateForm.tsx │ │ │ │ │ └── CreateForm.tsx │ │ │ ├── envset │ │ │ │ ├── data.d.ts │ │ │ │ ├── service.tsx │ │ │ │ └── components │ │ │ │ │ └── UpdateForm.tsx │ │ │ └── template │ │ │ │ ├── data.d.ts │ │ │ │ ├── service.tsx │ │ │ │ └── components │ │ │ │ ├── CreateForm.tsx │ │ │ │ └── AddFileForm.tsx │ │ ├── 404.tsx │ │ ├── dashboard │ │ │ └── Dashboard.tsx │ │ ├── project │ │ │ └── project │ │ │ │ ├── data.d.ts │ │ │ │ ├── service.tsx │ │ │ │ └── components │ │ │ │ └── CreateForm.tsx │ │ └── deploy │ │ │ ├── varset │ │ │ ├── data.d.ts │ │ │ ├── service.tsx │ │ │ └── components │ │ │ │ └── CreateForm.tsx │ │ │ ├── deploy │ │ │ ├── service.tsx │ │ │ └── data.d.ts │ │ │ ├── config │ │ │ ├── data.d.ts │ │ │ ├── service.tsx │ │ │ └── components │ │ │ │ ├── UpdateForm.tsx │ │ │ │ └── AddFileForm.tsx │ │ │ └── app │ │ │ ├── service.tsx │ │ │ ├── data.d.ts │ │ │ └── components │ │ │ ├── UpdateForm.tsx │ │ │ └── DeployForm.tsx │ ├── App.less │ ├── layouts │ │ ├── service.tsx │ │ └── BaseLayout.tsx │ ├── index.tsx │ ├── utils │ │ ├── icons.tsx │ │ ├── local.tsx │ │ ├── auth.tsx │ │ └── request.tsx │ ├── components │ │ └── RightContent │ │ │ ├── RightContent.less │ │ │ ├── components │ │ │ └── LogoutModal.tsx │ │ │ └── RightContent.tsx │ ├── App.tsx │ ├── i18n │ │ └── config.tsx │ ├── index.css │ ├── assets │ │ └── logo.svg │ └── routers │ │ └── Routers.tsx ├── .env.development ├── tsconfig.node.json ├── .gitignore ├── index.html ├── tsconfig.json ├── vite.config.ts └── package.json ├── .dockerignore ├── hack ├── deploy │ └── docker-compose │ │ ├── mysql │ │ ├── init │ │ │ └── init.sql │ │ └── config │ │ │ └── my.cnf │ │ ├── docker-compose.yaml │ │ └── config │ │ └── magi-config.yaml └── docker │ ├── Dockerfile │ └── chn.Dockerfile ├── internal ├── utils │ ├── magiyaml │ │ ├── manifests │ │ │ ├── kustomization-project.yamltmpl │ │ │ ├── kustomization-config.yamltmpl │ │ │ ├── var-configmap.yamltmpl │ │ │ ├── kustomization-vars.yamltmpl │ │ │ ├── kustomization-cluster.yamltmpl │ │ │ └── kustomization-app.yamltmpl │ │ └── models.go │ ├── db │ │ ├── db.go │ │ └── mysql │ │ │ ├── migrations │ │ │ ├── 2_magi_seed.down.sql │ │ │ └── 1_magi_init.down.sql │ │ │ ├── migrate.go │ │ │ └── mysql.go │ ├── conf │ │ └── conf.go │ ├── git │ │ ├── provider │ │ │ └── provider.go │ │ ├── git.go │ │ └── client.go │ ├── utils.go │ └── logger │ │ └── logger.go ├── auth │ ├── model.go │ ├── jwt.go │ └── casbin.go ├── mid │ ├── cors.go │ ├── casbin.go │ ├── jwt.go │ ├── syslog.go │ └── logger.go ├── labels │ ├── model.go │ └── labels.go ├── roles │ └── model.go ├── global │ └── model.go ├── groups │ └── model.go ├── templates │ └── model.go ├── envs │ ├── model.go │ └── envs.go ├── projects │ └── model.go ├── k8s │ └── clusters │ │ └── model.go ├── menus │ └── model.go ├── deploys │ └── model.go ├── envsets │ └── model.go ├── users │ └── model.go ├── varsets │ └── model.go ├── apps │ └── model.go └── configs │ └── model.go ├── cmd └── magi │ ├── handlers │ ├── base │ │ └── health.go │ ├── v1 │ │ ├── system.go │ │ ├── user.go │ │ ├── auth.go │ │ ├── labels.go │ │ ├── apps.go │ │ ├── groups.go │ │ ├── clusters.go │ │ ├── envsets.go │ │ ├── envs.go │ │ ├── varsets.go │ │ ├── deploys.go │ │ ├── users.go │ │ ├── projects.go │ │ ├── templates.go │ │ ├── menus.go │ │ ├── roles.go │ │ └── configs.go │ └── http │ │ └── response.go │ └── main.go ├── .gitignore ├── Dockerfile ├── Makefile ├── pkg └── k8s │ ├── client │ └── client.go │ └── cluster │ └── cluster.go ├── .github └── workflows │ ├── docker-publish.yml │ └── docker-publish-chn.yml ├── config └── magi-config-example.yaml ├── README.zh_CN.md └── README.md /web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/.env.development: -------------------------------------------------------------------------------- 1 | # only used fot dev 2 | VITE_BASE_URL="http://localhost:8086" -------------------------------------------------------------------------------- /web/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basefas/magi/HEAD/web/src/favicon.ico -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | build 2 | .gitignore 3 | .dockerignore 4 | .github 5 | .vscode 6 | README.md -------------------------------------------------------------------------------- /hack/deploy/docker-compose/mysql/init/init.sql: -------------------------------------------------------------------------------- 1 | CREATE USER 'magi'@'%' IDENTIFIED BY 'Magi@1'; 2 | GRANT ALL PRIVILEGES ON *.* TO 'magi'@'%'; -------------------------------------------------------------------------------- /internal/utils/magiyaml/manifests/kustomization-project.yamltmpl: -------------------------------------------------------------------------------- 1 | resources: 2 | {{- range .Magi.KustomizationResources }} 3 | - {{ . }} 4 | {{- end}} -------------------------------------------------------------------------------- /internal/utils/magiyaml/manifests/kustomization-config.yamltmpl: -------------------------------------------------------------------------------- 1 | configMapGenerator: 2 | - name: configmap-{{ .Magi.AppName }} 3 | files: 4 | {{- range .Magi.ConfigFiles }} 5 | - {{ . }} 6 | {{- end}} -------------------------------------------------------------------------------- /hack/deploy/docker-compose/mysql/config/my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | user=mysql 3 | default-storage-engine=INNODB 4 | character-set-server=utf8 5 | [client] 6 | default-character-set=utf8 7 | [mysql] 8 | default-character-set=utf8 -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": [ 8 | "vite.config.ts" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /internal/utils/magiyaml/manifests/var-configmap.yamltmpl: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ .Magi.VarName }} 5 | namespace: flux-system 6 | data: 7 | {{- range .Magi.VarData }} 8 | {{ . }} 9 | {{- end}} -------------------------------------------------------------------------------- /cmd/magi/handlers/base/health.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | ) 7 | 8 | func Health(c *gin.Context) { 9 | http.Re(c, 0, "success", nil) 10 | } 11 | -------------------------------------------------------------------------------- /internal/auth/model.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | type CreatePolicy struct { 4 | UID string `json:"uid" binding:"required"` 5 | PolicyUrl string `json:"policy_url" binding:"required"` 6 | PolicyMethod string `json:"policy_method" binding:"required"` 7 | } 8 | -------------------------------------------------------------------------------- /web/src/pages/login/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface UserLogIn { 2 | username: string; 3 | password: string; 4 | } 5 | 6 | export interface UserLogInInfo { 7 | id: number; 8 | username: string; 9 | full_name: string; 10 | token: string; 11 | } 12 | -------------------------------------------------------------------------------- /web/src/pages/settings/role/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface RoleListItem { 2 | id: number; 3 | name: string; 4 | } 5 | 6 | export interface RoleCreateInfo { 7 | name: string; 8 | } 9 | 10 | export interface RoleUpdateInfo { 11 | name: string; 12 | } 13 | -------------------------------------------------------------------------------- /web/src/pages/login/service.tsx: -------------------------------------------------------------------------------- 1 | import { post, ResponseData } from "../../utils/request"; 2 | import { UserLogIn, UserLogInInfo } from "./data"; 3 | 4 | export async function login(user: UserLogIn): Promise> { 5 | return await post("/api/v1/login", user); 6 | } 7 | -------------------------------------------------------------------------------- /web/src/App.less: -------------------------------------------------------------------------------- 1 | .App { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | #root { 8 | height: 100%; 9 | width: 100%; 10 | background-color: #f0f2f5; 11 | background-repeat: no-repeat; 12 | background-position: center 110px; 13 | background-size: 100%; 14 | } 15 | -------------------------------------------------------------------------------- /web/src/layouts/service.tsx: -------------------------------------------------------------------------------- 1 | import { get, ResponseData } from "../utils/request"; 2 | import { MenuDataItem } from "@ant-design/pro-layout"; 3 | 4 | export async function systemMenuList(): Promise> { 5 | return await get("/api/v1/system/menus"); 6 | } 7 | -------------------------------------------------------------------------------- /web/src/pages/settings/menu/service.tsx: -------------------------------------------------------------------------------- 1 | import { get, ResponseData } from "../../../utils/request"; 2 | import { MenuListItem } from "./data"; 3 | 4 | export async function menuList(): Promise> { 5 | return await get("/api/v1/menus?type=tree"); 6 | } 7 | -------------------------------------------------------------------------------- /web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | import "./i18n/config"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById("root"), 12 | ); -------------------------------------------------------------------------------- /web/src/pages/ops/label/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface LabelListItem { 2 | id: number; 3 | name: string; 4 | code: string; 5 | } 6 | 7 | export interface LabelCreateInfo { 8 | name: string; 9 | code: string; 10 | } 11 | 12 | export interface LabelUpdateInfo { 13 | name: string; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /web/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from "antd"; 2 | import React, { FC } from "react"; 3 | 4 | const NoFoundPage: FC = () => ( 5 | 10 | ); 11 | export default NoFoundPage; 12 | -------------------------------------------------------------------------------- /web/src/pages/settings/menu/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface MenuListItem { 2 | id: number; 3 | locale: string; 4 | path: string; 5 | type: number; 6 | method: string; 7 | icon: string; 8 | parent_id: number; 9 | order_id: number; 10 | children: MenuListItem[]; 11 | funs: MenuListItem[]; 12 | } 13 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX trash 2 | .DS_Store 3 | 4 | # Files generated by JetBrains IDEs, e.g. IntelliJ IDEA 5 | .idea/ 6 | *.iml 7 | 8 | # Vscode files 9 | .vscode 10 | 11 | # This is where the result of the go build goes 12 | build 13 | 14 | # build file 15 | dist 16 | 17 | # node_modules 18 | node_modules 19 | 20 | # local file 21 | *.local -------------------------------------------------------------------------------- /web/src/pages/login/Login.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .login { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100vh; 7 | overflow: auto; 8 | 9 | .login-card { 10 | margin: 160px auto; 11 | width: 480px; 12 | 13 | .login-form-button { 14 | width: 100%; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/utils/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github/basefas/magi/internal/global" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | var ( 9 | Mysql *gorm.DB 10 | ) 11 | 12 | func SaveAuthLog(user string, ip string, status uint64) { 13 | log := global.AuthLog{Username: user, ClientIP: ip, AuthStatus: status} 14 | Mysql.Create(&log) 15 | } 16 | -------------------------------------------------------------------------------- /internal/utils/magiyaml/manifests/kustomization-vars.yamltmpl: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 3 | kind: Kustomization 4 | metadata: 5 | name: magi-vars 6 | namespace: flux-system 7 | spec: 8 | interval: 30s 9 | sourceRef: 10 | kind: GitRepository 11 | name: flux-system 12 | path: ./{{ .Magi.VarPath }} 13 | prune: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX trash 2 | .DS_Store 3 | 4 | # Files generated by JetBrains IDEs, e.g. IntelliJ IDEA 5 | .idea/ 6 | *.iml 7 | 8 | # Vscode files 9 | .vscode 10 | 11 | # This is where the result of the go build goes 12 | build 13 | 14 | # local config file 15 | config/magi-config.yaml 16 | 17 | # frontend build assets 18 | ui 19 | 20 | # node_modules 21 | web/node_modules -------------------------------------------------------------------------------- /web/src/pages/settings/group/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface GroupListItem { 2 | id: number; 3 | name: string; 4 | role_id?: number; 5 | role_name?: string; 6 | } 7 | 8 | export interface GroupCreateInfo { 9 | name: string; 10 | role_id: number; 11 | } 12 | 13 | export interface GroupUpdateInfo { 14 | name?: string; 15 | role_id?: number; 16 | } 17 | -------------------------------------------------------------------------------- /web/src/pages/dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Card } from "antd"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | const Dashboard: FC = () => { 6 | const {t} = useTranslation(); 7 | 8 | return ( 9 | 10 |

{t("welcome")}

11 |
12 | ); 13 | }; 14 | 15 | export default Dashboard; 16 | -------------------------------------------------------------------------------- /web/src/pages/project/project/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface ProjectListItem { 2 | id: number; 3 | name: string; 4 | code: string; 5 | template: string; 6 | commit: string; 7 | description: string; 8 | create_time: string; 9 | update_time: string; 10 | } 11 | 12 | export interface ProjectCreateInfo { 13 | name: string; 14 | description: string; 15 | } 16 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/system.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/menus" 7 | ) 8 | 9 | func SystemMenu(c *gin.Context) { 10 | ml := make([]menus.MenuInfo, 0) 11 | var err error 12 | uid := http.GetUID(c) 13 | 14 | ml, err = menus.System(uid) 15 | http.CheckResErrData(c, err, ml) 16 | } 17 | -------------------------------------------------------------------------------- /web/src/utils/icons.tsx: -------------------------------------------------------------------------------- 1 | import { ClusterOutlined, ControlOutlined, HomeOutlined, ProjectOutlined, SettingOutlined } from "@ant-design/icons"; 2 | import React from "react"; 3 | 4 | export const menuIcons: any = { 5 | ClusterOutlined: , 6 | ControlOutlined: , 7 | HomeOutlined: , 8 | ProjectOutlined: , 9 | SettingOutlined: , 10 | }; 11 | -------------------------------------------------------------------------------- /cmd/magi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github/basefas/magi/cmd/magi/handlers/router" 5 | "github/basefas/magi/internal/auth" 6 | "github/basefas/magi/internal/utils/conf" 7 | "github/basefas/magi/internal/utils/db/mysql" 8 | "github/basefas/magi/internal/utils/logger" 9 | ) 10 | 11 | func main() { 12 | conf.Init() 13 | logger.Init() 14 | mysql.Init() 15 | auth.Init() 16 | router.Init() 17 | } 18 | -------------------------------------------------------------------------------- /internal/utils/db/mysql/migrations/2_magi_seed.down.sql: -------------------------------------------------------------------------------- 1 | TRUNCATE TABLE casbin_rule; 2 | TRUNCATE TABLE magi_menu; 3 | TRUNCATE TABLE magi_role_menu; 4 | TRUNCATE TABLE magi_user; 5 | TRUNCATE TABLE magi_group; 6 | TRUNCATE TABLE magi_role; 7 | TRUNCATE TABLE magi_group_role; 8 | TRUNCATE TABLE magi_user_role; 9 | TRUNCATE TABLE magi_user_group; 10 | TRUNCATE TABLE magi_label; 11 | TRUNCATE TABLE magi_template; 12 | TRUNCATE TABLE magi_template_manifest; -------------------------------------------------------------------------------- /web/src/components/RightContent/RightContent.less: -------------------------------------------------------------------------------- 1 | //@import '~antd/es/style/themes/default.less'; 2 | 3 | .right { 4 | display: flex; 5 | float: right; 6 | align-items: center; 7 | height: 48px; 8 | margin-left: auto; 9 | overflow: hidden; 10 | 11 | .action { 12 | display: flex; 13 | align-items: center; 14 | height: 48px; 15 | padding: 0 12px; 16 | cursor: pointer; 17 | transition: all 0.3s; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Magi 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/src/pages/ops/env/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface EnvListItem { 2 | id: number; 3 | name: string; 4 | code: string; 5 | type: string; 6 | cluster: string; 7 | namespace: string; 8 | label: string; 9 | } 10 | 11 | export interface EnvCreateInfo { 12 | name: string; 13 | code: string; 14 | type: string; 15 | cluster_code: string; 16 | label_code: string; 17 | namespace: string; 18 | } 19 | 20 | export interface EnvUpdateInfo { 21 | name: string; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /web/src/pages/ops/cluster/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface ClusterListItem { 2 | id: number; 3 | name: string; 4 | code: string; 5 | k8s_cluster_version: string; 6 | k8s_cluster_nodes_number: number; 7 | k8s_cluster_nodes_ready_number: number; 8 | k8s_cluster_status: string; 9 | status: string; 10 | } 11 | 12 | export interface ClusterCreateInfo { 13 | name: string; 14 | code: string; 15 | kube_config: string; 16 | } 17 | 18 | export interface ClusterUpdateInfo { 19 | name: string; 20 | } 21 | -------------------------------------------------------------------------------- /web/src/pages/ops/envset/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface EnvSetListItem { 2 | id: number; 3 | name: string; 4 | code: string; 5 | type: string; 6 | envs: EnvInfo[]; 7 | } 8 | 9 | export interface EnvInfo { 10 | name: string; 11 | code: string; 12 | } 13 | 14 | export interface EnvSetCreateInfo { 15 | name: string; 16 | code: string; 17 | type: string; 18 | envs: string[]; 19 | } 20 | 21 | export interface EnvSetUpdateInfo { 22 | name: string; 23 | code: string; 24 | envs: string[]; 25 | } -------------------------------------------------------------------------------- /internal/mid/cors.go: -------------------------------------------------------------------------------- 1 | package mid 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | func Cors() gin.HandlerFunc { 8 | return func(c *gin.Context) { 9 | c.Header("Access-Control-Allow-Origin", "*") 10 | c.Header("Access-Control-Allow-Headers", "*") 11 | c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, UPDATE") 12 | 13 | if c.Request.Method == "OPTIONS" { 14 | c.AbortWithStatus(204) 15 | return 16 | } 17 | 18 | c.Next() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/utils/magiyaml/manifests/kustomization-cluster.yamltmpl: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 2 | kind: Kustomization 3 | metadata: 4 | name: {{ .Magi.Cluster }}-{{ .Magi.Namespace }}-{{ .Magi.AppName }} 5 | namespace: flux-system 6 | spec: 7 | interval: 1m0s 8 | sourceRef: 9 | kind: GitRepository 10 | name: flux-system 11 | path: ./{{ .Magi.AppPath }}/{{ .Magi.Cluster }}/{{ .Magi.Namespace }}/{{ .Magi.AppName }} 12 | prune: true 13 | postBuild: 14 | substituteFrom: 15 | - kind: ConfigMap 16 | name: {{ .Magi.VarName }} -------------------------------------------------------------------------------- /web/src/utils/local.tsx: -------------------------------------------------------------------------------- 1 | import zhCN from "antd/lib/locale/zh_CN"; 2 | import enUS from "antd/lib/locale/en_US"; 3 | 4 | export function getLocal() { 5 | if (localStorage.getItem("i18nextLng") === "zh-CN") { 6 | return zhCN; 7 | } 8 | if (localStorage.getItem("i18nextLng") === "en-US") { 9 | return enUS; 10 | } 11 | return enUS; 12 | } 13 | 14 | export function getLocalString() { 15 | return localStorage.getItem("i18nextLng"); 16 | } 17 | 18 | export function setLocal(local: string) { 19 | localStorage.setItem("i18nextLng", local); 20 | } 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine3.15 AS ui-builder 2 | WORKDIR /magi 3 | COPY web/package.json web/package-lock.json ./ 4 | RUN npm install 5 | COPY web ./ 6 | RUN npm run build 7 | 8 | FROM golang:1.18-alpine3.15 as server-builder 9 | ENV CGO_ENABLED 0 10 | WORKDIR /magi 11 | COPY go.mod go.sum ./ 12 | RUN go mod download 13 | COPY cmd cmd 14 | COPY internal internal 15 | COPY pkg pkg 16 | RUN go build -o ./build/magi -v ./cmd/magi/main.go 17 | 18 | FROM alpine:3.15 19 | WORKDIR /magi 20 | 21 | COPY --from=ui-builder /magi/dist ./ui 22 | COPY --from=server-builder /magi/build/magi ./ 23 | 24 | EXPOSE 8086 25 | 26 | ENTRYPOINT ["./magi"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # local 2 | setup: config 3 | 4 | .PHONY: config run-server build-server run-ui build-ui clean clean-web build-docker-dev 5 | config: config/magi-config-example.yaml 6 | cp -n config/magi-config-example.yaml config/magi-config.yaml 7 | 8 | run-server: 9 | go run ./cmd/magi/main.go 10 | 11 | build-server: 12 | go build -o ./build/magi -v ./cmd/magi/main.go 13 | 14 | run-ui: 15 | npm --prefix web run dev 16 | 17 | build-ui: 18 | npm --prefix web run build 19 | 20 | clean: 21 | rm -rf build 22 | 23 | clean-web: 24 | rm -rf web/node_modules 25 | 26 | ## Docker 27 | build-docker-dev: 28 | docker build --tag magi/magi:dev . -------------------------------------------------------------------------------- /internal/mid/casbin.go: -------------------------------------------------------------------------------- 1 | package mid 2 | 3 | import ( 4 | "github.com/casbin/casbin/v2" 5 | "github.com/gin-gonic/gin" 6 | "github/basefas/magi/internal/auth" 7 | "net/http" 8 | ) 9 | 10 | func Casbin(e *casbin.Enforcer) gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | allowed := auth.CheckPermission(c, e) 13 | if !allowed { 14 | c.JSON(http.StatusOK, gin.H{ 15 | "code": -1, 16 | "message": "auth error", 17 | "data": nil, 18 | }) 19 | c.Abort() 20 | return 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/user.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/users" 7 | ) 8 | 9 | func UserGet(c *gin.Context) { 10 | id := http.CheckUint64(c, "id") 11 | http.IsMe(c, id) 12 | 13 | u, err := users.Get(id) 14 | http.CheckResErrData(c, err, u) 15 | } 16 | 17 | func UserUpdate(c *gin.Context) { 18 | id := http.CheckUint64(c, "id") 19 | http.IsMe(c, id) 20 | var uu users.UpdateUser 21 | http.CheckJSON(c, &uu) 22 | 23 | err := users.Update(id, uu) 24 | http.CheckResErr(c, err) 25 | } 26 | -------------------------------------------------------------------------------- /web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from "react"; 2 | import "./App.less"; 3 | import Routers from "./routers/Routers"; 4 | import { ConfigProvider } from "antd"; 5 | import { getLocal } from "./utils/local"; 6 | 7 | const App: FC = () => { 8 | const [local, setLocal] = useState(getLocal()); 9 | 10 | useEffect(() => { 11 | setLocal(getLocal()); 12 | }, [getLocal()]); 13 | 14 | return ( 15 | 16 |
17 | 18 |
19 |
20 | ); 21 | 22 | }; 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /web/src/pages/ops/template/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TemplateListItem { 2 | id: number; 3 | name: string; 4 | creator: string; 5 | create_time: string; 6 | update_time: string; 7 | } 8 | 9 | export interface TemplateCreateInfo { 10 | name: string; 11 | } 12 | 13 | export interface TemplateFile { 14 | filename: string; 15 | content: string; 16 | } 17 | 18 | export interface TemplateFileEdit { 19 | add: TemplateFile[]; 20 | update: TemplateFile[]; 21 | delete: TemplateFile[]; 22 | } 23 | 24 | export interface CodeErrorAlert { 25 | filename: string; 26 | visible: boolean; 27 | errors: string; 28 | } -------------------------------------------------------------------------------- /hack/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine3.15 AS ui-builder 2 | WORKDIR /magi 3 | COPY web/package*.json ./ 4 | RUN npm install 5 | COPY web ./ 6 | RUN npm run build 7 | 8 | FROM golang:1.18-alpine3.15 as server-builder 9 | ENV CGO_ENABLED 0 10 | WORKDIR /magi 11 | COPY go.mod go.sum ./ 12 | RUN go mod download 13 | COPY cmd cmd 14 | COPY internal internal 15 | COPY pkg pkg 16 | RUN go build -o ./build/magi -v ./cmd/magi/main.go 17 | 18 | FROM alpine:3.15 19 | WORKDIR /magi 20 | RUN apk --no-cache add ca-certificates 21 | 22 | COPY --from=ui-builder /magi/dist ./ui 23 | COPY --from=server-builder /magi/build/magi ./ 24 | 25 | EXPOSE 8086 26 | 27 | ENTRYPOINT ["./magi"] -------------------------------------------------------------------------------- /internal/utils/magiyaml/manifests/kustomization-app.yamltmpl: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: {{ .Magi.Namespace }} 4 | commonLabels: 5 | magi-app: {{ .Magi.AppName }} 6 | commonAnnotations: 7 | magi-deploy-type: {{ .Magi.DeployType }} 8 | resources: 9 | - ../../../../{{ .Magi.ProjectPath }}/{{ .Magi.AppName }} 10 | {{- if eq .Magi.LinkConfig 1 }} 11 | - {{ .Magi.ConfigPath }} 12 | {{- end }} 13 | {{- if eq .Magi.UsePatch 1 }} 14 | patchesStrategicMerge: 15 | - patch.yaml 16 | {{- end }} 17 | images: 18 | - name: {{ .Magi.AppName }} 19 | newName: {{ .Magi.ImageRegistry }}/{{ .Magi.ImageName }} 20 | newTag: {{ .Magi.ImageTag }} -------------------------------------------------------------------------------- /hack/deploy/docker-compose/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | magi-mysql: 6 | image: mysql/mysql-server:8.0 7 | environment: 8 | MYSQL_ROOT_PASSWORD: "P@ssw0rd" 9 | MYSQL_USER: "root" 10 | MYSQL_DATABASE: "magi" 11 | volumes: 12 | - ./mysql/config/my.cnf:/etc/my.cnf 13 | - ./mysql/init:/docker-entrypoint-initdb.d/ 14 | networks: 15 | - magi-net 16 | 17 | magi: 18 | image: ghcr.io/magi/magi:latest 19 | volumes: 20 | - ./config:/magi/config 21 | depends_on: 22 | - magi-mysql 23 | restart: always 24 | networks: 25 | - magi-net 26 | ports: 27 | - "8086:8086" 28 | 29 | networks: 30 | magi-net: -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/auth.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/global" 7 | "github/basefas/magi/internal/users" 8 | "github/basefas/magi/internal/utils/db" 9 | ) 10 | 11 | func LogIn(c *gin.Context) { 12 | var u users.Login 13 | http.CheckJSON(c, &u) 14 | token, err := users.Token(u) 15 | if err != nil { 16 | db.SaveAuthLog(u.Username, c.ClientIP(), global.LogInFailed) 17 | http.Re(c, -1, err.Error(), nil) 18 | } else { 19 | db.SaveAuthLog(u.Username, c.ClientIP(), global.LogInSuccess) 20 | http.Re(c, 0, "success", token) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/k8s/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "k8s.io/client-go/kubernetes" 6 | "k8s.io/client-go/tools/clientcmd" 7 | ) 8 | 9 | var ( 10 | ErrKubeConfigError = errors.New("kubeConfig error") 11 | ErrCreateK8sClient = errors.New("creat K8s client error") 12 | ) 13 | 14 | func GetK8sClient(k8sConf string) (*kubernetes.Clientset, error) { 15 | config, err := clientcmd.RESTConfigFromKubeConfig([]byte(k8sConf)) 16 | 17 | if err != nil { 18 | return nil, ErrKubeConfigError 19 | } 20 | 21 | clientSet, err := kubernetes.NewForConfig(config) 22 | if err != nil { 23 | return nil, ErrCreateK8sClient 24 | } 25 | return clientSet, nil 26 | } 27 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "DOM", 7 | "DOM.Iterable", 8 | "ESNext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": false, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "references": [ 27 | { 28 | "path": "./tsconfig.node.json" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /web/src/pages/settings/user/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface UserListItem { 2 | id: number; 3 | username: string; 4 | full_name: string; 5 | email: string; 6 | group_id: number; 7 | group_name: string; 8 | role_id: number; 9 | role_name: string; 10 | status: number; 11 | } 12 | 13 | export interface UserCreateInfo { 14 | username: string; 15 | full_name: string; 16 | email: string; 17 | group_id: number; 18 | role_id: number; 19 | } 20 | 21 | export interface UserUpdateInfo { 22 | id?: number; 23 | username?: string; 24 | full_name?: string; 25 | email?: string; 26 | group_id?: number; 27 | role_id?: number; 28 | status?: number; 29 | } 30 | 31 | export interface UserResetPassword { 32 | password: string; 33 | } -------------------------------------------------------------------------------- /web/src/pages/settings/group/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { GroupCreateInfo, GroupListItem, GroupUpdateInfo } from "./data"; 3 | 4 | export async function groupList(): Promise> { 5 | return await get("/api/v1/groups"); 6 | } 7 | 8 | export async function createGroup(group: GroupCreateInfo): Promise> { 9 | return await post<{}>("/api/v1/groups", group); 10 | } 11 | 12 | export async function updateGroup(id: number, group: GroupUpdateInfo): Promise> { 13 | return await put<{}>("/api/v1/groups/" + id, group); 14 | } 15 | 16 | export async function deleteGroup(id: number): Promise> { 17 | return await del<{}>("/api/v1/groups/" + id); 18 | } 19 | -------------------------------------------------------------------------------- /hack/docker/chn.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 AS ui-builder 2 | WORKDIR /magi 3 | COPY web/package*.json ./ 4 | RUN npm install 5 | COPY web ./ 6 | RUN npm run build 7 | 8 | FROM golang:1.18-alpine3.15 as server-builder 9 | ENV CGO_ENABLED 0 10 | ENV GOPROXY https://goproxy.cn,direct 11 | WORKDIR /magi 12 | COPY go.mod go.sum ./ 13 | RUN go mod download 14 | COPY cmd cmd 15 | COPY internal internal 16 | COPY pkg pkg 17 | RUN go build -o ./build/magi -v ./cmd/magi/main.go 18 | 19 | FROM alpine:3.15 20 | WORKDIR /magi 21 | RUN apk --no-cache add ca-certificates tzdata && \ 22 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ 23 | echo 'Asia/Shanghai' >/etc/timezone 24 | 25 | COPY --from=ui-builder /magi/dist ./ui 26 | COPY --from=server-builder /magi/build/magi ./ 27 | 28 | EXPOSE 8086 29 | 30 | ENTRYPOINT ["./magi"] -------------------------------------------------------------------------------- /web/src/pages/ops/label/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { LabelCreateInfo, LabelListItem, LabelUpdateInfo } from "./data"; 3 | 4 | export async function labelList(): Promise> { 5 | return await get("/api/v1/labels"); 6 | } 7 | 8 | export async function createLabel(label: LabelCreateInfo): Promise> { 9 | return await post<{}>("/api/v1/labels", label); 10 | } 11 | 12 | export async function updateLabel(code: string, label: LabelUpdateInfo): Promise> { 13 | return await put<{}>("/api/v1/labels/" + code, label); 14 | } 15 | 16 | export async function deleteLabel(code: string): Promise> { 17 | return await del<{}>("/api/v1/labels/" + code); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /internal/mid/jwt.go: -------------------------------------------------------------------------------- 1 | package mid 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/internal/auth" 6 | "net/http" 7 | ) 8 | 9 | func JWT() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | code := 0 12 | token := c.GetHeader("token") 13 | if token == "" { 14 | code = -1 15 | } else { 16 | _, err := auth.ParseToken(token) 17 | if err != nil { 18 | code = -2 19 | } 20 | } 21 | 22 | if code != 0 { 23 | c.JSON(http.StatusOK, gin.H{ 24 | "code": code, 25 | "message": "Token Error", 26 | "data": nil, 27 | }) 28 | c.Abort() 29 | return 30 | } 31 | c.Next() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import vitePluginImp from "vite-plugin-imp"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | react(), 9 | vitePluginImp({ 10 | libList: [ 11 | { 12 | libName: "antd", 13 | style: (name) => `antd/es/${name}/style`, 14 | }, 15 | ], 16 | }), 17 | ], 18 | css: { 19 | preprocessorOptions: { 20 | less: { 21 | modifyVars: {}, 22 | javascriptEnabled: true, 23 | }, 24 | }, 25 | }, 26 | resolve: { 27 | alias: [ 28 | { find: /^~/, replacement: "" }, 29 | ], 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /internal/labels/model.go: -------------------------------------------------------------------------------- 1 | package labels 2 | 3 | import ( 4 | "github/basefas/magi/internal/global" 5 | "time" 6 | ) 7 | 8 | type Label struct { 9 | global.Model 10 | Name string `json:"name" gorm:"NOT NULL;"` 11 | Code string `json:"code" gorm:"unique;NOT NULL;"` 12 | } 13 | 14 | func (Label) TableName() string { 15 | return "magi_label" 16 | } 17 | 18 | type CreateLabel struct { 19 | Name string `json:"name" binding:"required"` 20 | Code string `json:"code" binding:"required"` 21 | } 22 | 23 | type UpdateLabel struct { 24 | Name string `json:"name"` 25 | } 26 | 27 | type LabelInfo struct { 28 | ID uint64 `json:"id"` 29 | Name string `json:"name"` 30 | Code string `json:"code"` 31 | CreatedAt time.Time `json:"create_time"` 32 | UpdatedAt time.Time `json:"update_time"` 33 | } 34 | -------------------------------------------------------------------------------- /web/src/pages/ops/cluster/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { ClusterCreateInfo, ClusterListItem, ClusterUpdateInfo } from "./data"; 3 | 4 | export async function clusterList(): Promise> { 5 | return await get("/api/v1/clusters"); 6 | } 7 | 8 | export async function createCluster(cluster: ClusterCreateInfo): Promise> { 9 | return await post<{}>("/api/v1/clusters", cluster); 10 | } 11 | 12 | export async function updateCluster(code: string, cluster: ClusterUpdateInfo): Promise> { 13 | return await put<{}>("/api/v1/clusters/" + code, cluster); 14 | } 15 | 16 | export async function deleteCluster(code: string): Promise> { 17 | return await del<{}>("/api/v1/clusters/" + code); 18 | } 19 | -------------------------------------------------------------------------------- /web/src/i18n/config.tsx: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import LanguageDetector from "i18next-browser-languagedetector"; 3 | import { initReactI18next } from "react-i18next"; 4 | import enUsTranslation from "./en-US/translation.json"; 5 | import zhCnTranslation from "./zh-CN/translation.json"; 6 | 7 | export const resources = { 8 | "en-US": { 9 | translation: enUsTranslation, 10 | }, 11 | "zh-CN": { 12 | translation: zhCnTranslation, 13 | }, 14 | } as const; 15 | 16 | i18n 17 | .use(LanguageDetector) 18 | .use(initReactI18next) 19 | .init({ 20 | fallbackLng: "zh-CN", 21 | // debug: true, 22 | detection: { 23 | caches: ["localStorage"], 24 | }, 25 | interpolation: { 26 | escapeValue: false, // not needed for react as it escapes by default 27 | }, 28 | resources, 29 | }).then(); 30 | -------------------------------------------------------------------------------- /web/src/pages/deploy/varset/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface VarSetListItem { 2 | id: number; 3 | name: string; 4 | code: string; 5 | label: string; 6 | description: string; 7 | create_time: string; 8 | update_time: string; 9 | } 10 | 11 | export interface VarSetCreateInfo { 12 | name: string; 13 | code: string; 14 | label_code: string; 15 | description: string; 16 | } 17 | 18 | export interface VarSetUpdateInfo { 19 | name: string; 20 | description: string; 21 | add: Var[]; 22 | update: Var[]; 23 | delete: Var[]; 24 | } 25 | 26 | export interface VarSetInfo { 27 | name: string; 28 | code: string; 29 | label_code: string; 30 | description: string; 31 | vars: Var[]; 32 | } 33 | 34 | export interface Var { 35 | id: number; 36 | v_key: string; 37 | v_value: string; 38 | commit: string; 39 | editor: string; 40 | update_time: string; 41 | } -------------------------------------------------------------------------------- /web/src/pages/project/project/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, ResponseData } from "../../../utils/request"; 2 | import { ProjectCreateInfo, ProjectListItem } from "./data"; 3 | import { ConfigListItem } from "../../deploy/config/data"; 4 | 5 | export async function projectList(): Promise> { 6 | return await get("/api/v1/projects"); 7 | } 8 | 9 | export async function createProject(project: ProjectCreateInfo): Promise> { 10 | return await post<{}>("/api/v1/projects", project); 11 | } 12 | 13 | export async function deleteProject(code: string): Promise> { 14 | return await del<{}>("/api/v1/projects/" + code); 15 | } 16 | 17 | export async function projectConfigListByLabel(code: string, label: string): Promise> { 18 | return await get("/api/v1/projects/" + code + "/configs?label=" + label + "&linked=false"); 19 | } -------------------------------------------------------------------------------- /web/src/pages/deploy/deploy/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { DeployCreateInfo, DeployInfo, DeployListItem } from "./data"; 3 | 4 | export async function deployList(): Promise> { 5 | return await get("/api/v1/deploys"); 6 | } 7 | 8 | export async function createDeploy(deploy: DeployCreateInfo): Promise> { 9 | return await post<{}>("/api/v1/deploys", deploy); 10 | } 11 | 12 | export async function deployDo(version: string): Promise> { 13 | return await put<{}>("/api/v1/deploys/" + version); 14 | } 15 | 16 | export async function deleteDeploy(code: string): Promise> { 17 | return await del<{}>("/api/v1/deploys/" + code); 18 | } 19 | 20 | export async function deployInfo(code: string): Promise> { 21 | return await get("/api/v1/deploys/" + code); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /internal/utils/magiyaml/models.go: -------------------------------------------------------------------------------- 1 | package magiyaml 2 | 3 | type AppKustomization struct { 4 | ApiVersion string `yaml:"apiVersion"` 5 | Kind string `yaml:"kind"` 6 | Namespace string `yaml:"namespace"` 7 | CommonLabels CommonLabels `yaml:"commonLabels"` 8 | CommonAnnotations CommonAnnotations `yaml:"commonAnnotations"` 9 | Resources []string `yaml:"resources"` 10 | PatchesStrategicMerge []string `yaml:"patchesStrategicMerge"` 11 | Images []Image `yaml:"images"` 12 | } 13 | 14 | type Image struct { 15 | Name string `yaml:"name"` 16 | NewName string `yaml:"newName"` 17 | NewTag string `yaml:"newTag"` 18 | } 19 | 20 | type CommonLabels struct { 21 | MagiApp string `yaml:"magi-app"` 22 | } 23 | 24 | type CommonAnnotations struct { 25 | MagiDeployType string `yaml:"magi-deploy-type"` 26 | } 27 | -------------------------------------------------------------------------------- /internal/roles/model.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "github/basefas/magi/internal/global" 5 | "time" 6 | ) 7 | 8 | type Role struct { 9 | global.Model 10 | Name string `json:"name" gorm:"NOT NULL"` 11 | } 12 | 13 | func (Role) TableName() string { 14 | return "magi_role" 15 | } 16 | 17 | type CreateRole struct { 18 | Name string `json:"name" binding:"required"` 19 | } 20 | 21 | type UpdateRole struct { 22 | Name string `json:"name"` 23 | } 24 | 25 | type RoleInfo struct { 26 | ID uint64 `json:"id"` 27 | Name string `json:"name"` 28 | CreatedAt time.Time `json:"create_time"` 29 | UpdatedAt time.Time `json:"update_time"` 30 | } 31 | 32 | type RoleMenu struct { 33 | global.Model 34 | RoleID uint64 `json:"role_id" gorm:"type:uint;size:32;NOT NULL;"` 35 | MenuID uint64 `json:"menu_id" gorm:"type:uint;size:32;NOT NULL;"` 36 | } 37 | 38 | func (RoleMenu) TableName() string { 39 | return "magi_role_menu" 40 | } 41 | -------------------------------------------------------------------------------- /web/src/utils/auth.tsx: -------------------------------------------------------------------------------- 1 | export function getToken() { 2 | return localStorage.getItem("token"); 3 | } 4 | 5 | export function setToken(token: string) { 6 | localStorage.setItem("token", token); 7 | } 8 | 9 | export function loggedIn(): boolean { 10 | return !!localStorage.getItem("token") || !!localStorage.getItem("username") || !!localStorage.getItem("full_name"); 11 | } 12 | 13 | export function deleteToken() { 14 | localStorage.removeItem("token"); 15 | localStorage.removeItem("username"); 16 | localStorage.removeItem("full_name"); 17 | } 18 | 19 | export function getCurrentUser() { 20 | return localStorage.getItem("full_name"); 21 | } 22 | 23 | export function getUsername() { 24 | return localStorage.getItem("username"); 25 | } 26 | 27 | export function setUsername(username: string) { 28 | localStorage.setItem("username", username); 29 | } 30 | 31 | export function setFullName(full_name: string) { 32 | localStorage.setItem("full_name", full_name); 33 | } -------------------------------------------------------------------------------- /web/src/pages/settings/user/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { UserCreateInfo, UserListItem, UserResetPassword, UserUpdateInfo } from "./data"; 3 | 4 | export async function userList(): Promise> { 5 | return await get("/api/v1/users"); 6 | } 7 | 8 | export async function createUser(user: UserCreateInfo): Promise> { 9 | return await post<{}>("/api/v1/users", user); 10 | } 11 | 12 | export async function updateUser(id: number, user: UserUpdateInfo): Promise> { 13 | return await put<{}>("/api/v1/users/" + id, user); 14 | } 15 | 16 | export async function deleteUser(id: number): Promise> { 17 | return await del<{}>("/api/v1/users/" + id); 18 | } 19 | 20 | export async function resetPassword(id: number, password: UserResetPassword): Promise> { 21 | return await put<{}>("/api/v1/users/" + id + "/password", password); 22 | } -------------------------------------------------------------------------------- /web/src/index.css: -------------------------------------------------------------------------------- 1 | /*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */ 2 | html, 3 | body, 4 | p, 5 | ol, 6 | ul, 7 | li, 8 | dl, 9 | dt, 10 | dd, 11 | blockquote, 12 | figure, 13 | fieldset, 14 | legend, 15 | textarea, 16 | pre, 17 | iframe, 18 | hr, 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6 { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | h1, 30 | h2, 31 | h3, 32 | h4, 33 | h5, 34 | h6 { 35 | font-size: 100%; 36 | font-weight: normal; 37 | } 38 | 39 | ul { 40 | list-style: none; 41 | } 42 | 43 | button, 44 | input, 45 | select { 46 | margin: 0; 47 | } 48 | 49 | html { 50 | box-sizing: border-box; 51 | } 52 | 53 | *, *::before, *::after { 54 | box-sizing: inherit; 55 | } 56 | 57 | img, 58 | video { 59 | height: auto; 60 | max-width: 100%; 61 | } 62 | 63 | iframe { 64 | border: 0; 65 | } 66 | 67 | table { 68 | border-collapse: collapse; 69 | border-spacing: 0; 70 | } 71 | 72 | td, 73 | th { 74 | padding: 0; 75 | } 76 | 77 | #root { 78 | height: 100%; 79 | } 80 | -------------------------------------------------------------------------------- /web/src/pages/ops/envset/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { EnvSetCreateInfo, EnvSetListItem, EnvSetUpdateInfo } from "./data"; 3 | 4 | export async function envSetList(): Promise> { 5 | return await get("/api/v1/env_sets"); 6 | } 7 | 8 | export async function createEnvSet(envSet: EnvSetCreateInfo): Promise> { 9 | return await post<{}>("/api/v1/env_sets", envSet); 10 | } 11 | 12 | export async function updateEnvSet(code: string, envSet: EnvSetUpdateInfo): Promise> { 13 | return await put<{}>("/api/v1/env_sets/" + code, envSet); 14 | } 15 | 16 | export async function deleteEnvSet(code: string): Promise> { 17 | return await del<{}>("/api/v1/env_sets/" + code); 18 | } 19 | 20 | export async function envSetListByLabel(label: string): Promise> { 21 | return await get("/api/v1/env_sets?label=" + label); 22 | } 23 | -------------------------------------------------------------------------------- /internal/mid/syslog.go: -------------------------------------------------------------------------------- 1 | package mid 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/internal/auth" 6 | "github/basefas/magi/internal/global" 7 | "github/basefas/magi/internal/utils" 8 | "github/basefas/magi/internal/utils/db" 9 | "strings" 10 | ) 11 | 12 | func Syslog() gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | token := c.GetHeader("token") 15 | if token == "" { 16 | c.Next() 17 | return 18 | } 19 | uid, _ := auth.GetUID(token) 20 | path := c.Request.URL.Path 21 | method := c.Request.Method 22 | body := utils.GetRequestBody(c) 23 | clientIP := c.ClientIP() 24 | m := method == "POST" || method == "PUT" || method == "DELETE" 25 | p := !strings.Contains(path, "login") 26 | if m && p { 27 | log := global.OptLog{UserID: uid, Url: path, Method: method, Body: body, ClientIP: clientIP} 28 | db.Mysql.Create(&log) 29 | } 30 | c.Next() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/labels.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/labels" 7 | ) 8 | 9 | func LabelCreate(c *gin.Context) { 10 | var cl labels.CreateLabel 11 | http.CheckJSON(c, &cl) 12 | 13 | err := labels.Create(cl) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func LabelGet(c *gin.Context) { 18 | code := c.Param("code") 19 | 20 | l, err := labels.GetInfo(code) 21 | http.CheckResErrData(c, err, l) 22 | } 23 | 24 | func LabelUpdate(c *gin.Context) { 25 | code := c.Param("code") 26 | var ul labels.UpdateLabel 27 | http.CheckJSON(c, &ul) 28 | 29 | err := labels.Update(code, ul) 30 | http.CheckResErr(c, err) 31 | } 32 | 33 | func LabelDelete(c *gin.Context) { 34 | code := c.Param("code") 35 | 36 | err := labels.Delete(code) 37 | http.CheckResErr(c, err) 38 | } 39 | 40 | func LabelList(c *gin.Context) { 41 | ll, err := labels.List() 42 | http.CheckResErrData(c, err, ll) 43 | } 44 | -------------------------------------------------------------------------------- /internal/utils/db/mysql/migrations/1_magi_init.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS magi_var_set; 2 | DROP TABLE IF EXISTS magi_var; 3 | DROP TABLE IF EXISTS magi_user_role; 4 | DROP TABLE IF EXISTS magi_user_group; 5 | DROP TABLE IF EXISTS magi_user; 6 | DROP TABLE IF EXISTS magi_template_manifest; 7 | DROP TABLE IF EXISTS magi_template; 8 | DROP TABLE IF EXISTS magi_role_menu; 9 | DROP TABLE IF EXISTS magi_role; 10 | DROP TABLE IF EXISTS magi_project; 11 | DROP TABLE IF EXISTS magi_opt_log; 12 | DROP TABLE IF EXISTS magi_menu; 13 | DROP TABLE IF EXISTS magi_label; 14 | DROP TABLE IF EXISTS magi_group_role; 15 | DROP TABLE IF EXISTS magi_group; 16 | DROP TABLE IF EXISTS magi_env_set_env; 17 | DROP TABLE IF EXISTS magi_env_set; 18 | DROP TABLE IF EXISTS magi_env; 19 | DROP TABLE IF EXISTS magi_deploy; 20 | DROP TABLE IF EXISTS magi_config_history; 21 | DROP TABLE IF EXISTS magi_config_file; 22 | DROP TABLE IF EXISTS magi_config; 23 | DROP TABLE IF EXISTS magi_cluster; 24 | DROP TABLE IF EXISTS magi_auth_log; 25 | DROP TABLE IF EXISTS magi_app; 26 | DROP TABLE IF EXISTS casbin_rule; -------------------------------------------------------------------------------- /web/src/pages/ops/template/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { TemplateCreateInfo, TemplateFile, TemplateFileEdit, TemplateListItem } from "./data"; 3 | 4 | export async function templateList(): Promise> { 5 | return await get("/api/v1/templates"); 6 | } 7 | 8 | export async function createTemplate(template: TemplateCreateInfo): Promise> { 9 | return await post<{}>("/api/v1/templates", template); 10 | } 11 | 12 | export async function deleteTemplate(id: number): Promise> { 13 | return await del<{}>("/api/v1/templates/" + id); 14 | } 15 | 16 | export async function editTemplateFiles(id: number, files: TemplateFileEdit): Promise> { 17 | return await put<{}>("/api/v1/templates/" + id + "/files", files); 18 | } 19 | 20 | export async function templateFileList(id: number): Promise> { 21 | return await get("/api/v1/templates/" + id + "/files"); 22 | } -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/apps.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/apps" 7 | ) 8 | 9 | func AppCreate(c *gin.Context) { 10 | var ca apps.CreateApp 11 | http.CheckJSON(c, &ca) 12 | 13 | err := apps.Create(ca) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func AppGet(c *gin.Context) { 18 | code := c.Param("code") 19 | 20 | a, err := apps.GetInfo(code) 21 | http.CheckResErrData(c, err, a) 22 | } 23 | 24 | func AppUpdate(c *gin.Context) { 25 | code := c.Param("code") 26 | var ua apps.UpdateApp 27 | http.CheckJSON(c, &ua) 28 | 29 | err := apps.Update(code, ua) 30 | http.CheckResErr(c, err) 31 | } 32 | 33 | func AppDelete(c *gin.Context) { 34 | code := c.Param("code") 35 | 36 | err := apps.Delete(code) 37 | http.CheckResErr(c, err) 38 | } 39 | 40 | func AppList(c *gin.Context) { 41 | label := c.Query("label") 42 | 43 | al, err := apps.ListByLabel(label) 44 | http.CheckResErrData(c, err, al) 45 | } 46 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/groups.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/groups" 7 | ) 8 | 9 | func GroupCreate(c *gin.Context) { 10 | var cg groups.CreateGroup 11 | http.CheckJSON(c, &cg) 12 | 13 | err := groups.Create(cg) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func GroupGet(c *gin.Context) { 18 | id := http.CheckUint64(c, "id") 19 | 20 | g, err := groups.GetInfo(id) 21 | http.CheckResErrData(c, err, g) 22 | } 23 | 24 | func GroupUpdate(c *gin.Context) { 25 | id := http.CheckUint64(c, "id") 26 | var ug groups.UpdateGroup 27 | http.CheckJSON(c, &ug) 28 | 29 | err := groups.Update(id, ug) 30 | http.CheckResErr(c, err) 31 | } 32 | 33 | func GroupDelete(c *gin.Context) { 34 | id := http.CheckUint64(c, "id") 35 | 36 | err := groups.Delete(id) 37 | http.CheckResErr(c, err) 38 | } 39 | 40 | func GroupList(c *gin.Context) { 41 | gl, err := groups.List() 42 | http.CheckResErrData(c, err, gl) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/clusters.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/k8s/clusters" 7 | ) 8 | 9 | func ClusterAdd(c *gin.Context) { 10 | var cc clusters.AddCluster 11 | http.CheckJSON(c, &cc) 12 | 13 | err := clusters.Create(cc) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func ClusterGet(c *gin.Context) { 18 | code := c.Param("code") 19 | 20 | kc, err := clusters.GetInfo(code) 21 | http.CheckResErrData(c, err, kc) 22 | } 23 | 24 | func ClusterUpdate(c *gin.Context) { 25 | code := c.Param("code") 26 | var uc clusters.UpdateCluster 27 | http.CheckJSON(c, &uc) 28 | 29 | err := clusters.Update(code, uc) 30 | http.CheckResErr(c, err) 31 | } 32 | 33 | func ClusterDelete(c *gin.Context) { 34 | code := c.Param("code") 35 | 36 | err := clusters.Delete(code) 37 | http.CheckResErr(c, err) 38 | } 39 | 40 | func ClusterList(c *gin.Context) { 41 | cl, err := clusters.List() 42 | http.CheckResErrData(c, err, cl) 43 | } 44 | -------------------------------------------------------------------------------- /web/src/pages/deploy/config/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface ConfigListItem { 2 | id: number; 3 | name: string; 4 | code: string; 5 | description: string; 6 | create_time: string; 7 | update_time: string; 8 | } 9 | 10 | export interface ConfigCreateInfo { 11 | name: string; 12 | code: string; 13 | description: string; 14 | } 15 | 16 | export interface ConfigUpdateInfo { 17 | name: string; 18 | description: string; 19 | } 20 | 21 | export interface ConfigInfo { 22 | name: string; 23 | code: string; 24 | description: string; 25 | } 26 | 27 | export interface EditConfigFile { 28 | label: string; 29 | files: ConfigFile[]; 30 | } 31 | 32 | export interface ConfigFile { 33 | filename: string; 34 | content: string; 35 | } 36 | 37 | export interface ConfigHistoryListItem { 38 | id: number; 39 | version: string; 40 | status: number; 41 | creator: string; 42 | create_time: string; 43 | } 44 | 45 | export interface ConfigFileListItem { 46 | config_history_id: number; 47 | filename: string; 48 | content: string; 49 | } -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/envsets.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/envsets" 7 | ) 8 | 9 | func EnvSetCreate(c *gin.Context) { 10 | var ce envsets.CreateEnvSet 11 | http.CheckJSON(c, &ce) 12 | 13 | err := envsets.Create(ce) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func EnvSetGet(c *gin.Context) { 18 | code := c.Param("code") 19 | 20 | e, err := envsets.GetInfo(code) 21 | http.CheckResErrData(c, err, e) 22 | } 23 | 24 | func EnvSetUpdate(c *gin.Context) { 25 | code := c.Param("code") 26 | var ue envsets.UpdateEnvSet 27 | http.CheckJSON(c, &ue) 28 | 29 | err := envsets.Update(code, ue) 30 | http.CheckResErr(c, err) 31 | } 32 | 33 | func EnvSetDelete(c *gin.Context) { 34 | code := c.Param("code") 35 | 36 | err := envsets.Delete(code) 37 | http.CheckResErr(c, err) 38 | } 39 | 40 | func EnvSetList(c *gin.Context) { 41 | label := c.Query("label") 42 | 43 | el, err := envsets.ListByLabel(label) 44 | http.CheckResErrData(c, err, el) 45 | } 46 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/envs.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/envs" 7 | ) 8 | 9 | func EnvCreate(c *gin.Context) { 10 | var ce envs.CreateEnv 11 | http.CheckJSON(c, &ce) 12 | 13 | err := envs.Create(ce) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func EnvGet(c *gin.Context) { 18 | code := c.Param("code") 19 | 20 | e, err := envs.GetInfo(code) 21 | http.CheckResErrData(c, err, e) 22 | } 23 | 24 | func EnvUpdate(c *gin.Context) { 25 | code := c.Param("code") 26 | var ue envs.UpdateEnv 27 | http.CheckJSON(c, &ue) 28 | 29 | err := envs.Update(code, ue) 30 | http.CheckResErr(c, err) 31 | } 32 | 33 | func EnvDelete(c *gin.Context) { 34 | code := c.Param("code") 35 | 36 | err := envs.Delete(code) 37 | http.CheckResErr(c, err) 38 | } 39 | 40 | func EnvList(c *gin.Context) { 41 | label := c.Query("label") 42 | envType := c.Query("type") 43 | 44 | el, err := envs.ListByLabelAndType(label, envType) 45 | http.CheckResErrData(c, err, el) 46 | } 47 | -------------------------------------------------------------------------------- /internal/global/model.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "time" 6 | ) 7 | 8 | const ( 9 | Unknown = iota 10 | LogInSuccess 11 | LogInFailed 12 | ) 13 | 14 | type Model struct { 15 | ID uint64 `gorm:"primaryKey;type:uint;size:32;"` 16 | CreatedAt time.Time 17 | UpdatedAt time.Time 18 | DeletedAt gorm.DeletedAt `gorm:"index"` 19 | //gorm.Model 20 | } 21 | 22 | type OptLog struct { 23 | Model 24 | UserID uint64 `json:"user_id" gorm:"type:uint;size:32;"` 25 | Url string `json:"url"` 26 | Method string `json:"method"` 27 | Body string `json:"body" gorm:"type:text;"` 28 | ClientIP string `json:"client_ip"` 29 | } 30 | 31 | func (OptLog) TableName() string { 32 | return "magi_opt_log" 33 | } 34 | 35 | type AuthLog struct { 36 | Model 37 | Username string `json:"username"` 38 | ClientIP string `json:"client_ip"` 39 | AuthStatus uint64 `json:"auth_status" gorm:"default:0;type:uint;size:32;comment:'0:unknown,1:success,2:failed'"` 40 | } 41 | 42 | func (AuthLog) TableName() string { 43 | return "magi_auth_log" 44 | } 45 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magi-ui", 3 | "private": true, 4 | "version": "0.0.2", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@ant-design/pro-layout": "6.38.22", 12 | "@codemirror/language": "6.2.1", 13 | "@codemirror/legacy-modes": "6.1.0", 14 | "@uiw/react-codemirror": "4.12.3", 15 | "antd": "4.23.2", 16 | "axios": "0.27.2", 17 | "i18next": "21.9.2", 18 | "i18next-browser-languagedetector": "6.1.5", 19 | "i18next-http-backend": "1.4.1", 20 | "js-yaml": "4.1.0", 21 | "react": "17.0.2", 22 | "react-dom": "17.0.2", 23 | "react-i18next": "11.18.6", 24 | "react-router-dom": "5.3.3" 25 | }, 26 | "devDependencies": { 27 | "@types/react": "17.0.2", 28 | "@types/react-dom": "17.0.2", 29 | "@types/react-router-dom": "5.3.3", 30 | "@vitejs/plugin-react": "2.1.0", 31 | "@types/js-yaml": "4.0.5", 32 | "less": "4.1.3", 33 | "less-loader": "11.0.0", 34 | "typescript": "4.8.3", 35 | "vite": "3.1.2", 36 | "vite-plugin-imp": "2.3.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/src/pages/settings/role/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { RoleCreateInfo, RoleListItem, RoleUpdateInfo } from "./data"; 3 | 4 | export async function roleList(): Promise> { 5 | return await get("/api/v1/roles"); 6 | } 7 | 8 | export async function createRole(role: RoleCreateInfo): Promise> { 9 | return await post<{}>("/api/v1/roles", role); 10 | } 11 | 12 | export async function updateRole(id: number, role: RoleUpdateInfo): Promise> { 13 | return await put<{}>("/api/v1/roles/" + id, role); 14 | } 15 | 16 | export async function deleteRole(id: number): Promise> { 17 | return await del<{}>("/api/v1/roles/" + id); 18 | } 19 | 20 | export async function roleMenus(id: number): Promise> { 21 | return await get("/api/v1/roles/" + id + "/menus"); 22 | } 23 | 24 | export async function updateRoleMenus(id: number, menus: number[]): Promise> { 25 | return await put<{}>("/api/v1/roles/" + id + "/menus", menus); 26 | } 27 | -------------------------------------------------------------------------------- /web/src/pages/deploy/deploy/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface DeployListItem { 2 | id: number; 3 | code: string; 4 | project: string; 5 | status: number; 6 | version: string; 7 | label: string; 8 | env_type: string; 9 | env: string; 10 | env_set: string; 11 | var_set: string; 12 | description: string; 13 | image_registry: string; 14 | image_name: string; 15 | create_time: string; 16 | update_time: string; 17 | } 18 | 19 | export interface DeployCreateInfo { 20 | app_code: string; 21 | image_tag?: string; 22 | config_version?: string; 23 | patch_content?: string; 24 | } 25 | 26 | export interface DeployUpdateInfo { 27 | name: string; 28 | description: string; 29 | } 30 | 31 | export interface DeployInfo { 32 | id: number; 33 | code: string; 34 | project: string; 35 | label: string; 36 | env_type: string; 37 | env: string; 38 | env_set: string; 39 | var_set: string; 40 | description: string; 41 | image_registry: string; 42 | image_name: string; 43 | commit: string; 44 | create_time: string; 45 | update_time: string; 46 | } 47 | 48 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/varsets.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/varsets" 7 | ) 8 | 9 | func VarSetCreate(c *gin.Context) { 10 | var cv varsets.CreateVarSet 11 | http.CheckJSON(c, &cv) 12 | 13 | err := varsets.Create(cv) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func VarSetGet(c *gin.Context) { 18 | code := c.Param("code") 19 | 20 | v, err := varsets.GetInfo(code) 21 | http.CheckResErrData(c, err, v) 22 | } 23 | 24 | func VarSetUpdate(c *gin.Context) { 25 | code := c.Param("code") 26 | var uv varsets.UpdateVarSet 27 | http.CheckJSON(c, &uv) 28 | uid := http.GetUID(c) 29 | 30 | err := varsets.Update(code, uv, uid) 31 | http.CheckResErr(c, err) 32 | } 33 | 34 | func VarSetDelete(c *gin.Context) { 35 | code := c.Param("code") 36 | 37 | err := varsets.Delete(code) 38 | http.CheckResErr(c, err) 39 | } 40 | 41 | func VarSetList(c *gin.Context) { 42 | label := c.Query("label") 43 | 44 | vl, err := varsets.List(label) 45 | http.CheckResErrData(c, err, vl) 46 | } 47 | -------------------------------------------------------------------------------- /web/src/pages/ops/env/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { EnvCreateInfo, EnvListItem, EnvUpdateInfo } from "./data"; 3 | 4 | export async function envList(): Promise> { 5 | return await get("/api/v1/envs"); 6 | } 7 | 8 | export async function createEnv(env: EnvCreateInfo): Promise> { 9 | return await post<{}>("/api/v1/envs", env); 10 | } 11 | 12 | export async function updateEnv(code: string, env: EnvUpdateInfo): Promise> { 13 | return await put<{}>("/api/v1/envs/" + code, env); 14 | } 15 | 16 | export async function deleteEnv(code: string): Promise> { 17 | return await del<{}>("/api/v1/envs/" + code); 18 | } 19 | 20 | export async function envListByLabel(label: string): Promise> { 21 | return await get("/api/v1/envs?label=" + label); 22 | } 23 | 24 | export async function envListByLabelAndType(label: string, type: string): Promise> { 25 | return await get("/api/v1/envs?label=" + label + "&type=" + type); 26 | } -------------------------------------------------------------------------------- /internal/utils/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | func Init() { 9 | viper.SetConfigName("magi-config") 10 | viper.AddConfigPath("./config") 11 | 12 | // default config 13 | viper.SetDefault("app.name", "Magi") 14 | viper.SetDefault("app.runMode", "release") 15 | viper.SetDefault("app.host", "0.0.0.0") 16 | viper.SetDefault("app.port", 8086) 17 | viper.SetDefault("app.logLevel", "info") 18 | viper.SetDefault("app.dbLogLevel", "warn") 19 | viper.SetDefault("app.uiLog", false) 20 | viper.SetDefault("app.jwtSecret", "Magi") 21 | viper.SetDefault("app.jwtTimeout", 86400) 22 | viper.SetDefault("db.mysql.name", "magi") 23 | viper.SetDefault("db.mysql.port", 3306) 24 | viper.SetDefault("db.mysql.migrate", true) 25 | viper.SetDefault("git.branch", "main") 26 | viper.SetDefault("db.mysql.slowThreshold", 2000) 27 | 28 | err := viper.ReadInConfig() 29 | if err != nil { 30 | fmt.Println("[magi] " + err.Error()) 31 | panic("config init failed.") 32 | } else { 33 | fmt.Println("[magi] Config Load Completed.") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /web/src/pages/deploy/varset/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { VarSetCreateInfo, VarSetInfo, VarSetListItem, VarSetUpdateInfo } from "./data"; 3 | 4 | export async function varSetList(): Promise> { 5 | return await get("/api/v1/var_sets"); 6 | } 7 | 8 | export async function createVarSet(var_set: VarSetCreateInfo): Promise> { 9 | return await post<{}>("/api/v1/var_sets", var_set); 10 | } 11 | 12 | export async function updateVarSet(code: string, var_set: VarSetUpdateInfo): Promise> { 13 | return await put<{}>("/api/v1/var_sets/" + code, var_set); 14 | } 15 | 16 | export async function deleteVarSet(code: string): Promise> { 17 | return await del<{}>("/api/v1/var_sets/" + code); 18 | } 19 | 20 | export async function varSetInfo(code: string): Promise> { 21 | return await get("/api/v1/var_sets/" + code); 22 | } 23 | 24 | export async function varSetListByLabel(label: string): Promise> { 25 | return await get("/api/v1/var_sets?label=" + label); 26 | } -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/deploys.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/deploys" 7 | ) 8 | 9 | func DeployCreate(c *gin.Context) { 10 | var cd deploys.CreateDeploy 11 | http.CheckJSON(c, &cd) 12 | uid := http.GetUID(c) 13 | 14 | err := deploys.Create(cd, uid) 15 | http.CheckResErr(c, err) 16 | } 17 | 18 | func DeployDo(c *gin.Context) { 19 | version := c.Param("version") 20 | uid := http.GetUID(c) 21 | 22 | err := deploys.DeployDo(version, uid) 23 | http.CheckResErr(c, err) 24 | } 25 | 26 | //func DeployUpdate(c *gin.Context) { 27 | // code := c.Param("code") 28 | // var ud deploys.UpdateDeploy 29 | // http.CheckJSON(c, &ud) 30 | // 31 | // err := deploys.Update(code, ud) 32 | // http.CheckResErr(c, err) 33 | //} 34 | // 35 | //func DeployDelete(c *gin.Context) { 36 | // code := c.Param("code") 37 | // 38 | // err := deploys.Delete(code) 39 | // http.CheckResErr(c, err) 40 | //} 41 | 42 | func DeployList(c *gin.Context) { 43 | code := c.Param("code") 44 | 45 | dl, err := deploys.List(code) 46 | http.CheckResErrData(c, err, dl) 47 | } 48 | -------------------------------------------------------------------------------- /internal/utils/git/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fluxcd/go-git-providers/github" 6 | "github.com/fluxcd/go-git-providers/gitprovider" 7 | ) 8 | 9 | type GitProvider string 10 | 11 | const ( 12 | GitProviderGitHub GitProvider = "github" 13 | GitProviderGitLab GitProvider = "gitlab" 14 | ) 15 | 16 | type Config struct { 17 | Provider GitProvider 18 | Hostname string 19 | Username string 20 | Token string 21 | } 22 | 23 | func GitProviderBuilder(config Config) (gitprovider.Client, error) { 24 | var client gitprovider.Client 25 | var err error 26 | switch config.Provider { 27 | case GitProviderGitHub: 28 | opts := []gitprovider.ClientOption{ 29 | gitprovider.WithOAuth2Token(config.Token), 30 | } 31 | if config.Hostname != "" { 32 | opts = append(opts, gitprovider.WithDomain(config.Hostname)) 33 | } 34 | if client, err = github.NewClient(opts...); err != nil { 35 | return nil, err 36 | } 37 | default: 38 | return nil, fmt.Errorf("unsupported Git provider '%s'", config.Provider) 39 | } 40 | return client, err 41 | } 42 | -------------------------------------------------------------------------------- /pkg/k8s/cluster/cluster.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "context" 5 | v1 "k8s.io/api/core/v1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/client-go/kubernetes" 8 | ) 9 | 10 | func GetK8sClusterVersion(c *kubernetes.Clientset) (string, error) { 11 | version, err := c.ServerVersion() 12 | 13 | if err != nil { 14 | return "", err 15 | } 16 | 17 | return version.String(), nil 18 | } 19 | 20 | func GetK8sClusterNodesNumber(c *kubernetes.Clientset) (string, int, int, error) { 21 | nodes, err := c.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) 22 | readyNodes := 0 23 | clusterStatus := "Inactive" 24 | for _, node := range nodes.Items { 25 | for _, condition := range node.Status.Conditions { 26 | if condition.Type == v1.NodeReady { 27 | if condition.Status == v1.ConditionTrue { 28 | readyNodes += 1 29 | } 30 | } 31 | } 32 | } 33 | if err != nil { 34 | return clusterStatus, readyNodes, 0, err 35 | } 36 | if len(nodes.Items) == readyNodes { 37 | clusterStatus = "Active" 38 | } 39 | return clusterStatus, readyNodes, len(nodes.Items), nil 40 | } 41 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/users.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/users" 7 | ) 8 | 9 | func UsersCreate(c *gin.Context) { 10 | var cu users.CreateUser 11 | http.CheckJSON(c, &cu) 12 | 13 | err := users.Create(cu) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func UsersGet(c *gin.Context) { 18 | uid := http.CheckUint64(c, "id") 19 | 20 | u, err := users.GetInfo(uid) 21 | http.CheckResErrData(c, err, u) 22 | } 23 | 24 | func UsersUpdate(c *gin.Context) { 25 | uid := http.CheckUint64(c, "id") 26 | var uu users.UpdateUser 27 | http.CheckJSON(c, &uu) 28 | 29 | err := users.Update(uid, uu) 30 | http.CheckResErr(c, err) 31 | } 32 | 33 | func UsersDelete(c *gin.Context) { 34 | uid := http.CheckUint64(c, "id") 35 | 36 | err := users.Delete(uid) 37 | http.CheckResErr(c, err) 38 | } 39 | 40 | func UsersList(c *gin.Context) { 41 | ul, err := users.List() 42 | http.CheckResErrData(c, err, ul) 43 | } 44 | 45 | func ResetPassword(c *gin.Context) { 46 | uid := http.CheckUint64(c, "id") 47 | var rp users.UserResetPassword 48 | http.CheckJSON(c, &rp) 49 | 50 | err := users.ResetPassword(uid, rp.Password) 51 | http.CheckResErr(c, err) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/projects.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/projects" 7 | ) 8 | 9 | func ProjectCreate(c *gin.Context) { 10 | var cp projects.CreateProject 11 | http.CheckJSON(c, &cp) 12 | 13 | err := projects.Create(cp) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func ProjectGet(c *gin.Context) { 18 | code := c.Param("code") 19 | 20 | p, err := projects.GetInfo(code) 21 | http.CheckResErrData(c, err, p) 22 | } 23 | 24 | func ProjectUpdate(c *gin.Context) { 25 | code := c.Param("code") 26 | var up projects.UpdateProject 27 | http.CheckJSON(c, &up) 28 | 29 | err := projects.Update(code, up) 30 | http.CheckResErr(c, err) 31 | } 32 | 33 | func ProjectDelete(c *gin.Context) { 34 | code := c.Param("code") 35 | 36 | err := projects.Delete(code) 37 | http.CheckResErr(c, err) 38 | } 39 | 40 | func ProjectList(c *gin.Context) { 41 | pl, err := projects.List() 42 | http.CheckResErrData(c, err, pl) 43 | } 44 | 45 | func ProjectConfigList(c *gin.Context) { 46 | label := c.Query("label") 47 | linked := c.Query("linked") 48 | code := c.Param("code") 49 | 50 | cl, err := projects.ConfigListByLabel(code, label, linked) 51 | http.CheckResErrData(c, err, cl) 52 | } 53 | -------------------------------------------------------------------------------- /web/src/pages/deploy/app/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { AppCreateInfo, AppInfo, AppListItem, AppUpdateInfo } from "./data"; 3 | import { DeployListItem } from "../deploy/data"; 4 | 5 | export async function appList(): Promise> { 6 | return await get("/api/v1/apps"); 7 | } 8 | 9 | export async function createApp(app: AppCreateInfo): Promise> { 10 | return await post<{}>("/api/v1/apps", app); 11 | } 12 | 13 | export async function updateApp(code: string, app: AppUpdateInfo): Promise> { 14 | return await put<{}>("/api/v1/apps/" + code, app); 15 | } 16 | 17 | export async function deleteApp(code: string): Promise> { 18 | return await del<{}>("/api/v1/apps/" + code); 19 | } 20 | 21 | export async function appInfo(code: string): Promise> { 22 | return await get("/api/v1/apps/" + code); 23 | } 24 | 25 | export async function appListByLabel(label: string): Promise> { 26 | return await get("/api/v1/apps?label=" + label); 27 | } 28 | 29 | export async function deployListByAppCode(appCode: string): Promise> { 30 | return await get("/api/v1/apps/" + appCode + "/deploys"); 31 | } -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/templates.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/templates" 7 | ) 8 | 9 | func TemplateCreate(c *gin.Context) { 10 | var ct templates.CreateTemplate 11 | http.CheckJSON(c, &ct) 12 | uid := http.GetUID(c) 13 | 14 | err := templates.Create(ct, uid) 15 | http.CheckResErr(c, err) 16 | } 17 | 18 | func TemplateGet(c *gin.Context) { 19 | id := http.CheckUint64(c, "id") 20 | 21 | t, err := templates.GetInfo(id) 22 | http.CheckResErrData(c, err, t) 23 | } 24 | 25 | func TemplateDelete(c *gin.Context) { 26 | id := http.CheckUint64(c, "id") 27 | 28 | err := templates.Delete(id) 29 | http.CheckResErr(c, err) 30 | } 31 | 32 | func TemplateList(c *gin.Context) { 33 | tl, err := templates.List() 34 | http.CheckResErrData(c, err, tl) 35 | } 36 | 37 | func TemplateFileList(c *gin.Context) { 38 | id := http.CheckUint64(c, "id") 39 | 40 | fl, err := templates.FileList(id) 41 | http.CheckResErrData(c, err, fl) 42 | } 43 | 44 | func TemplateFileEdit(c *gin.Context) { 45 | id := http.CheckUint64(c, "id") 46 | uid := http.GetUID(c) 47 | var utf templates.EditTemplateFile 48 | http.CheckJSON(c, &utf) 49 | 50 | err := templates.EditFiles(id, uid, utf) 51 | http.CheckResErr(c, err) 52 | } 53 | -------------------------------------------------------------------------------- /internal/groups/model.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "github/basefas/magi/internal/global" 5 | "time" 6 | ) 7 | 8 | type Group struct { 9 | global.Model 10 | Name string `json:"name" gorm:"NOT NULL"` 11 | } 12 | 13 | func (Group) TableName() string { 14 | return "magi_group" 15 | } 16 | 17 | type CreateGroup struct { 18 | Name string `json:"name" binding:"required"` 19 | RoleID uint64 `json:"role_id" binding:"required"` 20 | } 21 | 22 | type UpdateGroup struct { 23 | Name string `json:"name"` 24 | RoleID uint64 `json:"role_id"` 25 | } 26 | 27 | type GroupInfo struct { 28 | ID uint64 `json:"id"` 29 | Name string `json:"name"` 30 | RoleID uint64 `json:"role_id"` 31 | RoleName string `json:"role_name"` 32 | CreatedAt time.Time `json:"create_time"` 33 | UpdatedAt time.Time `json:"update_time"` 34 | } 35 | 36 | type GroupRole struct { 37 | global.Model 38 | GroupID uint64 `json:"group_id" gorm:"type:uint;size:32;NOT NULL;"` 39 | RoleID uint64 `json:"role_id" gorm:"type:uint;size:32;NOT NULL;"` 40 | } 41 | 42 | func (GroupRole) TableName() string { 43 | return "magi_group_role" 44 | } 45 | 46 | type UserGroup struct { 47 | UserID uint64 `json:"user_id"` 48 | GroupID uint64 `json:"group_id"` 49 | } 50 | 51 | type User struct { 52 | UserID uint64 `json:"id"` 53 | Username string `json:"name"` 54 | } 55 | -------------------------------------------------------------------------------- /web/src/components/RightContent/components/LogoutModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import { deleteToken } from "../../../utils/auth"; 4 | import { message, Modal } from "antd"; 5 | import { useTranslation } from "react-i18next"; 6 | 7 | interface LogoutModalProps { 8 | open: boolean; 9 | changeLogoutModalVisible: Function; 10 | } 11 | 12 | const LogoutModal: React.FC = (props) => { 13 | const {t} = useTranslation(); 14 | const history = useHistory(); 15 | const {open, changeLogoutModalVisible} = props; 16 | 17 | useEffect(() => { 18 | changeLogoutModalVisible(open); 19 | }, [open, changeLogoutModalVisible]); 20 | 21 | const okHandle = () => { 22 | deleteToken(); 23 | message.success(t("logout_successful")).then(); 24 | changeLogoutModalVisible(false); 25 | history.replace("/login"); 26 | }; 27 | const cancelHandle = () => { 28 | changeLogoutModalVisible(false); 29 | }; 30 | 31 | return ( 32 | 40 |
{t("confirm_logging_out") + t("question_mark")}
41 |
42 | ); 43 | 44 | }; 45 | export default LogoutModal; 46 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/menus.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/menus" 7 | ) 8 | 9 | func MenuCreate(c *gin.Context) { 10 | var cm menus.CreateMenu 11 | http.CheckJSON(c, &cm) 12 | 13 | err := menus.Create(cm) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func MenuGet(c *gin.Context) { 18 | id := http.CheckUint64(c, "id") 19 | menuType := c.Query("type") 20 | var mi menus.MenuInfo 21 | var err error 22 | 23 | if menuType == "tree" { 24 | mi, err = menus.GetTree(id) 25 | } else { 26 | mi, err = menus.GetInfo(id) 27 | } 28 | http.CheckResErrData(c, err, mi) 29 | } 30 | 31 | func MenuUpdate(c *gin.Context) { 32 | id := http.CheckUint64(c, "id") 33 | var um menus.UpdateMenu 34 | http.CheckJSON(c, &um) 35 | 36 | err := menus.Update(id, um) 37 | http.CheckResErr(c, err) 38 | } 39 | 40 | func MenuDelete(c *gin.Context) { 41 | id := http.CheckUint64(c, "id") 42 | 43 | err := menus.Delete(id) 44 | http.CheckResErr(c, err) 45 | } 46 | 47 | func MenuList(c *gin.Context) { 48 | menuType := c.Query("type") 49 | ml := make([]menus.MenuInfo, 0) 50 | var err error 51 | 52 | switch menuType { 53 | case "tree": 54 | ml, err = menus.Tree() 55 | default: 56 | ml, err = menus.List() 57 | } 58 | http.CheckResErrData(c, err, ml) 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: { } 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@v2 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v2 29 | 30 | - name: Log into registry ${{ env.REGISTRY }} 31 | uses: docker/login-action@v2 32 | with: 33 | registry: ${{ env.REGISTRY }} 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GHCR_PAT }} 36 | 37 | - name: Extract Docker metadata 38 | id: meta 39 | uses: docker/metadata-action@v4 40 | with: 41 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 42 | 43 | - name: Build and push Docker image 44 | id: build-and-push 45 | uses: docker/build-push-action@v3 46 | with: 47 | context: . 48 | platforms: linux/amd64, linux/arm64 49 | file: ./hack/docker/Dockerfile 50 | push: true 51 | tags: ${{ steps.meta.outputs.tags }} 52 | labels: ${{ steps.meta.outputs.labels }} 53 | cache-from: type=gha 54 | cache-to: type=gha,mode=max -------------------------------------------------------------------------------- /config/magi-config-example.yaml: -------------------------------------------------------------------------------- 1 | # rename filename to magi-config.yaml 2 | 3 | # base configs 4 | app: 5 | # Optional, default: Magi 6 | name: MagiMax 7 | # Optional, release or debug, default: release 8 | runMode: release 9 | # Optional, default: 0.0.0.0 10 | host: 0.0.0.0 11 | # Optional, default: 8086 12 | port: 8086 13 | # Optional, debug, info, warn, error, panic, default: info 14 | logLevel: info 15 | # Optional, info, warn, error, silent, default: warn 16 | dbLogLevel: warn 17 | # Optional, default: false 18 | uiLog: false 19 | # Optional, ==it is best to modify it==,default: Magi 20 | jwtSecret: Magi 21 | # Optional, uint second, default: 86400 22 | jwtTimeout: 86400 23 | 24 | # db configs 25 | db: 26 | mysql: 27 | # Required 28 | host: 127.0.0.1 29 | # Optional, default: 3306 30 | port: 3306 31 | # Required 32 | user: root 33 | # Required 34 | password: root 35 | # Optional, default: magi 36 | name: magi 37 | # Optional, default: true 38 | migrate: true 39 | # Optional, uint millisecond, default: 2000 40 | slowThreshold: 2000 41 | 42 | # git configs 43 | git: 44 | # Required 45 | type: github 46 | # Required 47 | hostname: github.com 48 | # Required 49 | repo: "https://github.com/xxx/magi" 50 | # Required 51 | username: magi 52 | # Required 53 | token: "xxx" 54 | # Required 55 | personal: true 56 | # Required 57 | private: true 58 | # Optional, default: main 59 | branch: main -------------------------------------------------------------------------------- /hack/deploy/docker-compose/config/magi-config.yaml: -------------------------------------------------------------------------------- 1 | # rename filename to magi-config.yaml 2 | 3 | # base configs 4 | app: 5 | # Optional, default: Magi 6 | name: MagiMax 7 | # Optional, release or debug, default: release 8 | runMode: release 9 | # Optional, default: 0.0.0.0 10 | host: 0.0.0.0 11 | # Optional, default: 8086 12 | port: 8086 13 | # Optional, debug, info, warn, error, panic, default: info 14 | logLevel: info 15 | # Optional, info, warn, error, silent, default: warn 16 | dbLogLevel: warn 17 | # Optional, default: false 18 | uiLog: false 19 | # Optional, ==it is best to modify it==,default: Magi 20 | jwtSecret: Magi 21 | # Optional, uint second, default: 86400 22 | jwtTimeout: 86400 23 | 24 | # db configs 25 | db: 26 | mysql: 27 | # Required 28 | host: magi-mysql 29 | # Optional, default: 3306 30 | port: 3306 31 | # Required 32 | user: magi 33 | # Required 34 | password: Magi@1 35 | # Optional, default: magi 36 | name: magi 37 | # Optional, default: true 38 | migrate: true 39 | # Optional, uint millisecond, default: 2000 40 | slowThreshold: 2000 41 | 42 | # git configs 43 | git: 44 | # Required 45 | type: github 46 | # Required 47 | hostname: github.com 48 | # Required 49 | repo: "https://github.com/xxx/magi" 50 | # Required 51 | username: magi 52 | # Required 53 | token: "xxx" 54 | # Required 55 | personal: true 56 | # Required 57 | private: true 58 | # Optional, default: main 59 | branch: main -------------------------------------------------------------------------------- /internal/auth/jwt.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/golang-jwt/jwt/v4" 5 | "github.com/spf13/viper" 6 | "time" 7 | ) 8 | 9 | type Claims struct { 10 | UID uint64 11 | jwt.RegisteredClaims 12 | } 13 | 14 | func GenerateToken(uid uint64) (string, error) { 15 | jwtSecret := []byte(viper.GetString("app.jwtSecret")) 16 | now := time.Now() 17 | expireTime := now.Add(time.Second * time.Duration(viper.GetInt("app.jwtTimeout"))) 18 | claims := Claims{ 19 | uid, 20 | jwt.RegisteredClaims{ 21 | ExpiresAt: jwt.NewNumericDate(expireTime), 22 | Issuer: viper.GetString("app.name"), 23 | IssuedAt: jwt.NewNumericDate(now), 24 | }, 25 | } 26 | 27 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 28 | tokenString, err := token.SignedString(jwtSecret) 29 | return tokenString, err 30 | } 31 | 32 | func ParseToken(tokenString string) (*Claims, error) { 33 | jwtSecret := []byte(viper.GetString("app.jwtSecret")) 34 | 35 | token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { 36 | return jwtSecret, nil 37 | }) 38 | 39 | if err != nil { 40 | return nil, err 41 | } 42 | claims := token.Claims.(*Claims) 43 | return claims, nil 44 | } 45 | 46 | func GetUID(tokenString string) (uint64, error) { 47 | claims, err := ParseToken(tokenString) 48 | if err != nil { 49 | return 0, err 50 | } 51 | return claims.UID, nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/templates/model.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "github/basefas/magi/internal/global" 5 | "time" 6 | ) 7 | 8 | type Template struct { 9 | global.Model 10 | Name string `json:"name" gorm:"NOT NULL;"` 11 | CreatorID uint64 `json:"creator_id" gorm:"type:uint;size:32;NOT NULL;"` 12 | UpdaterID uint64 `json:"updater_id" gorm:"type:uint;size:32;"` 13 | } 14 | 15 | func (Template) TableName() string { 16 | return "magi_template" 17 | } 18 | 19 | type TemplateManifest struct { 20 | global.Model 21 | TemplateID uint64 `json:"template_id" gorm:"NOT NULL;type:uint;size:32;"` 22 | Filename string `json:"filename" gorm:"NOT NULL;"` 23 | Content string `json:"content" gorm:"type:text;"` 24 | } 25 | 26 | func (TemplateManifest) TableName() string { 27 | return "magi_template_manifest" 28 | } 29 | 30 | type CreateTemplate struct { 31 | Name string `json:"name" binding:"required"` 32 | } 33 | 34 | type TemplateInfo struct { 35 | ID uint64 `json:"id"` 36 | Name string `json:"name"` 37 | Creator string `json:"creator"` 38 | CreatedAt time.Time `json:"create_time"` 39 | UpdatedAt time.Time `json:"update_time"` 40 | } 41 | 42 | type EditTemplateFile struct { 43 | Add []TemplateFile `json:"add"` 44 | Delete []TemplateFile `json:"delete"` 45 | Update []TemplateFile `json:"update"` 46 | } 47 | 48 | type TemplateFile struct { 49 | Filename string `json:"filename" binding:"required"` 50 | Content string `json:"content"` 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish-chn.yml: -------------------------------------------------------------------------------- 1 | name: Release-chn 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: { } 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@v2 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v2 29 | 30 | - name: Log into registry ${{ env.REGISTRY }} 31 | uses: docker/login-action@v2 32 | with: 33 | registry: ${{ env.REGISTRY }} 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GHCR_PAT }} 36 | 37 | - name: Extract Docker metadata 38 | id: meta-chn 39 | uses: docker/metadata-action@v4 40 | with: 41 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 42 | flavor: | 43 | latest=false 44 | suffix=-chn 45 | 46 | - name: Build and push Docker chn image 47 | id: build-and-push-chn 48 | uses: docker/build-push-action@v3 49 | with: 50 | context: . 51 | platforms: linux/amd64, linux/arm64 52 | file: ./hack/docker/chn.Dockerfile 53 | push: true 54 | tags: ${{ steps.meta-chn.outputs.tags }} 55 | labels: ${{ steps.meta-chn.outputs.labels }} 56 | cache-from: type=gha 57 | cache-to: type=gha,mode=max 58 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/roles.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/roles" 7 | ) 8 | 9 | func RoleCreate(c *gin.Context) { 10 | var cr roles.CreateRole 11 | http.CheckJSON(c, &cr) 12 | 13 | err := roles.Create(cr) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func RoleGet(c *gin.Context) { 18 | id := http.CheckUint64(c, "id") 19 | 20 | r, err := roles.GetInfo(id) 21 | http.CheckResErrData(c, err, r) 22 | } 23 | 24 | func RoleUpdate(c *gin.Context) { 25 | id := http.CheckUint64(c, "id") 26 | var ur roles.UpdateRole 27 | http.CheckJSON(c, &ur) 28 | 29 | err := roles.Update(id, ur) 30 | http.CheckResErr(c, err) 31 | } 32 | 33 | func RoleDelete(c *gin.Context) { 34 | id := http.CheckUint64(c, "id") 35 | 36 | err := roles.Delete(id) 37 | http.CheckResErr(c, err) 38 | } 39 | 40 | func RoleList(c *gin.Context) { 41 | rl, err := roles.List() 42 | http.CheckResErrData(c, err, rl) 43 | } 44 | 45 | func RoleMenusList(c *gin.Context) { 46 | id := http.CheckUint64(c, "id") 47 | 48 | rm, err := roles.GetRoleMenus(id) 49 | l := make([]uint64, 0) 50 | for _, menu := range rm { 51 | l = append(l, menu.MenuID) 52 | } 53 | http.CheckResErrData(c, err, l) 54 | } 55 | 56 | func RoleMenusUpdate(c *gin.Context) { 57 | id := http.CheckUint64(c, "id") 58 | ml := make([]uint64, 0) 59 | http.CheckJSON(c, &ml) 60 | 61 | err := roles.UpdateRoleMenu(id, ml) 62 | http.CheckResErr(c, err) 63 | } 64 | -------------------------------------------------------------------------------- /internal/envs/model.go: -------------------------------------------------------------------------------- 1 | package envs 2 | 3 | import ( 4 | "github/basefas/magi/internal/global" 5 | "time" 6 | ) 7 | 8 | type Env struct { 9 | global.Model 10 | Name string `json:"name" gorm:"NOT NULL;"` 11 | Code string `json:"code" gorm:"unique;NOT NULL;"` 12 | Type string `json:"type" gorm:"NOT NULL;"` 13 | ClusterCode string `json:"cluster_code" gorm:"NOT NULL;"` 14 | LabelCode string `json:"label_code" gorm:"NOT NULL;"` 15 | Namespace string `json:"namespace"` 16 | } 17 | 18 | func (Env) TableName() string { 19 | return "magi_env" 20 | } 21 | 22 | type CreateEnv struct { 23 | Name string `json:"name" binding:"required"` 24 | Code string `json:"code" binding:"required"` 25 | Type string `json:"type" binding:"required"` 26 | ClusterCode string `json:"cluster_code" binding:"required"` 27 | Namespace string `json:"namespace"` 28 | LabelCode string `json:"label_code" binding:"required"` 29 | } 30 | 31 | type UpdateEnv struct { 32 | Name string `json:"name"` 33 | } 34 | 35 | type EnvInfo struct { 36 | ID uint64 `json:"id"` 37 | Name string `json:"name"` 38 | Code string `json:"code"` 39 | Type string `json:"type"` 40 | Cluster string `json:"cluster"` 41 | Label string `json:"label"` 42 | ClusterCode string `json:"cluster_code"` 43 | LabelCode string `json:"label_code"` 44 | Namespace string `json:"namespace"` 45 | CreatedAt time.Time `json:"create_time"` 46 | UpdatedAt time.Time `json:"update_time"` 47 | } 48 | -------------------------------------------------------------------------------- /web/src/pages/settings/role/components/CreateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { RoleCreateInfo } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const {Item} = Form; 7 | 8 | interface CreateFormProps { 9 | open: boolean; 10 | onOk: (role: RoleCreateInfo) => void; 11 | onCancel: () => void; 12 | } 13 | 14 | const CreateForm: FC = (props) => { 15 | const {t} = useTranslation(); 16 | const {open, onOk, onCancel} = props; 17 | const [form] = Form.useForm(); 18 | 19 | const ok = () => { 20 | form 21 | .validateFields() 22 | .then(values => { 23 | form.resetFields(); 24 | onOk(values); 25 | }) 26 | .catch(info => { 27 | console.log(t("parameter_validation_failed"), t("colon"), info); 28 | }); 29 | }; 30 | 31 | const form_layout = { 32 | labelCol: {span: 4}, 33 | }; 34 | 35 | return ( 36 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | ); 50 | }; 51 | export default CreateForm; 52 | -------------------------------------------------------------------------------- /internal/projects/model.go: -------------------------------------------------------------------------------- 1 | package projects 2 | 3 | import ( 4 | "github/basefas/magi/internal/global" 5 | "time" 6 | ) 7 | 8 | type Project struct { 9 | global.Model 10 | Name string `json:"name" gorm:"NOT NULL;"` 11 | Code string `json:"code" gorm:"unique;NOT NULL;"` 12 | TemplateID uint64 `json:"template_id" gorm:"type:uint;size:32"` 13 | Commit string `json:"commit" gorm:"NOT NULL;"` 14 | Description string `json:"description"` 15 | } 16 | 17 | func (Project) TableName() string { 18 | return "magi_project" 19 | } 20 | 21 | type CreateProject struct { 22 | Name string `json:"name" binding:"required"` 23 | Code string `json:"code" binding:"required"` 24 | TemplateID uint64 `json:"template_id" binding:"required"` 25 | Description string `json:"description"` 26 | } 27 | 28 | type UpdateProject struct { 29 | Name string `json:"name"` 30 | Description string `json:"description"` 31 | } 32 | 33 | type ProjectInfo struct { 34 | ID uint64 `json:"id"` 35 | Name string `json:"name"` 36 | Code string `json:"code"` 37 | Commit string `json:"commit"` 38 | Template string `json:"template"` 39 | Description string `json:"description"` 40 | CreatedAt time.Time `json:"create_time"` 41 | UpdatedAt time.Time `json:"update_time"` 42 | } 43 | 44 | type ProjectConfigInfo struct { 45 | ID uint64 `json:"id"` 46 | Name string `json:"name"` 47 | Code string `json:"code"` 48 | CreatedAt time.Time `json:"create_time"` 49 | UpdatedAt time.Time `json:"update_time"` 50 | } 51 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github/basefas/magi/internal/utils/logger" 8 | "io/ioutil" 9 | "time" 10 | ) 11 | 12 | func LogRequestBody(c *gin.Context) { 13 | reqBody := GetRequestBody(c) 14 | logger.Debug("body: " + reqBody) 15 | } 16 | 17 | func LogRequestHeader(c *gin.Context) { 18 | logger.Debug("header: " + fmt.Sprint(c.Request.Header)) 19 | } 20 | 21 | func LogRequest(c *gin.Context) { 22 | LogRequestHeader(c) 23 | LogRequestBody(c) 24 | } 25 | 26 | func GetRequestBody(c *gin.Context) string { 27 | buf, _ := c.GetRawData() 28 | reqBody := string(buf) 29 | c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buf)) 30 | return reqBody 31 | } 32 | 33 | func Intersect[T any](a, b []T) []T { 34 | m := make(map[any]bool) 35 | n := make([]T, 0, 0) 36 | for _, v := range a { 37 | m[v] = true 38 | } 39 | 40 | for _, v := range b { 41 | if m[v] { 42 | n = append(n, v) 43 | } 44 | } 45 | return n 46 | } 47 | 48 | func Difference[T any](a, b []T) []T { 49 | m := make(map[any]bool) 50 | n := make([]T, 0, 0) 51 | inter := Intersect(a, b) 52 | for _, v := range inter { 53 | m[v] = true 54 | } 55 | 56 | for _, v := range a { 57 | if !m[v] { 58 | n = append(n, v) 59 | } 60 | } 61 | return n 62 | } 63 | 64 | func GetNowString() string { 65 | return time.Now().Format("20060102150405") 66 | } 67 | 68 | func Contains[T comparable](s []T, e T) bool { 69 | for _, v := range s { 70 | if v == e { 71 | return true 72 | } 73 | } 74 | return false 75 | } 76 | -------------------------------------------------------------------------------- /internal/k8s/clusters/model.go: -------------------------------------------------------------------------------- 1 | package clusters 2 | 3 | import ( 4 | "github/basefas/magi/internal/global" 5 | "time" 6 | ) 7 | 8 | type Cluster struct { 9 | global.Model 10 | Name string `json:"name" gorm:"NOT NULL"` 11 | Code string `json:"code" gorm:"NOT NULL;unique"` 12 | K8sVersion string `json:"k8s_version" gorm:"NOT NULL"` 13 | KubeConfig string `json:"kube_config" gorm:"NOT NULL;type:text;"` 14 | Status uint64 `json:"status" gorm:"type:uint;size:32;"` 15 | Common string `json:"common"` 16 | } 17 | 18 | func (Cluster) TableName() string { 19 | return "magi_cluster" 20 | } 21 | 22 | type AddCluster struct { 23 | Name string `json:"name"` 24 | Code string `json:"code"` 25 | KubeConfig string `json:"kube_config"` 26 | Common string `json:"common"` 27 | } 28 | 29 | type UpdateCluster struct { 30 | Name string `json:"name"` 31 | Common string `json:"common"` 32 | } 33 | 34 | type ClusterInfo struct { 35 | ID uint64 `json:"id"` 36 | CreatedAt time.Time `json:"create_time"` 37 | UpdatedAt time.Time `json:"update_time"` 38 | Name string `json:"name"` 39 | Code string `json:"code"` 40 | KubeConfig string `json:"kube_config"` 41 | K8sClusterVersion string `json:"k8s_cluster_version"` 42 | K8sClusterNodesNumber int `json:"k8s_cluster_nodes_number"` 43 | K8sClusterNodesReadyNumber int `json:"k8s_cluster_nodes_ready_number"` 44 | Status string `json:"k8s_cluster_status"` 45 | Common string `json:"common"` 46 | } 47 | -------------------------------------------------------------------------------- /web/src/pages/ops/cluster/components/UpdateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { ClusterListItem, ClusterUpdateInfo } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const {Item} = Form; 7 | 8 | interface UpdateFormProps { 9 | open: boolean; 10 | cluster: ClusterListItem; 11 | onOk: (code: string, cluster: ClusterUpdateInfo) => void; 12 | onCancel: () => void; 13 | } 14 | 15 | const UpdateForm: FC = (props) => { 16 | const {t} = useTranslation(); 17 | const {open, cluster, onOk, onCancel} = props; 18 | const [form] = Form.useForm(); 19 | 20 | const ok = () => { 21 | form 22 | .validateFields() 23 | .then(values => { 24 | form.resetFields(); 25 | onOk(cluster.code, values); 26 | }) 27 | .catch(info => { 28 | console.log(t("parameter_validation_failed"), t("colon"), info); 29 | }); 30 | }; 31 | 32 | const form_layout = { 33 | labelCol: {span: 4}, 34 | }; 35 | 36 | return ( 37 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | ); 51 | }; 52 | export default UpdateForm; 53 | -------------------------------------------------------------------------------- /web/src/pages/deploy/app/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface AppListItem { 2 | id: number; 3 | name: string; 4 | code: string; 5 | project: string; 6 | label: string; 7 | target_type: string; 8 | env_type: string; 9 | env: string; 10 | env_set: string; 11 | var_set: string; 12 | description: string; 13 | image_registry: string; 14 | image_name: string; 15 | image_tag: string; 16 | link_config: number; 17 | config_code: string; 18 | use_patch: number; 19 | patch_content: string; 20 | create_time: string; 21 | update_time: string; 22 | } 23 | 24 | export interface AppCreateInfo { 25 | code: string; 26 | project: string; 27 | label: string; 28 | target_type: string; 29 | env_type: string; 30 | env?: string; 31 | env_set?: string; 32 | var_set: string; 33 | description: string; 34 | image_registry: string; 35 | image_name: string; 36 | link_config: number; 37 | config_code?: string; 38 | use_patch: number; 39 | patch_content?: string; 40 | } 41 | 42 | export interface AppUpdateInfo { 43 | name: string; 44 | description: string; 45 | add: Var[]; 46 | update: Var[]; 47 | delete: Var[]; 48 | } 49 | 50 | export interface AppInfo { 51 | id: number; 52 | code: string; 53 | project: string; 54 | label: string; 55 | target_type: string; 56 | env_type: string; 57 | env: string; 58 | env_set: string; 59 | var_set: string; 60 | description: string; 61 | image_registry: string; 62 | image_name: string; 63 | image_tag: string; 64 | commit: string; 65 | create_time: string; 66 | update_time: string; 67 | } 68 | 69 | export interface Var { 70 | id: number; 71 | v_key: string; 72 | v_value: string; 73 | } -------------------------------------------------------------------------------- /web/src/pages/ops/env/components/UpdateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { EnvListItem, EnvUpdateInfo } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const {Item} = Form; 7 | 8 | interface UpdateFormProps { 9 | open: boolean; 10 | env: EnvListItem; 11 | onOk: (code: string, env: EnvUpdateInfo) => void; 12 | onCancel: () => void; 13 | } 14 | 15 | const UpdateForm: FC = (props) => { 16 | const {t} = useTranslation(); 17 | const {open, env, onOk, onCancel} = props; 18 | const [form] = Form.useForm(); 19 | 20 | 21 | const ok = () => { 22 | form 23 | .validateFields() 24 | .then(values => { 25 | form.resetFields(); 26 | onOk(env.code, values); 27 | }) 28 | .catch(info => { 29 | console.log(t("parameter_validation_failed"), t("colon"), info); 30 | }); 31 | }; 32 | 33 | const form_layout = { 34 | labelCol: {span: 4}, 35 | }; 36 | 37 | return ( 38 | 44 | 45 | 50 | 51 | 52 | 53 | 54 | ); 55 | }; 56 | export default UpdateForm; 57 | -------------------------------------------------------------------------------- /cmd/magi/handlers/v1/configs.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github/basefas/magi/cmd/magi/handlers/http" 6 | "github/basefas/magi/internal/configs" 7 | ) 8 | 9 | func ConfigCreate(c *gin.Context) { 10 | var cc configs.CreateConfig 11 | http.CheckJSON(c, &cc) 12 | 13 | err := configs.Create(cc) 14 | http.CheckResErr(c, err) 15 | } 16 | 17 | func ConfigGet(c *gin.Context) { 18 | code := c.Param("code") 19 | 20 | config, err := configs.GetInfo(code) 21 | http.CheckResErrData(c, err, config) 22 | } 23 | 24 | func ConfigUpdate(c *gin.Context) { 25 | code := c.Param("code") 26 | var uc configs.UpdateConfig 27 | http.CheckJSON(c, &uc) 28 | 29 | err := configs.Update(code, uc) 30 | http.CheckResErr(c, err) 31 | } 32 | 33 | func ConfigDelete(c *gin.Context) { 34 | code := c.Param("code") 35 | 36 | err := configs.Delete(code) 37 | http.CheckResErr(c, err) 38 | } 39 | 40 | func ConfigList(c *gin.Context) { 41 | label := c.Query("label") 42 | linked := c.Query("linked") 43 | 44 | cl, err := configs.List(label, linked) 45 | http.CheckResErrData(c, err, cl) 46 | } 47 | 48 | func ConfigHistoryList(c *gin.Context) { 49 | code := c.Param("code") 50 | 51 | hl, err := configs.HistoryList(code) 52 | http.CheckResErrData(c, err, hl) 53 | } 54 | 55 | func ConfigFileList(c *gin.Context) { 56 | version := c.Param("version") 57 | 58 | fl, err := configs.FileList(version) 59 | http.CheckResErrData(c, err, fl) 60 | } 61 | 62 | func ConfigEditFile(c *gin.Context) { 63 | code := c.Param("code") 64 | var cf configs.EditConfigFile 65 | http.CheckJSON(c, &cf) 66 | uid := http.GetUID(c) 67 | 68 | err := configs.EditFiles(code, uid, cf) 69 | http.CheckResErr(c, err) 70 | } 71 | -------------------------------------------------------------------------------- /web/src/pages/ops/label/components/UpdateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { LabelListItem, LabelUpdateInfo } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const {Item} = Form; 7 | 8 | interface UpdateFormProps { 9 | open: boolean; 10 | label: LabelListItem; 11 | onOk: (code: string, label: LabelUpdateInfo) => void; 12 | onCancel: () => void; 13 | } 14 | 15 | const UpdateForm: FC = (props) => { 16 | const {t} = useTranslation(); 17 | const {open, label, onOk, onCancel} = props; 18 | const [form] = Form.useForm(); 19 | 20 | 21 | const ok = () => { 22 | form 23 | .validateFields() 24 | .then(values => { 25 | form.resetFields(); 26 | onOk(label.code, values); 27 | }) 28 | .catch(info => { 29 | console.log(t("parameter_validation_failed"), t("colon"), info); 30 | }); 31 | }; 32 | 33 | const form_layout = { 34 | labelCol: {span: 4}, 35 | }; 36 | 37 | return ( 38 | 44 | 45 | 50 | 51 | 52 | 53 | 54 | ); 55 | }; 56 | export default UpdateForm; 57 | -------------------------------------------------------------------------------- /internal/auth/casbin.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "github.com/casbin/casbin/v2" 6 | "github.com/casbin/casbin/v2/model" 7 | gormadapter "github.com/casbin/gorm-adapter/v3" 8 | "github.com/gin-gonic/gin" 9 | "github.com/spf13/viper" 10 | "github/basefas/magi/internal/utils/logger" 11 | ) 12 | 13 | var Casbin *casbin.Enforcer 14 | 15 | func Init() { 16 | m := model.NewModel() 17 | m.AddDef("r", "r", "sub, obj, act") 18 | m.AddDef("p", "p", "sub, obj, act") 19 | m.AddDef("g", "g", "_, _") 20 | m.AddDef("e", "e", "some(where (p.eft == allow))") 21 | m.AddDef("m", "m", `g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)`) 22 | 23 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", 24 | viper.GetString("db.mysql.user"), 25 | viper.GetString("db.mysql.password"), 26 | viper.GetString("db.mysql.host"), 27 | viper.GetString("db.mysql.port"), 28 | viper.GetString("db.mysql.name")) 29 | e, err := gormadapter.NewAdapter("mysql", dsn, true) 30 | if err != nil { 31 | fmt.Println("[magi] " + err.Error()) 32 | panic("auth init failed.") 33 | } 34 | Casbin, _ = casbin.NewEnforcer(m, e) 35 | 36 | errLoadPolicy := Casbin.LoadPolicy() 37 | if errLoadPolicy != nil { 38 | fmt.Println("[magi] " + errLoadPolicy.Error()) 39 | panic("auth init failed.") 40 | } 41 | } 42 | 43 | func CheckPermission(c *gin.Context, e *casbin.Enforcer) bool { 44 | token := c.GetHeader("token") 45 | uid, _ := GetUID(token) 46 | path := c.Request.URL.Path 47 | method := c.Request.Method 48 | 49 | allowed, err := e.Enforce(fmt.Sprintf("user::%d", uid), path, method) 50 | if err != nil { 51 | logger.Warnf("auth error", err.Error()) 52 | } 53 | return allowed 54 | } 55 | -------------------------------------------------------------------------------- /web/src/pages/ops/template/components/CreateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { TemplateCreateInfo } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const {Item} = Form; 7 | 8 | interface CreateFormProps { 9 | open: boolean; 10 | onOk: (template: TemplateCreateInfo) => void; 11 | onCancel: () => void; 12 | } 13 | 14 | const CreateForm: FC = (props) => { 15 | const {t} = useTranslation(); 16 | const {open, onOk, onCancel} = props; 17 | const [form] = Form.useForm(); 18 | 19 | const ok = () => { 20 | form 21 | .validateFields() 22 | .then(values => { 23 | form.resetFields(); 24 | onOk(values); 25 | }) 26 | .catch(info => { 27 | console.log(t("parameter_validation_failed"), t("colon"), info); 28 | }); 29 | }; 30 | 31 | const form_layout = { 32 | labelCol: {span: 5}, 33 | }; 34 | 35 | return ( 36 | 44 | 45 | 50 | 51 | 52 | 53 | 54 | ); 55 | }; 56 | export default CreateForm; 57 | -------------------------------------------------------------------------------- /web/src/pages/settings/role/components/UpdateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { RoleListItem, RoleUpdateInfo } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const {Item} = Form; 7 | 8 | interface UpdateFormProps { 9 | open: boolean; 10 | role: RoleListItem; 11 | onOk: (id: number, user: RoleUpdateInfo) => void; 12 | onCancel: () => void; 13 | } 14 | 15 | const UpdateForm: FC = (props) => { 16 | const {t} = useTranslation(); 17 | const {open, role, onOk, onCancel} = props; 18 | const [form] = Form.useForm(); 19 | 20 | const ok = () => { 21 | form 22 | .validateFields() 23 | .then(values => { 24 | form.resetFields(); 25 | onOk(role.id, values); 26 | }) 27 | .catch(info => { 28 | console.log(t("parameter_validation_failed"), t("colon"), info); 29 | }); 30 | }; 31 | 32 | const form_layout = { 33 | labelCol: {span: 4}, 34 | }; 35 | 36 | return ( 37 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | ); 56 | }; 57 | export default UpdateForm; 58 | -------------------------------------------------------------------------------- /web/src/pages/settings/user/components/ResetPasswordForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { useTranslation } from "react-i18next"; 4 | import { UserResetPassword } from "../data"; 5 | 6 | const {Item} = Form; 7 | 8 | interface CreateFormProps { 9 | open: boolean; 10 | uid: number; 11 | onOk: (uid: number, password: UserResetPassword) => void; 12 | onCancel: () => void; 13 | } 14 | 15 | const ResetPasswordForm: FC = (props) => { 16 | const {t} = useTranslation(); 17 | const {open, uid, onOk, onCancel} = props; 18 | const [form] = Form.useForm(); 19 | const title = t("reset_password"); 20 | 21 | const ok = () => { 22 | form 23 | .validateFields() 24 | .then(values => { 25 | form.resetFields(); 26 | onOk(uid, values); 27 | }) 28 | .catch(info => { 29 | console.log(t("parameter_validation_failed"), t("colon"), info); 30 | }); 31 | }; 32 | 33 | const form_layout = { 34 | labelCol: {span: 4}, 35 | }; 36 | 37 | return ( 38 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | ); 55 | }; 56 | export default ResetPasswordForm; 57 | -------------------------------------------------------------------------------- /web/src/pages/deploy/config/service.tsx: -------------------------------------------------------------------------------- 1 | import { del, get, post, put, ResponseData } from "../../../utils/request"; 2 | import { ConfigCreateInfo, ConfigFileListItem, ConfigHistoryListItem, ConfigInfo, ConfigListItem, ConfigUpdateInfo, EditConfigFile } from "./data"; 3 | 4 | export async function configList(): Promise> { 5 | return await get("/api/v1/configs"); 6 | } 7 | 8 | export async function createConfig(config: ConfigCreateInfo): Promise> { 9 | return await post<{}>("/api/v1/configs", config); 10 | } 11 | 12 | export async function updateConfig(code: string, config: ConfigUpdateInfo): Promise> { 13 | return await put<{}>("/api/v1/configs/" + code, config); 14 | } 15 | 16 | export async function deleteConfig(code: string): Promise> { 17 | return await del<{}>("/api/v1/configs/" + code); 18 | } 19 | 20 | export async function configInfo(code: string): Promise> { 21 | return await get("/api/v1/configs/" + code); 22 | } 23 | 24 | export async function configListByLabel(label: string): Promise> { 25 | return await get("/api/v1/configs?label=" + label); 26 | } 27 | 28 | export async function configHistoryList(code: string): Promise> { 29 | return await get("/api/v1/configs/" + code + "/histories"); 30 | } 31 | 32 | export async function configFileList(code: string, version: string): Promise> { 33 | return await get("/api/v1/configs/" + code + "/histories/" + version + "/files"); 34 | } 35 | 36 | export async function editConfigFiles(code: string, configs: EditConfigFile): Promise> { 37 | return await post<{}>("/api/v1/configs/" + code + "/histories", configs); 38 | } -------------------------------------------------------------------------------- /internal/utils/db/mysql/migrate.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "github.com/golang-migrate/migrate/v4" 7 | _ "github.com/golang-migrate/migrate/v4/database/mysql" 8 | "github.com/golang-migrate/migrate/v4/source/iofs" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | //go:embed migrations/*.sql 13 | var fs embed.FS 14 | 15 | type Migration struct { 16 | client *migrate.Migrate 17 | } 18 | 19 | func migration() error { 20 | m, err := newMigration() 21 | if err != nil { 22 | return err 23 | } 24 | needMigrate := viper.GetBool("db.mysql.migrate") 25 | if needMigrate { 26 | err = m.Up() 27 | } 28 | return err 29 | } 30 | 31 | func newMigration() (migration *Migration, err error) { 32 | user := viper.GetString("db.mysql.user") 33 | password := viper.GetString("db.mysql.password") 34 | host := viper.GetString("db.mysql.host") 35 | port := viper.GetUint64("db.mysql.port") 36 | name := viper.GetString("db.mysql.name") 37 | dsn := fmt.Sprintf("mysql://%s:%s@tcp(%s:%d)/%s", user, password, host, port, name) 38 | d, err := iofs.New(fs, "migrations") 39 | if err != nil { 40 | return nil, err 41 | } 42 | mig := Migration{} 43 | mig.client, err = migrate.NewWithSourceInstance("iofs", d, dsn) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return &mig, err 48 | } 49 | 50 | func (m *Migration) Up() error { 51 | currentVersion, _, _ := m.client.Version() 52 | 53 | err := m.client.Up() 54 | if err != nil { 55 | if err == migrate.ErrNoChange { 56 | return nil 57 | } else { 58 | return err 59 | } 60 | } 61 | 62 | newVersion, _, _ := m.client.Version() 63 | if err != nil { 64 | return err 65 | } 66 | fmt.Printf("[magi] Migration up form version: %d to version: %d success.\n", currentVersion, newVersion) 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /cmd/magi/handlers/http/response.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "errors" 5 | "github.com/gin-gonic/gin" 6 | "github/basefas/magi/internal/auth" 7 | "github/basefas/magi/internal/utils/logger" 8 | "go.uber.org/zap" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | var ( 14 | AuthError = errors.New("auth error") 15 | ) 16 | 17 | type Response struct { 18 | Code int `json:"code"` 19 | Message string `json:"message"` 20 | Data interface{} `json:"data"` 21 | } 22 | 23 | func Re(c *gin.Context, code int, msg string, data interface{}) { 24 | c.JSON(http.StatusOK, Response{ 25 | Code: code, 26 | Message: msg, 27 | Data: data, 28 | }) 29 | } 30 | 31 | func CheckResErr(c *gin.Context, err error) { 32 | if err != nil { 33 | logger.InfoF("Response Error", zap.Any("content", err.Error())) 34 | Re(c, -1, err.Error(), nil) 35 | } else { 36 | Re(c, 0, "success", nil) 37 | } 38 | } 39 | 40 | func CheckResErrData(c *gin.Context, err error, data interface{}) { 41 | if err != nil { 42 | logger.InfoF("Response Error", zap.Any("content", err.Error())) 43 | Re(c, -1, err.Error(), nil) 44 | } else { 45 | Re(c, 0, "success", data) 46 | } 47 | } 48 | 49 | func CheckUint64(c *gin.Context, key string) uint64 { 50 | id, err := strconv.ParseUint(c.Param(key), 10, 64) 51 | if err != nil { 52 | Re(c, -1, err.Error(), nil) 53 | return 0 54 | } 55 | return id 56 | } 57 | 58 | func CheckJSON(c *gin.Context, obj interface{}) { 59 | if err := c.ShouldBindJSON(obj); err != nil { 60 | Re(c, -1, err.Error(), nil) 61 | return 62 | } 63 | } 64 | 65 | func GetUID(c *gin.Context) uint64 { 66 | token := c.GetHeader("token") 67 | uid, _ := auth.GetUID(token) 68 | return uid 69 | } 70 | 71 | func IsMe(c *gin.Context, id uint64) { 72 | if !(id == GetUID(c)) { 73 | Re(c, -1, AuthError.Error(), nil) 74 | return 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /web/src/pages/deploy/app/components/UpdateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { AppInfo, AppUpdateInfo } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const {Item} = Form; 7 | 8 | interface UpdateFormProps { 9 | open: boolean; 10 | loading: boolean; 11 | app: AppInfo; 12 | onOk: (code: string, app: AppUpdateInfo) => void; 13 | onCancel: () => void; 14 | } 15 | 16 | const UpdateForm: FC = (props) => { 17 | const {t} = useTranslation(); 18 | const {open, loading, app, onOk, onCancel} = props; 19 | const [form] = Form.useForm(); 20 | 21 | const ok = () => { 22 | form 23 | .validateFields() 24 | .then(values => { 25 | form.resetFields(); 26 | onOk(app.code, values); 27 | }) 28 | .catch(info => { 29 | console.log(t("parameter_validation_failed"), t("colon"), info); 30 | }); 31 | }; 32 | 33 | const form_layout = { 34 | labelCol: {span: 2}, 35 | }; 36 | 37 | return ( 38 | 46 | 47 | 52 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | export default UpdateForm; 63 | -------------------------------------------------------------------------------- /internal/menus/model.go: -------------------------------------------------------------------------------- 1 | package menus 2 | 3 | import ( 4 | "github/basefas/magi/internal/global" 5 | "time" 6 | ) 7 | 8 | type Menu struct { 9 | global.Model 10 | Locale string `json:"locale" gorm:"NOT NULL"` 11 | Path string `json:"path" gorm:"NOT NULL"` 12 | Method string `json:"method" gorm:"NOT NULL"` 13 | Type uint64 `json:"type" gorm:"type:uint;size:32;NOT NULL;"` 14 | Icon string `json:"icon" gorm:"NOT NULL"` 15 | ParentID uint64 `json:"parent_id" gorm:"type:uint;size:32;NOT NULL;"` 16 | OrderID uint64 `json:"order_id" gorm:"type:uint;size:32;NOT NULL;default:999999;"` 17 | } 18 | 19 | func (Menu) TableName() string { 20 | return "magi_menu" 21 | } 22 | 23 | type CreateMenu struct { 24 | Locale string `json:"locale" binding:"required"` 25 | Path string `json:"path" binding:"required"` 26 | Method string `json:"method" binding:"-"` 27 | Type uint64 `json:"type" binding:"required"` 28 | Icon string `json:"icon" binding:"-"` 29 | ParentID uint64 `json:"parent_id" binding:"-"` 30 | OrderID uint64 `json:"order_id" binding:"-"` 31 | } 32 | 33 | type UpdateMenu struct { 34 | Name string `json:"name"` 35 | Locale string `json:"locale"` 36 | Path string `json:"path"` 37 | Type uint64 `json:"type"` 38 | Method string `json:"method"` 39 | Icon string `json:"icon"` 40 | ParentID uint64 `json:"parent_id"` 41 | OrderID uint64 `json:"order_id"` 42 | } 43 | 44 | type MenuInfo struct { 45 | ID uint64 `json:"id"` 46 | Locale string `json:"locale"` 47 | Path string `json:"path"` 48 | Type uint64 `json:"type"` 49 | Method string `json:"method"` 50 | Icon string `json:"icon"` 51 | ParentID uint64 `json:"parent_id"` 52 | OrderID uint64 `json:"order_id"` 53 | CreatedAt time.Time `json:"create_time"` 54 | UpdatedAt time.Time `json:"update_time"` 55 | Children []MenuInfo `json:"children" gorm:"-"` 56 | Funs []MenuInfo `json:"funs" gorm:"-"` 57 | } 58 | -------------------------------------------------------------------------------- /web/src/pages/deploy/config/components/UpdateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { ConfigInfo, ConfigUpdateInfo } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const {Item} = Form; 7 | 8 | interface UpdateFormProps { 9 | open: boolean; 10 | loading: boolean; 11 | config: ConfigInfo; 12 | onOk: (code: string, config: ConfigUpdateInfo) => void; 13 | onCancel: () => void; 14 | } 15 | 16 | const UpdateForm: FC = (props) => { 17 | const {t} = useTranslation(); 18 | const {open, loading, config, onOk, onCancel} = props; 19 | const [form] = Form.useForm(); 20 | 21 | const ok = () => { 22 | form 23 | .validateFields() 24 | .then(values => { 25 | form.resetFields(); 26 | onOk(config.code, values); 27 | }) 28 | .catch(info => { 29 | console.log(t("parameter_validation_failed"), t("colon"), info); 30 | }); 31 | }; 32 | 33 | const form_layout = { 34 | labelCol: {span: 4}, 35 | }; 36 | 37 | return ( 38 | 45 | 46 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 | ); 60 | }; 61 | export default UpdateForm; 62 | -------------------------------------------------------------------------------- /web/src/pages/deploy/config/components/AddFileForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { ConfigFileListItem } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const {Item} = Form; 7 | 8 | interface AddFileFormProps { 9 | open: boolean; 10 | files: ConfigFileListItem[]; 11 | onOk: (filename: string) => void; 12 | onCancel: () => void; 13 | } 14 | 15 | const AddFileForm: FC = (props) => { 16 | const {t} = useTranslation(); 17 | const {open, files, onOk, onCancel} = props; 18 | const [form] = Form.useForm(); 19 | 20 | const ok = () => { 21 | form 22 | .validateFields() 23 | .then(values => { 24 | form.resetFields(); 25 | onOk(values["filename"]); 26 | }) 27 | .catch(info => { 28 | console.log(t("parameter_validation_failed"), t("colon"), info); 29 | }); 30 | }; 31 | 32 | return ( 33 | 39 |
40 | { 47 | if (value && files.find(item => item.filename === value)) { 48 | return Promise.reject(t("file_already_exists")); 49 | } 50 | return Promise.resolve(); 51 | }, 52 | }, 53 | ]} 54 | > 55 | 56 | 57 |
58 |
59 | ); 60 | }; 61 | export default AddFileForm; 62 | -------------------------------------------------------------------------------- /internal/deploys/model.go: -------------------------------------------------------------------------------- 1 | package deploys 2 | 3 | import ( 4 | "github/basefas/magi/internal/global" 5 | "gorm.io/gorm" 6 | "time" 7 | ) 8 | 9 | type Deploy struct { 10 | global.Model 11 | Version string `json:"version" gorm:"unique;NOT NULL;"` 12 | AppName string `json:"app_name" gorm:"NOT NULL;"` 13 | AppCode string `json:"app_code" gorm:"NOT NULL;"` 14 | ImageTag string `json:"image_tag" gorm:"NOT NULL;"` 15 | ConfigVersion string `json:"config_version"` 16 | PatchContent string `json:"patch_content" gorm:"type:text;"` 17 | Status uint64 `json:"status" gorm:"type:uint;size:32;NOT NULL"` 18 | DeployerID uint64 `json:"deployer_id" gorm:"type:uint;size:32"` 19 | CreatorID uint64 `json:"creator_id" gorm:"type:uint;size:32;NOT NULL"` 20 | Commit string `json:"commit"` 21 | FinishedAt gorm.DeletedAt `json:"finish_time"` 22 | } 23 | 24 | func (Deploy) TableName() string { 25 | return "magi_deploy" 26 | } 27 | 28 | type CreateDeploy struct { 29 | AppCode string `json:"app_code" binding:"required"` 30 | ImageTag string `json:"image_tag" binding:"required"` 31 | ConfigVersion string `json:"config_version"` 32 | PatchContent string `json:"patch_content"` 33 | } 34 | 35 | type UpdateDeploy struct { 36 | Status string `json:"status"` 37 | Deployer string `json:"deployer"` 38 | } 39 | 40 | type DeployInfo struct { 41 | ID uint64 `json:"id"` 42 | Version string `json:"version"` 43 | ImageTag string `json:"image_tag"` 44 | ConfigVersion string `json:"config_version"` 45 | Status uint64 `json:"status"` 46 | Deployer string `json:"deployer"` 47 | Creator string `json:"creator"` 48 | PatchContent string `json:"patch_content"` 49 | Commit string `json:"commit"` 50 | CreatedAt time.Time `json:"create_time"` 51 | UpdatedAt time.Time `json:"update_time"` 52 | FinishedAt gorm.DeletedAt `json:"finish_time"` 53 | } 54 | -------------------------------------------------------------------------------- /web/src/pages/ops/label/components/CreateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { LabelCreateInfo } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const {Item} = Form; 7 | 8 | interface CreateFormProps { 9 | open: boolean; 10 | onOk: (label: LabelCreateInfo) => void; 11 | onCancel: () => void; 12 | } 13 | 14 | const CreateForm: FC = (props) => { 15 | const {t} = useTranslation(); 16 | const {open, onOk, onCancel} = props; 17 | const [form] = Form.useForm(); 18 | 19 | const ok = () => { 20 | form 21 | .validateFields() 22 | .then(values => { 23 | form.resetFields(); 24 | onOk(values); 25 | }) 26 | .catch(info => { 27 | console.log(t("parameter_validation_failed"), t("colon"), info); 28 | }); 29 | }; 30 | 31 | const form_layout = { 32 | labelCol: {span: 5}, 33 | }; 34 | 35 | return ( 36 | 43 | 44 | 49 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | export default CreateForm; 64 | -------------------------------------------------------------------------------- /internal/labels/labels.go: -------------------------------------------------------------------------------- 1 | package labels 2 | 3 | import ( 4 | "errors" 5 | "github/basefas/magi/internal/utils/db" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | var ( 10 | ErrLabelNotFound = errors.New("label not found ") 11 | ErrLabelExists = errors.New("label already exists") 12 | ) 13 | 14 | func Create(ce CreateLabel) error { 15 | e := Label{} 16 | 17 | if err := db.Mysql. 18 | Where("code = ?", ce.Code). 19 | Find(&e).Error; err != nil { 20 | if !errors.Is(err, gorm.ErrRecordNotFound) { 21 | return err 22 | } 23 | } 24 | 25 | if e.ID != 0 { 26 | return ErrLabelExists 27 | } 28 | ne := Label{ 29 | Name: ce.Name, 30 | Code: ce.Code, 31 | } 32 | 33 | if err := db.Mysql.Create(&ne).Error; err != nil { 34 | return err 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func GetInfo(code string) (ei LabelInfo, err error) { 41 | if err = db.Mysql. 42 | Model(&Label{}). 43 | Where("code", code). 44 | Take(&ei).Error; err != nil { 45 | if errors.Is(err, gorm.ErrRecordNotFound) { 46 | return ei, ErrLabelNotFound 47 | } else { 48 | return ei, err 49 | } 50 | } 51 | return ei, err 52 | } 53 | 54 | func Update(code string, ue UpdateLabel) error { 55 | updateLabel := make(map[string]interface{}) 56 | 57 | if ue.Name != "" { 58 | updateLabel["name"] = ue.Name 59 | } 60 | 61 | if err := db.Mysql. 62 | Model(&Label{}). 63 | Where("code = ?", code). 64 | Updates(updateLabel).Error; err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func Delete(code string) error { 72 | err := db.Mysql. 73 | Where("code = ?", code). 74 | Delete(&Label{}).Error 75 | 76 | return err 77 | } 78 | 79 | func List() (labels []LabelInfo, err error) { 80 | labels = make([]LabelInfo, 0) 81 | 82 | q := db.Mysql. 83 | Table("magi_label AS l"). 84 | Select("l.id, l.name, l.code, l.created_at, l.updated_at"). 85 | Where("l.deleted_at IS NULL"). 86 | Order("l.id") 87 | 88 | err = q.Find(&labels).Error 89 | return labels, err 90 | } 91 | -------------------------------------------------------------------------------- /web/src/pages/ops/template/components/AddFileForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { TemplateFile } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const {Item} = Form; 7 | 8 | interface AddFileFormProps { 9 | open: boolean; 10 | files: TemplateFile[]; 11 | onOk: (filename: string) => void; 12 | onCancel: () => void; 13 | } 14 | 15 | const AddFileForm: FC = (props) => { 16 | const {open, files, onOk, onCancel} = props; 17 | const [form] = Form.useForm(); 18 | const {t} = useTranslation(); 19 | 20 | const ok = () => { 21 | form 22 | .validateFields() 23 | .then(values => { 24 | form.resetFields(); 25 | onOk(values["filename"]); 26 | }) 27 | .catch(info => { 28 | console.log(t("parameter_validation_failed"), t("colon"), info); 29 | }); 30 | }; 31 | 32 | return ( 33 | 39 |
40 | { 48 | if (value && files.find(item => item.filename === value)) { 49 | return Promise.reject(t("file_already_exists")); 50 | } 51 | return Promise.resolve(); 52 | }, 53 | }, 54 | ]} 55 | > 56 | 57 | 58 |
59 |
60 | ); 61 | }; 62 | export default AddFileForm; 63 | -------------------------------------------------------------------------------- /web/src/utils/request.tsx: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from "axios"; 2 | 3 | import { deleteToken, getToken } from "./auth"; 4 | import { message, Modal } from "antd"; 5 | import i18next from "i18next"; 6 | 7 | export interface ResponseData { 8 | code: number; 9 | data: T; 10 | message: string; 11 | } 12 | 13 | const key = 'axios'; 14 | 15 | const instance = axios.create({ 16 | baseURL: import.meta.env.VITE_BASE_URL || undefined 17 | }); 18 | 19 | instance.interceptors.request.use( 20 | function (config: AxiosRequestConfig) { 21 | // @ts-ignore 22 | config.headers["token"] = getToken(); 23 | return config; 24 | }, 25 | function (error: any) { 26 | return Promise.reject(error); 27 | }); 28 | 29 | instance.interceptors.response.use( 30 | response => { 31 | if (response.data.code === -2) { 32 | Modal.warning({ 33 | title: i18next.t("token_error"), 34 | onOk() { 35 | window.location.href = "/login"; 36 | deleteToken(); 37 | }, 38 | okText: i18next.t("ok"), 39 | }); 40 | } 41 | return Promise.resolve(response.data); 42 | }, 43 | error => { 44 | if (error.response.status) { 45 | message.error({ 46 | content: error.response.status + i18next.t("colon") + error.response.statusText, 47 | key 48 | }).then(); 49 | } else { 50 | message.error({content: i18next.t("server_error") + i18next.t("colon") + error.message, key}).then(); 51 | } 52 | return Promise.reject(error); 53 | }); 54 | 55 | export function get(url: string, params?: any): Promise> { 56 | return instance.get(url, {params}); 57 | } 58 | 59 | export function post(url: string, data?: any, params?: any): Promise> { 60 | return instance.post(url, data, {params}); 61 | } 62 | 63 | export function put(url: string, data?: any, params?: any): Promise> { 64 | return instance.put(url, data, {params}); 65 | } 66 | 67 | export function del(url: string, params?: any): Promise> { 68 | return instance.delete(url, {params}); 69 | } 70 | -------------------------------------------------------------------------------- /web/src/layouts/BaseLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from "react"; 2 | import BasicLayout, { MenuDataItem } from "@ant-design/pro-layout"; 3 | import { useHistory, useLocation, withRouter } from "react-router-dom"; 4 | import RightContent from "../components/RightContent/RightContent"; 5 | import logo from "../assets/logo.svg"; 6 | import { menuIcons } from "../utils/icons"; 7 | import { systemMenuList } from "./service"; 8 | import { useTranslation } from "react-i18next"; 9 | 10 | const BaseLayout: FC = (props) => { 11 | const {t} = useTranslation(); 12 | 13 | const history = useHistory(); 14 | const location = useLocation(); 15 | const [menuData, setMenuData] = useState([]); 16 | const [pathname, setPathname] = useState(location.pathname); 17 | const [loading, setLoading] = useState(true); 18 | 19 | const loopMenuItem = (menus: MenuDataItem[]): MenuDataItem[] => { 20 | if (menus != null && menus.length > 0) { 21 | return menus.map(({icon, children, locale, ...item}) => ({ 22 | ...item, 23 | name: t(locale as string), 24 | icon: icon && menuIcons[icon as string], 25 | children: children && loopMenuItem(children), 26 | })); 27 | } else { 28 | return []; 29 | } 30 | }; 31 | 32 | const fetchData = async () => { 33 | const result = await systemMenuList(); 34 | setMenuData(result.data); 35 | setLoading(false); 36 | }; 37 | 38 | useEffect(() => { 39 | setMenuData([]); 40 | setLoading(true); 41 | fetchData().then(); 42 | }, []); 43 | 44 | 45 | return ( 46 | loopMenuItem(menuData)} 53 | rightContentRender={() => } 54 | menuItemRender={(item, dom) => ( 55 |
{ 56 | setPathname(item.path || "/dashboard"); 57 | history.push(item.path || "/dashboard"); 58 | }}> 59 | {dom} 60 |
61 | )}> 62 |
{props.children}
63 |
64 | ); 65 | }; 66 | 67 | export default withRouter(BaseLayout); 68 | -------------------------------------------------------------------------------- /internal/utils/git/git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "context" 5 | "github.com/fluxcd/go-git-providers/gitprovider" 6 | "github.com/spf13/viper" 7 | "strings" 8 | ) 9 | 10 | type GitType string 11 | 12 | const ( 13 | TypeGitHub GitType = "github" 14 | TypeGitLab GitType = "gitlab" 15 | ) 16 | 17 | type Config struct { 18 | Type GitType 19 | Hostname string 20 | Repo string 21 | Username string 22 | Token string 23 | Personal bool 24 | Private bool 25 | Branch string 26 | } 27 | 28 | type CommitFile struct { 29 | Path *string `json:"path"` 30 | Content *string `json:"content"` 31 | } 32 | 33 | func GetGitConfig() *Config { 34 | return &Config{ 35 | Type: GitType(viper.GetString("git.type")), 36 | Hostname: viper.GetString("git.hostname"), 37 | Repo: viper.GetString("git.repo"), 38 | Username: viper.GetString("git.username"), 39 | Token: viper.GetString("git.token"), 40 | Personal: viper.GetBool("git.personal"), 41 | Private: viper.GetBool("git.private"), 42 | Branch: viper.GetString("git.branch"), 43 | } 44 | } 45 | 46 | func Get(path string) (string, error) { 47 | c, err := FileClient() 48 | if err != nil { 49 | return "", err 50 | } 51 | file, err := c.GetContent(context.Background(), path, GetGitConfig().Branch) 52 | if err != nil { 53 | return "", err 54 | } 55 | return *file, nil 56 | } 57 | 58 | func Commit(files []CommitFile) (gitprovider.Commit, error) { 59 | c, err := CommitClient() 60 | if err != nil { 61 | return nil, err 62 | } 63 | commitMsg := "Magi commit." 64 | commitFiles := make([]gitprovider.CommitFile, 0) 65 | for _, file := range files { 66 | commitFiles = append(commitFiles, gitprovider.CommitFile{Path: file.Path, Content: file.Content}) 67 | } 68 | commit, err := c.Create(context.Background(), GetGitConfig().Branch, commitMsg, commitFiles) 69 | return commit, err 70 | } 71 | 72 | func CommitWithShortSHA(files []CommitFile) (shortSha string, err error) { 73 | commit, err := Commit(files) 74 | if err != nil { 75 | return "", err 76 | } 77 | shortSha = commit.Get().Sha[0:7] 78 | return shortSha, nil 79 | } 80 | 81 | func HasContent(path string) (bool, error) { 82 | _, err := Get(path) 83 | if err != nil { 84 | if strings.Contains(err.Error(), "404 Not Found") { 85 | return false, nil 86 | } 87 | return false, err 88 | } 89 | return true, nil 90 | } 91 | -------------------------------------------------------------------------------- /web/src/pages/settings/group/components/UpdateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from "react"; 2 | import { Form, Input, Modal, Select } from "antd"; 3 | import { GroupListItem, GroupUpdateInfo } from "../data"; 4 | import { RoleListItem } from "../../role/data"; 5 | import { roleList } from "../../role/service"; 6 | import { useTranslation } from "react-i18next"; 7 | 8 | const {Item} = Form; 9 | const {Option} = Select; 10 | 11 | interface UpdateFormProps { 12 | open: boolean; 13 | group: GroupListItem; 14 | onOk: (id: number, group: GroupUpdateInfo) => void; 15 | onCancel: () => void; 16 | } 17 | 18 | const UpdateForm: FC = (props) => { 19 | const {t} = useTranslation(); 20 | const {open, group, onOk, onCancel} = props; 21 | const [roles, setRoles] = useState([]); 22 | const [form] = Form.useForm(); 23 | 24 | const getRoleList = async () => { 25 | const result = await roleList(); 26 | if (result.code === 0) { 27 | setRoles(result.data); 28 | } 29 | }; 30 | 31 | useEffect(() => { 32 | getRoleList().then(); 33 | }, []); 34 | 35 | const ok = () => { 36 | form 37 | .validateFields() 38 | .then(values => { 39 | form.resetFields(); 40 | onOk(group.id, values); 41 | }) 42 | .catch(info => { 43 | console.log(t("parameter_validation_failed"), t("colon"), info); 44 | }); 45 | }; 46 | 47 | const form_layout = { 48 | labelCol: {span: 4}, 49 | }; 50 | 51 | return ( 52 | 58 | 59 | 61 | 62 | 63 | 64 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | export default UpdateForm; 75 | -------------------------------------------------------------------------------- /web/src/pages/settings/group/components/CreateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from "react"; 2 | import { Form, Input, Modal, Select } from "antd"; 3 | import { roleList } from "../../role/service"; 4 | import { GroupCreateInfo } from "../data"; 5 | import { RoleListItem } from "../../role/data"; 6 | import { useTranslation } from "react-i18next"; 7 | 8 | const {Item} = Form; 9 | const {Option} = Select; 10 | 11 | interface CreateFormProps { 12 | open: boolean; 13 | onOk: (user: GroupCreateInfo) => void; 14 | onCancel: () => void; 15 | } 16 | 17 | const CreateForm: FC = (props) => { 18 | const {t} = useTranslation(); 19 | const {open, onOk, onCancel} = props; 20 | const [roles, setRoles] = useState([]); 21 | const [form] = Form.useForm(); 22 | 23 | const getRoleList = async () => { 24 | const result = await roleList(); 25 | if (result.code === 0) { 26 | setRoles(result.data); 27 | } 28 | }; 29 | 30 | useEffect(() => { 31 | getRoleList().then(); 32 | }, []); 33 | 34 | const ok = () => { 35 | form 36 | .validateFields() 37 | .then(values => { 38 | form.resetFields(); 39 | onOk(values); 40 | }) 41 | .catch(info => { 42 | console.log(t("parameter_validation_failed"), t("colon"), info); 43 | }); 44 | }; 45 | 46 | const form_layout = { 47 | labelCol: {span: 4}, 48 | }; 49 | 50 | return ( 51 | 58 | 59 | 64 | 65 | 66 | 69 | 74 | 75 | 76 | 77 | ); 78 | }; 79 | export default CreateForm; 80 | -------------------------------------------------------------------------------- /internal/utils/git/client.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "context" 5 | "github.com/fluxcd/go-git-providers/gitprovider" 6 | "github/basefas/magi/internal/utils/git/provider" 7 | ) 8 | 9 | func FileClient() (gitprovider.FileClient, error) { 10 | config := GetGitConfig() 11 | providerClient, err := GetClient() 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | switch config.Personal { 17 | case true: 18 | ref, err := gitprovider.ParseUserRepositoryURL(config.Repo) 19 | if err != nil { 20 | return nil, err 21 | } 22 | repo, err := providerClient.UserRepositories().Get(context.Background(), *ref) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return repo.Files(), nil 27 | case false: 28 | ref, err := gitprovider.ParseOrgRepositoryURL(config.Repo) 29 | if err != nil { 30 | return nil, err 31 | } 32 | repo, err := providerClient.OrgRepositories().Get(context.Background(), *ref) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return repo.Files(), nil 37 | default: 38 | return nil, err 39 | } 40 | } 41 | 42 | func CommitClient() (gitprovider.CommitClient, error) { 43 | config := GetGitConfig() 44 | providerClient, err := GetClient() 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | switch config.Personal { 50 | case true: 51 | ref, err := gitprovider.ParseUserRepositoryURL(config.Repo) 52 | if err != nil { 53 | return nil, err 54 | } 55 | repo, err := providerClient.UserRepositories().Get(context.Background(), *ref) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return repo.Commits(), nil 60 | case false: 61 | ref, err := gitprovider.ParseOrgRepositoryURL(config.Repo) 62 | if err != nil { 63 | return nil, err 64 | } 65 | repo, err := providerClient.OrgRepositories().Get(context.Background(), *ref) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return repo.Commits(), nil 70 | default: 71 | return nil, err 72 | } 73 | 74 | } 75 | 76 | func GetClient() (gitprovider.Client, error) { 77 | config := GetGitConfig() 78 | var providerCfg provider.Config 79 | 80 | switch config.Type { 81 | case TypeGitHub: 82 | providerCfg = provider.Config{ 83 | Provider: provider.GitProviderGitHub, 84 | Hostname: config.Hostname, 85 | Token: config.Token, 86 | } 87 | } 88 | 89 | return provider.GitProviderBuilder(providerCfg) 90 | } 91 | -------------------------------------------------------------------------------- /web/src/pages/ops/cluster/components/CreateForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Form, Input, Modal } from "antd"; 3 | import { ClusterCreateInfo } from "../data"; 4 | import { useTranslation } from "react-i18next"; 5 | import TextArea from "antd/lib/input/TextArea"; 6 | 7 | const {Item} = Form; 8 | 9 | interface CreateFormProps { 10 | open: boolean; 11 | loading: boolean; 12 | onOk: (cluster: ClusterCreateInfo) => void; 13 | onCancel: () => void; 14 | } 15 | 16 | const CreateForm: FC = (props) => { 17 | const {t} = useTranslation(); 18 | const {open, loading, onOk, onCancel} = props; 19 | const [form] = Form.useForm(); 20 | 21 | const ok = () => { 22 | form 23 | .validateFields() 24 | .then(values => { 25 | form.resetFields(); 26 | onOk(values); 27 | }) 28 | .catch(info => { 29 | console.log(t("parameter_validation_failed"), t("colon"), info); 30 | }); 31 | }; 32 | 33 | const form_layout = { 34 | labelCol: {span: 5}, 35 | }; 36 | 37 | return ( 38 | 46 | 47 | 52 | 53 | 54 | 60 | 61 | 62 | 67 |