3.EdgeX Foundry设备微服务工程代码分析

[TOC]

一、device-service项目目录结构

通过device-sdk-go提取的simple的目录结构

~/project/Go/workspace/src/github.com/edgexfoundry$ tree device-simple/
device-simple/
├── cmd
│   └── device-simple
│       ├── Dockerfile
│       ├── main.go
│       └── res
│           ├── configuration.toml
│           ├── docker
│           │   └── configuration.toml
│           ├── off.jpg
│           ├── on.png
│           └── Simple-Driver.yaml
├── driver
│   └── simpledriver.go
├── go.mod
├── go.sum
├── Makefile
├── VERSION
└── version.go

以温度传感器device-temperature-sensor项目为例,分析项目工程结构和代码

root@J0633788c6:/home/admin/edgex/device-temperature-sensor# tree
.
├── .gitlab-ci.yml  # Gitlab自动化ci/cd所需配置文件
├── .gitmodules    #  git submodules配置文件
├── Dockerfile        # 打docker镜像所需文件
├── Makefile            # 项目打包命令集合
├── README.md
├── VERSION
├── cmd         # 启动命令及相关配置文件
│   └── device-service
│       ├── main.go     # 项目启动main函数
│       └── res     # 项目所需配置文件
│           ├── TemperatureProfile.yaml     # 设备http接口定义
│           ├── configuration.toml      # 裸机启动所需配置文件
│           └── docker
│               └── configuration.toml   # docker启动配置文件
├── docker-compose.yml
├── go.mod  # gomod项目配置文件,通过go mod init生成,手动修改配置内部依赖库
├── go.sum #  gomod依赖版本管理,无需手动创建
├── internal
│   ├── driver   # 主要的驱动封装逻辑
│   │   ├── driver.go  # Driver结构主体,需在此实现相关callback方法
│   │   └── temperaturesensor   # 自定义设备相关结构与方法
│   │       └── temperaturesensor.go
│   └── mod    # 包含一些内部依赖库,添加submodule即可
│       ├── device-sdk-go               # 官方 device-sdk
│       ├── go-mod-core-contracts  
│       └── go-mod-registry
├── iotedge.yml
├── sensor.md
└── version.go

1.cmd文件夹

1)main.go

device-service项目程序入口

package main

# 导入包
import (
        "github.com/edgexfoundry/device-sdk-go/pkg/jxstartup"
        "gitlab.xxx.com/applications/edgex/device-service/device-temperature-sensor" # 包含version.go
        "gitlab.xxx.com/applications/edgex/device-service/device-temperature-sensor/internal/driver"  # 包含driver.go
)

const (
        serviceName string = "device-temperature-sensor"    # 定义字符串变量
)

func main() {
        d := driver.Driver{}    # driver.go下的Driver结构体
        jxstartup.StartService(serviceName, device.Version, &d)     # 启动device-service
}

2)configuration.toml

  • 用于描述当前dervice-service自身ip、端口以及所调用的edgex 微服务ip和端口信息。
  • 系统启动时由main函数执行

TOML v0.4.0语法

  • 项目通常有很多配置信息需要存储,比如数据库的账号密码、Redis的连接地址、各种环境参数,这些配置信息通常都是写在配置文件中。常用的配置文件存储格式有JSON文件、INI文件、YAML文件和TOML文件等等。
  • TOML被设计成可以无歧义地被映射为哈希表,从而很容易的被解析成各种语言中的数据结构。
[Writable]
LogLevel = 'INFO'

[Service]
Host = "172.17.0.1"
Port = 49990
ConnectRetries = 20
Labels = []
OpenMsg = "device service started"
Timeout = 5000
EnableAsyncReadings = true
AsyncBufferSize = 16

[Registry]
Host = "localhost"
Port = 8500
Type = "consul"
CheckInterval = "10s"
FailLimit = 3
FailWaitTime = 10

[Clients]
  [Clients.Data]
  Name = "edgex-core-data"
  Protocol = "http"
  Host = "localhost"
  Port = 48080
  Timeout = 5000

  [Clients.Metadata]
  Name = "edgex-core-metadata"
  Protocol = "http"
  Host = "localhost"
  Port = 48081
  Timeout = 5000

  [Clients.Logging]
  Name = "edgex-support-logging"
  Protocol = "http"
  Host = "localhost"
  Port = 48061

