Kubernetes Operator開(kāi)發(fā)之 一涩堤、Opeator SDK helloword

Operator SDK User Guide

Operator capability level

每種operator類(lèi)型需要不同的能力眷蜓。對(duì)于你的operator為項(xiàng)目選擇什么類(lèi)型時(shí),了解每個(gè)項(xiàng)目類(lèi)型的特性和限制是很重要的胎围。

Phase I Phase II Phase III Phase IV Phase V
基本安裝 無(wú)縫升級(jí) 完整生命周期 深入理解 自動(dòng)駕駛
自動(dòng)化應(yīng)用程序配置和配置管理 支持補(bǔ)丁和小版本升級(jí) 應(yīng)用程序生命周期,存儲(chǔ)(備份,故障恢復(fù)) metrics吁系、alert德召、log處理和工作負(fù)載分析 水平/橫向擴(kuò)展,自動(dòng)配置調(diào)優(yōu)汽纤,異常檢測(cè)上岗,調(diào)度調(diào)優(yōu)
Helm Helm
Ansible Ansible Ansible Ansible Ansible
GO GO GO GO GO

kubebuilder vs operator-sdk

kubebuilder和operator-sdk支持的Go項(xiàng)目之間沒(méi)有太大的區(qū)別。兩者都使用controller-tools和controller-runtime蕴坪,并且都支持基本相似的go包結(jié)構(gòu)肴掷。

他們的不同點(diǎn):

  • Operator SDK支持Ansible和Helm operator,這樣很容易編寫(xiě)operator而不需要go背传,如果你對(duì)Ansible和Helm比較熟悉呆瞻。
  • Operator SDK集成了Operator生命周期管理(OLM),這是Operator Framework的關(guān)鍵組件续室,對(duì)于2個(gè)集群操作很重要栋烤,比如管理員在線(xiàn)升級(jí)你的operator。
  • Operator SDK包含一個(gè)scorecard subcommand挺狰,幫助你了解operator是否遵循最佳實(shí)踐明郭。
  • Operator SDK包含一個(gè)e2e測(cè)試框架,它簡(jiǎn)化了針對(duì)實(shí)際集群測(cè)試操作符的工作丰泊。
  • Kubebuilder包含一個(gè)envtest包薯定,允許operator開(kāi)發(fā)人員使用獨(dú)立的etcd和apiserver運(yùn)行簡(jiǎn)單的測(cè)試。
  • Kubebuilder搭建了一個(gè)Makefile來(lái)幫助用戶(hù)完成operator的任務(wù)(構(gòu)建瞳购、測(cè)試话侄、運(yùn)行、代碼生成等);Operator SDK目前使用內(nèi)置的子命令学赛。每種方法都有優(yōu)缺點(diǎn)年堆。SDK團(tuán)隊(duì)將來(lái)可能會(huì)遷移到基于makefile的方法。
  • Kubebuilder使用Kustomize構(gòu)建部署清單;Operator SDK使用帶有占位符的靜態(tài)文件盏浇。
  • Kubebuilder最近改進(jìn)了對(duì)許可和CRD轉(zhuǎn)換webhook的支持变丧,但還沒(méi)有將其加入到SDK中。

Create a new project

operator-sdk new memcached-operator

SDK還支持使用Ansible或Helm開(kāi)發(fā)操作員绢掰。Helm使用指南痒蓬。

Operator project layout

File/Folder s Purpose
cmd 包含文件manager/main.go。operator的main程序滴劲。他將實(shí)例化一個(gè)新的manager攻晒,注冊(cè)所有pkg/apis/...下的自定義資源定義和啟動(dòng)所有在pkg/controllers/...下的controller。
pkg/apis 包含自定義資源(CRD)API的樹(shù)目錄班挖。用戶(hù)可以展開(kāi)編輯pkg/apis/<group>/<version>/<kind>_types.go文件鲁捏,為每種資源類(lèi)型定義API,并將這些包導(dǎo)入它們的控制器中萧芙,以監(jiān)視這些資源類(lèi)型碴萧。
pkg/controller 此pkg包含控制器實(shí)現(xiàn)乙嘀。用戶(hù)可以展開(kāi)編輯pkg/controller/<kind>/<kind>_controller.go文件末购。定義控制器的協(xié)調(diào)邏輯來(lái)處理指定kind的資源類(lèi)型破喻。
build 包含Dockerfile編譯輯腳本,用于編譯operator盟榴。
deploy 包含用于注冊(cè)CRD曹质、設(shè)置RBAC和將operator部署為Deployment的各種YAML清單。
go.mod go.sum 描述該操作符的外部依賴(lài)項(xiàng)的Go mod清單擎场。
vendor golang vendor目錄羽德,包含滿(mǎn)足此項(xiàng)目中Go import 的外部依賴(lài)項(xiàng)目的本地副本。Go modules 管理vendor目錄迅办。除非使用—vendor標(biāo)志初始化項(xiàng)目宅静,或者go mod vendor在項(xiàng)目根目錄中運(yùn)行,否則此目錄將不存在站欺。

