通過Jenkins流水線自動(dòng)部署.NetCore應(yīng)用到K8S集群

上篇我們講解了如何將.netCore程序以Docker部署的兩種方法秕衙,http://www.reibang.com/p/a6c78a2c15f2,這是我們本文自動(dòng)化部署k8s的基礎(chǔ)攻礼,便于我們理解自動(dòng)化過程主要的步驟周霉。
今天我們嘗試與Jenkins集成k8s自動(dòng)化發(fā)布K8S集群節(jié)點(diǎn)中。

基本思路:
發(fā)起流水線構(gòu)造時(shí)我們傳入一系列參數(shù)(JSON結(jié)構(gòu))格嘁,然后由jenkins從git拉取流水線腳本温技,腳本根據(jù)我們傳入的參數(shù)對(duì)腳本進(jìn)行動(dòng)態(tài)替換并執(zhí)行革为,構(gòu)建完docker鏡像后,將鏡像推送到私有倉庫舵鳞,然后我們會(huì)根據(jù)json數(shù)據(jù)篷角,docker鏡像路徑等數(shù)據(jù)動(dòng)態(tài)生成k8s的部署yaml文件并交由k8s執(zhí)行部署。
這一系列構(gòu)建過程是由jenkins連接在k8s 某一個(gè)master節(jié)點(diǎn)進(jìn)行執(zhí)行系任。

1.準(zhǔn)備工作

我們沒有搭建自己的GIT代碼倉庫恳蹲,為了測(cè)試我們使用Gitee進(jìn)行托管(github太慢)
Jenkins下載并安裝插件
下載gitee插件,配置gitee憑據(jù)
此外腳本中使用了readJSON,writeJSON
要使用這兩個(gè)方法俩滥,必須安裝插件Pipeline Utility Steps嘉蕾,否則報(bào)錯(cuò):java.lang.NoSuchMethodError: No such DSL method 'readJSON'
另外需要安裝pipline的基本插件:
Pipeline: GitHub
Pipeline: Basic Steps
Jenkins創(chuàng)建gitee帳戶憑據(jù)

然后在jenkins中創(chuàng)建一個(gè)憑據(jù)供使用:

Jenkins添加k8s Master節(jié)點(diǎn),用于部署
jenkins 系統(tǒng)管理--節(jié)點(diǎn)管理霜旧,添加一個(gè)節(jié)點(diǎn)
節(jié)點(diǎn)名稱及標(biāo)簽取為:k8s-master


master節(jié)點(diǎn)安裝依賴項(xiàng)
yum install lttng-ust libcurl openssl-libs krb5-libs libicu zlib -y
master節(jié)點(diǎn)安裝JDK
我們把JDK包直接放到 /root/jenkins/jdk目錄即可错忱,jenkins我們前面指定了節(jié)點(diǎn)工作目錄是/root/jenkins儡率,則添加節(jié)點(diǎn)時(shí)jenkins會(huì)自動(dòng)查找到這個(gè)jdk目錄。
master節(jié)點(diǎn)安裝git
由于我們使用master節(jié)點(diǎn)拉取代碼以清,master節(jié)點(diǎn)需要安裝git儿普,運(yùn)行以下命令安裝
yum install -y git
master節(jié)點(diǎn)放置dotnetsdk3.1包
從微軟下載https://dotnet.microsoft.com/download/dotnet-core/3.1
目錄:/root/jenkins/tools/dotnetsdk3.1

mkdir /root/jenkins/tools/dotnetsdk3.1
cd /root/jenkins/tools
rz 上傳壓縮包dotnet-sdk-3.1.102-linux-x64.tar.gz
tar -zxf dotnet-sdk-3.1.102-linux-x64.tar.gz -C dotnetsdk3.1

2.Jenkins創(chuàng)建流水線構(gòu)建模板

創(chuàng)建模板的好處是后續(xù)其他流水線可以直接使用該模板腳本,不用重復(fù)配置掷倔,實(shí)現(xiàn)標(biāo)準(zhǔn)化眉孩。
Jenkins中新建一個(gè)任務(wù),模板選擇【流水線】


添加一文本參數(shù)