[Device]
  DataTransform = true
  InitCmd = ""
  InitCmdArgs = ""
  MaxCmdOps = 128
  MaxCmdValueLen = 256
  RemoveCmd = ""
  RemoveCmdArgs = ""
  ProfilesDir = "./res"

[Logging]
EnableRemote = false
File = "./device-service.log"

# Pre-define Devices
[[DeviceList]]
  # 这里DeviceList的Name和Profile的值对应TemperatureProfile.yaml的name: "temperature-sensor"
  Name = "temperature-sensor"
  Profile = "temperature-sensor"        
  Description = "温度传感器"
  Labels = []
  [DeviceList.Protocols]
    [DeviceList.Protocols.other]
      Address = "/api/v1/device/temperature-sensor"

3)裸机与docker的configuration.toml 区别

colordiff configuration.toml docker/configuration.toml                                                                                       
5,6c5,6
< Host = "172.17.0.1"
< Port = 49990
---
> Host = "edgex-device-temperature-sensor"
> Port = 80
9c9
< OpenMsg = "device service started"
---
> OpenMsg = "device service temperature-sensor started"
15c15
< Host = "localhost"
---
> Host = "edgex-core-consul"
26c26
<   Host = "localhost"
---
>   Host = "edgex-core-data"
33c33
<   Host = "localhost"
---
>   Host = "edgex-core-metadata"
40c40
<   Host = "localhost"
---
>   Host = "edgex-support-logging"
54c54
< EnableRemote = false
---
> EnableRemote = true

4)TemperatureProfile.yaml

  • 设备微服务命令请求描述文件,提供给core-service的command层处理
  • 定义北向应用发往南向设备的命令请求。
  • 系统启动时由main函数执行

YAML 入门教程

name: "temperature-sensor"
manufacturer: "xxx"
model: "SP-01"
labels:
 - "temperature-sensor"
description: "temperature sensor"

deviceResources:
    -
        name: "temperature"
        description: "获取温度"
        properties:
            value:
                { type: "String", readWrite: "R", defaultValue: "{}" }
    
deviceCommands:
    -
        name: "temperature"
        get:
            - { operation: "get", object: "temperature" }

coreCommands:
    -
        name: "temperature"
        get:
            path: "/api/v1/device/{deviceId}/temperature"
            responses:
            -
                code: "200"
                description: ""
                expectedValues: ["temperature"]
            -
                code: "503"
                description: "service unavailable"
                expectedValues: []

注意:ymal文件不能出现tab键

2.internal文件夹

1)driver.go

  • edgex的驱动协议接口,设备微服务驱动初始化及对北向命令请求进行处理
    • Initialize
    • HandleReadCommands
    • HandleWriteCommands
    • Stop
    • AddDevice
    • UpdateDevice
    • RemoveDevice
// -*- Mode: Go; indent-tabs-mode: t -*-
//
// Copyright (C) 2018 Canonical Ltd
// Copyright (C) 2018-2019 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

// Package driver this package provides a simple example implementation of
// ProtocolDriver interface.
//
package driver

import (
    "fmt"
    "time"

    dsModels "github.com/edgexfoundry/device-sdk-go/pkg/models"
    "github.com/edgexfoundry/go-mod-core-contracts/clients/logger"
    contract "github.com/edgexfoundry/go-mod-core-contracts/models"
    "gitlab.xxx.com/applications/edgex/device-service/device-temperature-sensor/internal/driver/temperaturesensor"   //包含temperaturesensor包
)

// Driver 是对device的操作
type Driver struct {
    lc      logger.LoggingClient
    asyncCh chan<- *dsModels.AsyncValues

    temperatureSensor *temperaturesensor.TemperatureSensor
}

// Initialize performs protocol-specific initialization for the device
// service.
func (s *Driver) Initialize(lc logger.LoggingClient, asyncCh chan<- *dsModels.AsyncValues) error {
    s.lc = lc
    s.asyncCh = asyncCh

    s.temperatureSensor = temperaturesensor.NewTemperatureSensor(lc)
    return nil
}