我們主要是開(kāi)發(fā)pkg下api和controller姨夹。

Manager

main程序?yàn)閛perator里的cmd/manager/main.go, 用于初始化并運(yùn)行Manager。

Manager將為pkg/api /…下所有自定義資源自動(dòng)注冊(cè)schema矾策,和運(yùn)行pkg/controller/…下的所有controller磷账。

Manager可以限制namespace所有控制器要監(jiān)視的資源:

mgr, err := manager.New(cfg, manager.Options{Namespace: namespace})

默認(rèn)情況下這將是operator運(yùn)行的namespace。如果需要查看所有的namespace贾虽,請(qǐng)將namespace設(shè)置為空:

mgr, err := manager.New(cfg, manager.Options{Namespace: ""})

也可以使用 MultiNamespacedCacheBuilder 查看一組特定的namespace:

var namespaces []string // List of Namespaces
// Create a new Cmd to provide shared dependencies and start components
mgr, err := manager.New(cfg, manager.Options{
   NewCache: cache.MultiNamespacedCacheBuilder(namespaces),
   MapperProvider:     restmapper.NewDynamicRESTMapper,
   MetricsBindAddress: fmt.Sprintf("%s:%d", metricsHost, metricsPort),
})

默認(rèn)情況main程序?qū)⒃O(shè)置namager的namespace使用在deploy/operator.yaml定義的WATCH_NAMESPACE逃糟。

Add a new Custom Resource Definition

增加一個(gè)名稱(chēng)為Memcached的Custom Resource Definition(CRD) API ,APIVersion cache.example.com/v1alpha1 和 Kind Memcached蓬豁。

operator-sdk add api --api-version=cache.limingnihao.com/v1alpha1 --kind=Memcached

Define the spce and status

修改Memcached Custom Resource(CR)的spec和status在文件pkg/apis/cache/v1alpha1/memcached_types.go:

type MemcachedSpec struct {
    // Size is the size of the memcached deployment
    Size int32 `json:"size"`
}
type MemcachedStatus struct {
    // Nodes are the names of the memcached pods
    Nodes []string `json:"nodes"`
}

在修改*_types.go文件之后篷帅,運(yùn)行下面命令更新和生成代碼:

$ operator-sdk generate k8s
INFO[0000] Running deepcopy code-generation for Custom Resource group versions: [cache:[v1alpha1], ] 
INFO[0008] Code-generation complete.  

Updating CRD mainfests

現(xiàn)在MemcachedSpecMemcachedStatus 有一些 fields 和 possibly annotations, 必須更新API's group 和 kind 與 CRD一致. 所以需要運(yùn)行下面命令:

$ operator-sdk generate crds
INFO[0000] Running CRD generator.                       
INFO[0000] CRD generation complete.         

storage version

創(chuàng)建 CustomResourceDefinition 時(shí)拧咳,會(huì)在 CustomResourceDefinition spec.versions 列表設(shè)置適當(dāng)?shù)姆€(wěn)定級(jí)和版本號(hào)。例如v1beta1表示第一個(gè)版本尚未穩(wěn)定。所有自定義資源對(duì)象將首先存儲(chǔ)在這個(gè)版本.

所以你項(xiàng)目的CRD必須精確地指定一個(gè)storage version.

如果轉(zhuǎn)換涉及結(jié)構(gòu)變更正林,并且需要自定義邏輯,轉(zhuǎn)換應(yīng)該使用 webhook蕾总。如果沒(méi)有結(jié)構(gòu)變更囤屹, 則使用 None 默認(rèn)轉(zhuǎn)換策略,不同版本時(shí)只有apiVersion字段有變更付魔。

使用+kubebuilder:storageversion來(lái)marker指示API服務(wù)器應(yīng)該使用的存儲(chǔ)數(shù)據(jù)的GVK聊品。該marker應(yīng)該位于Memcached類(lèi)型之上的注釋中。
(沒(méi)明白什么意思)

OpenAPI validation

