Gitlab CI/CD 实践四:Golang 项目 CI/CD 流水线配置

.gitlab-ci.yml

流水线入口:阶段

stages:
  - lint
  - test
  - build
  - deploy
  • 流水线以job为单位运行,每个job就是自己想通过流水线做的事情,例如单元测试的job,核心是运行脚本:

    go test -short go list ./...
  • 每个阶段下可以定义多个job。

  • 同一阶段的job会并行执行。

  • 阶段是串行执行。

全局变量

variables:
  IMAGE_GROUP: xxx
  NAME_SPACE: xxx
  GOPATH: ${CI_PROJECT_DIR}/.go
  GOMODCACHE: ${CI_PROJECT_DIR}/.go/pkg/mod
  GOCACHE: ${CI_PROJECT_DIR}/.go/.cache/go-build
  GOLANGCI_LINT_CACHE: ${CI_PROJECT_DIR}/.go/.cache/golangci-lint
  • 自定义一些变量,在流水线执行过程中以环境变量的形式存在。
  • GOPATH:指定GOPATH为项目目录下的.go,原因是流水线缓存只能缓存项目目录下的文件。
  • GOMODCACHE:Go依赖缓存。
  • GOCACHE:go build 产生的缓存。
  • GOLANGCI_LINT_CACHE:golangci-lint 代码质量检查工具,也会产生缓存。

公共模板

job公共模板

.template:
  image: 172.30.3.150/xxx/go-tools:latest
  tags:
    - 172.30.3.219-runner
  interruptible: true
  • go-tools:运行流水线的基础镜像,里面封装了go运行环境。

    • Dockerfile
    FROM golang:1.17.9
    USER root
    
    ENV GOPATH /go
    ENV PATH ${GOPATH}/bin:$PATH
    
    # 设置私服
    RUN go env -w GOPRIVATE=xxx.com
    # 设置忽略私服的https证书校验
    RUN go env -w GOINSECURE=xxx.com
    RUN git config --global http.sslverify false
    RUN git config --global https.sslverify false
    
    RUN go env -w GOPROXY="https://goproxy.cn,direct"
    RUN go env -w GO111MODULE=on
    
    # install docker
    RUN curl -O https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz \
        && tar zxvf docker-latest.tgz \
        && cp docker/docker /usr/local/bin/ \
        && rm -rf docker docker-latest.tgz
  • tags:指定运行流水线的gitlab runner。

  • .template:gitlab流水线会认为.开头的脚本不是一个job,就不会去执行。这里只是作为脚本模板,后面真正的job会去extends此模板,达到代码复用的效果。

  • interruptible:支持可中断,例如上一次流水线还没跑完,又触发了一次,这种情况下会取消上一次流水线。还需要勾选gitlab的这个选项:

    image-20220531140614848

缓存模板

.go_cache:
  cache:
    key: go-cache-${CI_PROJECT_PATH_SLUG}
    paths:
      - .go
  • 创建流水线缓存,以项目名称为key,缓存的目录为项目目录下的.go

lint阶段

代码检测job

golangci_lint:
  stage: lint
  only:
    - merge_requests
    - /^release\/.*$/
  extends:
    - .go_cache
    - .template
  script:
    - make golangci_lint
  • stage:指定此job属于lint阶段
  • 触发方式为合并请求下的提交,和以release开头的分支上的提交。
  • extends:继承脚本模板,代码复用。
  • script:job核心内容,指定此job运行的脚本。

test阶段

单元测试、检查数据竞争job

unit_test:
  stage: test
  only:
    - merge_requests
    - /^release\/.*$/
  extends:
    - .go_cache
    - .template
  cache:
    policy: pull
  script:
    - make unit_test
  • cache.policy:默认缓存策略会pull and push,这个job中只使用了缓存,但没有产生缓存,所以不需要上传缓存。

build阶段

打包镜像的job模板

.build_image:
  stage: build
  only:
    - tags
    - /^release\/.*$/
  extends:
    - .go_cache
    - .template

打包镜像job

build_xxx_admin_image:
  extends:
    - .build_image
  script:
    - make build_admin_app
    - make build_admin_image
    - make push_admin_image