// HandleReadCommands triggers a protocol Read operation for the specified device.
func (s *Driver) HandleReadCommands(deviceName string, protocols map[string]contract.ProtocolProperties, reqs []dsModels.CommandRequest) (res []*dsModels.CommandValue, err error) {

    // 查看该handle的信息
    s.lc.Debug(fmt.Sprintf("protocols: %v resource: %v attributes: %v", protocols, reqs[0].DeviceResourceName, reqs[0].Attributes))

    now := time.Now().UnixNano()

    for _, req := range reqs {
        switch req.DeviceResourceName {
        case "temperature":
            {
                temperature, err := s.temperatureSensor.GetTemperature()
                if err != nil {
                    return nil, err
                }
                cv := dsModels.NewStringValue(reqs[0].DeviceResourceName, now, temperature)
                res = append(res, cv)
            }
        }
    }

    return res, nil
}

// HandleWriteCommands passes a slice of CommandRequest struct each representing
// a ResourceOperation for a specific device resource.
// Since the commands are actuation commands, params provide parameters for the individual
// command.
func (s *Driver) HandleWriteCommands(deviceName string, protocols map[string]contract.ProtocolProperties, reqs []dsModels.CommandRequest,
    params []*dsModels.CommandValue) error {
    s.lc.Info(fmt.Sprintf("Driver.HandleWriteCommands: protocols: %v, resource: %v, parameters: %v", protocols, reqs[0].DeviceResourceName, params))
    for _, param := range params {
        switch param.DeviceResourceName {
        }
    }
    return nil
}

// Stop the protocol-specific DS code to shutdown gracefully, or
// if the force parameter is 'true', immediately. The driver is responsible
// for closing any in-use channels, including the channel used to send async
// readings (if supported).
func (s *Driver) Stop(force bool) error {
    // Then Logging Client might not be initialized
    if s.lc != nil {
        s.lc.Debug(fmt.Sprintf("Driver.Stop called: force=%v", force))
    }
    return nil
}

// AddDevice is a callback function that is invoked
// when a new Device associated with this Device Service is added
func (s *Driver) AddDevice(deviceName string, protocols map[string]contract.ProtocolProperties, adminState contract.AdminState) error {
    s.lc.Debug(fmt.Sprintf("a new Device is added: %s", deviceName))
    return nil
}

// UpdateDevice is a callback function that is invoked
// when a Device associated with this Device Service is updated
func (s *Driver) UpdateDevice(deviceName string, protocols map[string]contract.ProtocolProperties, adminState contract.AdminState) error {
    s.lc.Debug(fmt.Sprintf("Device %s is updated", deviceName))
    return nil
}

// RemoveDevice is a callback function that is invoked
// when a Device associated with this Device Service is removed
func (s *Driver) RemoveDevice(deviceName string, protocols map[string]contract.ProtocolProperties) error {
    s.lc.Debug(fmt.Sprintf("Device %s is removed", deviceName))
    return nil
}

2)temperaturesensor.go

  • Go Library,提供传感器控制或数据提取相关的一些子方法
package temperaturesensor

import (
    "fmt"
    "io/ioutil"
    "os"
    "strings"

    "github.com/edgexfoundry/go-mod-core-contracts/clients/logger"
)

// Temperature Sensor 定义
type TemperatureSensor struct {
    lc      logger.LoggingClient // 用来发送日志
    enabled bool                 // 是否使能

    temperature float64
}

// NewTemperatureSensor 创建一个新的TemperatureSensor
func NewTemperatureSensor(lc logger.LoggingClient) *TemperatureSensor {
    return &TemperatureSensor{
        lc:      lc,
        enabled: false,
    }
}

// Enable 用来让传感器工作
func (t *TemperatureSensor) Enable() {
    if t.enabled {
        t.lc.Warn("temperature sensor already enabled")
        return
    }
    t.enabled = true
    t.lc.Info("temperature sensor enabled")
}

// Disable 用来让传感器工作
func (t *TemperatureSensor) Disable() {
    if !t.enabled {
        t.lc.Warn("temperature sensor already disabled")
        return
    }
    t.enabled = false
    t.lc.Info("temperature sensor disabled")
}