Jenkins中的參數(shù)是勒葱,用于向流水線腳本傳遞動(dòng)態(tài)數(shù)據(jù)浪汪,參數(shù)可以簡單理解為腳本替換用的占位符。
說明:因?yàn)槟0逯皇嵌x了整體執(zhí)行過程凛虽,模板不關(guān)注項(xiàng)目信息死遭,比如項(xiàng)目服務(wù)名,JDK/.net版本凯旋,代碼路徑呀潭,部署資源,發(fā)布后的名稱等信息至非,這些信息我們可以通過參數(shù)形式傳遞進(jìn)來钠署。
通常我們會(huì)定義定義很多個(gè)文本參數(shù),但每一參數(shù)都定義一個(gè)的模式我們傳參麻煩睡蟋,另外也不易擴(kuò)展踏幻,我這里采用一個(gè)文本參數(shù)類型(內(nèi)容用JSON結(jié)構(gòu))解決所有枷颊,避免頻繁的變更模板戳杀,因?yàn)楝F(xiàn)實(shí)中參數(shù)的變化的頻率是比較高的。
參數(shù)名稱我們命名為JSON_BODY夭苗,值隨意寫一個(gè)JSON結(jié)構(gòu)信卡,因?yàn)槭且粋€(gè)模板,我們這里的值只是一個(gè)參考题造,實(shí)際上是會(huì)被使用該模板的流水線重寫掉的傍菇。

流水線腳本我們采用可以采用從GIT拉取,也可以直接編寫腳本界赔,為了便于腳本管理與更新丢习,我們采用GIT來管理,目前測(cè)試我們托管到Gitee平臺(tái)淮悼。

流水線腳本參考

println("#############################################開始流水線##################################################")
//env.JOB_NAME ***
//env.WORKSPACE /var/jenkins_home/workspace/***
//env.K8S_TYPE="$params.K8S_TYPE"
//env.DOCKER_TYPE="$params.DOCKER_TYPE"
def jobName = "${JOB_NAME}"
def defaultEnv,defaultApiHost,defaultImageHost,defaultApolloMeta, imageVersion,versionTimestamp, codeUrl, branch, appId, serviceAndversion, sdkVersion, namespace, nodes, host, replicas, cpu, memory, sonarUrl, sonarKey, sonarToken, sonarResult, devopsUrl, version, service, imageName,nodePort
try {
    def paraBodyJson = readJSON text: "${params.JSON_BODY}"   
     defaultImageHost = paraBodyJson.defaultImageHost
    if (!defaultImageHost?.trim()) { 
        println("defaultImageHost 為空")
        defaultImageHost = "192.168.101.101:30083/janet/";
    }

   service = paraBodyJson.service
    if (!service?.trim()) {
        println("service 不能為空")
        sh "exit 1"
    }
    version = paraBodyJson.version
    if (!version?.trim()) {
        println("version 不能為空")
        sh "exit 1"
    }
    serviceAndversion = service + "-" + version
    println("service-version:"+serviceAndversion)
    appId = paraBodyJson.appId
    if (!appId?.trim()) {
        println("appId 不能為空")
        sh "exit 1"
    }   
   
    nodePort=paraBodyJson.nodePort
     if (!nodePort?.trim()) {
        println("nodePort 不能為空")
        sh "exit 1"
    }
    codeUrl = paraBodyJson.codeUrl
    imageVersion = paraBodyJson.imageVersion
    if (!codeUrl?.trim() && !imageVersion?.trim()) {
        println("codeUrl和imageVersion 不能同時(shí)為空")
        sh "exit 1"
    }
    //分支
    branch = paraBodyJson.branch
    if (!branch?.trim()) {
        branch = "master"
    }
    namespace = paraBodyJson.namespace
    if (!namespace?.trim()) {
        println("namespace 不能為空")
        sh "exit 1"
    }
   //版本
    versionTimestamp = paraBodyJson.versionTimestamp
    if (!versionTimestamp?.trim()) {
        versionTimestamp =  version + "." + System.currentTimeMillis()
    }

   //sdk版本咐低,java/.net均有不同的SDK版本,如果不填寫默認(rèn)jdk8
    sdkVersion = paraBodyJson.sdkVersion
    if (!sdkVersion?.trim()) {
        sdkVersion = "openjdk8"
    }
    defaultApolloMeta=paraBodyJson.defaultApolloMeta;
    nodes = paraBodyJson.node
    if (!nodes?.trim()) {
        nodes = ""
    }
    replicas = paraBodyJson.replicas
    if (!replicas?.trim()) {
        replicas = "1"
    }
    cpu = paraBodyJson.cpu
    if (!cpu?.trim()) {
        cpu = "0"
    }
    memory = paraBodyJson.memory
    if (!memory?.trim()) {
        memory = "0"
    }
    defaultApiHost = paraBodyJson.defaultApiHost
    if (!defaultApiHost?.trim()) {
        println("defaultApiHost 為空")
        defaultApiHost  = "api.test.com";
    }
    host = paraBodyJson.host
    if (!host?.trim()) {
        host = defaultApiHost
    }
     
} catch (errx) {
    println("參數(shù)解析錯(cuò)誤" + errx)
    sh "exit 1"
}
 println("參數(shù)解析完畢袜腥,開始構(gòu)建準(zhǔn)備")