當(dāng)清單生成時(shí)几苍,OpenAPIv3 schema被添加到spec.validation塊中的CRD清單中翻屈。這個(gè)驗(yàn)證塊允許Kubernetes在創(chuàng)建或更新Memcached Custom Resource時(shí)驗(yàn)證它的屬性。

Markers (annotations) 可用于驗(yàn)證您的API配置妻坝。這些markers將始終具有+kubebuilder:validation前綴伸眶。例如惊窖,可以通過(guò)添加以下標(biāo)記來(lái)添加enum類(lèi)型規(guī)范:

// +kubebuilder:validation:Enum=Lion;Wolf;Dragon
type Alias string

CRD generationmarker文檔討論了markers在代碼中的是用。完整的OpenAPIv3 validation markers請(qǐng)參考這里

運(yùn)行下面命令厘贼,更新CRDdeploy/crds/cache.example.com_memcacheds_crd.yaml.

operator-sdk generate crds

一個(gè)生成YAML的例子如下:

spec:
  validation:
    openAPIV3Schema:
      properties:
        spec:
          properties:
            size:
              format: int32
              type: integer

要了解有關(guān)Custom Resource Definitions中的OpenAPI v3.0驗(yàn)證模式的更多信息界酒,請(qǐng)參閱Kubernetes文檔

Add a new Contrller

添加一個(gè)新的Controller到項(xiàng)目嘴秸,將監(jiān)視和協(xié)調(diào)Memcached資源:

$ operator-sdk add controller --api-version=cache.limingnihao.com/v1alpha1 --kind=Memcached

這將在pkg/controller/memcached/...下搭建一個(gè)新的Controller實(shí)現(xiàn)毁欣。
這個(gè)例子將生成文件pkg/controller/memcached/memcached_controller.gomemcached_controller.go是具體實(shí)現(xiàn)岳掐。

示例中的Controller將為所有的Memcached CR執(zhí)行以下邏輯:

  • 創(chuàng)建一個(gè)memcached Deployment如果不存在凭疮。
  • 確保這些Deployment數(shù)量和Memcached CR spec定義的相同。
  • 使用帶有memcached pods名字的狀態(tài)更新Memcached CR狀態(tài)串述。

接下來(lái)的兩個(gè)小節(jié)將解釋Controller如何監(jiān)視資源以及如何觸發(fā)協(xié)調(diào)循環(huán)执解。

Resources watched by the Controller

查看pkg/controller/memcached/memcached_controller.go的Controller實(shí)現(xiàn),如何監(jiān)聽(tīng)資源纲酗。

第一個(gè)監(jiān)視對(duì)象是作為主要資源的Memcached類(lèi)型衰腌。對(duì)于每個(gè)添加/更新/刪除事件,會(huì)給那個(gè)Memcached對(duì)象發(fā)送一個(gè)調(diào)和Request(a namespace/name key):

// Watch for changes to primary resource Memcached
err = c.Watch(&source.Kind{Type: &cachev1alpha1.Memcached{}}, &handler.EnqueueRequestForObject{})
if err != nil {
    return err
}

下面是對(duì)Deployments的監(jiān)事耕姊,但是event handler將每個(gè)event映射到Deplyments所有者的Request桶唐。本例子中,Memcached對(duì)象是Deployment的創(chuàng)建者茉兰,這將允許controller將Deployments作為輔助資源監(jiān)視尤泽。

// Watch for changes to secondary resource Pods and requeue the owner Memcached
err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{
    IsController: true,
    OwnerType:    &cachev1alpha1.Memcached{},
})
if err != nil {
    return err
}

Controller configurations

在初始化controller和生命watch參數(shù)時(shí),有很多的配置规脸。更多配置細(xì)節(jié)請(qǐng)參考controller godocs

  • 通過(guò)MaxConcurrentReconciles選項(xiàng)設(shè)置控制器的最大并發(fā)協(xié)調(diào)數(shù)坯约。默認(rèn)為1。
_, err := controller.New("memcached-controller", mgr, controller.Options{
   MaxConcurrentReconciles: 2,
   ...
})
  • 使用predicates過(guò)濾監(jiān)視事件莫鸭。
  • 選擇EventHandelr的類(lèi)型闹丐,可以將reconcile loop更改監(jiān)聽(tīng)事件為reconcile request。對(duì)于operator更復(fù)雜的primary和secondary資源關(guān)系被因,可以使用EnqueueRequestsFromMapFunc handler卿拴,可以將監(jiān)聽(tīng)事件轉(zhuǎn)換為任務(wù)的reconcile request。EnqueueRequestForObject隊(duì)列的一個(gè)請(qǐng)求包含事件源對(duì)象的Name和Namespace梨与。