// GetTemperature 获取当前传感器的温度数据
func (t *TemperatureSensor) GetTemperature() (string, error) {
    //out, err := runCmd()
    //if err != nil {
    //  return "", err
    //} else {
    //  return strings.TrimSuffix(out, "\n"), nil
    //}
    files, err := ioutil.ReadDir("/sys/bus/w1/devices")
    if err != nil {
        return "", err
    }
    var sensorId string
    for _, file := range files {
        if strings.HasPrefix(file.Name(), "28") {
            sensorId = file.Name()
        }
    }

    path := fmt.Sprintf("/sys/bus/w1/devices/%s/w1_slave", sensorId)
    file, err := os.Open(path)
    if err != nil {
        return "", err
    }
    defer file.Close()

    b, err := ioutil.ReadAll(file)
    outs := strings.Split(string(b), " ")
    res := strings.Split(outs[len(outs)-1], "=")
    return strings.TrimSuffix(res[len(res)-1], "\n"), nil
}

3.go.mod

go mod 使用

module gitlab.xxx.com/applications/edgex/device-service/device-temperature-sensor //指定go项目包的名字

go 1.13

//映射路径,在import 包时,将后面的路径替换前面的路径
replace github.com/edgexfoundry/device-sdk-go => ./internal/mod/device-sdk-go

replace github.com/edgexfoundry/go-mod-core-contracts => ./internal/mod/go-mod-core-contracts

replace github.com/edgexfoundry/go-mod-registry => ./internal/mod/go-mod-registry

//指定仓库版本
require (
        github.com/edgexfoundry/device-sdk-go v1.0.0
        github.com/edgexfoundry/go-mod-core-contracts v0.1.30
        github.com/go-stack/stack v1.8.0 // indirect    //go get命令从github拉取仓库到$GOPATH/src/github.com/go-stack/stack
        github.com/pkg/errors v0.8.1
        gopkg.in/evanphx/json-patch.v4 v4.5.0
)

replace github.com/satori/go.uuid v1.2.0 => github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b

4. .gitmodules

  • submodules配置文件,git的submodules功能允许将一个Git存储库保留为另一个Git存储库的子目录。方便在一个项目中调用另一个项目。

  • 执行:git submodule update --init --remote 会自动把依赖仓库拉取到对应位置。

[submodule "internal/mod/device-sdk-go"]
        path = internal/mod/device-sdk-go
        url = http://gitlab.xxx.com/applications/edgex/device-sdk-go.git
        branch = jx_edinburgh
[submodule "internal/mod/go-mod-core-contracts"]
        path = internal/mod/go-mod-core-contracts
        url = http://gitlab.xxx.com/applications/edgex/go-mod-core-contracts.git
[submodule "internal/mod/go-mod-registry"]
        path = internal/mod/go-mod-registry
        url = http://gitlab.xxx.com/applications/edgex/go-mod-registry.git

5.Makefile

BUILDDIR=build/
CHANGELOG=$(BUILDDIR)/CHANGELOG.md

# Go的代理服务器,加速go包的拉取
GOPROXY=https://mirrors.aliyun.com/goproxy/
# 拉取submodule配置文件下的仓库到本项目
UPDATE_SUBMODULE:=$(shell git submodule update --init --remote)

# 存在交叉编译的情况时,cgo 工具是不可用的。
GO=CGO_ENABLED=0 GO111MODULE=on go  #CGO_ENABLED=0  声明cgo 工具不可用
# 定义在各平台上的镜像名称
DOCKER_IMAGE_NAME=registry.xxx.com:5000/edgex/device-service/temperature-sensor
HARBOR_IMAGE_NAME=harbor.xxx.iotedge/library/edgex-temperature-sensor/$(ARCHTAG)/others
HARBOR_IOTEDGE_IMAGE_NAME=harbor.xxx.com/library/edgex-temperature-sensor/$(ARCHTAG)/others
GOARCH=arm64
ARCHTAG=arm64v8
# 源程序路径
SRC_PATH=gitlab.xxx.com/applications/edgex/device-service/device-temperature-sensor

# 使用最新的git tag作为镜像版本号
VERSION=$(shell git tag -l "v*" --points-at HEAD | tail -n 1 | tail -c +2)
GIT_SHA=$(shell git rev-parse HEAD)
GOFLAGS=-ldflags "-X $(SRC_PATH).Version=$(VERSION)"

MICROSERVICES=device-service
.PHONY: $(MICROSERVICES)  # 避免参数与文件重名

check_version:
ifeq ($(VERSION),)
    $(error No version tag found)
endif
ifeq ($(shell git cat-file -t v$(VERSION)),commit)
    $(error Changelog should be in tag message)
