Kyverno外部数据源

在 Kyverno 策略中使用来自 ConfigMap、Kubernetes API Server和 image registry 的数据。

变量部分讨论了变量如何帮助创建更智能和可重用的策略定义,并介绍了存储所有变量的规则 context 的概念。

本节提供关于在策略中使用来自 ConfigMap、Kubernetes API Server和 image registry 的数据的详细信息。

注意

为了提高安全性和性能,Kyverno 被设计为不允许连接到集群 Kubernetes API Server和 image registry以外的系统。使用单独的控制器从任何来源获取数据并将其存储在可在策略中有效使用的 ConfigMap 中。这种设计可以实现关注点分离和安全边界的实施。

来自 ConfigMap 的变量

Kubernetes 中的 ConfigMap 资源通常用作应用程序可以使用的配置详细信息的来源。这些数据可以以多种格式写入,存储在命名空间中,并且可以轻松访问。 Kyverno 支持使用 ConfigMap 作为变量的数据源。评估引用 ConfigMap 资源的策略时,会检查 ConfigMap 数据,以确保对 ConfigMap 的引用始终是动态的。如果 ConfigMap 更新,后续策略查找将在该时间点获取最新数据。

为了在 rule 中使用来自 ConfigMap 的数据,需要一个context。对于您希望使用 ConfigMap 中的数据的每个 rule,您必须定义一个 context。然后可以使用 JMESPath 表示法在策略 rule 中引用上下文数据。

查找 ConfigMap 值

在规则的 context 中定义的 ConfigMap 可以使用其在上下文中的唯一名称来引用。可以使用 JMESPath 样式表达式引用 ConfigMap 值。

{{ <context-name>.data.<key-name> }}

考虑这样一个简单的 ConfigMap 定义。

apiVersion: v1
kind: ConfigMap
metadata:
  name: some-config-map
  namespace: some-namespace
data:
  env: production

要在 rule 内引用来自 ConfigMap 的值,请在 rule 内定义一个使用了一个或多个 ConfigMap 声明的 context。使用上面引用的示例 ConfigMap 片段,以下 rule 定义了一个按名称引用此特定 ConfigMap 的 context。

rules:
  - name: example-lookup
    # Define a context for the rule
    context:
    # A unique name for the ConfigMap
    - name: dictionary
      configMap:
        # Name of the ConfigMap which will be looked up
        name: some-config-map
        # Namespace in which this ConfigMap is stored
        namespace: some-namespace 

基于上面的示例,我们现在可以使用 {{dictionary.data.env}} 引用 ConfigMap 值。在策略执行期间,该变量将替换为 production。

放入完整 ClusterPolicy 的上下文中,将 ConfigMap 作为变量引用如下所示。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: cm-variable-example
  annotations:
    pod-policies.kyverno.io/autogen-controllers: DaemonSet,Deployment,StatefulSet
spec:
    rules:
    - name: example-configmap-lookup
      context:
      - name: dictionary
        configMap:
          name: some-config-map
          namespace: some-namespace
      match:
        any:
        - resources:
            kinds:
            - Pod
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              my-environment-name: "{{dictionary.data.env}}"

在上面的 ClusterPolicy 中,一个 mutate 规则匹配所有传入的 Pod 资源,并为其添加一个名为 my-environment-name 的 label。因为我们已经定义了一个 context,它指向我们之前名为 some-config-map 的 ConfigMap,所以我们可以使用表达式 {{dictionary.data.env}} 来引用该值。一个新创建的 Pod 将会接收到 label my-environment-name=production。

注意:ConfigMap 名称和键可以包含 JMESPath 不支持的字符,例如“-”(减号或破折号)或“/”(斜杠)。要将这些字符计算为文字,请在 JMESPath 表达式的该部分添加双引号,如下所示:

{{ "<name>".data."<key>" }}

有关格式化问题的更多信息,请参阅 JMESPath 页面

处理 ConfigMap 数组值

除了简单的字符串值之外,Kyverno 还能够使用 ConfigMap 中的数组值。

注意:自 Kyverno 1.7.0 起,将数组值存储在 YAML 块标量中已被删除。请改用 JSON 编码的字符串数组。

例如,假设您想在 ConfigMap 中定义允许的角色列表。Kyverno 策略可以引用此列表来拒绝注释中定义的角色的请求。

假设一个 ConfigMap,其内容为 YAML 多行值。