build_xxx_interface_image:
  extends:
    - .build_image
  script:
    - make build_interface_app
    - make build_interface_image
    - make push_interface_image

build_xxx_job_image:
  extends:
    - .build_image
  script:
    - make build_job_app
    - make build_job_image
    - make push_job_image

build_xxx_task_image:
  extends:
    - .build_image
  script:
    - make build_task_app
    - make build_task_image
    - make push_task_image
  • 一个阶段多个job。

deploy阶段

部署到测试环境的job模板

.deploy_to_test_k8s:
  stage: deploy
  only:
    - tags
    - /^release\/.*$/
  extends:
    - .template
  image:
    name: 172.30.3.150/xxx/kubectl:latest
    entrypoint: [ "" ]
  • image:指定运行此job的基础镜像,覆盖公共模板里的image。

  • 172.30.3.150/xxx/kubectl:封装kubectl镜像,增加git、make命令。

    • Dockerfile
    FROM bitnami/kubectl
    
    USER root
    RUN apt-get update && apt-get install -y --no-install-recommends \
        make git

部署到测试环境job

deploy_admin_to_test_k8s:
  extends:
    - .deploy_to_test_k8s
  needs: [ build_xxx_admin_image ]
  script:
    - make deploy_admin_to_test_k8s

deploy_interface_to_test_k8s:
  extends:
    - .deploy_to_test_k8s
  needs: [ build_xxx_interface_image ]
  script:
    - make deploy_interface_to_test_k8s

deploy_job_to_test_k8s:
  extends:
    - .deploy_to_test_k8s
  needs: [ build_xxx_job_image ]
  script:
    - make deploy_job_to_test_k8s

.deploy_task_to_test_k8s:
  extends:
    - .deploy_to_test_k8s
  needs: [ build_xxx_task_image ]
  script:
    - make deploy_task_to_test_k8s
  • needs:指定依赖关系,流水线默认是一个阶段一个阶段的运行,这里部署admin,只需要admin镜像制作好就运行,而不是等待整个build阶段运行完才执行。

完整的.gitlab-ci.yml

stages:
  - lint
  - test
  - build
  - deploy

#定义全局变量
variables:
  IMAGE_GROUP: xxx
  NAME_SPACE: xxx
  GOPATH: ${CI_PROJECT_DIR}/.go
  GOMODCACHE: ${CI_PROJECT_DIR}/.go/pkg/mod
  GOCACHE: ${CI_PROJECT_DIR}/.go/.cache/go-build
  GOLANGCI_LINT_CACHE: ${CI_PROJECT_DIR}/.go/.cache/golangci-lint

########################### 公共模板 ###########################

#job公共模板
.template:
  image: 172.30.3.150/xxx/go-tools:latest
  tags:
    - 172.30.3.219-runner
  interruptible: true

#缓存模板
.go_cache:
  cache:
    key: go-cache-${CI_PROJECT_PATH_SLUG}
    paths:
      - .go

########################### lint阶段 ###########################

#代码检测job
golangci_lint:
  stage: lint
  only:
    - merge_requests
    - /^release\/.*$/
  extends:
    - .go_cache
    - .template
  script:
    - make golangci_lint

########################### test阶段 ###########################

#单元测试、检查数据竞争job
unit_test:
  stage: test
  only:
    - merge_requests
    - /^release\/.*$/
  extends:
    - .go_cache
    - .template
  cache:
    policy: pull
  script:
    - make unit_test

########################### build阶段 ###########################

#打包镜像的job模板
.build_image:
  stage: build
  only:
    - tags
    - /^release\/.*$/
  extends:
    - .go_cache
    - .template

#打包镜像job
build_xxx_admin_image:
  extends:
    - .build_image
  script:
    - make build_admin_app
    - make build_admin_image
    - make push_admin_image

build_xxx_interface_image:
  extends:
    - .build_image
  script:
    - make build_interface_app
    - make build_interface_image
    - make push_interface_image

build_xxx_job_image:
  extends:
    - .build_image
  script:
    - make build_job_app
    - make build_job_image
    - make push_job_image