//使用k8s節(jié)點(diǎn)執(zhí)行
node('k8s-master') { 
        imageName = defaultImageHost + service + ":" +versionTimestamp
        stage('Clone Code') {
            println("#############################################開始拉取代碼##################################################")
            sh 'find /root/.m2/repository/ -name "*lastUpdated*" | xargs rm -rf'
            git branch: branch, url: codeUrl
            println("#############################################拉取代碼成功##################################################")
        }
        stage('Dotnet Build') {
            println("#############################################開始打包##################################################")
            def dockerfile = """
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
ENV ASPNETCORE_URLS http://+:8020
EXPOSE 8020
COPY ./build .
RUN sed -i 's/TLSv1.2/TLSv1.0/g' /etc/ssl/openssl.cnf
ENTRYPOINT ["dotnet", "{{dllname}}"]
        """
            if ("dotnetsdk3.1".equals(sdkVersion.toString())) {
                dockerfile = dockerfile.replace("aspnet:3.1", "aspnet:3.1")
            }
            else if("dotnetsdk2.0".equals(sdkVersion.toString())) {
             dockerfile = dockerfile.replace("aspnet:3.1", "aspnet:2.0")
            }
            withEnv(["DOTNET_HOME=/root/jenkins/tools/${sdkVersion}"]) {
                sh '"$DOTNET_HOME/dotnet" --version'

                def csprojname = sh(script: 'echo *.csproj', returnStdout: true).replace('\n', "")
                def out=sh(script:"ls "+csprojname,returnStatus:true)
                if(out == 2){
                    println("文件:" + csprojname+" 不存在")
                    sh "exit 1"
                }
               // sh '"$DOTNET_HOME/dotnet" restore '+csprojname+' -s http://xxxx.com/repository/nuget-group'   //如果有私有nuget倉庫可以加上-s 倉庫地址
                sh '"$DOTNET_HOME/dotnet" restore '+csprojname
                sh '"$DOTNET_HOME/dotnet" build '+csprojname+' -c Release -o ./build '
                sh 'rm -rf ${WORKSPACE}/docker'
                sh 'mkdir -p ${WORKSPACE}/docker'
                sh 'cp -r ${WORKSPACE}/build/ ./docker/'
                def dllname = csprojname.replace("csproj","dll")
                out=sh(script:"ls ./build/"+dllname,returnStatus:true)
                if(out == 2){
                    println("文件:" + dllname+" 不存在")
                    sh "exit 1"
                }
                dockerfile = dockerfile.replace("{{dllname}}", dllname)
                sh "echo '${dockerfile}' >./docker/Dockerfile"
            }
            println("#############################################打包成功##################################################")
        }
        stage('Build Image') {
            println("#############################################開始build docker鏡像##################################################")
            sh "docker build -t ${imageName} ${WORKSPACE}/docker/."
            sh "docker push ${imageName}" 
            println("#############################################build docker鏡像件成功##################################################")
        }
  
    stage('K8S Deploy') {
        println("#############################################開始部署到集群##################################################")
        def yamldir = "/root/jenkins/deploy/deploy-"
        def yamlTemplatedir = "/root/jenkins/deploy-template/deploy-project.yaml" 
        println(yamlTemplatedir)
        def fileContents = readFile file: yamlTemplatedir, encoding: "UTF-8"
     
        println("開始處理yaml模板文件");
        fileContents = fileContents.replace("{{namespace}}", namespace)
        fileContents = fileContents.replace("{{name}}", serviceAndversion)
        fileContents = fileContents.replace("{{replicas}}", replicas)        
        fileContents = fileContents.replace("{{cpu}}", cpu)
       
        fileContents = fileContents.replace("{{host}}", host)
        fileContents = fileContents.replace("{{memory}}", memory)
        //println("yaml02...")
        fileContents = fileContents.replace("{{image}}", imageName)
        fileContents = fileContents.replace("{{appId}}", appId)
       // println("yaml03...")
        fileContents = fileContents.replace("{{apolloMeta}}", defaultApolloMeta)
        fileContents = fileContents.replace("{{nodePort}}", nodePort)
        println("yaml模板處理完畢")
        println "${yamldir}${serviceAndversion}.yaml"
        sh "rm -rf ${yamldir}${serviceAndversion}.yaml"
        sh "echo '${fileContents}' >${yamldir}${serviceAndversion}.yaml"
        println "kubectl apply -f ${yamldir}${serviceAndversion}.yaml"
        //sh "kubectl apply -f /root/jenkins/deployment/deployment-${serviceAndversion}.yaml"
        def consoleApply = sh(script: 'kubectl apply -f '+yamldir + serviceAndversion + '.yaml', returnStdout: true)
        String[] consoleArr = consoleApply.split("\n|\r")
        for (console in consoleArr) {
            /* if(console.startsWith("deployment.apps") && console.endsWith("unchanged")){
                 println("#############################################部署到集群沒有變化见擦,流水線退出##################################################")
                 sh "exit 1"
             }*/
        }
     
            sleep 10
     
                println("#############################################部署到集群成功##################################################")
       
    }
    
    println("#############################################流水線執(zhí)行成功##################################################")
}