Reconcile loop

每個(gè)Controller都有一個(gè)Reconciler對(duì)象和一個(gè)實(shí)現(xiàn)了reconcile循環(huán)的reconcile()方法堕花。reconcile loop通過(guò)Request argument查看主資源對(duì)象的Namespace/Name key,Memcached粥鞋,從緩存:

// Reconcile讀取Memcached對(duì)象的集群狀態(tài)缘挽,并根據(jù)讀取的狀態(tài)和Memcached. spec中的內(nèi)容進(jìn)行更改
// 如果返回error為非空或Result.Requeue為true,控制器將再次請(qǐng)求處理Request,否則在完成后將從隊(duì)列中刪除壕曼。
func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Result, error) {
  // Lookup the Memcached instance for this reconcile request
  memcached := &cachev1alpha1.Memcached{}
  err := r.client.Get(context.TODO(), request.NamespacedName, memcached)
  ...
}

根據(jù)返回值苏研,Result和error,Request將重新請(qǐng)求腮郊,然后再次觸發(fā)Reconcile loop:

// Reconcile successful - don't requeue
return reconcile.Result{}, nil

// Reconcile failed due to error - requeue
return reconcile.Result{}, err

// Requeue for any reason other than error
return reconcile.Result{Requeue: true}, nil

你還可以設(shè)置Result.RequeueAfter摹蘑,設(shè)置在一些時(shí)間之后重新請(qǐng)求Request。

import "time"

// Reconcile for any reason than error after 5 seconds
return reconcile.Result{RequeueAfter: time.Second*5}, nil

Note: 返回帶RequeueAfter的Result伴榔,是定期調(diào)整CR的方法纹蝴。

For a guide on Reconcilers, Clients, and interacting with resource Events, see the Client API doc.

Build and run the operator

在運(yùn)行operator之前,CRD必須在 Kubernetes apiserver注冊(cè):

$ kubectl create -f deploy/crds/cache.limingnihao.com_memcacheds_crd.yaml

然后有兩種方法踪少,運(yùn)行operator:

  • 作為Kubernetes集群內(nèi)部的Deployment。
  • 集群外部的一個(gè)Go程序糠涛。

1. Run as a Deployment inside the cluster

Note:operator-sdk buikd調(diào)用docker build援奢,或者是buildah bud。如果使用buildah則跳過(guò)operator-sdk build的跳過(guò)下面的說(shuō)明忍捡。如果使用docker確保docker守護(hù)進(jìn)程正在運(yùn)行集漾,并且可以在沒(méi)有sudo的情況下運(yùn)行docker客戶(hù)機(jī)。您可以通過(guò)運(yùn)行docker version來(lái)檢查砸脊,

Note:存在vendor/目錄具篇,運(yùn)行

$ go mod vendor

構(gòu)建memcached-operator image發(fā)布和注冊(cè):

$ operator-sdk build quay.io/$USERNAME/memcached-operator:v0.0.1
$ docker login quay.io
$ docker push quay.io/$USERNAME/memcached-operator:v0.0.1

# Update the operator manifest to use the built image name (if you are performing these steps on OSX, see note below)
$ sed -i "s|REPLACE_IMAGE|quay.io/$USERNAME/app-operator|g" deploy/operator.yaml
# On OSX use:
$ sed -i "" "s|REPLACE_IMAGE|quay.io/$USERNAME/app-operator|g" deploy/operator.yaml

或者只發(fā)布到本地
$ operator-sdk build memcached-operator:v0.0.1

Deployment清單在deploy/operator.yaml中生成。請(qǐng)確認(rèn)更新deployment imageREPLACE_IMAGE凌埂,因?yàn)槟J(rèn)只是個(gè)占位符驱显。

containers:
  - name: memcached-operator
    # Replace this with the built image name
    image: REPLACE_IMAGE
    imagePullPolicy: IfNotPresent

安裝RBAC和發(fā)布memcached-operator:

kubectl create -f deploy/service_account.yaml
kubectl create -f deploy/role.yaml
kubectl create -f deploy/role_binding.yaml
kubectl create -f deploy/operator.yaml
kubectl create -f deploy/crds/cache.limingnihao.com_v1alpha1_memcached_cr.yaml

2.Run locally outside the cluster

此方法在開(kāi)發(fā)周期中是首選的,以便更快地部署和測(cè)試瞳抓。

在環(huán)境變量中設(shè)置operator的名稱(chēng):