endif

# 执行build会编译check_version、$(MICROSERVICES)、changelog三个步骤
build: check_version $(MICROSERVICES) changelog

build-amd64: GOARCH=amd64
build-amd64: build

build-arm64: GOARCH=arm64
build-arm64: build

device-service: | $(BUILDDIR)
    GOARCH=$(GOARCH) $(GO) build $(GOFLAGS) -o $(BUILDDIR)/bin/$@-$(GOARCH) ./cmd/device-service
    cp -r cmd/device-service/res $(BUILDDIR)/bin/

# 记录修改版本,如果check_version或 $(BUILDDIR) 步骤执行成功,则执行以下操作
changelog: check_version | $(BUILDDIR)
    echo "# Changelog\n" > $(CHANGELOG)
    git tag --sort=-taggerdate --format='## %(tag) - %(taggerdate:short)%0a### Author: %(taggername) %(taggeremail)%0a%(contents)%0a' >> $(CHANGELOG)

$(BUILDDIR):
    mkdir -p $(BUILDDIR)

docker-amd64: GOARCH=amd64
docker-amd64: ARCHTAG=x8664
docker-amd64: docker

docker-arm64: GOARCH=arm64
docker-arm64: ARCHTAG=arm64v8
docker-arm64: docker

docker: build
    docker build \
        --label "git_sha=$(GIT_SHA)" \
        --build-arg GOARCH=$(GOARCH) \
        -f Dockerfile \
        -t $(DOCKER_IMAGE_NAME):$(GIT_SHA) \
        -t $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-$(VERSION) \
    -t $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-latest \
        build

deploy: check_version
    docker push $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-$(VERSION)
    docker push $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-latest

deploy-arm64: ARCHTAG=arm64v8
deploy-arm64: deploy

deploy-harbor:
    docker login harbor.xxx.iotedge --username $(DOCKER_USERNAME) --password $(DOCKER_PASSWORD)
    docker tag $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-$(VERSION) $(HARBOR_IMAGE_NAME):$(VERSION)
    docker push $(HARBOR_IMAGE_NAME):$(VERSION)

deploy-harbor-arm64: ARCHTAG=arm64v8
deploy-harbor-arm64: deploy-harbor
deploy-harbor-iotedge:
    docker login harbor.xxx.com --username $(IOTEDGE_DOCKER_USERNAME) --password $(IOTEDGE_DOCKER_PASSWORD)
    docker tag $(DOCKER_IMAGE_NAME):$(ARCHTAG)-cpu-$(VERSION) $(HARBOR_IOTEDGE_IMAGE_NAME):$(VERSION)
    docker push $(HARBOR_IOTEDGE_IMAGE_NAME):$(VERSION)

deploy-harbor-arm64-iotedge: ARCHTAG=arm64v8
deploy-harbor-arm64-iotedge: deploy-harbor-iotedge

test:
    $(GO) vet ./...
    gofmt -l .
    $(GO) test -coverprofile=coverage.out ./...

clean:
    rm -rf build/

使用方法

# 编译main,go 测试
cd cmd/device-service
GOARCH=arm64 go build main.go

# 编译go工程
make build-arm64

# 编译go工程并制作docker镜像
make docker-arm64

# 部署docker镜像到registry
make deploy-arm64

# 部署docker镜像到harbor
make deploy-harbor-arm64

# 部署docker镜像到iotedge
make deploy-harbor-arm64-iotedge

6.docker-compose.yml

  • 设备微服务的docker-compose描述文件,是docker服务的清单,指定了所需的容器以及如何运行这些容器。
version: '3'

services:
  edgex-device-termperature-sensor:
    image: registry.xxx.com:5000/edgex/device-service/temperature-sensor:arm64v8-cpu-0.0.4  # Makefile编译并提交registry到的docker image
    container_name: edgex-device-temperature-sensor
    hostname: edgex-device-temperature-sensor
    restart: always
    privileged: true
    volumes:
      - "/sys/bus/w1/devices:/sys/bus/w1/devices"   #把裸机的设备映射到docker中

networks:
  default:
    external:
      name: edgex

7.iotedge.yml

  • 在IOTEdge上将device-service镜像注册成应用时所需文件