/root/jenkins/deploy-template/deploy-project.yaml文件模板參考:
這個(gè)Yaml文件就是一個(gè)yaml程序完整部署的模板,我們通過JSON傳入,然后進(jìn)行替換鲤屡,再交由k8s執(zhí)行损痰。

#create namespace
apiVersion: v1
kind: Namespace
metadata:
  name: {{namespace}}
spec:
  finalizers:
  - kubernetes
---
#deploy
apiVersion: apps/v1
kind: Deployment
#kind: StatefulSet
metadata:
  name: {{name}}
  namespace: {{namespace}}
spec:
  selector:
    matchLabels:
      app: {{name}}
  replicas: {{replicas}}
  #serviceName: {{name}}
  template:
    metadata:
      labels:
        app: {{name}}
    spec:
      containers:
      - name: {{name}}
        image: {{image}}
        imagePullPolicy: Always
        env:
        - name: image
          value: "{{name}}>{{image}}"
        - name: app.id
          value: "{{appId}}"
        - name: TZ
          value: Asia/Shanghai
        ports:
        - containerPort: 8020
        resources:
          limits:
            cpu: {{cpu}}
            memory: {{memory}}
          requests:
            cpu: {{cpu}}
            memory: {{memory}}
        livenessProbe:
          httpGet:
            path: /healthy
            port: 8020
            scheme: HTTP
          initialDelaySeconds: 30
          periodSeconds: 60
          failureThreshold: 2
          successThreshold: 1
          timeoutSeconds: 30
        readinessProbe:
          httpGet:
            path: /healthy
            port: 8020
          initialDelaySeconds: 30
          periodSeconds: 10
          failureThreshold: 2
          successThreshold: 1
          timeoutSeconds: 10
---
#service
apiVersion: v1
kind: Service
metadata:
  name: {{name}}
  namespace: {{namespace}}
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8020
    nodePort: {{nodePort}}
  selector:
    app: {{name}}
  type: NodePort
  sessionAffinity: ClientIP
---
#router 配合kong/nginx等任一網(wǎng)關(guān)使用,可以對(duì)外暴露統(tǒng)一API
#如service1 api.test.com/server1/user/gettoken
#如service2 api.test.com/service2/bill/getBill
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: {{name}}
  namespace: {{namespace}}
spec:
  rules:
  #host 網(wǎng)關(guān)域名或IP
  - host: {{host}}
    http:
      paths:
      #path路徑酒来,如service1
      - path: /{{name}}/
        backend:
          serviceName: {{name}}
          servicePort: 80

2.使用模板創(chuàng)建一個(gè)流水線

新建一流水線卢未,取名netCore01,復(fù)制自template-netCore模板


點(diǎn)擊保存役首,到下一步尝丐,無需任何修改,直接保存即可衡奥。

3 . netCore環(huán)境準(zhǔn)備

** 下載.netcore sdk 3.1**
https://dotnet.microsoft.com/download/dotnet-core/3.1