apiVersion: v1
kind: ConfigMap
metadata:
  name: roles-dictionary
  namespace: default
data:
  allowed-roles: "[\"cluster-admin\", \"cluster-operator\", \"tenant-admin\"]"

注意:如前所述,某些字符必须转义以进行 JMESPath 处理。在这种情况下,反斜杠 ("") 字符用于转义双引号,这允许将 ConfigMap 数据存储为 JSON 数组。

现在数组数据保存在 allowed-roles 键中,下面是一个示例 ClusterPolicy,其中包含一个规则,如果名为 role 的注解的值不在允许列表中,则该规则会阻止部署:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: cm-array-example
spec:
  validationFailureAction: enforce
  background: false
  rules:
  - name: validate-role-annotation
    context:
      - name: roles-dictionary
        configMap:
          name: roles-dictionary
          namespace: default
    match:
      any:
      - resources:
          kinds:
          - Deployment
    validate:
      message: "The role {{ request.object.metadata.annotations.role }} is not in the allowed list of roles: {{ \"roles-dictionary\".data.\"allowed-roles\" }}."
      deny:
        conditions:
          any:
          - key: "{{ request.object.metadata.annotations.role }}"
            operator: NotIn
            value:  "{{ \"roles-dictionary\".data.\"allowed-roles\" }}"

如果在我们之前定义的名为 roles-dictionary 的 ConfigMap 的数组中找不到注解 role,则此规则拒绝新建 Deployment 的请求。

注意:您可能还会注意到,此示例在单个规则中使用了来自 AdmissionReview 和 ConfigMap 源的变量。这种组合可以证明在制定有用的策略方面非常强大和灵活。

创建此示例 ClusterPolicy 后,尝试创建注解 role=super-user 的新 Deployment 并测试结果。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: busybox
  annotations:
    role: super-user
  labels:
    app: busybox
spec:
  replicas: 1
  selector:
    matchLabels:
      app: busybox
  template:
    metadata:
      labels:
        app: busybox
    spec:
      containers:
      - image: busybox:1.28
        name: busybox
        command: ["sleep", "9999"]

提交清单,看看 Kyverno 的反应。

$ kubectl create -f deploy.yaml
Error from server: error when creating "deploy.yaml": admission webhook "validate.kyverno.svc" denied the request:

resource Deployment/default/busybox was blocked due to the following policies

cm-array-example:
  validate-role-annotation: 'The role super-user is not in the allowed list of roles: ["cluster-admin", "cluster-operator", "tenant-admin"].'

将注解 role 更改为 ConfigMap 中存在的值之一,例如 tenant-admin,允许创建 Deployment 资源。

来自 Kubernetes API Server调用的变量

Kubernetes 由允许查询和操作资源的声明性 API 提供支持。Kyverno 策略可以使用 Kubernetes API 来获取资源,甚至是资源类型的集合,以在策略中使用。此外,Kyverno 允许将 JMESPath(JSON 匹配表达式)应用于资源数据,以提取值并将其转换为易于在策略中使用的格式。

Kyverno Kubernetes API 调用与 kubectl 和其他 API 客户端一样工作,并且可以使用现有工具进行测试。

例如,下面是一个命令行,它使用 kubectl 获取命名空间中的 Pod 列表,然后将输出通过管道传输到 kyverno jp,以计算 Pod 的数量:

kubectl get --raw /api/v1/namespaces/kyverno/pods | kyverno jp "items | length(@)"

使用 kubectl get --raw 和 kyverno jp 命令来测试 API 调用。

Kyverno 中相应的 API 调用定义如下。使用一个变量 {{request.namespace}} 来使用被操作对象的 Namespace,然后同样用 JMESPath 获取 Namespace 中 Pod 的数量,并以变量 podCount 的形式存储到 context 中。

rules:
- name: example-api-call
  context:
  - name: podCount
    apiCall:
      urlPath: "/api/v1/namespaces/{{request.namespace}}/pods"
      jmesPath: "items | length(@)"   

URL路径

Kubernetes API 使用 group 和 version来组织资源。例如,资源类型 Deployment 的 API Group 是 apps, version 是 v1。

API 调用的 HTTP URL 路径基于group、version和资源类型,如下所示:

  • /apis/{GROUP}/{VERSION}/{RESOURCETYPE}:获取一个资源集合

  • /apis/{GROUP}/{VERSION}/{RESOURCETYPE}/{NAME}:获取一个资源