build_xxx_task_image:
  extends:
    - .build_image
  script:
    - make build_task_app
    - make build_task_image
    - make push_task_image

########################### deploy阶段 ###########################

#部署到测试环境的job模板
.deploy_to_test_k8s:
  stage: deploy
  only:
    - tags
    - /^release\/.*$/
  extends:
    - .template
  image:
    name: 172.30.3.150/xxx/kubectl:latest
    entrypoint: [ "" ]

#部署到测试环境job
deploy_admin_to_test_k8s:
  extends:
    - .deploy_to_test_k8s
  needs: [ build_xxx_admin_image ]
  script:
    - make deploy_admin_to_test_k8s

deploy_interface_to_test_k8s:
  extends:
    - .deploy_to_test_k8s
  needs: [ build_xxx_interface_image ]
  script:
    - make deploy_interface_to_test_k8s

deploy_job_to_test_k8s:
  extends:
    - .deploy_to_test_k8s
  needs: [ build_xxx_job_image ]
  script:
    - make deploy_job_to_test_k8s

.deploy_task_to_test_k8s:
  extends:
    - .deploy_to_test_k8s
  needs: [ build_xxx_task_image ]
  script:
    - make deploy_task_to_test_k8s

Makefile

具体的脚本抽取到Makefile里,.gitlab-ci.yml只做流水线控制。而且Makefile支持任意环境执行,不依赖于gitlab流水线(使用到gitlab流水线变量的除外),开发可在本地运行Makefile的脚本。

VERSION=$(shell git describe --tags --always)

#################################### 代码检查 ####################################

.PHONY: golangci_lint
golangci_lint:
    go mod tidy # 必须先下载依赖,否则golangci-lint会报:could not load export data: no export data for
    go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.45.0
    golangci-lint run -v --timeout=5m --color always --out-format colored-line-number

#################################### 单元测试 ####################################

.PHONY: unit_test
unit_test:
    go test -short `go list ./...`

#################################### 构建应用二进制执行文件 ####################################

.PHONY: build_all_app
build_all_app: build_admin_app build_interface_app build_job_app build_task_app

.PHONY: build_admin_app
build_admin_app:
    make build_app SUB_MODULE=admin

.PHONY: build_interface_app
build_interface_app:
    make build_app SUB_MODULE=interface

.PHONY: build_job_app
build_job_app:
    make build_app SUB_MODULE=job

.PHONY: build_task_app
build_task_app:
    make build_app SUB_MODULE=task

.PHONY: build_app
build_app:
    mkdir -p bin
    cd cmd/$(SUB_MODULE) && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION)" -o ../../bin/ && cd -

#################################### 打镜像 ####################################

.PHONY: build_all_image
build_all_image: build_admin_image build_interface_image build_job_image build_task_image

.PHONY: build_admin_image
build_admin_image:
    make build_image SUB_MODULE=admin

.PHONY: build_interface_image
build_interface_image:
    make build_image SUB_MODULE=interface

.PHONY: build_job_image
build_job_image:
    make build_image SUB_MODULE=job

.PHONY: build_task_image
build_task_image:
    make build_image SUB_MODULE=task

.PHONY: build_image
build_image:
    docker build --build-arg SUB_MODULE=$(SUB_MODULE) -t xxx-$(SUB_MODULE):$(VERSION) .

#################################### 推镜像到私仓 ####################################

.PHONY: push_all_image
build_all_image: push_admin_image push_interface_image push_job_image push_task_image

.PHONY: push_admin_image
push_admin_image:
    make push_image SUB_MODULE=admin

.PHONY: push_interface_image
push_interface_image:
    make push_image SUB_MODULE=interface

.PHONY: push_job_image
push_job_image:
    make push_image SUB_MODULE=job

.PHONY: push_task_image
push_task_image:
    make push_image SUB_MODULE=task