下載到本地
上傳sdk包并解壓

master節(jié)點(diǎn)
mkdir /root/jenkins/tools/dotnetsdk3.1
cd /root/jenkins/tools/
rz 上傳壓縮包
tar -zxf dotnet-sdk-3.1.102-linux-x64.tar.gz -C dotnetsdk3.1

查看及安裝依賴項(xiàng)


yum install lttng-ust libcurl openssl-libs krb5-libs libicu zlib -y

4. 執(zhí)行自動(dòng)化部署

代碼我們提交到git爹袁,然后Jenkins選擇netCore01流水線,選擇使用參數(shù)構(gòu)建矮固,輸入JSON參數(shù)失息,如下圖:


構(gòu)建過程會(huì)下載aspnet:3.1-buster-slim,建議提前docker pull下載好档址,這樣構(gòu)建會(huì)快很多盹兢。

jenkins自動(dòng)下載有點(diǎn)慢
Step 1/6 : FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
3.1-buster-slim: Pulling from dotnet/core/aspnet
68ced04f60ab: Pulling fs layer
4ddb1a571238: Pulling fs layer

我們通過jenkins控制臺(tái)輸出查看構(gòu)建過程,已經(jīng)成功守伸。

[Pipeline] echo
#############################################部署到集群成功##################################################
[Pipeline] }
[Pipeline] // stage
[Pipeline] echo
#############################################流水線執(zhí)行成功##################################################
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

訪問測(cè)試下:
kubectl get pods --all-namespaces -owide
http://ip:31001/default/gettime #我們自己寫的一個(gè)測(cè)試api


另外我們看下鏡像倉庫中绎秒,該鏡像已經(jīng)存在

k8s查看pod信息

[root@k8s-master Controllers]# kubectl get pods -n mydemos
NAME                               READY   STATUS    RESTARTS   AGE
netcore-01-blue-7fdff4f9f7-9ll6p   1/1     Running   0          64s

附:JSON中主要參數(shù)解讀:
codeUrl:當(dāng)前項(xiàng)目git地址
sdkVersion:使用的.netcoreSDK版本
replicas:部署幾個(gè)pod
branch:拉取代碼的哪個(gè)分支
defaultImageHost:鏡像倉庫地址,鏡像構(gòu)建完成后需要推送到倉庫尼摹,供pod所在節(jié)點(diǎn)獲取生成容器见芹。
nodePort 映射到主機(jī)的端口,如果你搭建了網(wǎng)關(guān)蠢涝,可以不將端口映射到主機(jī)玄呛,可配合API網(wǎng)關(guān)+域名實(shí)現(xiàn)動(dòng)態(tài)路由。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末和二,一起剝皮案震驚了整個(gè)濱河市徘铝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惯吕,老刑警劉巖惕它,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異废登,居然都是意外死亡淹魄,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門钳宪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來揭北,“玉大人扳炬,你說我怎么就攤上這事∩μ澹” “怎么了恨樟?”我有些...
    開封第一講書人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長疚俱。 經(jīng)常有香客問我劝术,道長,這世上最難降的妖魔是什么呆奕? 我笑而不...
    開封第一講書人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任养晋,我火速辦了婚禮,結(jié)果婚禮上梁钾,老公的妹妹穿的比我還像新娘绳泉。我一直安慰自己,他們只是感情好姆泻,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開白布零酪。 她就那樣靜靜地躺著,像睡著了一般拇勃。 火紅的嫁衣襯著肌膚如雪四苇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,792評(píng)論 1 290
  • 那天方咆,我揣著相機(jī)與錄音月腋,去河邊找鬼。 笑死瓣赂,一個(gè)胖子當(dāng)著我的面吹牛榆骚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播钩述,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼寨躁,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼穆碎!你這毒婦竟也來了牙勘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤所禀,失蹤者是張志新(化名)和其女友劉穎方面,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體色徘,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡恭金,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了褂策。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片横腿。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡颓屑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耿焊,到底是詐尸還是另有隱情揪惦,我是刑警寧澤,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布罗侯,位于F島的核電站器腋,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏钩杰。R本人自食惡果不足惜纫塌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望讲弄。 院中可真熱鬧措左,春花似錦、人聲如沸避除。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽驹饺。三九已至钳枕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赏壹,已是汗流浹背鱼炒。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蝌借,地道東北人昔瞧。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像菩佑,于是被迫代替她去往敵國和親自晰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容