对于命名空间级别的资源,要通过名称获取特定资源或获取命名空间中的所有资源,还必须提供命名空间名称,如下所示:

  • /apis/{GROUP}/{VERSION}/namespaces/{NAMESPACE}/{RESOURCETYPE}:获取命名空间中的一组资源集合

  • /apis/{GROUP}/{VERSION}/namespaces/{NAMESPACE}/{RESOURCETYPE}/{NAME}:获取命名空间中的一个资源

对于历史资源,Kubernetes 核心 API 都在 /api/v1 下。例如,要查询所有命名空间资源,使用路径 /api/v1/namespaces。

v1.22 的 API 参考文档中定义了 Kubernetes API 组,也可以通过如下所示的 kubectl api-resources 命令检索:

$ kubectl api-resources
NAME                              SHORTNAMES   APIGROUP                       NAMESPACED   KIND
bindings                                                                      true         Binding
componentstatuses                 cs                                          false        ComponentStatus
configmaps                        cm                                          true         ConfigMap
endpoints                         ep                                          true         Endpoints
events                            ev                                          true         Event
limitranges                       limits                                      true         LimitRange
namespaces                        ns                                          false        Namespace
nodes                             no                                          false        Node
persistentvolumeclaims            pvc                                         true         PersistentVolumeClaim

...

kubectl api-versions 命令打印出每个 API 组的可用版本。这是一个示例:

$ kubectl api-versions
admissionregistration.k8s.io/v1
admissionregistration.k8s.io/v1beta1
apiextensions.k8s.io/v1
apiextensions.k8s.io/v1beta1
apiregistration.k8s.io/v1
apiregistration.k8s.io/v1beta1
apps/v1
authentication.k8s.io/v1
authentication.k8s.io/v1beta1
authorization.k8s.io/v1
authorization.k8s.io/v1beta1
autoscaling/v1
autoscaling/v2beta1
autoscaling/v2beta2
batch/v1
...

您可以结合使用这些命令来查找资源的 URL 路径,如下所示:

要查找资源的 API 组和版本,请使用 kubectl api-resources 查找组,然后使用 kubectl api-versions 查找可用版本。

本示例查找 Deployment 资源组,然后查询版本:

kubectl api-resources | grep deploy

API 组显示在输出的第三列中。然后,您可以使用组名来查找版本:

kubectl api-versions | grep apps

其输出将是 apps/v1。旧版本的 Kubernetes(1.18 之前)将显示 apps/v1beta2。

处理集合

请求资源集合的 URL 路径上的 HTTP GET 的 API 服务器响应将是具有项目(资源)列表的对象。

这是一个获取所有命名空间资源的示例:

kubectl get --raw /api/v1/namespaces | jq

使用 jq 格式化输出以提高可读性。

这将返回一个 NamespaceList 对象,其属性 items 包含命名空间列表:

{
    "kind": "NamespaceList",
    "apiVersion": "v1",
    "metadata": {
      "selfLink": "/api/v1/namespaces",
      "resourceVersion": "2009258"
    },
    "items": [
      {
        "metadata": {
          "name": "default",
          "selfLink": "/api/v1/namespaces/default",
          "uid": "5011b5d5-abb7-4fef-93f9-8b5fa4b2eba9",
          "resourceVersion": "155",
          "creationTimestamp": "2021-01-19T20:20:37Z",
          "managedFields": [
            {
              "manager": "kube-apiserver",
              "operation": "Update",
              "apiVersion": "v1",
              "time": "2021-01-19T20:20:37Z",
              "fieldsType": "FieldsV1",
              "fieldsV1": {
                "f:status": {
                  "f:phase": {}
                }
              }
            }
          ]
        },
        "spec": {
          "finalizers": [
            "kubernetes"
          ]
        },
        "status": {
          "phase": "Active"
        }
      },
      ...

要在 JMESPath 中处理此数据,请引用items。这是一个示例,它在所有命名空间资源中提取了一些元数据字段:

kubectl get --raw /api/v1/namespaces | kyverno jp "items[*].{name: metadata.name, creationTime: metadata.creationTimestamp}"

这将生成一个新的 JSON 对象列表,其中包含属性名称和创建时间。

[
  {
    "creationTimestamp": "2021-01-19T20:20:37Z",
    "name": "default"
  },
  {
    "creationTimestamp": "2021-01-19T20:20:36Z",
    "name": "kube-node-lease"
  },
  ...

要在列表中查找项目,您可以使用 JMESPath 过滤器。例如,此命令将按名称匹配命名空间:

kubectl get --raw /api/v1/namespaces | kyverno jp "items[?metadata.name == 'default'].{uid: metadata.uid, creationTimestamp: metadata.creationTimestamp}"

除了通配符和过滤器之外,JMESPath 还有许多其他强大的、有用的功能。请务必阅读 JMESPath 教程并尝试此处的 Kyverno JMESPath 页面之外的交互式示例。

示例策略:在命名空间中限制 LoadBalancer 类型的服务

这是一个完整的示例策略,它将限制每个命名空间只能有一个 LoadBalancer 类型的服务。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: limits
spec:
  validationFailureAction: enforce
  rules:
  - name: limit-lb-svc
    match:
      any:
      - resources:
          kinds:
          - Service
    context:
    - name: serviceCount
      apiCall:
        urlPath: "/api/v1/namespaces/{{ request.namespace }}/services"
        jmesPath: "items[?spec.type == 'LoadBalancer'] | length(@)"    
    preconditions:
      any:
      - key: "{{ request.operation }}"
        operator: Equals
        value: CREATE
    validate:
      message: "Only one LoadBalancer service is allowed per namespace"
      deny:
        conditions:
          any:
          - key: "{{ serviceCount }}"
            operator: GreaterThan
            value: 1

此示例策略检索命名空间中的 Service列表,并将 LoadBalancer 类型的 Service 计数,并存储在名为 serviceCount 的变量中。deny 规则用于确保计数不能超过 1。

来自 Image Registry 的变量

通过使用 imageRegistry context 类型,context也可以使用 OCI 镜像上的元数据。通过使用此外部数据源,Kyverno 策略可以根据作为传入资源的一部分出现的容器镜像的详细信息做出决策。

例如,如果您使用如下所示的 imageRegistry:

context: 
- name: imageData
  imageRegistry: 
    reference: "ghcr.io/kyverno/kyverno"

输出 imageData 变量将具有如下结构:

{
    "image":         "ghcr.io/kyverno/kyverno",
    "resolvedImage": "ghcr.io/kyverno/kyverno@sha256:17bfcdf276ce2cec0236e069f0ad6b3536c653c73dbeba59405334c0d3b51ecb",
    "registry":      "ghcr.io",
    "repository":    "kyverno/kyverno",
    "identifier":    "latest",
    "manifest":      manifest,
    "configData":    config,
}

注意

imageData 代表了一个镜像在 registry 执行任何重定向并由 Kyverno 进行内部修改之后的“归一化”的视图(Kyverno 默认将一个空注册表设置为 docker.io 并将一个空标签设置为 latest)。最值得注意的是,这会影响托管在 Docker Hub 上的官方镜像。Docker Hub 上的官方镜像与其他镜像的区别在于它们的存储库以 library/ 为前缀,即使被拉取的镜像不包含它。例如,使用 python:slim 拉取 python 官方图像会导致设置 imageData 的以下字段:

{
    "image":         "docker.io/python:slim",
    "resolvedImage": "index.docker.io/library/python@sha256:43705a7d3a22c5b954ed4bd8db073698522128cf2aaec07690a34aab59c65066",
    "registry":      "index.docker.io",
    "repository":    "library/python",
    "identifier":    "slim"
}

manifest 和 config 分别包含来自 crane manifest <image> 和 crane config <image> 的输出。

例如,可以检查给定图像的labels、entrypoint、volumes、history、layers、etc 等。使用工具 crane,显示镜像 ghcr.io/kyverno/kyverno:latest 的配置信息:

$ crane config ghcr.io/kyverno/kyverno:latest | jq
{
  "architecture": "amd64",
  "config": {
    "User": "10001",
    "Env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ],
    "Entrypoint": [
      "./kyverno"
    ],
    "WorkingDir": "/",
    "Labels": {
      "maintainer": "Kyverno"
    },
    "OnBuild": null
  },
  "created": "2022-02-04T08:57:38.818583756Z",
  "history": [
    {
      "created": "2022-02-04T08:57:38.454742161Z",
      "created_by": "LABEL maintainer=Kyverno",
      "comment": "buildkit.dockerfile.v0",
      "empty_layer": true
    },
    {
      "created": "2022-02-04T08:57:38.454742161Z",
      "created_by": "COPY /output/kyverno / # buildkit",
      "comment": "buildkit.dockerfile.v0"
    },
    {
      "created": "2022-02-04T08:57:38.802069102Z",
      "created_by": "COPY /etc/passwd /etc/passwd # buildkit",
      "comment": "buildkit.dockerfile.v0"
    },
    {
      "created": "2022-02-04T08:57:38.818583756Z",
      "created_by": "COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ # buildkit",
      "comment": "buildkit.dockerfile.v0"
    },
    {
      "created": "2022-02-04T08:57:38.818583756Z",
      "created_by": "USER 10001",
      "comment": "buildkit.dockerfile.v0",
      "empty_layer": true
    },
    {
      "created": "2022-02-04T08:57:38.818583756Z",
      "created_by": "ENTRYPOINT [\"./kyverno\"]",
      "comment": "buildkit.dockerfile.v0",
      "empty_layer": true
    }
  ],
  "os": "linux",
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:180b308b8730567d2d06a342148e1e9d274c8db84113077cfd0104a7e68db646",
      "sha256:99187eab8264c714d0c260ae8b727c4d2bda3a9962635aaea67d04d0f8b0f466",
      "sha256:26d825f3d198779c4990007ae907ba21e7c7b6213a7eb78d908122e435ec9958"
    ]
  }
}

在上面的输出中,我们可以在 config.User 下看到运行这个容器的 Dockerfile 的 USER 声明是 10001。可以编写 Kyverno 策略来利用此信息并执行,例如,验证镜像的 USER 是非 root。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: imageref-demo
spec:
  validationFailureAction: enforce
  rules:
  - name: no-root-images
    match:
      any:
      - resources:
          kinds:
          - Pod
    preconditions:
      all:
      - key: "{{request.operation}}"
        operator: NotEquals
        value: DELETE
    validate:
      message: "Images run as root are not allowed."  
      foreach:
      - list: "request.object.spec.containers"
        context: 
        - name: imageData
          imageRegistry: 
            reference: "{{ element.image }}"
        deny:
          conditions:
            any:
              - key: "{{ imageData.configData.config.User || ''}}"
                operator: Equals
                value: ""

在上面的示例策略中,已经编写了一个名为 imageData ,类型为 imageRegistry 的新 context。reference 键用于指示 Kyverno 存储镜像元数据的位置。其中 element 是 Pod 内的每个容器,因此 element.image 的容器镜像。然后可以在表达式中引用该值,例如在 deny.conditions 中通过键 {{ imageData.configData.config.User || ''}}。

使用示例“bad” Pod 来测试违反此政策的情况,如下所示,Pod 被阻止。

apiVersion: v1
kind: Pod
metadata:
  name: badpod
spec:
  containers:
  - name: ubuntu
    image: ubuntu:latest
$ kubectl apply -f bad.yaml 
Error from server: error when creating "bad.yaml": admission webhook "validate.kyverno.svc-fail" denied the request: 

resource Pod/default/badpod was blocked due to the following policies

imageref-demo:
  no-root-images: 'validation failure: Images run as root are not allowed.'

相比之下,当使用“good”的 Pod 时,例如上面提到的 Kyverno 容器镜像,该资源是允许的。

apiVersion: v1
kind: Pod
metadata:
  name: goodpod
spec:
  containers:
  - name: kyverno
    image: ghcr.io/kyverno/kyverno:latest
$ kubectl apply -f good.yaml 
pod/goodpod created

imageRegistry 上下文类型还有一个名为 jmesPath 的可选属性,能够应用一个 JMESPath 表达式,事先存储为 context 的值,并写入 imageRegistry 的返回的内容中。例如,下面的代码片段通过叠加其清单报告的镜像的所有组成层,将镜像的总大小存储在名为 imageSize 的 context 中(通过 crane manifest 可以得到各层的信息)。然后可以在稍后的表达式中评估 context 变量的值。

context: 
  - name: imageSize
    imageRegistry: 
      reference: "{{ element.image }}"
      # Note that we need to use `to_string` here to allow kyverno to treat it like a resource quantity of type memory
      # the total size of an image as calculated by docker is the total sum of its layer sizes
      jmesPath: "to_string(sum(manifest.layers[*].size))"

要访问存储在私有 registry 中的图像,请参阅使用私有 registry

有关使用 imageRegistry con text的更多示例,请参阅示例页面

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容