.PHONY: push_image
push_image:
    docker login "${DOCKER_REGISTRY_SERVER}" --username "${DOCKER_REGISTRY_USER}" --password "${DOCKER_REGISTRY_PASSWORD}"

    docker tag "xxx-$(SUB_MODULE):$(VERSION)" "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/xxx-$(SUB_MODULE):$(VERSION)"
    docker push "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/xxx-$(SUB_MODULE):$(VERSION)"
    docker rmi -f "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/xxx-$(SUB_MODULE):$(VERSION)"

    docker tag "xxx-$(SUB_MODULE):$(VERSION)" "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/xxx-$(SUB_MODULE):latest"
    docker push "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/xxx-$(SUB_MODULE):latest"
    docker rmi -f "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/xxx-$(SUB_MODULE):latest"

    docker rmi -f "xxx-$(SUB_MODULE):$(VERSION)"

#################################### 部署到k8s ####################################

.PHONY: deploy_all_to_test_k8s
deploy_all_to_test_k8s: deploy_admin_to_test_k8s deploy_interface_to_test_k8s deploy_job_to_test_k8s deploy_task_to_test_k8s

.PHONY: deploy_admin_to_test_k8s
deploy_admin_to_test_k8s:
    make deploy_to_test_k8s SUB_MODULE=admin

.PHONY: deploy_interface_to_test_k8s
deploy_interface_to_test_k8s:
    make deploy_to_test_k8s SUB_MODULE=interface

.PHONY: deploy_job_to_test_k8s
deploy_job_to_test_k8s:
    make deploy_to_test_k8s SUB_MODULE=job

.PHONY: deploy_task_to_test_k8s
deploy_task_to_test_k8s:
    make deploy_to_test_k8s SUB_MODULE=task