export OPERATOR_NAME=memcached-operator

使用$HOME/.kube/config中的默認(rèn)Kubernetes配置文件在本地運(yùn)行該operator:

$ operator-sdk run --local --namespace=default
2018/09/30 23:10:11 Go Version: go1.10.2
2018/09/30 23:10:11 Go OS/Arch: darwin/amd64
2018/09/30 23:10:11 operator-sdk Version: 0.0.6+git
2018/09/30 23:10:12 Registering Components.
2018/09/30 23:10:12 Starting the Cmd.

Create a Memcached CR

創(chuàng)建示例Memcached的CR使用deploy/crds/cache.example.com_v1alpha1_memcached_cr.yaml`生成:

$ cat deploy/crds/cache.limingnihao.com_v1alpha1_memcached_cr.yaml
apiVersion: "cache.example.com/v1alpha1"
kind: "Memcached"
metadata:
  name: "example-memcached"
spec:
  size: 3

$ kubectl apply -f deploy/crds/cache.limingnihao.com_v1alpha1_memcached_cr.yaml

確保memcache -operator為CR創(chuàng)建部署:

$ kubectl get deployment
NAME                     DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
memcached-operator       1         1         1            1           2m
example-memcached        3         3         3            3           1m

檢查pod和CR狀態(tài)埃疫,確認(rèn)memcached pods的狀態(tài)變化:

Update the size

在memcached CR 將spec.size字段從3改成4:

apiVersion: "cache.example.com/v1alpha1"
kind: "Memcached"
metadata:
  name: "example-memcached"
spec:
  size: 4

$ kubectl apply -f deploy/crds/cache.example.com_v1alpha1_memcached_cr.yaml

確認(rèn)operator的deployment數(shù)量發(fā)生改變:

kubectl get deployment

cleanup

清除所有resources:

kubectl delete -f deploy/crds/cache.limingnihao.com_v1alpha1_memcached_cr.yaml
kubectl delete -f deploy/operator.yaml
kubectl delete -f deploy/role_binding.yaml
kubectl delete -f deploy/role.yaml
kubectl delete -f deploy/service_account.yaml

Advanced Topics

Manage CR status conditions

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市孩哑,隨后出現(xiàn)的幾起案子栓霜,更是在濱河造成了極大的恐慌,老刑警劉巖横蜒,帶你破解...
    沈念sama閱讀 216,919評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胳蛮,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡丛晌,警方通過(guò)查閱死者的電腦和手機(jī)仅炊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)茵乱,“玉大人茂洒,你說(shuō)我怎么就攤上這事。” “怎么了督勺?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,316評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵渠羞,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我智哀,道長(zhǎng)次询,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,294評(píng)論 1 292
  • 正文 為了忘掉前任瓷叫,我火速辦了婚禮屯吊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘摹菠。我一直安慰自己盒卸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布次氨。 她就那樣靜靜地躺著蔽介,像睡著了一般。 火紅的嫁衣襯著肌膚如雪煮寡。 梳的紋絲不亂的頭發(fā)上虹蓄,一...
    開(kāi)封第一講書(shū)人閱讀 51,245評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音幸撕,去河邊找鬼薇组。 笑死,一個(gè)胖子當(dāng)著我的面吹牛坐儿,可吹牛的內(nèi)容都是我干的律胀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,120評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼挑童,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼累铅!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起站叼,我...
    開(kāi)封第一講書(shū)人閱讀 38,964評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤娃兽,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后尽楔,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體投储,經(jīng)...
    沈念sama閱讀 45,376評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評(píng)論 2 333
  • 正文 我和宋清朗相戀三年阔馋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了玛荞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,764評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡呕寝,死狀恐怖勋眯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤客蹋,帶...
    沈念sama閱讀 35,460評(píng)論 5 344
  • 正文 年R本政府宣布塞蹭,位于F島的核電站,受9級(jí)特大地震影響讶坯,放射性物質(zhì)發(fā)生泄漏番电。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評(píng)論 3 327
  • 文/蒙蒙 一辆琅、第九天 我趴在偏房一處隱蔽的房頂上張望漱办。 院中可真熱鬧,春花似錦婉烟、人聲如沸娩井。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,697評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)撞牢。三九已至,卻和暖如春叔营,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背所宰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,846評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工绒尊, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人仔粥。 一個(gè)月前我還...
    沈念sama閱讀 47,819評(píng)論 2 370
  • 正文 我出身青樓婴谱,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親躯泰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子谭羔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評(píng)論 2 354

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