worker_node:
  service_name: app-jx-temperature-sensor
  portname: port9995
  arch: ["arm64v8"]
  engine: "docker-compose"
  yaml:             # 对docker-compose.yml的拷贝
    version: '3'
    services:
      edgex-device-temperature-sensor:
        image: edgex-temperature-sensor
        container_name: edgex-device-temperature-sensor
        hostname: edgex-device-temperature-sensor
        restart: always
        privileged: true
        volumes:
          - "/sys/bus/w1/devices:/sys/bus/w1/devices"
    networks:
      default:
        external:
          name: edgex

8.Dockerfile

  • 用于构建镜像,文本内容包含了一条条构建镜像所需的指令和说明。
  • Dockerfile文件详解
#
# Copyright (c) 2018, 2019 Intel
#
# SPDX-License-Identifier: Apache-2.0
#

# FROM golang:1.11-alpine AS builder
# ARG BUILD_PATH
# ARG GOARCH

# RUN sed -e 's/dl-cdn[.]alpinelinux.org/nl.alpinelinux.org/g' -i~ /etc/apk/repositories

# # add git for go modules
# RUN apk update && apk add make git

# WORKDIR $BUILD_PATH

# COPY . .

# RUN GOPROXY="https://mirrors.aliyun.com/goproxy/" go get -d -v ./...

# RUN make build-$GOARCH

# Next image - Copy built Go binary into new workspace
FROM scratch
# ARG BUILD_PATH
ARG GOARCH

ENV APP_PORT=80
#expose command data port
EXPOSE $APP_PORT

WORKDIR /app
# COPY --from=builder $BUILD_PATH/bin/device-service-$GOARCH /app/device-service
# COPY --from=builder $BUILD_PATH/cmd/device-service/res /app/res
# COPY --from=builder $BUILD_PATH/cmd/device-service/res/docker/configuration.toml /app/res/configuration.toml
COPY bin/device-service-$GOARCH /app/device-service
COPY bin/res /app/res
COPY bin/res/docker/configuration.toml /app/res/configuration.toml
COPY CHANGELOG.md /app

ENTRYPOINT [ "/app/device-service"]

9. .gitlab-ci.yml

  • gitlab ci自动化部署所需描述文件
image: golang-dind:latest

variables:
  GO111MODULE: "on"
  GOPROXY: "https://mirrors.aliyun.com/goproxy/"

# 指明分以下4个阶段
stages:
  - build
  - deploy
  - deploy-harbor-review
  - deploy-harbor-iotedge

build:      # 1.编译
  tags:
    - docker-builder
  cache:
    paths:
      - .cache
  stage: build
  before_script:    # 编译前的处理
    - sed -i 's/http:\/\/gitlab\.xxx\.com/..\/..\/..\/../' .gitmodules
    - git submodule sync --recursive
    - git submodule update --init --recursive

    # cache needs to be inside $CI_PROJECT_DIR
    - mkdir -p .cache
    - export GOPATH="$CI_PROJECT_DIR/.cache"
  script:   # 执行脚本
    - make docker-arm64     # 编译go工程并制作docker-arm64镜像
  artifacts:
    paths:
      - build/*
    expire_in: 1 day
  only:
    - /^v[0-9](?:\.[0-9]){2,3}/

deploy:     # 2.部署镜像到registry
  tags:
    - docker-builder
  stage: deploy
  environment:
    name: registry
    url: http://registry.xxx.com:8080
  script:       # 执行脚本
    - make deploy-arm64  # 部署docker镜像
  only:
    - /^v[0-9](?:\.[0-9]){2,3}/

deploy-harbor-review:  # 3.部署镜像到harbor
  tags:
    - docker-builder
  stage: deploy-harbor-review
  environment:
    name: harbor
    url: http://harbor.xxx.iotedge
  script:       # 执行脚本
    - make deploy-harbor-arm64
  only:
    - /^v[0-9](?:\.[0-9]){2,3}/

deploy-harbor-iotedge:   # 4.部署镜像到harbor-iotedge
  tags:
    - docker-builder
  stage: deploy-harbor-iotedge
  environment:
    name: harbor
    url: http://harbor.xxx.com
  script:       # 执行脚本
    - make deploy-harbor-arm64-iotedge
  when: manual      # 指定手动执行脚本,非指定手动的就会自动执行
  only:
    - /^v[0-9](?:\.[0-9]){2,3}/
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容