.PHONY: deploy_to_test_k8s
deploy_to_test_k8s:
    echo ${K8S_CONFIG_192} | base64 -id > /.kube/config
    sed -i "s/IMAGE_VERSION/$(VERSION)/g" deploy/$(SUB_MODULE)/test/*.yaml
    sed -i "s/NAME_SPACE/${NAME_SPACE}/g" deploy/$(SUB_MODULE)/test/*.yaml
    sed -i "s/DOCKER_REGISTRY_ADDR/${DOCKER_REGISTRY_ADDR}/g" deploy/$(SUB_MODULE)/test/*.yaml
    sed -i "s/IMAGE_GROUP/${IMAGE_GROUP}/g" deploy/$(SUB_MODULE)/test/*.yaml
    kubectl apply -f deploy/$(SUB_MODULE)/test
  • VERSION:此变量用于打镜像的版本号,如果当前分支打了tag,此变量的值就为tag,没有打tag,值为commit hash标识。

  • .PHONY:伪目标,可以防止在Makefile中定义的命令目标和工作目录下的实际文件出现名字冲突。

  • DOCKER_REGISTRY_SERVER:在gitlab配置的变量,可根据实际情况,放到不同级别下。例如gitlab全局,项目群组,项目。

    image-20220531153438532

更新

Gitlab CI/CD 已在我司使用了 4 个月,期间不断迭代优化,现在基本稳定了。Gitlab CI/CD 系列 的文章是早期的版本,这里更新下20220927最新的情况。

Go 项目里的流水线脚本

.gitlab-ci.yaml

include:
  - project: 'devops/gitlab-cicd-template'
    ref: main
    file: '/golang/golangci-lint/1.48.0/.gitlab-ci.yml'
  - project: 'devops/gitlab-cicd-template'
    ref: main
    file: '/golang/unit-test/.gitlab-ci.yml'
  - project: 'devops/gitlab-cicd-template'
    ref: main
    file: '/golang/build-image/.gitlab-ci.yml'
  - project: 'devops/gitlab-cicd-template'
    ref: main
    file: '/common/deploy-to-k8s/.gitlab-ci.yml'

stages:
  - lint
  - test
  - build
  - deploy

variables: &global-variables
  # 用于拼接镜像名
  MODULE_PREFIX: google-ads
  # 镜像私仓里的项目
  IMAGE_GROUP: ads
  # 部署环境
  ENV: test
  # 拷贝部署文件到template仓库的目录
  TARGET_DIR: ads

########################### build阶段 ###########################

#打包镜像job
build_admin_image:
  extends:
    - .golang_build_image_base
  variables:
    <<: *global-variables
    SUB_MODULE: admin

build_interface_image:
  extends:
    - .golang_build_image_base
  variables:
    <<: *global-variables
    SUB_MODULE: interface

build_job_image:
  extends:
    - .golang_build_image_base
  variables:
    <<: *global-variables
    SUB_MODULE: job

build_task_image:
  extends:
    - .golang_build_image_base
  variables:
    <<: *global-variables
    SUB_MODULE: task

build_task_pop_image:
  extends:
    - .golang_build_image_base
  variables:
    <<: *global-variables
    SUB_MODULE: task-pop

########################### deploy阶段 ###########################

#部署到测试环境job
deploy_admin_to_test_k8s:
  extends:
    - .deploy_to_k8s_base
  before_script:
    - K8S_CONFIG=$K8S_CONFIG_192
  variables:
    <<: *global-variables
    SUB_MODULE: admin
    NAME_SPACE: gator-cloud
  needs: [ build_admin_image ]

deploy_interface_to_test_k8s:
  extends:
    - .deploy_to_k8s_base
  before_script:
    - K8S_CONFIG=$K8S_CONFIG_192
  variables:
    <<: *global-variables
    SUB_MODULE: interface
    NAME_SPACE: gator-cloud
  needs: [ build_interface_image ]

deploy_job_to_test_k8s:
  extends:
    - .deploy_to_k8s_base
  before_script:
    - K8S_CONFIG=$K8S_CONFIG_192
  variables:
    <<: *global-variables
    SUB_MODULE: job
    NAME_SPACE: gator-cloud
  needs: [ build_job_image ]

deploy_task_to_test_k8s:
  extends:
    - .deploy_to_k8s_base
  before_script:
    - K8S_CONFIG=$K8S_CONFIG_192
  variables:
    <<: *global-variables
    SUB_MODULE: task
    NAME_SPACE: gator-cloud
  needs: [ build_task_image ]

deploy_task_pop_to_test_k8s:
  extends:
    - .deploy_to_k8s_base
  before_script:
    - K8S_CONFIG=($K8S_CONFIG_192 $K8S_CONFIG_222 $K8S_CONFIG_229 $K8S_CONFIG_230)
    - NAME_SPACE=(google-cloud google-cloud google-cloud google-cloud)
  variables:
    <<: *global-variables
    SUB_MODULE: task-pop
  needs: [ build_task_pop_image ]

公共脚本库

/golang/golangci-lint/1.48.0/.gitlab-ci.yml

# golang1.19版本对应的golang-lint v1.48.0
golangci_lint:
  stage: lint
  image: 172.1.1.1/common/golangci-lint:1.48.0
  tags:
    - 172.1.1.2-runner
  interruptible: true
  variables:
    GOLANGCI_LINT_CACHE: /cache/${CI_PROJECT_PATH}/golangci-lint-cache
  script:
    - mkdir -p $GOLANGCI_LINT_CACHE
    - go mod tidy # 必须先下载依赖,否则golangci-lint会报:could not load export data: no export data for
    - golangci-lint run -v --timeout=5m

/golang/unit-test/.gitlab-ci.yml

unit_test:
  stage: test
  image: 172.1.1.1/common/golang:1.19
  tags:
    - 172.1.1.2-runner
  interruptible: true
  script:
    - go test -short `go list ./...`

/golang/build-image/.gitlab-ci.yml

.build_image: &build_image
  docker build --build-arg SUB_MODULE=$SUB_MODULE -t $MODULE_PREFIX-$SUB_MODULE:$VERSION -f $DOCKER_FILE .;

.push_image: &push_image
  VERSION=$(git describe --tags --always);
  echo "VERSION:$VERSION";               
  docker login "${DOCKER_REGISTRY_SERVER}" --username "${DOCKER_REGISTRY_USER}" --password "${DOCKER_REGISTRY_PASSWORD}";
  docker tag "$MODULE_PREFIX-$SUB_MODULE:$VERSION" "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/$MODULE_PREFIX-$SUB_MODULE:$VERSION";
  docker tag "$MODULE_PREFIX-$SUB_MODULE:$VERSION" "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/$MODULE_PREFIX-$SUB_MODULE:latest";
  docker push "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/$MODULE_PREFIX-$SUB_MODULE:$VERSION";
  docker push "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/$MODULE_PREFIX-$SUB_MODULE:latest";
  docker rmi -f "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/$MODULE_PREFIX-$SUB_MODULE:$VERSION";
  docker rmi -f "${DOCKER_REGISTRY_ADDR}/${IMAGE_GROUP}/$MODULE_PREFIX-$SUB_MODULE:latest";
  docker rmi -f "$MODULE_PREFIX-$SUB_MODULE:$VERSION";

.build_image_base:
  stage: build
  image: 172.1.1.1/common/golang:1.19
  tags:
    - 172.1.1.2-runner
  interruptible: true
  variables:
    SUB_MODULE: default
    MODULE_PREFIX: default
    IMAGE_GROUP: default
    DOCKER_FILE: Dockerfile
  before_script:
    - VERSION=$(git describe --tags --always)
    - echo "VERSION:$VERSION"
  script:
    - *build_image
    - *push_image

/common/deploy-to-k8s/.gitlab-ci.yml

.deploy_to_k8s: &deploy_to_k8s
  VERSION=$(git describe --tags --always);
  echo "VERSION:$VERSION";
  mkdir -p /.kube;                       
  mkdir -p /root/.kube; 
  for ((i=0;i<${#K8S_CONFIG[*]};i++)); do
    echo "index:$i";
    cp -r deploy /tmp;
    cd /tmp;
    sed -i "s/IMAGE_VERSION/$VERSION/g" deploy/$SUB_MODULE/$ENV/*.yaml;
    sed -i "s/NAME_SPACE/${NAME_SPACE[i]}/g" deploy/$SUB_MODULE/$ENV/*.yaml;
    sed -i "s/IMAGE_NAME/$MODULE_PREFIX-$SUB_MODULE/g" deploy/$SUB_MODULE/$ENV/*.yaml;
    sed -i "s/DOCKER_REGISTRY_ADDR/${DOCKER_REGISTRY_ADDR}/g" deploy/$SUB_MODULE/$ENV/*.yaml;
    sed -i "s/IMAGE_GROUP/${IMAGE_GROUP}/g" deploy/$SUB_MODULE/$ENV/*.yaml;
    echo ${K8S_CONFIG[i]} | base64 -id > /.kube/config;
    echo ${K8S_CONFIG[i]} | base64 -id > /root/.kube/config;
    kubectl apply -f deploy/$SUB_MODULE/$ENV;
    cd - > /dev/null;
    rm -rf /tmp/deploy;            
  done;

.deploy_to_k8s_base:
  stage: deploy
  image:
    name: 172.1.1.1/common/kubectl:1.19.10
    entrypoint: [ "" ]
  tags:
    - 172.1.1.2-runner
  interruptible: true
  variables:
    SUB_MODULE: default
    MODULE_PREFIX: default
    IMAGE_GROUP: default
    ENV: default
  script:
    - *deploy_to_k8s
作者:Yuyy
博客:https://yuyy.info

评论

  1. Windows Chrome
    4月前
    2022-6-10 10:36:53

    大佬,由于没找到您的关于或者留言板的评论界面,所以我在这里评论一下。主要是感觉您的博客写的不错,互链一波,以后多多学习!我已加你。
    站点名称:浮云翩迁之间
    站点地址:https://blognas.hwb0307.com
    站点描述:百代繁华一朝都,谁非过客;千秋明月吹角寒,花是主人。
    站点图标:https://blognas.hwb0307.com/logo.jpg

    • Yuyy
      博主
      Bensz
      Linux Chrome
      4月前
      2022-6-10 10:46:35

      好的,已添加。麻烦修改下你友链中我博客的描述:Golang、后端、Leetcode、程序员。谢谢!

      • Yuyy
        Windows Chrome
        4月前
        2022-6-10 10:50:23

        已经修改完成 回复这么快(~ ̄▽ ̄)~

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