Compare commits

...

No commits in common. "v0.2.1" and "master" have entirely different histories.

94 changed files with 84 additions and 6566 deletions

16
.SRCINFO Normal file
View File

@ -0,0 +1,16 @@
pkgbase = flucky
pkgdesc = A lightweight golang program to read values from different sensors
pkgver = 0.3.2
pkgrel = 2
url = https://git.cryptic.systems/flucky/flucky
arch = x86_64
arch = armv7h
license = Apache 2.0
makedepends = git
makedepends = go
makedepends = make
source = https://git.cryptic.systems/flucky/flucky/archive/v0.3.2.tar.gz
sha512sums = 1e53256f0b45c0e887b224ca697b3c1fadfb4135399595cbfee758f8f6c2357471da287a816d7fd0924adb96bc43676adf3149b0dd6bfa3f1c6c6947fcc6c760
pkgname = flucky

View File

@ -1 +0,0 @@
bin

View File

@ -1,25 +1,55 @@
---
kind: pipeline
type: docker
name: amd64
type: kubernetes
name: build
node_selector:
kubernetes.io/os: linux
kubernetes.io/arch: amd64
steps:
- name: build-linux-amd64
image: docker.io/volkerraschek/build-image:latest
- name: build-pkg
commands:
- make bin/linux/amd64/flucky
when:
event:
- push
- makepkg --sign
environment:
MAKEPKG_PACKAGER: "csrbot@cryptic.systems"
MAKEPKG_PKGEXT: ".pkg.tar.zst"
MAKEPKG_SRCEXT: ".src.tar.zst"
image: docker.io/volkerraschek/build-image:latest
trigger:
event:
include:
- pull_request
- push
exclude:
- tag
# steps:
# - name: test-unit
# image: docker.io/volkerraschek/build-image:latest
# commands:
# - make test/unit
# when:
# event:
# - push
# - pull_request
# - tag
---
kind: pipeline
type: kubernetes
name: deploy
node_selector:
kubernetes.io/os: linux
kubernetes.io/arch: amd64
steps:
- name: build-pkg
commands:
- makepkg --sign
- scp *.pkg.tar.zst* ${AUR_SERVER}:/${AUR_PATH}
environment:
MAKEPKG_PACKAGER: "csrbot@cryptic.systems"
MAKEPKG_PKGEXT: ".pkg.tar.zst"
MAKEPKG_SRCEXT: ".src.tar.zst"
SSH_KEY:
from_secret:
ssh_key
image: docker.io/volkerraschek/build-image:latest
trigger:
event:
include:
- pull_request
- push

View File

@ -1,12 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = false
[Makefile]
indent_style = tab

1
.gitattributes vendored
View File

@ -1 +0,0 @@
Makefile eol=lf

20
.gitignore vendored
View File

@ -1,16 +1,4 @@
# absolute files
.env
flucky
flucky.rpm
flucky.tar.bz2
flucky.tar.gz
flucky.tar.xz
# relative files
**/bindata*.go
# directories
.vscode/
# coverage files
coverage*
*
!.drone.yml
!.gitignore
!PKGBUILD

View File

@ -1,20 +0,0 @@
# ARGs
# ==================================
ARG BASE_IMAGE
ARG BUILD_IMAGE
ARG EXECUTABLE
ARG EXECUTABLE_TARGET
ARG GOPROXY
ARG GOPRIVATE
ARG VERSION
# BUILD
# ==================================
FROM ${BUILD_IMAGE} AS build
COPY . /workspace
RUN make ${EXECUTABLE_TARGET} VERSION=${VERSION} GOPROXY=${GOPROXY} GOPRIVATE=${GOPRIVATE}
# TARGET
# ==================================
FROM ${BASE_IMAGE}
COPY --from=build /workspace/${EXECUTABLE_TARGET} /usr/bin/${EXECUTABLE}

13
LICENSE
View File

@ -1,13 +0,0 @@
Copyright 2019 Markus Pesch
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

184
Makefile
View File

@ -1,184 +0,0 @@
# VERSION
# If no version is specified as a parameter of make, the last git hash
# value is taken.
VERSION?=$(shell git describe --abbrev=0)+$(shell date +'%Y%m%d%H%I%S')
# GOPROXY settings
# If no GOPROXY environment variable available, the pre-defined GOPROXY from go
# env to download and validate go modules is used. Exclude downloading and
# validation of all private modules which are pre-defined in GOPRIVATE. If no
# GOPRIVATE variable defined, the variable of go env is used.
GOPROXY?=$(shell go env GOPROXY)
GOPRIVATE?=$(shell go env GOPRIVATE)
# CONTAINER_RUNTIME
# The CONTAINER_RUNTIME variable will be used to specified the path to a
# container runtime. This is needed to start and run a container images.
CONTAINER_RUNTIME?=$(shell which docker)
CONTAINER_IMAGE_VERSION?=latest
# EXECUTABLE
EXECUTABLE=flucky
# BINARIES
# ==============================================================================
EXECUTABLE_TARGETS:= \
bin/linux/amd64/${EXECUTABLE} \
bin/linux/arm/5/${EXECUTABLE} \
bin/linux/arm/7/${EXECUTABLE} \
bin/tmp/${EXECUTABLE}
${EXECUTABLE}: bin/tmp/${EXECUTABLE}
bin/linux/amd64/${EXECUTABLE}: bindata
CGO_ENABLED=1 \
GOOS=linux \
GOARCH=amd64 \
GOPROXY=${GOPROXY} \
GOPRIVATE=${GOPRIVATE} \
go build -ldflags "-X main.version=${VERSION:v%=%}" -o ${@}
bin/linux/arm/5/${EXECUTABLE}: bindata
CGO_ENABLED=1 \
GOOS=linux \
GOARCH=arm \
GOARM=5 \
GOPROXY=${GOPROXY} \
GOPRIVATE=${GOPRIVATE} \
go build -ldflags "-X main.version=${VERSION:v%=%}" -o ${@}
bin/linux/arm/7/${EXECUTABLE}: bindata
CC=arm-linux-gnueabihf-gcc \
CGO_ENABLED=1 \
GOOS=linux \
GOARCH=arm \
GOARM=7 \
GOPROXY=${GOPROXY} \
GOPRIVATE=${GOPRIVATE} \
go build -ldflags "-X main.version=${VERSION:v%=%}" -o ${@}
bin/tmp/${EXECUTABLE}: bindata
CGO_ENABLED=1 \
GOPROXY=${GOPROXY} \
GOPRIVATE=${GOPRIVATE} \
go build -ldflags "-X main.version=${VERSION:v%=%}" -o ${@}
# GO-BINDATA
# ==============================================================================
BINDATA_TARGETS := \
pkg/repository/db/bindataSQL.go \
PHONY+=bindata
bindata: clean ${BINDATA_TARGETS}
pkg/repository/db/bindataSQL.go:
go-bindata -pkg db -o ./pkg/repository/db/bindataSQL.go pkg/repository/db/postgres/*** pkg/repository/db/sqlite3/***
# CLEAN
# ==============================================================================
PHONY+=clean
clean:
rm --force ${BINDATA_TARGETS} || true
rm --force --recursive bin/ || true
# TEST
# ==============================================================================
PHONY+=test/unit
test/unit: bindata
go test -v -race -coverprofile=coverage.txt -covermode=atomic -timeout 600s -count=1 ./pkg/...
PHONY+=test/integration
test/integration:
go test -v -count=1 -timeout 600s ./it/...
test/coverage: test/unit
go tool cover -html=coverage.txt
# CONTAINER IMAGE STEPS
# ==============================================================================
PHONY+=container-image/build/amd64
container-image/build/amd64:
${CONTAINER_RUNTIME} build \
--build-arg BASE_IMAGE=docker.io/library/alpine:3.11.2 \
--build-arg BUILD_IMAGE=docker.io/volkerraschek/build-image:latest \
--build-arg EXECUTABLE=${EXECUTABLE} \
--build-arg EXECUTABLE_TARGET=bin/linux/amd64/${EXECUTABLE} \
--build-arg GOPROXY=${GOPROXY} \
--build-arg GOPRIVATE=${GOPRIVATE} \
--build-arg VERSION=${VERSION} \
--file Dockerfile \
--no-cache \
--tag docker.io/volkerraschek/flucky:${CONTAINER_IMAGE_VERSION} \
--tag volkerraschek/flucky:${CONTAINER_IMAGE_VERSION} \
.
PHONY+=container-image/push/amd64
container-image/push/amd64: container-image/build/amd64
${CONTAINER_RUNTIME} login docker.io \
--username ${CONTAINER_IMAGE_REGISTRY_USER} \
--password ${CONTAINER_IMAGE_REGISTRY_PASSWORD}
${CONTAINER_RUNTIME} push docker.io/volkerraschek/flucky:${CONTAINER_IMAGE_VERSION}
# CONTAINER STEPS - BINARY
# ==============================================================================
# current os
PHONY+=container-run/${EXECUTABLE}
container-run/${EXECUTABLE}:
$(MAKE) container-run COMMAND=${@:container-run/%=%}
# build all binaries for any operating system
PHONY+=container-run/all
container-run/all:
$(MAKE) container-run COMMAND=${@:container-run/%=%}
PHONY+=${EXECUTABLE_TARGETS:%=container-run/%}
${EXECUTABLE_TARGETS:%=container-run/%}:
$(MAKE) container-run COMMAND=${@:container-run/%=%}
# CONTAINER STEPS - GO-BINDATA
# ==============================================================================
PHONY+=container-run/bindata
container-run/bindata:
$(MAKE) container-run COMMAND=${@:container-run/%=%}
# CONTAINER STEPS - CLEAN
# ==============================================================================
PHONY+=container-run/clean
container-run/clean:
$(MAKE) container-run COMMAND=${@:container-run/%=%}
# CONTAINER STEPS - TEST
# ==============================================================================
PHONY+=container-run/test/unit
container-run/test/unit:
$(MAKE) container-run COMMAND=${@:container-run/%=%}
# GENERAL CONTAINER COMMAND
# ==============================================================================
PHONY+=container-run
container-run:
${CONTAINER_RUNTIME} run \
--rm \
--volume ${PWD}:/workspace \
--volume /var/run/docker.sock:/var/run/docker.sock \
docker.io/volkerraschek/build-image:latest \
make ${COMMAND} \
GOPROXY=${GOPROXY} \
GOPRIVATE=${GOPRIVATE} \
VERSION=${VERSION:v%=%}
# REMOTE
# ==============================================================================
PHONY+=${FLUCKY_REMOTE:%=remote/%}
remote/${FLUCKY_REMOTE}: bin/linux/arm/7/${EXECUTABLE}
scp bin/linux/arm/7/${EXECUTABLE} root@${FLUCKY_REMOTE}:/usr/local/bin/${EXECUTABLE}
# ssh root@${FLUCKY_REMOTE} 'mkdir --parent /etc/bash_completion.d || true'
# ssh root@${FLUCKY_REMOTE} 'flucky completion bash > /etc/bash_completion.d/flucky.sh && chmod +x /etc/bash_completion.d/flucky.sh'
# ssh root@${FLUCKY_REMOTE} 'flucky completion zsh > /etc/bash_completion.d/flucky.zsh && chmod +x /etc/bash_completion.d/flucky.zsh'
ssh root@${FLUCKY_REMOTE} 'chmod +x /usr/local/bin/${EXECUTABLE}'
# PHONY
# ==============================================================================
# Declare the contents of the PHONY variable as phony. We keep that information
# in a variable so we can use it in if_changed.
.PHONY: ${PHONY}

16
PKGBUILD Normal file
View File

@ -0,0 +1,16 @@
# Maintainer: Markus Pesch <markus.pesch@cryptic.systems>
pkgname=flucky
pkgver=0.3.2
pkgrel=2
pkgdesc='A lightweight golang program to read values from different sensors'
arch=('x86_64' 'armv7h')
url=https://git.cryptic.systems/flucky/flucky
license=('Apache 2.0')
makedepends=('git' 'go' 'make')
source=("https://git.cryptic.systems/flucky/flucky/archive/v${pkgver}.tar.gz")
sha512sums=('1e53256f0b45c0e887b224ca697b3c1fadfb4135399595cbfee758f8f6c2357471da287a816d7fd0924adb96bc43676adf3149b0dd6bfa3f1c6c6947fcc6c760')
package() {
make --directory ${srcdir}/${pkgname} DESTDIR=${pkgdir} PREFIX=/usr VERSION=${pkgver} install
}

View File

@ -1,86 +0,0 @@
# flucky
[![Build Status](https://drone.cryptic.systems/api/badges/volker.raschek/flucky/status.svg)](https://drone.cryptic.systems/volker.raschek/flucky)
[![Docker Pulls](https://img.shields.io/docker/pulls/volkerraschek/flucky)](https://hub.docker.com/r/volkerraschek/flucky)
Flucky is a lightweight program written in go for reading data from sensors, for
example with a banana or raspberry pi. In addition, flucky provides a REST-API
to receive from other flucky installations measured values. All received
measured values, no matter if they come directly from flucky or from a server
instance of flucky with the provided REST-API, can be stored into a database.
## Supported and planned sensors
| Name | Measured values | Supported |
| ------- | --------------------------------------- | --------- |
| BME280 | humidity, pressure, temperature | Yes |
| DHT11 | temperature | Yes |
| DHT22 | temperature | Yes |
| DS18B20 | temperature | Yes |
| SDS011 | fine dust | Planning |
## Supported and planned backends
| Backend | Supported |
| --------------- | --------------- |
| flucky | WIP |
| Logfile | CSV, JSON, XML |
| MySQL, MariaDB | Planning |
| PostgreSQL | Yes |
| SQLite | Planning |
## Installation
Flucky can be installed over multiple ways. For example over a specific linux
distributions package or as container. The following table lists all
repositories where flucky can be otained from, but flucky can already be
compiled from source code.
| Distribution | Repo's |
| --------------------------- | ------------------------------------------------------------------------------------------- |
| Arch Linux | [armv7](https://arch.cryptic.systems/armv7), [x86_64](https://arch.cryptic.systems/x86_64) |
| Debian, Ubuntu, Linux Mint | Currently not supported |
| RHEL, Fedora, Centos | Planning |
| Container Image | [hub.docker.com](https://hub.docker.com/repository/docker/volkerraschek/flucky) |
### Compiling the source code
An additional ways to install flucky is to compile the source code. There are
two different ways to compile flucky from scratch. The easier ways is to use the
pre-defined container image, which has included all dependencies to compile
flucky. Alternatively, if all dependencies are met, flucky can also be compiled
without the container image. Both variants are briefly described.
#### Compiling the source code via container image
To compile flucky via container image it's necessary, that a container runtime
is installed. In the `Makefile` is predefined docker, but it's can be also used
podman. Execute `make container-run/flucky` to start the compiling process.
```bash
$ make container-run/flucky
make container-run COMMAND=flucky
make[1]: Directory „/home/markus/workspace/flucky“ is entered
/usr/bin/docker run \
--rm \
--volume /home/markus/workspace/flucky:/workspace \
volkerraschek/build-image:latest \
make go-build \
VERSION=60ee044-git \
GOOS=linux \
GOARCH=amd64
go-bindata -pkg db -o ./pkg/db/bindataSQL.go ./pkg/db/sql/***
go-bindata -pkg goldenfiles -o ./test/goldenfiles/bindata.go ./test/goldenfiles/json/***
GOOS=linux \
GOARCH=amd64 \
go build -ldflags "-X main.version=60ee044-git"
go: finding github.com/spf13/pflag v1.0.3
go: finding github.com/satori/go.uuid v1.2.0
...
```
#### Compiling the source code without container image
Make sure you have installed go >= v1.12. Execute `make flucky` to compile
flucky without a container-image. There should be a similar output as when
compiling flucky via the container image.

View File

@ -1,48 +0,0 @@
package daemon
import (
"fmt"
"git.cryptic.systems/volker.raschek/flucky/pkg/config"
"git.cryptic.systems/volker.raschek/flucky/pkg/daemon"
"git.cryptic.systems/volker.raschek/go-logger"
"github.com/spf13/cobra"
)
// InitCmd initialize all daemon subcommands
func InitCmd(cmd *cobra.Command) error {
daemonCmd := &cobra.Command{
Use: "daemon",
Short: "Read continuously data from all enabled sensors",
Example: "flucky daemon",
RunE: run,
}
daemonCmd.Flags().Bool("compression", true, "Compress measured values")
daemonCmd.Flags().Uint("cached-values", 500, "Number of cached values before saveing into the backend")
daemonCmd.Flags().Float64("round", 0.5, "Round values. The value 0 deactivates the function")
cmd.AddCommand(daemonCmd)
return nil
}
func run(cmd *cobra.Command, args []string) error {
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return fmt.Errorf("No config file defined: %v", err)
}
// logLevel, err := cmd.Flags().GetString("loglevel")
// if err != nil {
// return fmt.Errorf("No loglevel defined: %v", err)
// }
flogger := logger.NewLogger(logger.LogLevelDebug)
cnf, err := config.Read(configFile)
if err != nil {
return err
}
return daemon.Start(cnf, flogger)
}

View File

@ -1,135 +0,0 @@
package cli
import (
"fmt"
"math"
"os"
"os/user"
"path/filepath"
"time"
"git.cryptic.systems/volker.raschek/flucky/cli/daemon"
"git.cryptic.systems/volker.raschek/flucky/cli/sensor"
"git.cryptic.systems/volker.raschek/flucky/pkg/config"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
"github.com/Masterminds/semver"
uuid "github.com/satori/go.uuid"
"github.com/spf13/cobra"
)
// Execute a
func Execute(version *semver.Version) error {
rootCmd := &cobra.Command{
Use: "flucky",
// Short: "flucky - operate with differen sensors, his values and remote servers to synchronize measured values",
PersistentPreRunE: preRunError,
Version: version.String(),
}
defaultConfigFile, err := getDefaultConfigFile()
if err != nil {
return err
}
rootCmd.PersistentFlags().String("config", defaultConfigFile, "Config file")
rootCmd.PersistentFlags().String("loglevel", "info", "Set the Loglevel. Possible values: debug, info, warn, error, fatal")
subCommands := []func(cmd *cobra.Command) error{
daemon.InitCmd,
sensor.InitCmd,
}
for _, subCommand := range subCommands {
if err := subCommand(rootCmd); err != nil {
return err
}
}
err = rootCmd.Execute()
if err != nil {
return err
}
return nil
}
func preRunError(cmd *cobra.Command, args []string) error {
configFile := cmd.Flag("config").Value.String()
// check if config file exists
if _, err := os.Stat(configFile); os.IsNotExist(err) {
hostname, err := os.Hostname()
if err != nil {
return fmt.Errorf("Failed to determine the hostname: %v", err)
}
// Time must be truncted for postgres. Postgres currently does not support
// nanoseconds which is automatically include into the go time object
postgresTimeStamp := time.Now()
location, err := time.LoadLocation("Europe/Berlin")
if err != nil {
return err
}
postgresTimeStamp = time.Date(postgresTimeStamp.Year(), postgresTimeStamp.Month(), postgresTimeStamp.Day(), postgresTimeStamp.Hour(), postgresTimeStamp.Minute(), postgresTimeStamp.Second(), int(math.Round(float64(postgresTimeStamp.Nanosecond())/1000000)*1000000), location)
// default cache directory
defaultCacheDir, err := getDefaultCacheDir()
if err != nil {
return err
}
// Default configuration
dsn := fmt.Sprintf("sqlite3://%v/sqlite.db?cache=shared&mode=memory&foreign_keys=on", defaultCacheDir)
cnf := config.Config{
Device: &types.Device{
ID: uuid.NewV4().String(),
Name: hostname,
CreationDate: postgresTimeStamp,
},
DSN: dsn,
}
err = config.Write(&cnf, configFile)
if err != nil {
return err
}
}
return nil
}
// getDefaultConfigFile returns the default path to the configuration file of
// flucky
func getDefaultConfigFile() (string, error) {
u, err := user.Current()
if err != nil {
return "", fmt.Errorf("Can not read current user: %v", err)
}
switch u.Uid {
case "0":
return "/etc/flucky/config.json", nil
default:
return filepath.Join(u.HomeDir, ".config/flucky/config.json"), nil
}
}
// getDefaultCacheDir returns the default path to the cache directory where
// flucky stores his measured values.
func getDefaultCacheDir() (string, error) {
u, err := user.Current()
if err != nil {
return "", fmt.Errorf("Can not read current user: %v", err)
}
switch u.Uid {
case "0":
return "/var/cache/flucky", nil
default:
return filepath.Join(u.HomeDir, ".cache/flucky"), nil
}
}

View File

@ -1,370 +0,0 @@
package sensor
import (
"fmt"
"net/url"
"os"
"git.cryptic.systems/volker.raschek/flucky/pkg/cli"
"git.cryptic.systems/volker.raschek/flucky/pkg/config"
"git.cryptic.systems/volker.raschek/flucky/pkg/repository"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
"git.cryptic.systems/volker.raschek/go-logger"
uuid "github.com/satori/go.uuid"
"github.com/spf13/cobra"
)
// InitCmd initialize all sensor subcommands
func InitCmd(cmd *cobra.Command) error {
sensorCmd := &cobra.Command{
Use: "sensor",
Short: "Manage Sensors",
}
addSensorCmd := &cobra.Command{
Use: "add",
Short: "Add Sensor",
Aliases: []string{"append"},
Args: cobra.ExactArgs(2),
Example: `flucky sensor add --gpio GPIO14 indoor DHT11
flucky sensor add --wire-id 28-011432f0bb3d outdoor DS18B20
flucky sensor add --i2c-bus 1 --i2c-address 0x76 wetter-station BME280`,
RunE: addSensor,
}
addSensorCmd.Flags().Bool("enabled", true, "Enable new sensor")
addSensorCmd.Flags().String("gpio", "", "Defines the GPIO port")
addSensorCmd.Flags().Uint8("i2c-address", 0, "Defines the I2C address on the I2C bus")
addSensorCmd.Flags().Int("i2c-bus", 0, "Defines the I2C bus")
addSensorCmd.Flags().String("location", "", "Location of the sensor")
addSensorCmd.Flags().String("tick-duration", "1m", "Specifies how often measured values should be read from the sensor")
addSensorCmd.Flags().String("wire-id", "", "Defines the Wire-ID")
disableSensorCmd := &cobra.Command{
Use: "disable",
Short: "Disable Sensor",
Args: cobra.MinimumNArgs(1),
Example: "flucky sensor disable outdoor",
RunE: disableSensor,
}
enableSensorCmd := &cobra.Command{
Use: "enable",
Short: "Enable Sensor",
Example: "flucky sensor enable outdoor",
Args: cobra.MinimumNArgs(1),
RunE: enableSensor,
}
listSensorCmd := &cobra.Command{
Use: "list",
Short: "List Sensors",
Aliases: []string{"ls"},
RunE: listSensors,
}
removeSensorCmd := &cobra.Command{
Use: "remove",
Short: "Remove Sensor",
Aliases: []string{"rm"},
Example: "flucky sensor remove outdoor",
Args: cobra.MinimumNArgs(1),
RunE: removeSensor,
}
renameSensorCmd := &cobra.Command{
Use: "rename",
Short: "Rename Sensor",
Args: cobra.ExactArgs(2),
Example: `flucky sensor rename indoor outdoor
flucky sensor rename f98b00ea-a9b2-4e00-924f-113859d0af2d outdoor`,
RunE: renameSensor,
}
for _, subCommand := range []*cobra.Command{
addSensorCmd,
disableSensorCmd,
enableSensorCmd,
listSensorCmd,
removeSensorCmd,
renameSensorCmd,
} {
sensorCmd.AddCommand(subCommand)
}
cmd.AddCommand(sensorCmd)
return nil
}
func addSensor(cmd *cobra.Command, args []string) error {
sensor := &types.Sensor{
ID: uuid.NewV4().String(),
Name: args[0],
Model: args[1],
}
sensorLocation, err := cmd.Flags().GetString("location")
if err != nil {
return err
}
sensor.Location = sensorLocation
sensorEnabled, err := cmd.Flags().GetBool("enabled")
if err != nil {
return err
}
sensor.Enabled = sensorEnabled
sensorGPIO, err := cmd.Flags().GetString("gpio")
if err != nil {
return err
}
if len(sensorGPIO) > 0 {
sensor.GPIONumber = sensorGPIO
}
sensorI2CAddress, err := cmd.Flags().GetUint8("i2c-address")
if err != nil {
return err
}
if sensorI2CAddress > 0 {
sensor.I2CAddress = &sensorI2CAddress
}
sensorI2CBus, err := cmd.Flags().GetInt("i2c-bus")
if err != nil {
return err
}
if sensorI2CBus > 0 {
sensor.I2CBus = &sensorI2CBus
}
sensorTickDuration, err := cmd.Flags().GetString("tick-duration")
if err != nil {
return err
}
sensor.TickDuration = sensorTickDuration
sensorWireID, err := cmd.Flags().GetString("wire-id")
if err != nil {
return err
}
if len(sensorWireID) > 0 {
sensor.WireID = &sensorWireID
}
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return fmt.Errorf("No config file defined")
}
cnf, err := config.Read(configFile)
if err != nil {
return err
}
sensor.DeviceID = cnf.Device.ID
dsnURL, err := url.Parse(cnf.DSN)
if err != nil {
return err
}
// loglevel, err := cmd.Flags().GetString("loglevel")
// if err != nil {
// return fmt.Errorf("No loglevel defined")
// }
flogger := logger.NewLogger(logger.LogLevelDebug)
repo, err := repository.New(dsnURL, flogger)
if err != nil {
return err
}
// add sensor entry to list
err = repo.AddSensors(sensor)
if err != nil {
return err
}
// save new configuration
// err = config.Write(cnf, configFile)
// if err != nil {
// return err
// }
return nil
}
func disableSensor(cmd *cobra.Command, args []string) error {
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return fmt.Errorf("No config file defined")
}
cnf, err := config.Read(configFile)
if err != nil {
return err
}
dsnURL, err := url.Parse(cnf.DSN)
if err != nil {
return err
}
// loglevel, err := cmd.Flags().GetString("loglevel")
// if err != nil {
// return fmt.Errorf("No loglevel defined")
// }
flogger := logger.NewLogger(logger.LogLevelDebug)
repo, err := repository.New(dsnURL, flogger)
if err != nil {
return err
}
return repo.DisableSensorsByNames(args...)
}
func enableSensor(cmd *cobra.Command, args []string) error {
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return fmt.Errorf("No config file defined")
}
cnf, err := config.Read(configFile)
if err != nil {
return err
}
dsnURL, err := url.Parse(cnf.DSN)
if err != nil {
return err
}
// loglevel, err := cmd.Flags().GetString("loglevel")
// if err != nil {
// return fmt.Errorf("No loglevel defined")
// }
flogger := logger.NewLogger(logger.LogLevelDebug)
repo, err := repository.New(dsnURL, flogger)
if err != nil {
return err
}
return repo.EnableSensorsByNames(args...)
}
func listSensors(cmd *cobra.Command, args []string) error {
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return fmt.Errorf("No config file defined")
}
cnf, err := config.Read(configFile)
if err != nil {
return err
}
dsnURL, err := url.Parse(cnf.DSN)
if err != nil {
return err
}
// loglevel, err := cmd.Flags().GetString("loglevel")
// if err != nil {
// return fmt.Errorf("No loglevel defined")
// }
flogger := logger.NewLogger(logger.LogLevelDebug)
repo, err := repository.New(dsnURL, flogger)
if err != nil {
return err
}
// add sensor entry to list
sensors, err := repo.GetSensorsByDeviceID(cnf.Device.ID)
if err != nil {
return err
}
err = cli.PrintSensors(sensors, os.Stdout)
if err != nil {
return err
}
return nil
}
func removeSensor(cmd *cobra.Command, args []string) error {
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return fmt.Errorf("No config file defined")
}
cnf, err := config.Read(configFile)
if err != nil {
return err
}
dsnURL, err := url.Parse(cnf.DSN)
if err != nil {
return err
}
// loglevel, err := cmd.Flags().GetString("loglevel")
// if err != nil {
// return fmt.Errorf("No loglevel defined")
// }
flogger := logger.NewLogger(logger.LogLevelDebug)
repo, err := repository.New(dsnURL, flogger)
if err != nil {
return err
}
return repo.RemoveSensorsByNames(args...)
}
func renameSensor(cmd *cobra.Command, args []string) error {
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return fmt.Errorf("No config file defined")
}
cnf, err := config.Read(configFile)
if err != nil {
return err
}
dsnURL, err := url.Parse(cnf.DSN)
if err != nil {
return err
}
// loglevel, err := cmd.Flags().GetString("loglevel")
// if err != nil {
// return fmt.Errorf("No loglevel defined")
// }
flogger := logger.NewLogger(logger.LogLevelDebug)
repo, err := repository.New(dsnURL, flogger)
if err != nil {
return err
}
return repo.RenameSensors(args[0], args[1])
}

View File

@ -1,14 +0,0 @@
version: '3'
services:
flucky-db:
container_name: postgres
environment:
- PGTZ=Europe/Berlin
- POSTGRES_PASSWORD=postgres
- TZ=Europe/Berlin
image: postgres:11.5-alpine
ports:
- 5432:5432
restart: always
volumes:
- /etc/localtime:/etc/localtime:ro

24
go.mod
View File

@ -1,24 +0,0 @@
module git.cryptic.systems/volker.raschek/flucky
go 1.14
require (
git.cryptic.systems/volker.raschek/go-dht v0.1.2
git.cryptic.systems/volker.raschek/go-logger v0.1.0
github.com/Masterminds/semver v1.5.0
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/d2r2/go-bsbmp v0.0.0-20190515110334-3b4b3aea8375
github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc
github.com/d2r2/go-logger v0.0.0-20181221090742-9998a510495e
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.4.0 // indirect
github.com/lib/pq v1.7.0
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/satori/go.uuid v1.2.0
github.com/spf13/cobra v1.0.0
github.com/stretchr/testify v1.6.1
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
)

185
go.sum
View File

@ -1,185 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.cryptic.systems/volker.raschek/go-dht v0.1.2 h1:kGmfpaVUETQhSELCIrKXMjKwuUhQkRUz/7VbLYiTRJA=
git.cryptic.systems/volker.raschek/go-dht v0.1.2/go.mod h1:FUMwxa4cD+ATHPztXJntlO22I0DBTUPtXxfRF0JxXy8=
git.cryptic.systems/volker.raschek/go-logger v0.1.0 h1:JHBDesKBZaXjc2AlqYms1T3dGIX0oNIOBWl4cnVFWIo=
git.cryptic.systems/volker.raschek/go-logger v0.1.0/go.mod h1:GqeuxFj64SAolfj5kpbWup6E1vv37SaH5S+4wa40Tqs=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/d2r2/go-bsbmp v0.0.0-20190515110334-3b4b3aea8375 h1:vdUOwcZdV+bBfGUUh5oPPWSzw9p+lBnNSuGgQwGpCH4=
github.com/d2r2/go-bsbmp v0.0.0-20190515110334-3b4b3aea8375/go.mod h1:3iz1WHlYJU9b4NJei+Q8G7DN3K05arcCMlOQ+qNCDjo=
github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc h1:HLRSIWzUGMLCq4ldt0W1GLs3nnAxa5EGoP+9qHgh6j0=
github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc/go.mod h1:AwxDPnsgIpy47jbGXZHA9Rv7pDkOJvQbezPuK1Y+nNk=
github.com/d2r2/go-logger v0.0.0-20181221090742-9998a510495e h1:ZG3JBA6rPRl0xxQ+nNSfO7tor8w+CNCTs05DNJQYbLM=
github.com/d2r2/go-logger v0.0.0-20181221090742-9998a510495e/go.mod h1:oA+9PUt8F1aKJ6o4YU1T120i7sgo1T6/1LWEEBy0BSs=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
periph.io/x/periph v3.6.4+incompatible h1:8FyXTbu9lcMVofz8mf+cj1pzTLN4V6EuPY2EF+DoJF4=
periph.io/x/periph v3.6.4+incompatible/go.mod h1:EWr+FCIU2dBWz5/wSWeiIUJTriYv9v2j2ENBmgYyy7Y=

26
main.go
View File

@ -1,26 +0,0 @@
package main
import (
"log"
"os"
"git.cryptic.systems/volker.raschek/flucky/cli"
"github.com/Masterminds/semver"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
var (
version string
)
func main() {
sversion, err := semver.NewVersion(version)
if err != nil {
log.Printf("The sematic versioning is invalid: %v", version)
os.Exit(1)
}
cli.Execute(sversion)
}

View File

@ -1,48 +0,0 @@
package cli
import (
"fmt"
"io"
"text/tabwriter"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
)
// PrintSensors displays a list with all configured sensors
func PrintSensors(sensors []*types.Sensor, w io.Writer) error {
// declar tabwriter
tw := tabwriter.NewWriter(w, 0, 0, 3, ' ', 0)
fmt.Fprint(tw, "name\tlocation\ttype\twire-id\ti2c-bus\ti2c-address\tgpio\ttick-duration\tenabled\n")
for _, sensor := range sensors {
fmt.Fprintf(tw, "%v\t%v\t%v\t", sensor.Name, sensor.Location, sensor.Model)
if sensor.WireID != nil {
fmt.Fprintf(tw, "%v\t", *sensor.WireID)
} else {
fmt.Fprintf(tw, "\t")
}
if sensor.I2CBus != nil {
fmt.Fprintf(tw, "%v\t", *sensor.I2CBus)
} else {
fmt.Fprintf(tw, "\t")
}
if sensor.I2CAddress != nil {
fmt.Fprintf(tw, "%#v\t", *sensor.I2CAddress)
} else {
fmt.Fprintf(tw, "\t")
}
fmt.Fprintf(tw, "%v\t", sensor.GPIONumber)
fmt.Fprintf(tw, "%v\t%v\n", sensor.TickDuration, sensor.Enabled)
}
tw.Flush()
return nil
}

View File

@ -1,191 +0,0 @@
package config
import (
"fmt"
"os"
"path/filepath"
"regexp"
"time"
"git.cryptic.systems/volker.raschek/flucky/pkg/internal/format"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
uuid "github.com/satori/go.uuid"
)
var (
validUUID = regexp.MustCompile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
)
// Config represent the configuration
type Config struct {
Device *types.Device `json:"device"`
Sensors []*types.Sensor `json:"sensors"`
DSN string `json:"dsn"`
DSNFallback string `json:"dsn_fallback"`
}
// AddSensor add a new sensor
func (cnf *Config) AddSensor(sensor *types.Sensor) error {
// Verify that a device is configured
if cnf.Device == nil {
return fmt.Errorf("No device configured")
}
// Define a new UUID if the current UUID is invalid
if !validUUID.MatchString(sensor.ID) {
sensor.ID = uuid.NewV4().String()
}
// Verify that the sensor has a valid name
if len(sensor.Name) <= 0 {
return fmt.Errorf("No sensor name defined")
}
// check if sensor name and sensor uuid already exists
for _, cnfSensor := range cnf.Sensors {
if cnfSensor.Name == sensor.Name ||
cnfSensor.ID == sensor.ID {
return fmt.Errorf("Sensor with same name or id already exist")
}
if cnfSensor.WireID != nil &&
sensor.WireID != nil &&
*cnfSensor.WireID == *sensor.WireID {
return fmt.Errorf("Sensor with same wire id already exist")
}
}
// check if sensor has a valid tick time
if _, err := time.ParseDuration(sensor.TickDuration); err != nil {
return fmt.Errorf("Failed to parse tick duration: %v", err)
}
// check if sensor has a valid device id
if sensor.DeviceID != cnf.Device.ID {
sensor.DeviceID = cnf.Device.ID
}
// overwrite creation date
sensor.CreationDate = format.FormatedTime()
// check if wire socket is available
if sensor.WireID != nil {
socketPath := filepath.Join("/sys/bus/w1/devices", *sensor.WireID, "/w1_slave")
if _, err := os.Stat(socketPath); os.IsNotExist(err) {
return fmt.Errorf("Wire socket not found: %v", socketPath)
}
}
cnf.Sensors = append(cnf.Sensors, sensor)
return nil
}
// DisableSensor disables a sensor by its name or its unique UUID
func (cnf *Config) DisableSensor(name string) error {
found := false
for _, sensor := range cnf.Sensors {
// disable sensor matched after name
if !validUUID.MatchString(name) &&
sensor.Name == name {
sensor.Enabled = false
found = true
break
}
// remove machted uuid
if validUUID.MatchString(name) &&
sensor.ID == name {
sensor.Enabled = false
found = true
break
}
}
if !found {
return fmt.Errorf("Can not found sensor %v", name)
}
return nil
}
// EnableSensor enables a sensor by its name or its unique UUID
func (cnf *Config) EnableSensor(name string) error {
found := false
for _, sensor := range cnf.Sensors {
// disable sensor matched after name
if !validUUID.MatchString(name) &&
sensor.Name == name {
sensor.Enabled = true
found = true
break
}
// remove machted uuid
if validUUID.MatchString(name) &&
sensor.ID == name {
sensor.Enabled = true
found = true
break
}
}
if !found {
return fmt.Errorf("Can not found sensor %v", name)
}
return nil
}
// GetSensorByID returns a sensor matched by his id. If no sensor has this id,
// the function returns nil
func (cnf *Config) GetSensorByID(id string) *types.Sensor {
for _, sensor := range cnf.Sensors {
if sensor.ID == id {
return sensor
}
}
return nil
}
// RemoveSensor deletes a sensor by its name or its unique UUID, If definitive
// is set to true, the sensor will not only be removed in the configuration file
// but also in the backend.
func (cnf *Config) RemoveSensor(name string) error {
for i, sensor := range cnf.Sensors {
// remove machted name
if !validUUID.MatchString(name) &&
sensor.Name == name {
cnf.Sensors = append(cnf.Sensors[:i], cnf.Sensors[i+1:]...)
return nil
}
// remove machted uuid
if validUUID.MatchString(name) &&
sensor.ID == name {
cnf.Sensors = append(cnf.Sensors[:i], cnf.Sensors[i+1:]...)
return nil
}
}
return fmt.Errorf("Can not find sensor %v", name)
}
// RenameSensor renamed a sensor
func (cnf *Config) RenameSensor(oldName string, newName string) error {
for _, cnfSensor := range cnf.Sensors {
if cnfSensor.Name == oldName {
cnfSensor.Name = newName
return nil
}
}
return fmt.Errorf("No sensor %v found", oldName)
}

View File

@ -1,31 +0,0 @@
package config_test
// func TestAddRemoveSensor(t *testing.T) {
// require := require.New(t)
// // Test: No device configured
// cnf := new(config.Config)
// err := cnf.AddSensor(&types.Sensor{ID: "1aa32c9a-b505-456d-868b-0403344f4cdf"})
// require.Error(err)
// // wireID := "sdfsdff"
// // i2cBus := 1
// // i2cAddress := 78
// cnf.Device = &types.Device{ID: "d6176a06-2b0b-41af-a85c-913e8f61c35d"}
// testCases := map[*types.Sensor]error{
// {ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d"}: fmt.Errorf("No sensor name defined"),
// {ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01"}: fmt.Errorf("Failed to parse tick duration: time: invalid duration "),
// {ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01", TickDuration: "5s"}: nil,
// {ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01", TickDuration: "5s"}: fmt.Errorf("Sensor with same name or id already exist"),
// // {ID: "f90cfc18-f141-4cfd-a8d2-fb40082de5cc", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01", TickDuration: "5s"}: fmt.Errorf("Sensor with same name or id already exist"),
// // {ID: "860a9922-62cb-4c9b-b5af-5fa783cebe9d", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test02", TickDuration: "5s", WireID: &wireID}: fmt.Errorf("Wire socket not found: /sys/bus/w1/devices/sdfsdff/w1_slave"),
// // {ID: "9be8989c-b2a1-4401-a82f-d6989ec226fe", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test02", TickDuration: "5s"}: nil,
// }
// for sensor, expectedErr := range testCases {
// err := cnf.AddSensor(sensor)
// require.Equal(expectedErr, err)
// }
// }

View File

@ -1,61 +0,0 @@
package config
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
)
// Decode a configuration from a reader
func Decode(r io.Reader) (*Config, error) {
cnf := new(Config)
jsonDecoder := json.NewDecoder(r)
if err := jsonDecoder.Decode(&cnf); err != nil {
return nil, fmt.Errorf("Can not unmarshal JSON: %v", err)
}
return cnf, nil
}
// Encode a configuration to a writer
func Encode(cnf *Config, w io.Writer) error {
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
err := encoder.Encode(cnf)
if err != nil {
return fmt.Errorf("Error encoding config to json: %v", err)
}
return nil
}
// Read the configuration file
func Read(configFile string) (*Config, error) {
f, err := os.Open(configFile)
if err != nil {
return nil, fmt.Errorf("Can not open file %v: %v", configFile, err)
}
defer f.Close()
return Decode(f)
}
// Write the configuration into a file, specified by the configuration filepath
func Write(cnf *Config, configFile string) error {
if _, err := os.Stat(configFile); os.IsNotExist(err) {
configDir := filepath.Dir(configFile)
err := os.MkdirAll(configDir, 0775)
if err != nil {
return fmt.Errorf("Failed to create config directory %v: %v", configDir, err)
}
}
f, err := os.Create(configFile)
if err != nil {
return fmt.Errorf("Failed not create config file %v: %v", configFile, err)
}
defer f.Close()
return Encode(cnf, f)
}

View File

@ -1,137 +0,0 @@
package daemon
import (
"context"
"fmt"
"net/url"
"os"
"os/signal"
"git.cryptic.systems/volker.raschek/flucky/pkg/config"
"git.cryptic.systems/volker.raschek/flucky/pkg/repository"
"git.cryptic.systems/volker.raschek/flucky/pkg/sensor"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
"git.cryptic.systems/volker.raschek/go-logger"
)
func Start(cnf *config.Config, flogger logger.Logger) error {
measuredValueChannel := make(chan *types.MeasuredValue, 0)
// load data source name (dsn)
dsnURL, err := url.Parse(cnf.DSN)
if err != nil {
return err
}
repo, err := repository.New(dsnURL, flogger)
if err != nil {
return err
}
// Add
repoDevice, err := repo.GetDevice(cnf.Device.ID)
switch {
case err != nil:
return err
case repoDevice == nil:
err = repo.AddDevices(cnf.Device)
if err != nil {
return err
}
repoDevice, err = repo.GetDevice(cnf.Device.ID)
if err != nil {
return err
}
}
repoSensors, err := repo.GetSensorsByDeviceID(repoDevice.ID)
switch {
case err != nil:
return err
case repoSensors == nil, len(repoSensors) <= 0:
return fmt.Errorf("No sensors found")
}
sensors := make([]sensor.Sensor, 0)
for _, repoSensor := range repoSensors {
if !repoSensor.Enabled || repoSensor.DeviceID != repoDevice.ID {
continue
}
flogger.Debug("Found sensor %v", repoSensor.GetName())
sensor, err := sensor.New(repoSensor)
if err != nil {
return err
}
sensors = append(sensors, sensor)
}
interruptChannel := make(chan os.Signal, 1)
signal.Notify(interruptChannel, os.Interrupt, os.Kill)
// Collection
parentCtx := context.Background()
ctx, cancel := context.WithCancel(parentCtx)
for _, s := range sensors {
go func(sensor sensor.Sensor) {
for {
select {
case <-ctx.Done():
return
case <-sensor.GetTicker().C:
measuredValues, err := sensor.Read()
if err != nil {
flogger.Error("%v", err)
continue
}
for _, measuredValue := range measuredValues {
measuredValueChannel <- measuredValue
}
}
}
}(s)
}
measuredValues := make([]*types.MeasuredValue, 0, 10)
for {
select {
case measuredValue := <-measuredValueChannel:
flogger.Debug("%v\t%v\t%v", measuredValue.ID, measuredValue.ValueType, measuredValue.Value)
measuredValues = append(measuredValues, measuredValue)
if cap(measuredValues) == len(measuredValues) {
flogger.Debug("Flush cache with %v values", len(measuredValues))
err := repo.AddMeasuredValues(measuredValues...)
if err != nil {
flogger.Error("%v", err)
}
measuredValues = make([]*types.MeasuredValue, 0, 10)
}
case signal := <-interruptChannel:
cancel()
close(measuredValueChannel)
flogger.Info("Stopping daemon: Received process signal %v", signal.String())
flogger.Debug("Flush cache with %v remaining values", len(measuredValues))
err := repo.AddMeasuredValues(measuredValues...)
if err != nil {
flogger.Error("%v", err)
}
flogger.Debug("Close repository")
err = repo.Close()
if err != nil {
flogger.Error("%v", err)
}
return nil
}
}
}

View File

@ -1,23 +0,0 @@
package format
import (
"errors"
"math"
"time"
)
var (
errorPraseTime = errors.New("Can not parse time")
TimeFormat = "2006-01-02T15:04:05.999999Z07:00"
)
// FormatedTime returns a current timestamp without nano seconds. Postgres
// currently does not support nanoseconds which is automatically include into
// the go time object
func FormatedTime() time.Time {
t := time.Now()
l, _ := time.LoadLocation("Europe/Berlin")
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), int(math.Round(float64(t.Nanosecond())/1000000)*1000000), l)
}

View File

@ -1,124 +0,0 @@
package db
import (
"context"
"database/sql"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
"git.cryptic.systems/volker.raschek/go-logger"
)
// Database is a general interface for a database backend like postgres, oracle
// or sqlite
type Database interface {
Close() error
DeleteDevices(ctx context.Context, deviceIDs ...string) error
DeleteSensors(ctx context.Context, sensorIDs ...string) error
InsertDevices(ctx context.Context, devices ...*types.Device) error
InsertMeasuredValues(ctx context.Context, measuredValues ...*types.MeasuredValue) error
InsertSensors(ctx context.Context, sensors ...*types.Sensor) error
Scheme(ctx context.Context) error
SelectDevice(ctx context.Context, deviceID string) (*types.Device, error)
SelectDevices(ctx context.Context) ([]*types.Device, error)
SelectHumidity(ctx context.Context, id string) (*types.MeasuredValue, error)
SelectHumidities(ctx context.Context) ([]*types.MeasuredValue, error)
SelectPressure(ctx context.Context, id string) (*types.MeasuredValue, error)
SelectPressures(ctx context.Context) ([]*types.MeasuredValue, error)
SelectSensor(ctx context.Context, sensorID string) (*types.Sensor, error)
SelectSensors(ctx context.Context) ([]*types.Sensor, error)
SelectTemperature(ctx context.Context, id string) (*types.MeasuredValue, error)
SelectTemperatures(ctx context.Context) ([]*types.MeasuredValue, error)
UpdateDevices(ctx context.Context, devices ...*types.Device) error
UpdateSensors(ctx context.Context, sensors ...*types.Sensor) error
}
// New returns a new database backend interface
func New(dsnURL *url.URL, flogger logger.Logger) (Database, error) {
// Check of nil pointer
for _, parameter := range []interface{}{
dsnURL,
flogger,
} {
if parameter == nil {
return nil, fmt.Errorf("Parameter does not be nil")
}
}
// Load Queryfiles
queries := make(map[string]string, 0)
for _, asset := range AssetNames() {
if !strings.Contains(asset, dsnURL.Scheme) {
continue
}
body, err := Asset(asset)
if err != nil {
return nil, err
}
queryFile := filepath.Base(asset)
queries[queryFile] = string(body)
}
var (
database Database
err error
)
switch dsnURL.Scheme {
case "postgres":
// postgres://[user]:[password]@[host]:[port]/[path]?[query]
newDBO, err := sql.Open(dsnURL.Scheme, dsnURL.String())
if err != nil {
return nil, err
}
database = &Postgres{
dbo: newDBO,
flogger: flogger,
queries: queries,
}
case "sqlite3":
// Create directory where the db file will be created if not exists.
if _, err := os.Stat(filepath.Dir(dsnURL.Path)); os.IsNotExist(err) {
err := os.MkdirAll(filepath.Dir(dsnURL.Path), 0755)
if err != nil {
return nil, err
}
}
// sqlite3:///[path]?[query] flucky dsn
// file:///[path]?[query] sql-lib dsn
newDBO, err := sql.Open(dsnURL.Scheme, fmt.Sprintf("file://%v?%v", dsnURL.Path, dsnURL.RawQuery))
if err != nil {
return nil, err
}
database = &SQLite{
dbo: newDBO,
flogger: flogger,
queries: queries,
}
default:
return nil, fmt.Errorf("Unsupported database scheme: %v", dsnURL.Scheme)
}
// Initialize database scheme if not exists
err = database.Scheme(context.Background())
if err != nil {
return nil, err
}
return database, nil
}

View File

@ -1,746 +0,0 @@
package db
import (
"context"
"database/sql"
"fmt"
"time"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
"git.cryptic.systems/volker.raschek/go-logger"
)
// Postgres implementation
type Postgres struct {
dbo *sql.DB
flogger logger.Logger
queries map[string]string
}
// Close closes the database and prevents new queries from starting. Close then
// waits for all queries that have started processing on the server to finish.
func (postgres *Postgres) Close() error {
return postgres.dbo.Close()
}
// DeleteDevices from the database
func (postgres *Postgres) DeleteDevices(ctx context.Context, deviceIDs ...string) error {
queryFile := "deleteDevice.sql"
query, present := postgres.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return fmt.Errorf("Failed to begin new transaction: %v", err)
}
stmt, err := tx.Prepare(query)
if err != nil {
return fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
for _, deviceID := range deviceIDs {
_, err = stmt.Exec(deviceID)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
// DeleteSensors from the database
func (postgres *Postgres) DeleteSensors(ctx context.Context, sensorIDs ...string) error {
queryFile := "deleteSensor.sql"
query, present := postgres.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return fmt.Errorf("Failed to begin new transaction: %v", err)
}
stmt, err := tx.Prepare(query)
if err != nil {
return fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
for _, sensorID := range sensorIDs {
_, err = stmt.Exec(sensorID)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
// InsertDevices into the database
func (postgres *Postgres) InsertDevices(ctx context.Context, devices ...*types.Device) error {
queryFile := "insertDevice.sql"
query, present := postgres.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return fmt.Errorf("Failed to begin new transaction: %v", err)
}
stmt, err := tx.Prepare(query)
if err != nil {
return fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
for _, device := range devices {
_, err = stmt.Exec(&device.ID, &device.Name, &device.Location, &device.CreationDate, &device.UpdateDate)
if err != nil {
tx.Rollback()
return fmt.Errorf("Failed to execute statement: %v", err)
}
}
return tx.Commit()
}
// InsertMeasuredValues into the database
func (postgres *Postgres) InsertMeasuredValues(ctx context.Context, measuredValues ...*types.MeasuredValue) error {
splittedMeasuredValues := make(map[string][]*types.MeasuredValue, 0)
for _, measuredValue := range measuredValues {
if _, ok := splittedMeasuredValues[measuredValue.ValueType]; !ok {
splittedMeasuredValues[measuredValue.ValueType] = make([]*types.MeasuredValue, 0)
}
splittedMeasuredValues[measuredValue.ValueType] = append(splittedMeasuredValues[measuredValue.ValueType], measuredValue)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return fmt.Errorf("Failed to begin new transaction: %v", err)
}
// General insert function
insert := func(tx *sql.Tx, queryFile string, measuredValues []*types.MeasuredValue) error {
query, present := postgres.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
stmt, err := tx.Prepare(query)
if err != nil {
return fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
for _, measuredValue := range measuredValues {
_, err := stmt.Exec(
&measuredValue.ID,
&measuredValue.Value,
&measuredValue.Date,
&measuredValue.SensorID,
&measuredValue.CreationDate,
&measuredValue.UpdateDate,
)
if err != nil {
return fmt.Errorf("Failed to execute statement: %v", err)
}
}
return nil
}
for measuredValueType, measuredValues := range splittedMeasuredValues {
var queryFile string
switch measuredValueType {
case "humidity":
queryFile = "insertHumidity.sql"
case "pressure":
queryFile = "insertPressure.sql"
case "temperature":
queryFile = "insertTemperature.sql"
default:
tx.Rollback()
return fmt.Errorf("Measured value type %v not supported", measuredValueType)
}
err := insert(tx, queryFile, measuredValues)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
// InsertSensors into the database
func (postgres *Postgres) InsertSensors(ctx context.Context, sensors ...*types.Sensor) error {
queryFile := "insertSensor.sql"
query, present := postgres.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return fmt.Errorf("Failed to begin new transaction: %v", err)
}
stmt, err := tx.Prepare(query)
if err != nil {
return fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
for _, sensor := range sensors {
_, err = stmt.Exec(
&sensor.ID,
&sensor.Name,
&sensor.Location,
&sensor.WireID,
&sensor.I2CBus,
&sensor.I2CAddress,
&sensor.GPIONumber,
&sensor.Model,
&sensor.Enabled,
&sensor.TickDuration,
&sensor.DeviceID,
&sensor.CreationDate,
&sensor.UpdateDate,
)
if err != nil {
tx.Rollback()
return fmt.Errorf("Failed to execute statement: %v", err)
}
}
return tx.Commit()
}
// Scheme creates all required tables if not exist
func (postgres *Postgres) Scheme(ctx context.Context) error {
for _, query := range []string{
postgres.queries["createTableDevices.sql"],
postgres.queries["createTableSensors.sql"],
postgres.queries["createTableHumidities.sql"],
postgres.queries["createTablePressures.sql"],
postgres.queries["createTableTemperatures.sql"],
} {
_, err := postgres.dbo.ExecContext(ctx, query)
if err != nil {
return err
}
}
return nil
}
// SelectDevice from database
func (postgres *Postgres) SelectDevice(ctx context.Context, id string) (*types.Device, error) {
queryFile := "selectDevice.sql"
query, present := postgres.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, fmt.Errorf("Failed to begin new transaction: %v", err)
}
devices, err := postgres.selectDevices(tx, query, id)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
if len(devices) == 0 {
return nil, nil
}
return devices[0], nil
}
// SelectDevices from the database
func (postgres *Postgres) SelectDevices(ctx context.Context) ([]*types.Device, error) {
queryFile := "selectDevices.sql"
query, present := postgres.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, fmt.Errorf("Failed to begin new transaction: %v", err)
}
devices, err := postgres.selectDevices(tx, query)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, fmt.Errorf("Failed to commit transaction: %v", err)
}
return devices, nil
}
func (postgres *Postgres) selectDevices(tx *sql.Tx, query string, args ...interface{}) ([]*types.Device, error) {
stmt, err := tx.Prepare(query)
if err != nil {
return nil, fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
rows, err := stmt.Query(args...)
if err != nil {
return nil, fmt.Errorf("Failed to query statement: %v", err)
}
devices := make([]*types.Device, 0)
for rows.Next() {
device := new(types.Device)
err = rows.Scan(
&device.ID,
&device.Name,
&device.Location,
&device.CreationDate,
&device.UpdateDate,
)
if err != nil {
return nil, fmt.Errorf("Failed to scan row: %v", err)
}
devices = append(devices, device)
}
return devices, nil
}
// SelectHumidity returns humidity from the database
func (postgres *Postgres) SelectHumidity(ctx context.Context, id string) (*types.MeasuredValue, error) {
queryFile := "selectHumidity.sql"
query, present := postgres.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := postgres.selectMeasuredValue(tx, query, id)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
if measuredValues == nil {
return nil, nil
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "humidity"
}
return measuredValues[0], nil
}
// SelectHumidities returns humidities from the database
func (postgres *Postgres) SelectHumidities(ctx context.Context) ([]*types.MeasuredValue, error) {
queryFile := "selectHumidities.sql"
query, present := postgres.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := postgres.selectMeasuredValue(tx, query, nil)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "humidity"
}
return measuredValues, nil
}
func (postgres *Postgres) selectMeasuredValue(tx *sql.Tx, query string, args ...interface{}) ([]*types.MeasuredValue, error) {
stmt, err := tx.Prepare(query)
if err != nil {
tx.Rollback()
return nil, err
}
defer stmt.Close()
rows, err := stmt.Query(args...)
if err != nil {
tx.Rollback()
return nil, err
}
measuredValues := make([]*types.MeasuredValue, 0)
for rows.Next() {
measuredValue := new(types.MeasuredValue)
err := rows.Scan(
&measuredValue.ID,
&measuredValue.Value,
&measuredValue.SensorID,
&measuredValue.Date,
&measuredValue.CreationDate,
&measuredValue.UpdateDate,
)
if err != nil {
tx.Rollback()
return nil, err
}
measuredValues = append(measuredValues, measuredValue)
}
return measuredValues, nil
}
// SelectPressure returns pressure from the database
func (postgres *Postgres) SelectPressure(ctx context.Context, id string) (*types.MeasuredValue, error) {
queryFile := "selectPressure.sql"
query, present := postgres.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := postgres.selectMeasuredValue(tx, query, id)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
if measuredValues == nil {
return nil, nil
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "pressure"
}
return measuredValues[0], nil
}
// SelectPressures returns pressure from the database
func (postgres *Postgres) SelectPressures(ctx context.Context) ([]*types.MeasuredValue, error) {
queryFile := "selectPressures.sql"
query, present := postgres.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := postgres.selectMeasuredValue(tx, query, nil)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "pressure"
}
return measuredValues, nil
}
// SelectSensor from database
func (postgres *Postgres) SelectSensor(ctx context.Context, id string) (*types.Sensor, error) {
queryFile := "selectSensor.sql"
query, present := postgres.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, fmt.Errorf("Failed to begin new transaction: %v", err)
}
sensors, err := postgres.selectSensors(tx, query, id)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, fmt.Errorf("Failed to commit transaction: %v", err)
}
if len(sensors) == 0 {
return nil, nil
}
return sensors[0], nil
}
// SelectSensors from the database
func (postgres *Postgres) SelectSensors(ctx context.Context) ([]*types.Sensor, error) {
queryFile := "selectSensors.sql"
query, present := postgres.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, fmt.Errorf("Failed to begin new transaction: %v", err)
}
sensors, err := postgres.selectSensors(tx, query)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, fmt.Errorf("Failed to commit transaction: %v", err)
}
return sensors, nil
}
func (postgres *Postgres) selectSensors(tx *sql.Tx, query string, args ...interface{}) ([]*types.Sensor, error) {
stmt, err := tx.Prepare(query)
if err != nil {
return nil, fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
rows, err := stmt.Query(args...)
if err != nil {
return nil, fmt.Errorf("Failed to query statement: %v", err)
}
sensors := make([]*types.Sensor, 0)
for rows.Next() {
sensor := new(types.Sensor)
err = rows.Scan(
&sensor.ID,
&sensor.Name,
&sensor.Location,
&sensor.WireID,
&sensor.I2CBus,
&sensor.I2CAddress,
&sensor.GPIONumber,
&sensor.Model,
&sensor.Enabled,
&sensor.TickDuration,
&sensor.DeviceID,
&sensor.CreationDate,
&sensor.UpdateDate,
)
if err != nil {
return nil, fmt.Errorf("Failed to scan row: %v", err)
}
sensors = append(sensors, sensor)
}
return sensors, nil
}
// SelectTemperature returns temperatures from the database
func (postgres *Postgres) SelectTemperature(ctx context.Context, id string) (*types.MeasuredValue, error) {
queryFile := "selectTemperature.sql"
query, present := postgres.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := postgres.selectMeasuredValue(tx, query, id)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
if measuredValues == nil {
return nil, nil
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "temperatures"
}
return measuredValues[0], nil
}
// SelectTemperatures returns temperatures from the database
func (postgres *Postgres) SelectTemperatures(ctx context.Context) ([]*types.MeasuredValue, error) {
queryFile := "selectTemperatures.sql"
query, present := postgres.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := postgres.selectMeasuredValue(tx, query, nil)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "temperatures"
}
return measuredValues, nil
}
// UpdateDevices updates a device in the database
func (postgres *Postgres) UpdateDevices(ctx context.Context, devices ...*types.Device) error {
queryFile := "updateDevice.sql"
query, present := postgres.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return err
}
stmt, err := tx.Prepare(query)
if err != nil {
tx.Rollback()
return err
}
defer stmt.Close()
for _, device := range devices {
now := time.Now()
device.UpdateDate = &now
_, err := stmt.Exec(
&device.Name,
&device.Location,
&device.CreationDate,
&device.UpdateDate,
&device.ID,
)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
// UpdateSensors updates a sensor in the database
func (postgres *Postgres) UpdateSensors(ctx context.Context, sensors ...*types.Sensor) error {
queryFile := "updateSensor.sql"
query, present := postgres.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := postgres.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return err
}
stmt, err := tx.Prepare(query)
if err != nil {
tx.Rollback()
return err
}
defer stmt.Close()
for _, sensor := range sensors {
now := time.Now()
sensor.UpdateDate = &now
_, err := stmt.Exec(
&sensor.Name,
&sensor.Location,
&sensor.WireID,
&sensor.I2CBus,
&sensor.I2CAddress,
&sensor.GPIONumber,
&sensor.Model,
&sensor.Enabled,
&sensor.TickDuration,
&sensor.DeviceID,
&sensor.CreationDate,
&sensor.UpdateDate,
&sensor.ID,
)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}

View File

@ -1,7 +0,0 @@
CREATE TABLE IF NOT EXISTS devices (
device_id CHAR(36) CONSTRAINT pk_devices PRIMARY KEY,
device_name VARCHAR(64) NOT NULL,
device_location VARCHAR(64),
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP
);

View File

@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS humidities (
id CHAR(36) CONSTRAINT pk_humidities PRIMARY KEY,
value NUMERIC(10,3) NOT NULL,
date TIMESTAMP NOT NULL,
sensor_id CHAR(36) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP
);
ALTER TABLE humidities
ADD FOREIGN KEY (sensor_id)
REFERENCES sensors(sensor_id)
ON DELETE CASCADE
ON UPDATE CASCADE;

View File

@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS pressures (
id CHAR(36) CONSTRAINT pk_pressures PRIMARY KEY,
value NUMERIC(10,3) NOT NULL,
date TIMESTAMP NOT NULL,
sensor_id CHAR(36) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP
);
ALTER TABLE pressures
ADD FOREIGN KEY (sensor_id)
REFERENCES sensors(sensor_id)
ON DELETE CASCADE
ON UPDATE CASCADE;

View File

@ -1,21 +0,0 @@
CREATE TABLE IF NOT EXISTS sensors (
sensor_id CHAR(36) CONSTRAINT pk_sensors PRIMARY KEY,
sensor_name VARCHAR(64) NOT NULL,
sensor_location VARCHAR(64),
wire_id VARCHAR(64),
i2c_bus VARCHAR(255),
i2c_address VARCHAR(12),
gpio_number VARCHAR(6),
sensor_model VARCHAR(16) NOT NULL,
sensor_enabled BOOLEAN DEFAULT TRUE NOT NULL,
tick_duration VARCHAR(6) NOT NULL,
device_id CHAR(36) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP
);
ALTER TABLE sensors
ADD FOREIGN KEY (device_id)
REFERENCES devices(device_id)
ON DELETE CASCADE
ON UPDATE CASCADE;

View File

@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS temperatures (
id CHAR(36) CONSTRAINT pk_temperatures PRIMARY KEY,
value NUMERIC(10,3) NOT NULL,
date TIMESTAMP NOT NULL,
sensor_id CHAR(36) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP
);
ALTER TABLE temperatures
ADD FOREIGN KEY (sensor_id)
REFERENCES sensors(sensor_id)
ON DELETE CASCADE
ON UPDATE CASCADE;

View File

@ -1,2 +0,0 @@
DELETE FROM devices
WHERE device_id = $1;

View File

@ -1,2 +0,0 @@
DELETE FROM sensors
WHERE sensor_id = $1;

View File

@ -1,8 +0,0 @@
INSERT INTO devices (
device_id,
device_name,
device_location,
creation_date,
update_date
)
VALUES ($1, $2, $3, $4, $5);

View File

@ -1,9 +0,0 @@
INSERT INTO humidities (
id,
value,
date,
sensor_id,
creation_date,
update_date
)
VALUES ($1, $2, $3, $4, $5, $6);

View File

@ -1,9 +0,0 @@
INSERT INTO pressures (
id,
value,
date,
sensor_id,
creation_date,
update_date
)
VALUES ($1, $2, $3, $4, $5, $6);

View File

@ -1,16 +0,0 @@
INSERT INTO sensors (
sensor_id,
sensor_name,
sensor_location,
wire_id,
i2c_bus,
i2c_address,
gpio_number,
sensor_model,
sensor_enabled,
tick_duration,
device_id,
creation_date,
update_date
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13);

View File

@ -1,9 +0,0 @@
INSERT INTO temperatures (
id,
value,
date,
sensor_id,
creation_date,
update_date
)
VALUES ($1, $2, $3, $4, $5, $6);

View File

@ -1,9 +0,0 @@
SELECT
device_id,
device_name,
device_location,
creation_date,
update_date
FROM
devices
WHERE device_id = $1;

View File

@ -1,10 +0,0 @@
SELECT
device_id,
device_name,
device_location,
creation_date,
update_date
FROM
devices
ORDER BY
device_id ASC;

View File

@ -1,9 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
humidities;

View File

@ -1,11 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
humidities
WHERE
humidity_id = $1

View File

@ -1,11 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
pressures
WHERE
pressure_id = $1

View File

@ -1,9 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
pressures;

View File

@ -1,18 +0,0 @@
SELECT
sensor_id,
sensor_name,
sensor_location,
wire_id,
i2c_bus,
i2c_address,
gpio_number,
sensor_model,
sensor_enabled,
tick_duration,
device_id,
creation_date,
update_date
FROM
sensors
WHERE
sensor_id = $1;

View File

@ -1,18 +0,0 @@
SELECT
sensor_id,
sensor_name,
sensor_location,
wire_id,
i2c_bus,
i2c_address,
gpio_number,
sensor_model,
sensor_enabled,
tick_duration,
device_id,
creation_date,
update_date
FROM
sensors
ORDER BY
sensor_id ASC;

View File

@ -1,11 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
temperatures
WHERE
temperature_id = $1

View File

@ -1,9 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
temperatures

View File

@ -1,8 +0,0 @@
UPDATE devices
SET
device_name = $1,
device_location = $2,
creation_date = $3,
update_date = $4
WHERE
device_id = $5

View File

@ -1,16 +0,0 @@
UPDATE sensors
SET
sensor_name = $1,
sensor_location = $2,
wire_id = $3,
i2c_bus = $4,
i2c_address = $5,
gpio_number = $6,
sensor_model = $7,
sensor_enabled = $8,
tick_duration = $9,
device_id = $10,
creation_date = $11,
update_date = $12
WHERE
sensor_id = $13;

View File

@ -1,740 +0,0 @@
package db
import (
"context"
"database/sql"
"fmt"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
"git.cryptic.systems/volker.raschek/go-logger"
)
// SQLite implementation
type SQLite struct {
dbo *sql.DB
flogger logger.Logger
queries map[string]string
}
// Close closes the database and prevents new queries from starting. Close then
// waits for all queries that have started processing on the server to finish.
func (sqlite *SQLite) Close() error {
return sqlite.dbo.Close()
}
// DeleteDevices from the database
func (sqlite *SQLite) DeleteDevices(ctx context.Context, deviceIDs ...string) error {
queryFile := "deleteDevice.sql"
query, present := sqlite.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return fmt.Errorf("Failed to begin new transaction: %v", err)
}
stmt, err := tx.Prepare(query)
if err != nil {
return fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
for _, deviceID := range deviceIDs {
_, err = stmt.Exec(deviceID)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
// DeleteSensors from the database
func (sqlite *SQLite) DeleteSensors(ctx context.Context, sensorIDs ...string) error {
queryFile := "deleteSensor.sql"
query, present := sqlite.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return fmt.Errorf("Failed to begin new transaction: %v", err)
}
stmt, err := tx.Prepare(query)
if err != nil {
return fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
for _, sensorID := range sensorIDs {
_, err = stmt.Exec(sensorID)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
// InsertDevices into the database
func (sqlite *SQLite) InsertDevices(ctx context.Context, devices ...*types.Device) error {
queryFile := "insertDevice.sql"
query, present := sqlite.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return fmt.Errorf("Failed to begin new transaction: %v", err)
}
stmt, err := tx.Prepare(query)
if err != nil {
return fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
for _, device := range devices {
_, err = stmt.Exec(&device.ID, &device.Name, &device.Location, &device.CreationDate, &device.UpdateDate)
if err != nil {
tx.Rollback()
return fmt.Errorf("Failed to execute statement: %v", err)
}
}
return tx.Commit()
}
// InsertMeasuredValues into the database
func (sqlite *SQLite) InsertMeasuredValues(ctx context.Context, measuredValues ...*types.MeasuredValue) error {
splittedMeasuredValues := make(map[string][]*types.MeasuredValue, 0)
for _, measuredValue := range measuredValues {
if _, ok := splittedMeasuredValues[measuredValue.ValueType]; !ok {
splittedMeasuredValues[measuredValue.ValueType] = make([]*types.MeasuredValue, 0)
}
splittedMeasuredValues[measuredValue.ValueType] = append(splittedMeasuredValues[measuredValue.ValueType], measuredValue)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return fmt.Errorf("Failed to begin new transaction: %v", err)
}
// General insert function
insert := func(tx *sql.Tx, queryFile string, measuredValues []*types.MeasuredValue) error {
query, present := sqlite.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
stmt, err := tx.Prepare(query)
if err != nil {
return fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
for _, measuredValue := range measuredValues {
_, err := stmt.Exec(
&measuredValue.ID,
&measuredValue.Value,
&measuredValue.Date,
&measuredValue.SensorID,
&measuredValue.CreationDate,
&measuredValue.UpdateDate,
)
if err != nil {
return fmt.Errorf("Failed to execute statement: %v", err)
}
}
return nil
}
for measuredValueType, measuredValues := range splittedMeasuredValues {
var queryFile string
switch measuredValueType {
case "humidity":
queryFile = "insertHumidity.sql"
case "pressure":
queryFile = "insertPressure.sql"
case "temperature":
queryFile = "insertTemperature.sql"
default:
tx.Rollback()
return fmt.Errorf("Measured value type %v not supported", measuredValueType)
}
err := insert(tx, queryFile, measuredValues)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
// InsertSensors into the database
func (sqlite *SQLite) InsertSensors(ctx context.Context, sensors ...*types.Sensor) error {
queryFile := "insertSensor.sql"
query, present := sqlite.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return fmt.Errorf("Failed to begin new transaction: %v", err)
}
stmt, err := tx.Prepare(query)
if err != nil {
return fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
for _, sensor := range sensors {
_, err = stmt.Exec(
&sensor.ID,
&sensor.Name,
&sensor.Location,
&sensor.WireID,
&sensor.I2CBus,
&sensor.I2CAddress,
&sensor.GPIONumber,
&sensor.Model,
&sensor.Enabled,
&sensor.TickDuration,
&sensor.DeviceID,
&sensor.CreationDate,
&sensor.UpdateDate,
)
if err != nil {
tx.Rollback()
return fmt.Errorf("Failed to execute statement: %v", err)
}
}
return tx.Commit()
}
// Scheme creates all required tables if not exist
func (sqlite *SQLite) Scheme(ctx context.Context) error {
for _, query := range []string{
sqlite.queries["createTableDevices.sql"],
sqlite.queries["createTableSensors.sql"],
sqlite.queries["createTableHumidities.sql"],
sqlite.queries["createTablePressures.sql"],
sqlite.queries["createTableTemperatures.sql"],
} {
_, err := sqlite.dbo.ExecContext(ctx, query)
if err != nil {
return err
}
}
return nil
}
// SelectDevice from database
func (sqlite *SQLite) SelectDevice(ctx context.Context, id string) (*types.Device, error) {
queryFile := "selectDevice.sql"
query, present := sqlite.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, fmt.Errorf("Failed to begin new transaction: %v", err)
}
devices, err := sqlite.selectDevices(tx, query, id)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
if len(devices) == 0 {
return nil, nil
}
return devices[0], nil
}
// SelectDevices from the database
func (sqlite *SQLite) SelectDevices(ctx context.Context) ([]*types.Device, error) {
queryFile := "selectDevices.sql"
query, present := sqlite.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, fmt.Errorf("Failed to begin new transaction: %v", err)
}
devices, err := sqlite.selectDevices(tx, query)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, fmt.Errorf("Failed to commit transaction: %v", err)
}
return devices, nil
}
func (sqlite *SQLite) selectDevices(tx *sql.Tx, query string, args ...interface{}) ([]*types.Device, error) {
stmt, err := tx.Prepare(query)
if err != nil {
return nil, fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
rows, err := stmt.Query(args...)
if err != nil {
return nil, fmt.Errorf("Failed to query statement: %v", err)
}
devices := make([]*types.Device, 0)
for rows.Next() {
device := new(types.Device)
err = rows.Scan(
&device.ID,
&device.Name,
&device.Location,
&device.CreationDate,
&device.UpdateDate,
)
if err != nil {
return nil, fmt.Errorf("Failed to scan row: %v", err)
}
devices = append(devices, device)
}
return devices, nil
}
// SelectHumidity returns humidity from the database
func (sqlite *SQLite) SelectHumidity(ctx context.Context, id string) (*types.MeasuredValue, error) {
queryFile := "selectHumidity.sql"
query, present := sqlite.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := sqlite.selectMeasuredValue(tx, query, id)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
if measuredValues == nil {
return nil, nil
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "humidity"
}
return measuredValues[0], nil
}
// SelectHumidities returns humidities from the database
func (sqlite *SQLite) SelectHumidities(ctx context.Context) ([]*types.MeasuredValue, error) {
queryFile := "selectHumidities.sql"
query, present := sqlite.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := sqlite.selectMeasuredValue(tx, query, nil)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "humidity"
}
return measuredValues, nil
}
func (sqlite *SQLite) selectMeasuredValue(tx *sql.Tx, query string, args ...interface{}) ([]*types.MeasuredValue, error) {
stmt, err := tx.Prepare(query)
if err != nil {
tx.Rollback()
return nil, err
}
defer stmt.Close()
rows, err := stmt.Query(args...)
if err != nil {
tx.Rollback()
return nil, err
}
measuredValues := make([]*types.MeasuredValue, 0)
for rows.Next() {
measuredValue := new(types.MeasuredValue)
err := rows.Scan(
&measuredValue.ID,
&measuredValue.Value,
&measuredValue.SensorID,
&measuredValue.Date,
&measuredValue.CreationDate,
&measuredValue.UpdateDate,
)
if err != nil {
tx.Rollback()
return nil, err
}
measuredValues = append(measuredValues, measuredValue)
}
return measuredValues, nil
}
// SelectPressure returns pressure from the database
func (sqlite *SQLite) SelectPressure(ctx context.Context, id string) (*types.MeasuredValue, error) {
queryFile := "selectPressure.sql"
query, present := sqlite.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := sqlite.selectMeasuredValue(tx, query, id)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
if measuredValues == nil {
return nil, nil
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "pressure"
}
return measuredValues[0], nil
}
// SelectPressures returns pressure from the database
func (sqlite *SQLite) SelectPressures(ctx context.Context) ([]*types.MeasuredValue, error) {
queryFile := "selectPressures.sql"
query, present := sqlite.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := sqlite.selectMeasuredValue(tx, query, nil)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "pressure"
}
return measuredValues, nil
}
// SelectSensor from database
func (sqlite *SQLite) SelectSensor(ctx context.Context, id string) (*types.Sensor, error) {
queryFile := "selectSensor.sql"
query, present := sqlite.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, fmt.Errorf("Failed to begin new transaction: %v", err)
}
sensors, err := sqlite.selectSensors(tx, query, id)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, fmt.Errorf("Failed to commit transaction: %v", err)
}
if len(sensors) == 0 {
return nil, nil
}
return sensors[0], nil
}
// SelectSensors from the database
func (sqlite *SQLite) SelectSensors(ctx context.Context) ([]*types.Sensor, error) {
queryFile := "selectSensors.sql"
query, present := sqlite.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, fmt.Errorf("Failed to begin new transaction: %v", err)
}
sensors, err := sqlite.selectSensors(tx, query)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, fmt.Errorf("Failed to commit transaction: %v", err)
}
return sensors, nil
}
func (sqlite *SQLite) selectSensors(tx *sql.Tx, query string, args ...interface{}) ([]*types.Sensor, error) {
stmt, err := tx.Prepare(query)
if err != nil {
return nil, fmt.Errorf("Failed to prepare statement: %v", err)
}
defer stmt.Close()
rows, err := stmt.Query(args...)
if err != nil {
return nil, fmt.Errorf("Failed to query statement: %v", err)
}
sensors := make([]*types.Sensor, 0)
for rows.Next() {
sensor := new(types.Sensor)
err = rows.Scan(
&sensor.ID,
&sensor.Name,
&sensor.Location,
&sensor.WireID,
&sensor.I2CBus,
&sensor.I2CAddress,
&sensor.GPIONumber,
&sensor.Model,
&sensor.Enabled,
&sensor.TickDuration,
&sensor.DeviceID,
&sensor.CreationDate,
&sensor.UpdateDate,
)
if err != nil {
return nil, fmt.Errorf("Failed to scan row: %v", err)
}
sensors = append(sensors, sensor)
}
return sensors, nil
}
// SelectTemperature returns temperatures from the database
func (sqlite *SQLite) SelectTemperature(ctx context.Context, id string) (*types.MeasuredValue, error) {
queryFile := "selectTemperature.sql"
query, present := sqlite.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := sqlite.selectMeasuredValue(tx, query, id)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
if measuredValues == nil {
return nil, nil
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "temperatures"
}
return measuredValues[0], nil
}
// SelectTemperatures returns temperatures from the database
func (sqlite *SQLite) SelectTemperatures(ctx context.Context) ([]*types.MeasuredValue, error) {
queryFile := "selectTemperatures.sql"
query, present := sqlite.queries[queryFile]
if !present {
return nil, fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
measuredValues, err := sqlite.selectMeasuredValue(tx, query, nil)
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
for _, measuredValue := range measuredValues {
measuredValue.ValueType = "temperatures"
}
return measuredValues, nil
}
// UpdateDevices updates a device in the database
func (sqlite *SQLite) UpdateDevices(ctx context.Context, devices ...*types.Device) error {
queryFile := "updateDevice.sql"
query, present := sqlite.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return err
}
stmt, err := tx.Prepare(query)
if err != nil {
tx.Rollback()
return err
}
defer stmt.Close()
for _, device := range devices {
_, err := stmt.Exec(
&device.Name,
&device.Location,
&device.CreationDate,
&device.UpdateDate,
&device.ID,
)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
// UpdateSensors updates a sensor in the database
func (sqlite *SQLite) UpdateSensors(ctx context.Context, sensors ...*types.Sensor) error {
queryFile := "updateSensor.sql"
query, present := sqlite.queries[queryFile]
if !present {
return fmt.Errorf("SQLite-Backend: File %v not found", queryFile)
}
tx, err := sqlite.dbo.BeginTx(ctx, &sql.TxOptions{ReadOnly: false})
if err != nil {
return err
}
stmt, err := tx.Prepare(query)
if err != nil {
tx.Rollback()
return err
}
defer stmt.Close()
for _, sensor := range sensors {
_, err := stmt.Exec(
&sensor.Name,
&sensor.Location,
&sensor.WireID,
&sensor.I2CBus,
&sensor.I2CAddress,
&sensor.GPIONumber,
&sensor.Model,
&sensor.Enabled,
&sensor.TickDuration,
&sensor.DeviceID,
&sensor.CreationDate,
&sensor.UpdateDate,
&sensor.ID,
)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}

View File

@ -1,7 +0,0 @@
CREATE TABLE IF NOT EXISTS devices (
device_id CHAR(36) PRIMARY KEY,
device_name VARCHAR(64) NOT NULL,
device_location VARCHAR(64),
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP
);

View File

@ -1,9 +0,0 @@
CREATE TABLE IF NOT EXISTS humidities (
id CHAR(36) PRIMARY KEY,
value NUMERIC(10,3) NOT NULL,
date TIMESTAMP NOT NULL,
sensor_id CHAR(36) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP,
FOREIGN KEY(sensor_id) REFERENCES sensors(sensor_id)
);

View File

@ -1,9 +0,0 @@
CREATE TABLE IF NOT EXISTS pressures (
id CHAR(36) PRIMARY KEY,
value NUMERIC(10,3) NOT NULL,
date TIMESTAMP NOT NULL,
sensor_id CHAR(36) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP,
FOREIGN KEY(sensor_id) REFERENCES sensors(sensor_id)
);

View File

@ -1,16 +0,0 @@
CREATE TABLE IF NOT EXISTS sensors (
sensor_id CHAR(36) PRIMARY KEY,
sensor_name VARCHAR(64) NOT NULL,
sensor_location VARCHAR(64),
wire_id VARCHAR(64),
i2c_bus VARCHAR(255),
i2c_address VARCHAR(12),
gpio_number VARCHAR(6),
sensor_model VARCHAR(16) NOT NULL,
sensor_enabled INTEGER(1) DEFAULT 1 NOT NULL,
tick_duration VARCHAR(6) NOT NULL,
device_id CHAR(36) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_date TIMESTAMP,
FOREIGN KEY(device_id) REFERENCES devices(device_id)
);

View File

@ -1,9 +0,0 @@
CREATE TABLE IF NOT EXISTS temperatures (
id CHAR(36) PRIMARY KEY,
value NUMERIC(10,3) NOT NULL,
date TIMESTAMP NOT NULL,
sensor_id CHAR(36) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP,
FOREIGN KEY(sensor_id) REFERENCES sensors(sensor_id)
);

View File

@ -1,2 +0,0 @@
DELETE FROM devices
WHERE device_id = $1;

View File

@ -1,2 +0,0 @@
DELETE FROM sensors
WHERE sensor_id = $1;

View File

@ -1,8 +0,0 @@
INSERT INTO devices (
device_id,
device_name,
device_location,
creation_date,
update_date
)
VALUES ($1, $2, $3, $4, $5);

View File

@ -1,9 +0,0 @@
INSERT INTO humidities (
id,
value,
date,
sensor_id,
creation_date,
update_date
)
VALUES ($1, $2, $3, $4, $5, $6);

View File

@ -1,9 +0,0 @@
INSERT INTO pressures (
id,
value,
date,
sensor_id,
creation_date,
update_date
)
VALUES ($1, $2, $3, $4, $5, $6);

View File

@ -1,16 +0,0 @@
INSERT INTO sensors (
sensor_id,
sensor_name,
sensor_location,
wire_id,
i2c_bus,
i2c_address,
gpio_number,
sensor_model,
sensor_enabled,
tick_duration,
device_id,
creation_date,
update_date
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13);

View File

@ -1,9 +0,0 @@
INSERT INTO temperatures (
id,
value,
date,
sensor_id,
creation_date,
update_date
)
VALUES ($1, $2, $3, $4, $5, $6);

View File

@ -1,9 +0,0 @@
SELECT
device_id,
device_name,
device_location,
creation_date,
update_date
FROM
devices
WHERE device_id = $1;

View File

@ -1,8 +0,0 @@
SELECT
device_id,
device_name,
device_location,
creation_date,
update_date
FROM
devices;

View File

@ -1,9 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
humidities

View File

@ -1,11 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
humidities
WHERE
humidity_id = $1

View File

@ -1,11 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
pressures
WHERE
pressure_id = $1

View File

@ -1,9 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
pressures

View File

@ -1,18 +0,0 @@
SELECT
sensor_id,
sensor_name,
sensor_location,
wire_id,
i2c_bus,
i2c_address,
gpio_number,
sensor_model,
sensor_enabled,
tick_duration,
device_id,
creation_date,
update_date
FROM
sensors
WHERE
sensor_id = $1;

View File

@ -1,18 +0,0 @@
SELECT
sensor_id,
sensor_name,
sensor_location,
wire_id,
i2c_bus,
i2c_address,
gpio_number,
sensor_model,
sensor_enabled,
tick_duration,
device_id,
creation_date,
update_date
FROM
sensors
ORDER BY
sensor_id;

View File

@ -1,11 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
temperatures
WHERE
temperature_id = $1

View File

@ -1,9 +0,0 @@
SELECT
id,
value,
date,
sensor_id,
creation_date,
update_date
FROM
temperatures

View File

@ -1,8 +0,0 @@
UPDATE devices
SET
device_name = $1,
device_location = $2,
creation_date = $3,
update_date = $4
WHERE
device_id = $5

View File

@ -1,16 +0,0 @@
UPDATE sensors
SET
sensor_name = $1,
sensor_location = $2,
wire_id = $3,
i2c_bus = $4,
i2c_address = $5,
gpio_number = $6,
sensor_model = $7,
sensor_enabled = $8,
tick_duration = $9,
device_id = $10,
creation_date = $11,
update_date = $12
WHERE
sensor_id = $13;

View File

@ -1,196 +0,0 @@
package repository
import (
"context"
"net/url"
"strings"
"git.cryptic.systems/volker.raschek/flucky/pkg/repository/db"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
"git.cryptic.systems/volker.raschek/go-logger"
)
// Repository represent a repository where all devices, sensors and measured
// values are stored.
type Repository struct {
database db.Database
}
// AddDevices to the repository
func (repo *Repository) AddDevices(devices ...*types.Device) error {
return repo.database.InsertDevices(context.Background(), devices...)
}
// AddMeasuredValues to the repository
func (repo *Repository) AddMeasuredValues(measuredValues ...*types.MeasuredValue) error {
return repo.database.InsertMeasuredValues(context.Background(), measuredValues...)
}
// AddSensors to the repository
func (repo *Repository) AddSensors(sensors ...*types.Sensor) error {
return repo.database.InsertSensors(context.Background(), sensors...)
}
// Close closes the repository and prevents new queries from starting. Close
// then waits for all queries that have started processing on the server to
// finish.
func (repo *Repository) Close() error {
return repo.database.Close()
}
// DisableSensorsByNames disable all sensors which match bei their name
func (repo *Repository) DisableSensorsByNames(sensorNames ...string) error {
sensors, err := repo.GetSensors()
if err != nil {
return err
}
matchedSensors := make([]*types.Sensor, 0)
for _, sensor := range sensors {
for _, sensorName := range sensorNames {
if strings.Compare(sensor.Name, sensorName) == 0 {
sensor.Enabled = false
matchedSensors = append(matchedSensors, sensor)
}
}
}
return repo.UpdateSensors(matchedSensors...)
}
// EnableSensorsByNames enable all sensors which match bei their name
func (repo *Repository) EnableSensorsByNames(sensorNames ...string) error {
sensors, err := repo.GetSensors()
if err != nil {
return err
}
matchedSensors := make([]*types.Sensor, 0)
for _, sensor := range sensors {
for _, sensorName := range sensorNames {
if strings.Compare(sensor.Name, sensorName) == 0 {
sensor.Enabled = true
matchedSensors = append(matchedSensors, sensor)
}
}
}
return repo.UpdateSensors(matchedSensors...)
}
// GetDevice returns a device by his id. If no device has been found, the
// function returns nil.
func (repo *Repository) GetDevice(deviceID string) (*types.Device, error) {
return repo.database.SelectDevice(context.Background(), deviceID)
}
// GetDevices returns all devices. If no devices has been found, the function
// returns nil.
func (repo *Repository) GetDevices() ([]*types.Device, error) {
return repo.database.SelectDevices(context.Background())
}
// GetSensor returns a sensor by his id. If no sensor has been found, the
// function returns nil.
func (repo *Repository) GetSensor(sensorID string) (*types.Sensor, error) {
return repo.database.SelectSensor(context.Background(), sensorID)
}
// GetSensors returns all sensors. If no sensors has been found, the function
// returns nil.
func (repo *Repository) GetSensors() ([]*types.Sensor, error) {
return repo.database.SelectSensors(context.Background())
}
// GetSensorsByDeviceID returns all sensors by a device id. If no sensor has
// been found by the id, the function returns nil.
func (repo *Repository) GetSensorsByDeviceID(deviceID string) ([]*types.Sensor, error) {
cachedSensors := make([]*types.Sensor, 0)
sensors, err := repo.GetSensors()
if err != nil {
return nil, err
}
for _, sensor := range sensors {
if strings.Compare(sensor.DeviceID, deviceID) == 0 {
cachedSensors = append(cachedSensors, sensor)
}
}
return cachedSensors, nil
}
// RemoveDevices removes devices by their ids from the repository. Additional
// all sensors and measured values, which are in relation with the device
// respectively the sensors will also be deleted.
func (repo *Repository) RemoveDevices(deviceIDs ...string) error {
return repo.database.DeleteDevices(context.Background(), deviceIDs...)
}
// RenameSensors all sensors which match by their current name to the new name
func (repo *Repository) RenameSensors(oldName string, newName string) error {
sensors, err := repo.GetSensors()
if err != nil {
return err
}
matchedSensors := make([]*types.Sensor, 0)
for _, sensor := range sensors {
if strings.Compare(sensor.Name, oldName) == 0 {
sensor.Name = newName
matchedSensors = append(matchedSensors, sensor)
}
}
return repo.UpdateSensors(matchedSensors...)
}
// RemoveSensors removes sensors by their ids from the repository. Additional
// all measured values, which are in relation with the sensor will also be
// deleted.
func (repo *Repository) RemoveSensors(sensorIDs ...string) error {
return repo.database.DeleteSensors(context.Background(), sensorIDs...)
}
// RemoveSensorsByNames removes all sensors which match bei their name
func (repo *Repository) RemoveSensorsByNames(sensorNames ...string) error {
sensors, err := repo.GetSensors()
if err != nil {
return err
}
matchedSensorIDs := make([]string, 0)
for _, sensor := range sensors {
for _, sensorName := range sensorNames {
if strings.Compare(sensor.Name, sensorName) == 0 {
matchedSensorIDs = append(matchedSensorIDs, sensor.ID)
}
}
}
return repo.RemoveSensors(matchedSensorIDs...)
}
// UpdateDevices update devices which are stored into the repository by their
// id. The id of the device can not be updated.
func (repo *Repository) UpdateDevices(devices ...*types.Device) error {
return repo.database.UpdateDevices(context.Background(), devices...)
}
// UpdateSensors update sensors which are stored into the repository by their
// id. The id of the sensor can not be updated.
func (repo *Repository) UpdateSensors(sensors ...*types.Sensor) error {
return repo.database.UpdateSensors(context.Background(), sensors...)
}
// New returns a new repository based on the data source name (dsn)
func New(dsnURL *url.URL, flogger logger.Logger) (*Repository, error) {
database, err := db.New(dsnURL, flogger)
if err != nil {
return nil, err
}
return &Repository{
database: database,
}, nil
}

View File

@ -1,326 +0,0 @@
package repository_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"math/rand"
"net/url"
"os"
"path/filepath"
"testing"
"time"
"git.cryptic.systems/volker.raschek/flucky/pkg/repository"
"git.cryptic.systems/volker.raschek/flucky/pkg/testutils/dockerutils"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
"git.cryptic.systems/volker.raschek/go-logger"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/require"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
func TestPostgresBackend(t *testing.T) {
require := require.New(t)
dockerClient, err := dockerutils.New()
require.NoError(err)
rand.Seed(time.Now().Unix())
postgresHostPort := rand.Intn(10024-1024) + 1024
postgresDBPasswort := "postgres"
postgresContainerID, err := dockerClient.NewBuilder("postgres:13-alpine").
Port(fmt.Sprintf("%v:5432/tcp", postgresHostPort)).
Pull().
AddEnv("PGTZ", "Europe/Berlin").
AddEnv("POSTGRES_PASSWORD", postgresDBPasswort).
AddEnv("TZ", "Europe/Berlin").
Mount("/etc/localtime", "/etc/localtime").
Start(context.Background())
cleanup := func() {
dockerClient.ContainerRemoveByIDs(context.Background(), postgresContainerID)
}
t.Cleanup(cleanup)
require.NoError(err)
time.Sleep(time.Second * 10)
// postgres://[user]:[password]@[host]:[port]/[path]?[query]
dsnURL, err := url.Parse(fmt.Sprintf("postgres://postgres:%v@127.0.0.1:%v?sslmode=disable", postgresDBPasswort, postgresHostPort))
require.NoError(err)
repo, err := repository.New(dsnURL, logger.NewLogger(logger.LogLevelDebug))
require.NoError(err)
testBackend(t, repo)
}
func TestSQLiteBackend(t *testing.T) {
require := require.New(t)
workspace := filepath.Join(os.TempDir(), uuid.NewV4().String())
err := os.MkdirAll(workspace, 0755)
require.NoError(err)
t.Cleanup(func() { os.RemoveAll(workspace) })
dsnURL, err := url.Parse(fmt.Sprintf("sqlite3://%v/test.db", workspace))
require.NoError(err)
repo, err := repository.New(dsnURL, logger.NewLogger(logger.LogLevelDebug))
require.NoError(err)
testBackend(t, repo)
}
func testBackend(t *testing.T, repo *repository.Repository) {
require := require.New(t)
location := uuid.NewV4().String()
expectedDevices := []*types.Device{
{
ID: "39b8f150-8abf-4539-9f16-7f68cedb1649",
Name: "62e3978f-2198-4aa9-9d6f-cdc91a468b00",
Location: &location,
CreationDate: *timeNow(require),
},
{
ID: "ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f",
Name: "f2b245eb-b15f-40e1-9212-9a645907b710",
Location: &location,
CreationDate: *timeNow(require),
},
}
// Test: AddDevice
err := repo.AddDevices(expectedDevices...)
require.NoError(err)
// Test: GetDevices
devices, err := repo.GetDevices()
require.NoError(err)
require.Len(devices, len(expectedDevices))
require.JSONEq(jsonEncoder(expectedDevices), jsonEncoder(devices))
// Test: GetDevice
device, err := repo.GetDevice(expectedDevices[0].ID)
require.NoError(err)
require.JSONEq(jsonEncoder(expectedDevices[0]), jsonEncoder(device))
// Test: RemoveDevice
err = repo.RemoveDevices(expectedDevices[0].ID)
require.NoError(err)
devices, err = repo.GetDevices()
require.NoError(err)
require.Len(devices, 1)
device, err = repo.GetDevice(expectedDevices[0].ID)
require.NoError(err)
require.Nil(device)
err = repo.AddDevices(expectedDevices[0])
require.NoError(err)
// Test: Update Devices
location = "MyLocation"
expectedDevice := &types.Device{
ID: "ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f",
Name: "Hello World",
Location: &location,
CreationDate: *timeNow(require),
}
err = repo.UpdateDevices(expectedDevice)
require.NoError(err)
device, err = repo.GetDevice(expectedDevice.ID)
require.NoError(err)
// require.JSONEq(jsonEncoder(expectedDevice), jsonEncoder(device))
var (
wireID = "50473fdc-f6ef-4227-b3c4-484d8e9c1323"
i2cBus = 1
i2cAddress uint8 = 76
expectedSensors = []*types.Sensor{
{
ID: "0f8b88b0-c20d-42b2-ab51-b09ca99c0752",
Name: "e1fbdbe9-cebf-42ed-8065-bf4882ccf76b",
Location: "6d5b5450-1f87-47cb-b185-f64c35fae3c1",
GPIONumber: "GPIO14",
Model: "DHT11",
Enabled: true,
TickDuration: "1m",
DeviceID: "ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f",
CreationDate: *timeNow(require),
},
{
ID: "80b1c4bd-abec-4ff0-afb4-bd70aeed0c83",
Name: "544365ee-ece9-44ea-911d-5d920a68d4ba",
Location: "7ae2d05e-9e6b-4d2d-b26a-cb4acca83778",
WireID: &wireID,
Model: "DS18B20",
Enabled: true,
TickDuration: "5m",
DeviceID: "ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f",
CreationDate: *timeNow(require),
},
{
ID: "8c74397f-8e60-4c9d-960d-3197747cef9a",
Name: "4b808675-de02-4866-893d-1c77f23b9304",
Location: "8a085c0f-dd3c-447f-8c4e-c4c1d869c7b6",
I2CBus: &i2cBus,
I2CAddress: &i2cAddress,
Model: "BME280",
Enabled: false,
TickDuration: "10m",
DeviceID: "39b8f150-8abf-4539-9f16-7f68cedb1649",
CreationDate: *timeNow(require),
},
}
)
// Test: AddSensors
err = repo.AddSensors(expectedSensors...)
require.NoError(err)
// Test: GetSensors
sensors, err := repo.GetSensors()
require.NoError(err)
require.Len(sensors, len(expectedSensors))
// Test: GetSensor
sensor, err := repo.GetSensor(expectedSensors[0].ID)
require.NoError(err)
require.JSONEq(jsonEncoder(expectedSensors[0]), jsonEncoder(sensor))
// Test: GetSensorsByDeviceID
sensors, err = repo.GetSensorsByDeviceID("ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f")
require.NoError(err)
require.Len(sensors, 2)
require.JSONEq(jsonEncoder(expectedSensors[0:2]), jsonEncoder(sensors))
// Test: RemoveSensors
err = repo.RemoveSensors(expectedSensors[0].ID)
require.NoError(err)
sensors, err = repo.GetSensors()
require.NoError(err)
require.Len(sensors, 2)
sensors, err = repo.GetSensorsByDeviceID("ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f")
require.NoError(err)
require.Len(sensors, 1)
sensor, err = repo.GetSensor(expectedSensors[0].ID)
require.NoError(err)
require.Nil(sensor)
// Test: RemoveSensorsByNames
err = repo.RemoveSensorsByNames(expectedSensors[1].Name)
require.NoError(err)
sensors, err = repo.GetSensors()
require.NoError(err)
require.Len(sensors, 1)
sensor, err = repo.GetSensor(expectedSensors[1].ID)
require.NoError(err)
require.Nil(sensor)
// Test: RenameSensor
err = repo.RenameSensors(expectedSensors[2].Name, "Hello")
require.NoError(err)
sensor, err = repo.GetSensor(expectedSensors[2].ID)
require.Equal("Hello", sensor.Name)
require.NotNil(sensor)
// Test: DisableSensorsByNames
err = repo.DisableSensorsByNames("Hello")
require.NoError(err)
sensor, err = repo.GetSensor(expectedSensors[2].ID)
require.False(sensor.Enabled)
require.NotNil(sensor)
// Test: EnableSensorsByName
err = repo.EnableSensorsByNames("Hello")
require.NoError(err)
sensor, err = repo.GetSensor(expectedSensors[2].ID)
require.True(sensor.Enabled)
require.NotNil(sensor)
// Test: UpdateSensors
expectedSensor := &types.Sensor{
ID: "8c74397f-8e60-4c9d-960d-3197747cef9a",
Name: "4b808675-de02-4866-893d-1c77f23b9304",
Location: "Ăśber den Wolken muss die Freiheit wohl grenzenlos sein...",
I2CBus: nil,
I2CAddress: nil,
Model: "SDS011",
Enabled: true,
TickDuration: "6h",
DeviceID: "39b8f150-8abf-4539-9f16-7f68cedb1649",
CreationDate: *timeNow(require),
}
err = repo.UpdateSensors(expectedSensor)
require.NoError(err)
sensor, err = repo.GetSensor(expectedSensor.ID)
require.NoError(err)
require.NotNil(sensor)
// require.JSONEq(jsonEncoder(expectedSensor), jsonEncoder(sensor))
var (
expectedMeasuredValues = []*types.MeasuredValue{
{
ID: "2e5a297a-3da0-46ae-89d2-0fcab0f1d5f7",
Value: 32,
ValueType: "humidity",
Date: *timeNow(require),
SensorID: "8c74397f-8e60-4c9d-960d-3197747cef9a",
CreationDate: *timeNow(require),
UpdateDate: nil,
},
{
ID: "d69f1b62-0c6c-4058-b42c-4a2821bd220c",
Value: 38,
ValueType: "pressure",
Date: *timeNow(require),
SensorID: "8c74397f-8e60-4c9d-960d-3197747cef9a",
CreationDate: *timeNow(require),
UpdateDate: nil,
},
{
ID: "ea945ae0-412b-4561-a191-1f8f1f909fa4",
Value: 35.4,
ValueType: "temperature",
Date: *timeNow(require),
SensorID: "8c74397f-8e60-4c9d-960d-3197747cef9a",
CreationDate: *timeNow(require),
UpdateDate: nil,
},
}
)
// Test: AddMeasuredValues
err = repo.AddMeasuredValues(expectedMeasuredValues...)
require.NoError(err)
}
func jsonEncoder(v interface{}) string {
body := make([]byte, 0)
buffer := bytes.NewBuffer(body)
jsonEncoder := json.NewEncoder(buffer)
jsonEncoder.SetIndent("", " ")
jsonEncoder.Encode(v)
return buffer.String()
}
func timeNow(require *require.Assertions) *time.Time {
now, err := time.Parse("2006-01-02 15:04:05.999999Z", time.Now().Format("2006-01-02 15:04:05.999999Z"))
require.NoError(err)
return &now
}

View File

@ -1,90 +0,0 @@
package sensor
import (
"fmt"
"sync"
"github.com/d2r2/go-bsbmp"
"github.com/d2r2/go-i2c"
"github.com/d2r2/go-logger"
uuid "github.com/satori/go.uuid"
"git.cryptic.systems/volker.raschek/flucky/pkg/internal/format"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
)
// BME280 is a sensor to measure humidity and temperature.
type BME280 struct {
*types.Sensor
mutex *sync.Mutex
}
// Read measured values
func (bme280 *BME280) Read() ([]*types.MeasuredValue, error) {
// Lock multiple access
bme280.mutex.Lock()
defer bme280.mutex.Unlock()
// Create new connection to i2c-bus on 1 line with address 0x76.
// Use i2cdetect utility to find device address over the i2c-bus
i2c, err := i2c.NewI2C(*bme280.I2CAddress, *bme280.I2CBus)
if err != nil {
return nil, err
}
defer i2c.Close()
// Reduce loglevel
for _, pkg := range []string{"bsbmp", "i2c"} {
err = logger.ChangePackageLogLevel(pkg, logger.InfoLevel)
if err != nil {
return nil, fmt.Errorf("Failed to change package log level: %v", err)
}
}
sensor, err := bsbmp.NewBMP(bsbmp.BME280, i2c)
if err != nil {
return nil, err
}
temperatureValue, err := sensor.ReadTemperatureC(bsbmp.ACCURACY_STANDARD)
if err != nil {
return nil, err
}
pressureValue, err := sensor.ReadPressurePa(bsbmp.ACCURACY_STANDARD)
if err != nil {
return nil, err
}
_, humidityValue, err := sensor.ReadHumidityRH(bsbmp.ACCURACY_STANDARD)
if err != nil {
return nil, err
}
measuredValues := []*types.MeasuredValue{
{
ID: uuid.NewV4().String(),
Value: float64(humidityValue),
ValueType: "humidity",
Date: format.FormatedTime(),
SensorID: bme280.ID,
},
{
ID: uuid.NewV4().String(),
Value: float64(pressureValue),
ValueType: "pressure",
Date: format.FormatedTime(),
SensorID: bme280.ID,
},
{
ID: uuid.NewV4().String(),
Value: float64(temperatureValue),
ValueType: "temperature",
Date: format.FormatedTime(),
SensorID: bme280.ID,
},
}
return measuredValues, nil
}

View File

@ -1,59 +0,0 @@
package sensor
import (
"fmt"
"sync"
"git.cryptic.systems/volker.raschek/flucky/pkg/internal/format"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
"git.cryptic.systems/volker.raschek/go-dht"
uuid "github.com/satori/go.uuid"
)
// DHT11 is a sensor to measure humidity and temperature.
type DHT11 struct {
*types.Sensor
mutex *sync.Mutex
}
// Read measured values
func (dht11 *DHT11) Read() ([]*types.MeasuredValue, error) {
// Lock multiple access
dht11.mutex.Lock()
defer dht11.mutex.Unlock()
err := dht.HostInit()
if err != nil {
return nil, fmt.Errorf("Failed to initialize periph: %v", err)
}
dht, err := dht.NewDHT(dht11.GPIONumber, dht.Celsius, "")
if err != nil {
return nil, fmt.Errorf("Failed to initialize new DHT11 sensor: %v", err)
}
humidityValue, temperatureValue, err := dht.Read()
if err != nil {
return nil, fmt.Errorf("Read error: %v", err)
}
measuredValues := []*types.MeasuredValue{
{
ID: uuid.NewV4().String(),
Value: float64(humidityValue),
ValueType: "humidity",
Date: format.FormatedTime(),
SensorID: dht11.ID,
},
{
ID: uuid.NewV4().String(),
Value: float64(temperatureValue),
ValueType: "temperature",
Date: format.FormatedTime(),
SensorID: dht11.ID,
},
}
return measuredValues, nil
}

View File

@ -1,59 +0,0 @@
package sensor
import (
"fmt"
"sync"
"git.cryptic.systems/volker.raschek/flucky/pkg/internal/format"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
"git.cryptic.systems/volker.raschek/go-dht"
uuid "github.com/satori/go.uuid"
)
// DHT22 is a sensor to measure humidity and temperature.
type DHT22 struct {
*types.Sensor
mutex *sync.Mutex
}
// Read measured values
func (dht22 *DHT22) Read() ([]*types.MeasuredValue, error) {
// Lock multiple access
dht22.mutex.Lock()
defer dht22.mutex.Unlock()
err := dht.HostInit()
if err != nil {
return nil, fmt.Errorf("Failed to initialize periph: %v", err)
}
dht, err := dht.NewDHT(dht22.GPIONumber, dht.Celsius, "")
if err != nil {
return nil, fmt.Errorf("Failed to initialize new DHT22 sensor: %v", err)
}
humidityValue, temperatureValue, err := dht.Read()
if err != nil {
return nil, fmt.Errorf("Read error: %v", err)
}
measuredValues := []*types.MeasuredValue{
{
ID: uuid.NewV4().String(),
Value: float64(humidityValue),
ValueType: "humidity",
Date: format.FormatedTime(),
SensorID: dht22.ID,
},
{
ID: uuid.NewV4().String(),
Value: float64(temperatureValue),
ValueType: "temperature",
Date: format.FormatedTime(),
SensorID: dht22.ID,
},
}
return measuredValues, nil
}

View File

@ -1,69 +0,0 @@
package sensor
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"git.cryptic.systems/volker.raschek/flucky/pkg/internal/format"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
uuid "github.com/satori/go.uuid"
)
// DS18B20 is a sensor to measure humidity and temperature.
type DS18B20 struct {
*types.Sensor
mutex *sync.Mutex
}
// Read measured values
func (ds18b20 *DS18B20) Read() ([]*types.MeasuredValue, error) {
// Lock multiple access
ds18b20.mutex.Lock()
defer ds18b20.mutex.Unlock()
if ds18b20.WireID == nil {
return nil, fmt.Errorf("WireID is not specified")
}
socketPath := filepath.Join("/sys/bus/w1/devices", *ds18b20.WireID, "/w1_slave")
if _, err := os.Stat(socketPath); os.IsNotExist(err) {
return nil, fmt.Errorf("Socket path not found: %v", socketPath)
}
data, err := ioutil.ReadFile(socketPath)
if err != nil {
return nil, fmt.Errorf("Can not read data from sensor %v", ds18b20.Name)
}
raw := string(data)
i := strings.LastIndex(raw, "t=")
if i == -1 {
return nil, errorReadData
}
c, err := strconv.ParseFloat(raw[i+2:len(raw)-1], 64)
if err != nil {
return nil, errorParseData
}
temperatureValue := c / 1000
measuredValues := []*types.MeasuredValue{
{
ID: uuid.NewV4().String(),
Value: float64(temperatureValue),
ValueType: "temperature",
Date: format.FormatedTime(),
SensorID: ds18b20.ID,
},
}
return measuredValues, nil
}

View File

@ -1,10 +0,0 @@
package sensor
import (
"errors"
)
var (
errorParseData = errors.New("Failed to parse data")
errorReadData = errors.New("Failed to read data from sensor")
)

View File

@ -1,13 +0,0 @@
package sensor
import (
"time"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
)
type Sensor interface {
GetID() string
GetTicker() *time.Ticker
Read() ([]*types.MeasuredValue, error)
}

View File

@ -1,40 +0,0 @@
package sensor
import (
"errors"
"sync"
"git.cryptic.systems/volker.raschek/flucky/pkg/types"
)
var (
ErrSensorModelNotMatched = errors.New("Sensor model not matched")
)
// New returns a new sensor
func New(sensor *types.Sensor) (Sensor, error) {
switch sensor.Model {
case "BME280":
return &BME280{
Sensor: sensor,
mutex: new(sync.Mutex),
}, nil
case "DHT11":
return &DHT11{
Sensor: sensor,
mutex: new(sync.Mutex),
}, nil
case "DHT22":
return &DHT22{
Sensor: sensor,
mutex: new(sync.Mutex),
}, nil
case "DS18B20":
return &DS18B20{
Sensor: sensor,
mutex: new(sync.Mutex),
}, nil
default:
return nil, ErrSensorModelNotMatched
}
}

View File

@ -1,285 +0,0 @@
package database
import (
"context"
"database/sql"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"testing"
"time"
"git.cryptic.systems/volker.raschek/flucky/pkg/testutils/dockerutils"
"github.com/docker/docker/api/types"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/require"
)
var (
ErrInvalidAttr = errors.New("Invalid DatabaseOption attribute")
)
type PostgresOptions struct {
ContainerEnv map[string]string
ContainerImage string
ContainerLabels map[string]string
ContainerName string
ContainerNetworks map[string][]string
ContainerNetworkLabels map[string]map[string]string
ContainerPort string
Driver string
DSN string
HostPort string
}
// Validate the DatabaseOption struct, if all required atrributes are valid
func (dbOptions *PostgresOptions) Validate() error {
// Required strings
for _, values := range [][]string{
0: {"ContainerImage", dbOptions.ContainerImage},
1: {"ContainerPort", dbOptions.ContainerPort},
2: {"Driver", dbOptions.Driver},
3: {"HostPort", dbOptions.HostPort},
} {
if len(values[1]) <= 0 {
return fmt.Errorf("%w: Attribute %v is empty", ErrInvalidAttr, values[0])
}
}
// Require initialized maps
for key, value := range map[string]interface{}{
"ContainerEnv": dbOptions.ContainerEnv,
"ContainerLabels": dbOptions.ContainerLabels,
"ContainerNetworks": dbOptions.ContainerNetworks,
"ContainerNetworkLabels": dbOptions.ContainerNetworkLabels,
} {
if value == nil {
return fmt.Errorf("%w: Attribut %v is not initialized", ErrInvalidAttr, key)
}
}
// Required postgres environment variables
for _, key := range []string{
"POSTGRES_PASSWORD",
"POSTGRES_USER",
"POSTGRES_DB",
} {
if _, present := dbOptions.ContainerEnv[key]; !present {
return fmt.Errorf("%w: Required env %v not defined", ErrInvalidAttr, key)
}
}
// Supported drivers
found := false
for _, supportedDriver := range []string{
"postgres",
} {
if supportedDriver == dbOptions.Driver {
found = true
break
}
}
if !found {
return fmt.Errorf("%w: Driver %v not supported", ErrInvalidAttr, dbOptions.Driver)
}
// Protect well-known ports
hostPort, err := strconv.ParseInt(dbOptions.HostPort, 10, 64)
if err != nil {
return fmt.Errorf("Failed to parse hostport %v: %w", dbOptions.HostPort, err)
}
if hostPort > 0 && hostPort < 1024 {
return fmt.Errorf("%w: ContainerPort %v: Protect well-known ports between 1-1023", ErrInvalidAttr, dbOptions.HostPort)
}
return nil
}
// NewPostgresDatabase starts a new postgres database based on default values
func NewPostgresDatabase(t *testing.T) (*PostgresOptions, func()) {
return StartPostgres(t, NewPostgresOptions())
}
// NewPostgresOptions returns a new PostgresOptions structs with default values
func NewPostgresOptions() *PostgresOptions {
return &PostgresOptions{
ContainerEnv: map[string]string{
"POSTGRES_PASSWORD": "postgres",
"POSTGRES_USER": "postgres",
"POSTGRES_DB": "postgres",
},
ContainerImage: "docker.io/library/postgres:13-alpine",
ContainerLabels: make(map[string]string, 0),
ContainerName: uuid.NewV4().String(),
ContainerNetworks: make(map[string][]string, 0),
ContainerNetworkLabels: make(map[string]map[string]string, 0),
ContainerPort: "5432",
Driver: "postgres",
DSN: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable",
HostPort: "0",
}
}
// StartPostgres starts a postgres container image based on the PostgresOption
// struct
func StartPostgres(t *testing.T, pgOptions *PostgresOptions) (*PostgresOptions, func()) {
var (
ctx = context.Background()
require = require.New(t)
cleanupLabels = map[string]string{
uuid.NewV4().String(): uuid.NewV4().String(),
}
)
dockerClient, err := dockerutils.New()
require.NoError(err)
// Validate the PostgresOptions struct
require.NoError(pgOptions.Validate())
// Pre-Define DSN based on the pgOption attributes
pgOptions.DSN = fmt.Sprintf("postgres://%v:%v@localhost:%v/%v?sslmode=disable",
pgOptions.ContainerEnv["POSTGRES_USER"],
pgOptions.ContainerEnv["POSTGRES_PASSWORD"],
pgOptions.HostPort,
pgOptions.ContainerEnv["POSTGRES_DB"],
)
// Create missing networks
// The dockerutils package check only if the network exist and append the
// container with his aliasses as network endpoint. Additionally every network
// get his own network labels. If no network labels are defined, new labels
// will be generated. This is required to remove the networks by their labels.
networks, err := dockerClient.NetworkList(ctx, types.NetworkListOptions{})
require.NoError(err)
for networkName := range pgOptions.ContainerNetworks {
found := false
for _, network := range networks {
if network.Name == networkName {
found = true
break
}
}
if !found {
// Create network labels if there are no one defined
if _, present := pgOptions.ContainerNetworkLabels[networkName]; !present {
pgOptions.ContainerNetworkLabels[networkName] = make(map[string]string, 0)
}
// Append cleanup labels to the network
for key, value := range cleanupLabels {
pgOptions.ContainerNetworkLabels[networkName][key] = value
}
_, err := dockerClient.NetworkCreate(ctx, networkName, types.NetworkCreate{
Labels: pgOptions.ContainerNetworkLabels[networkName],
})
require.NoError(err)
}
}
// Append cleanup labels to the container
for key, value := range cleanupLabels {
pgOptions.ContainerLabels[key] = value
}
// Build database container
databaseContainerBuilder := dockerClient.NewBuilder(pgOptions.ContainerImage).
Env(pgOptions.ContainerEnv).
Labels(pgOptions.ContainerLabels).
Port(fmt.Sprintf("%s:%s", pgOptions.HostPort, pgOptions.ContainerPort)).
Pull().
WithName(pgOptions.ContainerName)
networkNames := make([]string, 0)
for networkName, aliasses := range pgOptions.ContainerNetworks {
if aliasses == nil {
aliasses = make([]string, 0)
}
if len(aliasses) <= 0 {
aliasses = append(aliasses, pgOptions.ContainerName)
}
databaseContainerBuilder.Network(networkName, aliasses...)
pgOptions.ContainerNetworks[networkName] = aliasses
networkNames = append(networkNames, networkName)
}
dbContainerID, err := databaseContainerBuilder.Start(ctx)
require.NoError(err)
// cleanupFunction to remove the database container with all defined networks.
// Skip network if the network has an additional endpoint
cleanupFunc := func() {
err := dockerClient.ContainerRemove(ctx, dbContainerID, types.ContainerRemoveOptions{Force: true})
require.NoError(err)
for networkName := range pgOptions.ContainerNetworks {
networks, err := dockerClient.NetworkListByLabels(ctx, pgOptions.ContainerNetworkLabels[networkName])
require.NoError(err)
for _, network := range networks {
if len(network.Containers) <= 0 {
err := dockerClient.NetworkRemove(ctx, network.ID)
require.NoError(err)
}
}
}
}
// Search for allocated port if public port is defined as random by 0
if pgOptions.HostPort == "0" {
containers, err := dockerClient.ContainerListByLabels(ctx, true, pgOptions.ContainerLabels)
require.NoError(err)
var pubPort string
for _, port := range containers[0].Ports {
if port.PrivatePort == 5432 {
pubPort = strconv.Itoa(int(port.PublicPort))
break
}
}
require.Greater(len(pubPort), 0, "Failed to detect allocated random port of the postgres container")
connectionURL, err := url.Parse(pgOptions.DSN)
require.NoError(err)
parts := strings.Split(connectionURL.Host, ":")
connectionURL.Host = fmt.Sprintf("%v:%v", parts[0], pubPort)
pgOptions.DSN = connectionURL.String()
pgOptions.HostPort = pubPort
}
var db *sql.DB
for i := 0; i < 10; i++ {
db, err = sql.Open(pgOptions.Driver, pgOptions.DSN)
if err != nil {
t.Log("Database not ready - wait 10 seconds")
time.Sleep(10 * time.Second)
continue
}
err = db.Ping()
if err != nil {
t.Log("Database not ready - wait 10 seconds")
db.Close()
time.Sleep(10 * time.Second)
continue
}
break
}
require.NoError(err)
err = db.Close()
require.NoError(err)
return pgOptions, cleanupFunc
}

View File

@ -1,331 +0,0 @@
package database_test
import (
"context"
"database/sql"
"errors"
"fmt"
"testing"
"git.cryptic.systems/volker.raschek/flucky/pkg/testutils/database"
"git.cryptic.systems/volker.raschek/flucky/pkg/testutils/dockerutils"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/require"
_ "github.com/lib/pq"
)
func requireCountNetworks(ctx context.Context, dockerClient *dockerutils.Client, dbOptions *database.PostgresOptions, equal int, t *testing.T) {
require := require.New(t)
containers, err := dockerClient.ContainerListByLabels(ctx, true, dbOptions.ContainerLabels)
require.NoError(err)
require.Equal(equal, len(containers[0].NetworkSettings.Networks))
}
func requireContainerHasNetworksAndAliasses(ctx context.Context, dockerClient *dockerutils.Client, dbOptions *database.PostgresOptions, t *testing.T) {
require := require.New(t)
containers, err := dockerClient.ContainerListByNames(ctx, true, dbOptions.ContainerName)
require.NoError(err)
require.Contains(containers[0].Names, fmt.Sprintf("/%v", dbOptions.ContainerName))
for networkName, _ := range dbOptions.ContainerNetworks {
networks, err := dockerClient.NetworkListByNames(ctx, networkName)
require.NoError(err)
require.Equal(networkName, networks[0].Name)
require.Equal(dbOptions.ContainerName, networks[0].Containers[containers[0].ID].Name)
}
}
func requireTestCleanup(ctx context.Context, dockerClient *dockerutils.Client, cleanupLabels map[string]string, t *testing.T) {
require := require.New(t)
containers, err := dockerClient.ContainerListByLabels(ctx, true, cleanupLabels)
require.NoError(err)
require.NotNil(containers)
require.Len(containers, 0)
networks, err := dockerClient.NetworkListByLabels(ctx, cleanupLabels)
require.NoError(err)
require.NotNil(networks)
require.Len(networks, 0)
}
func TestPostgresDatabase(t *testing.T) {
require := require.New(t)
dbOptions, cleanup := database.NewPostgresDatabase(t)
t.Cleanup(func() { cleanup() })
require.NotEqual(0, dbOptions.HostPort)
dockerClient, err := dockerutils.New()
require.NoError(err)
dbContainers, err := dockerClient.ContainerListByLabels(context.Background(), true, dbOptions.ContainerLabels)
require.NoError(err)
require.NotNil(dbContainers)
require.Len(dbContainers, 1)
dbo, err := sql.Open(dbOptions.Driver, dbOptions.DSN)
require.NoError(err)
require.NoError(dbo.Ping())
query := "SELECT 1"
row := dbo.QueryRow(query)
var i int
err = row.Scan(&i)
require.NoError(err)
require.Equal(1, i)
}
func TestPostgresNetwork(t *testing.T) {
var (
ctx = context.Background()
require = require.New(t)
cleanupLabels = map[string]string{
uuid.NewV4().String(): uuid.NewV4().String(),
}
)
dockerClient, err := dockerutils.New()
require.NoError(err)
t.Cleanup(func() {
dockerClient.ContainerRemoveByLabels(ctx, cleanupLabels)
dockerClient.NetworkRemoveByLabels(ctx, cleanupLabels)
dockerClient.Close()
})
// TestDefaultNetwork
dbOptionsPointer, cleanup := database.NewPostgresDatabase(t)
t.Cleanup(cleanup)
require.NotNil(dbOptionsPointer.ContainerNetworks)
require.Len(dbOptionsPointer.ContainerNetworks, 0)
requireContainerHasNetworksAndAliasses(ctx, dockerClient, dbOptionsPointer, t)
// TestRandomNetwork
dbOptions := database.NewPostgresOptions()
dbOptions.HostPort = "0"
dbOptions.ContainerName = "Volker"
dbOptions.ContainerLabels = cleanupLabels
dbOptions.ContainerNetworks[uuid.NewV4().String()] = []string{
uuid.NewV4().String(),
uuid.NewV4().String(),
}
_, cleanup = database.StartPostgres(t, dbOptions)
t.Cleanup(cleanup)
requireCountNetworks(ctx, dockerClient, dbOptions, 1, t)
requireContainerHasNetworksAndAliasses(ctx, dockerClient, dbOptions, t)
// TestRandomMultipleNetworks
containerNetworkNameA := uuid.NewV4().String()
containerNetworkNameB := uuid.NewV4().String()
containerNetworks := map[string][]string{
containerNetworkNameA: {
uuid.NewV4().String(),
uuid.NewV4().String(),
},
containerNetworkNameB: {
uuid.NewV4().String(),
uuid.NewV4().String(),
},
}
containerNetworkLabels := map[string]map[string]string{
containerNetworkNameA: cleanupLabels,
containerNetworkNameB: cleanupLabels,
}
dbOptions = database.NewPostgresOptions()
dbOptions.HostPort = "0"
dbOptions.ContainerName = "Raschek"
dbOptions.ContainerNetworks = containerNetworks
dbOptions.ContainerNetworkLabels = containerNetworkLabels
dbOptions.ContainerLabels = cleanupLabels
_, cleanup = database.StartPostgres(t, dbOptions)
t.Cleanup(cleanup)
requireCountNetworks(ctx, dockerClient, dbOptions, 2, t)
requireContainerHasNetworksAndAliasses(ctx, dockerClient, dbOptions, t)
// TestCleanup
// Test if a network with an additional endpoint will not be removed
containerNetwork := uuid.NewV4().String()
containerNetworkA := map[string][]string{
containerNetwork: {
uuid.NewV4().String(),
uuid.NewV4().String(),
},
}
containerNetworkB := map[string][]string{
containerNetwork: {
uuid.NewV4().String(),
uuid.NewV4().String(),
},
}
containerNetworkLabels = map[string]map[string]string{
containerNetwork: cleanupLabels,
}
dbOptions = database.NewPostgresOptions()
dbOptions.HostPort = "0"
dbOptions.ContainerName = "is"
dbOptions.ContainerLabels = cleanupLabels
dbOptions.ContainerNetworks = containerNetworkA
dbOptions.ContainerNetworkLabels = containerNetworkLabels
_, cleanup = database.StartPostgres(t, dbOptions)
dbOptions = database.NewPostgresOptions()
dbOptions.HostPort = "0"
dbOptions.ContainerName = "the_best"
dbOptions.ContainerLabels = cleanupLabels
dbOptions.ContainerNetworks = containerNetworkB
dbOptions.ContainerNetworkLabels = containerNetworkLabels
_, cleanupSecond := database.StartPostgres(t, dbOptions)
cleanup()
networkResources, err := dockerClient.NetworkListByNames(ctx, containerNetwork)
require.NoError(err)
require.Len(networkResources[0].Containers, 1)
cleanupSecond()
requireTestCleanup(ctx, dockerClient, cleanupLabels, t)
}
func TestPostgresOptionsValidate(t *testing.T) {
require := require.New(t)
// ContainerImage is empty
dbOptions := database.PostgresOptions{}
err := dbOptions.Validate()
require.Error(err)
require.True(errors.Is(err, database.ErrInvalidAttr))
require.EqualError(err, "Invalid DatabaseOption attribute: Attribute ContainerImage is empty")
// ContainerPort is empty
dbOptions = database.PostgresOptions{
ContainerImage: "docker.io/library/postgres:13-alpine",
}
err = dbOptions.Validate()
require.Error(err)
require.True(errors.Is(err, database.ErrInvalidAttr))
require.EqualError(err, "Invalid DatabaseOption attribute: Attribute ContainerPort is empty")
// Driver is empty
dbOptions = database.PostgresOptions{
ContainerImage: "docker.io/library/postgres:13-alpine",
ContainerPort: "5432",
}
err = dbOptions.Validate()
require.Error(err)
require.True(errors.Is(err, database.ErrInvalidAttr))
require.EqualError(err, "Invalid DatabaseOption attribute: Attribute Driver is empty")
// HostPort is empty
dbOptions = database.PostgresOptions{
ContainerImage: "docker.io/library/postgres:13-alpine",
ContainerPort: "5432",
Driver: "sdfsdfsdf",
}
err = dbOptions.Validate()
require.Error(err)
require.True(errors.Is(err, database.ErrInvalidAttr))
require.EqualError(err, "Invalid DatabaseOption attribute: Attribute HostPort is empty")
// ContainerEnv: POSTGRES_PASSWORD
dbOptions = database.PostgresOptions{
ContainerImage: "docker.io/library/postgres:13-alpine",
ContainerPort: "5432",
Driver: "sdfsdfsdf",
HostPort: "asdasd",
}
err = dbOptions.Validate()
require.Error(err)
require.True(errors.Is(err, database.ErrInvalidAttr))
require.EqualError(err, "Invalid DatabaseOption attribute: Required env POSTGRES_PASSWORD not defined")
// ContainerEnv: POSTGRES_USER
dbOptions = database.PostgresOptions{
ContainerEnv: map[string]string{
"POSTGRES_PASSWORD": "HelloWorld",
},
ContainerImage: "docker.io/library/postgres:13-alpine",
ContainerPort: "5432",
Driver: "sdfsdfsdf",
HostPort: "asdasd",
}
err = dbOptions.Validate()
require.Error(err)
require.True(errors.Is(err, database.ErrInvalidAttr))
require.EqualError(err, "Invalid DatabaseOption attribute: Required env POSTGRES_USER not defined")
// ContainerEnv: POSTGRES_USER
dbOptions = database.PostgresOptions{
ContainerEnv: map[string]string{
"POSTGRES_PASSWORD": "postgres",
"POSTGRES_USER": "postgres",
},
ContainerImage: "docker.io/library/postgres:13-alpine",
ContainerPort: "5432",
Driver: "sdfsdfsdf",
HostPort: "asdasd",
}
err = dbOptions.Validate()
require.Error(err)
require.True(errors.Is(err, database.ErrInvalidAttr))
require.EqualError(err, "Invalid DatabaseOption attribute: Required env POSTGRES_DB not defined")
// Driver not supported
dbOptions = database.PostgresOptions{
ContainerEnv: map[string]string{
"POSTGRES_PASSWORD": "postgres",
"POSTGRES_USER": "postgres",
"POSTGRES_DB": "postgres",
},
ContainerImage: "docker.io/library/postgres:13-alpine",
ContainerPort: "5432",
Driver: "sdfsdfsdf",
HostPort: "asdasd",
}
err = dbOptions.Validate()
require.Error(err)
require.True(errors.Is(err, database.ErrInvalidAttr))
require.EqualError(err, "Invalid DatabaseOption attribute: Driver sdfsdfsdf not supported")
// HostPort: Failed to parse
dbOptions = database.PostgresOptions{
ContainerEnv: map[string]string{
"POSTGRES_PASSWORD": "postgres",
"POSTGRES_USER": "postgres",
"POSTGRES_DB": "postgres",
},
ContainerImage: "docker.io/library/postgres:13-alpine",
ContainerPort: "5432",
Driver: "postgres",
HostPort: "asdasd",
}
err = dbOptions.Validate()
require.Error(err)
require.EqualError(err, "Failed to parse hostport asdasd: strconv.ParseInt: parsing \"asdasd\": invalid syntax")
// HostPort: Well-Known port
dbOptions = database.PostgresOptions{
ContainerEnv: map[string]string{
"POSTGRES_PASSWORD": "postgres",
"POSTGRES_USER": "postgres",
"POSTGRES_DB": "postgres",
},
ContainerImage: "docker.io/library/postgres:13-alpine",
ContainerPort: "5432",
Driver: "postgres",
HostPort: "443",
}
err = dbOptions.Validate()
require.Error(err)
require.EqualError(err, "Invalid DatabaseOption attribute: ContainerPort 443: Protect well-known ports between 1-1023")
}

View File

@ -1,232 +0,0 @@
package dockerutils
import (
"context"
"fmt"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat"
)
type Builder struct {
client *Client
containerConfig *container.Config
containerName string
hostConfig *container.HostConfig
networkConfig *network.NetworkingConfig
networks map[string][]string
ports []string
pull bool
waitForHealthy bool
}
// AddEnv to the container
func (builder *Builder) AddEnv(key string, value string) *Builder {
builder.containerConfig.Env = append(builder.containerConfig.Env, fmt.Sprintf("%v=%v", key, value))
return builder
}
// AddLabel to the container
func (builder *Builder) AddLabel(key string, value string) *Builder {
builder.containerConfig.Labels[key] = value
return builder
}
// Env set environment variables to the container
func (builder *Builder) Env(env map[string]string) *Builder {
builder.containerConfig.Labels = make(map[string]string)
for key, value := range env {
builder.containerConfig.Env = append(builder.containerConfig.Env, fmt.Sprintf("%v=%v", key, value))
}
return builder
}
// Labels set labels to the container
func (builder *Builder) Labels(labels map[string]string) *Builder {
builder.containerConfig.Labels = labels
return builder
}
// Memory defines a memory limit for the container
func (builder *Builder) Memory(limit int64) *Builder {
builder.hostConfig.Memory = limit
return builder
}
// Mount a source volume or hostpath into the filesystem of the container
func (builder *Builder) Mount(source string, dest string) *Builder {
builder.hostConfig.Binds = append(builder.hostConfig.Binds, fmt.Sprintf("%v:%v", source, dest))
return builder
}
// Mounts a set of source volumes or hostpath into the filesystem of the
// container
func (builder *Builder) Mounts(mounts map[string]string) *Builder {
for source, dest := range mounts {
builder.Mount(source, dest)
}
return builder
}
// Network add the container with aliasses to a specific network
func (builder *Builder) Network(networkName string, aliasses ...string) *Builder {
builder.networks[networkName] = aliasses
return builder
}
// Port defines a port forwarding from the host machine to the container
// Examples:
// - 8080:8080
// - 10.6.231.10:8080:8080
// - 10.6.231.10:8080:8080/tcp
func (builder *Builder) Port(port string) *Builder {
builder.ports = append(builder.ports, port)
return builder
}
// Ports defines a set port forwarding rules from the host machine to the
// container
// Examples:
// - 8080:8080
// - 10.6.231.10:8080:8080
// - 10.6.231.10:8080:8080/tcp
func (builder *Builder) Ports(ports []string) *Builder {
builder.ports = ports
return builder
}
// Pull the image if absent
func (builder *Builder) Pull() *Builder {
builder.pull = true
return builder
}
// Start the container
func (builder *Builder) Start(ctx context.Context) (string, error) {
// Pull container image if absent
if builder.pull {
err := builder.client.PullQuiet(ctx, builder.containerConfig.Image)
if err != nil {
return "", err
}
}
// Network: portbinding Host->Container
exposedPorts, portBindings, err := nat.ParsePortSpecs(builder.ports)
if err != nil {
return "", fmt.Errorf("unabel to parse ports: %v", err)
}
if len(portBindings) > 0 {
time.Sleep(1 * time.Second)
builder.containerConfig.ExposedPorts = exposedPorts
builder.hostConfig.PortBindings = portBindings
}
// Network: Add container to container networks
// Add the container to the first defined container network, if any one is
// defined. If no one is defined, the docker API will add the container to
// their default bridge docker0. The other networks will be added to the
// container after the container start.
var (
networkNames = make([]string, 0)
networks = make([]types.NetworkResource, 0)
)
if len(builder.networks) > 0 {
for networkName := range builder.networks {
networkNames = append(networkNames, networkName)
}
var err error
networks, err = builder.client.NetworkListByNames(ctx, networkNames...)
if err != nil {
return "", err
}
endpointSetting := &network.EndpointSettings{
NetworkID: networks[0].ID,
Aliases: builder.networks[networkNames[0]],
}
builder.networkConfig.EndpointsConfig[networkNames[0]] = endpointSetting
networkNames = networkNames[1:]
networks = networks[1:]
}
// Container: Create
resp, err := builder.client.ContainerCreate(
ctx,
builder.containerConfig,
builder.hostConfig,
builder.networkConfig,
builder.containerName,
)
if err != nil {
return "", fmt.Errorf("Unable to create container: %v", err)
}
err = builder.client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
if err != nil {
shutdownErr := builder.client.ContainerStopByIDs(ctx, 1*time.Second, resp.ID)
if shutdownErr != nil {
return "", fmt.Errorf("Unable to start container: %v\nUnable to remove container %s: %v\nManual cleanup necessary", err, resp.ID, shutdownErr)
}
return "", fmt.Errorf("Unable to start container: %v", err)
}
// Network: Add more container networks
for i, networkName := range networkNames {
endpointSetting := &network.EndpointSettings{
NetworkID: networks[i].ID,
Aliases: builder.networks[networkName],
}
err := builder.client.NetworkConnect(ctx, networks[i].ID, resp.ID, endpointSetting)
if err != nil {
return "", fmt.Errorf("Unable to append container endpoint to network %v", networkName)
}
}
if builder.waitForHealthy {
watcher := builder.client.GetWatcher()
errors := make(chan error, 1)
done := make(chan struct{})
err = watcher.AddListener(resp.ID, errors, done)
if err != nil {
containerRemoveError := builder.client.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true})
if containerRemoveError != nil {
return "", fmt.Errorf("error while watching for container status: %v - unable to remove container: %v", err, containerRemoveError)
}
return "", fmt.Errorf("error while watching for container status: %v", err)
}
select {
case err := <-errors:
if err != nil {
containerRemoveError := builder.client.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true})
if containerRemoveError != nil {
return "", fmt.Errorf("%v - unable to remove container: %v", err, containerRemoveError)
}
return "", err
}
case <-done:
}
}
return resp.ID, err
}
func (builder *Builder) WaitForHealthy() *Builder {
builder.waitForHealthy = true
return builder
}
// WithName set the name of the container
func (builder *Builder) WithName(containerName string) *Builder {
builder.containerName = containerName
return builder
}

View File

@ -1,402 +0,0 @@
package dockerutils
import (
"context"
"fmt"
"io"
"io/ioutil"
"strings"
"sync"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
)
// Client from the docker API with additional functions
type Client struct {
*client.Client
watcher *Watcher
mutex *sync.Mutex
}
// Close docker connection
func (client *Client) Close() error {
client.mutex.Lock()
defer client.mutex.Unlock()
if client.watcher != nil {
client.watcher.stop()
}
return client.Client.Close()
}
// ContainerListByLabels returns only containers which match by given labels
func (client *Client) ContainerListByLabels(ctx context.Context, all bool, containerLabels map[string]string) ([]types.Container, error) {
filterArgs := filters.NewArgs()
for key, value := range containerLabels {
filterArgs.Add("label", fmt.Sprintf("%v=%v", key, value))
}
containers, err := client.ContainerList(ctx, types.ContainerListOptions{
All: all,
Filters: filterArgs,
})
if err != nil {
return nil, err
}
if containers == nil {
return nil, fmt.Errorf("No containers found by given labels")
}
return containers, nil
}
// ContainerListByNames returns only containers which match by given labels
func (client *Client) ContainerListByNames(ctx context.Context, all bool, containerNames ...string) ([]types.Container, error) {
filterArgs := filters.NewArgs()
for _, name := range containerNames {
filterArgs.Add("name", name)
}
containers, err := client.ContainerList(ctx, types.ContainerListOptions{
All: all,
Filters: filterArgs,
})
if err != nil {
return nil, err
}
if containers == nil {
return nil, fmt.Errorf("No containers found by given names")
}
return containers, nil
}
// ContainerRemoveByIDs deletes all containers which match by their container ids
func (client *Client) ContainerRemoveByIDs(ctx context.Context, containerIDs ...string) error {
for _, containerID := range containerIDs {
err := client.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{Force: true})
if err != nil {
return err
}
}
return nil
}
// ContainerRemoveByLabels deletes all containers which match by given labels
func (client *Client) ContainerRemoveByLabels(ctx context.Context, containerLabels map[string]string) error {
containers, err := client.ContainerListByLabels(ctx, true, containerLabels)
if err != nil {
return err
}
for _, container := range containers {
err := client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{Force: true})
if err != nil {
return err
}
}
return nil
}
// ContainerRemoveByNames deletes all containers which match by their names
func (client *Client) ContainerRemoveByNames(ctx context.Context, containerNames ...string) error {
containers, err := client.ContainerListByNames(ctx, true, containerNames...)
if err != nil {
return err
}
for _, container := range containers {
err := client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{Force: true})
if err != nil {
return err
}
}
return nil
}
// ContainerStopByIDs deletes all containers which match by their container ids
func (client *Client) ContainerStopByIDs(ctx context.Context, timeout time.Duration, containerIDs ...string) error {
for _, containerID := range containerIDs {
err := client.ContainerStop(ctx, containerID, &timeout)
if err != nil {
return err
}
}
return nil
}
// ContainerStopByLabels shutdown containters which match by given labels
func (client *Client) ContainerStopByLabels(ctx context.Context, timeout time.Duration, containerLabels map[string]string) error {
containers, err := client.ContainerListByLabels(ctx, true, containerLabels)
if err != nil {
return err
}
for _, container := range containers {
err := client.ContainerStop(ctx, container.ID, &timeout)
if err != nil {
return err
}
}
return nil
}
// ContainerStopByNames shutdown containters matching by their names
func (client *Client) ContainerStopByNames(ctx context.Context, timeout time.Duration, containerNames ...string) error {
containers, err := client.ContainerListByNames(ctx, true, containerNames...)
if err != nil {
return err
}
for _, container := range containers {
err := client.ContainerStop(ctx, container.ID, &timeout)
if err != nil {
return err
}
}
return nil
}
// GetWatcher returns a watcher for container health states
func (client *Client) GetWatcher() *Watcher {
if client.watcher != nil {
return client.watcher
}
client.mutex.Lock()
defer client.mutex.Unlock()
client.watcher = &Watcher{
client: client,
errorChannels: make(map[string]chan<- error),
doneChannels: make(map[string]chan<- struct{}),
errorMapper: make(map[string]ErrorMapper),
mutex: new(sync.RWMutex),
}
client.watcher.start()
return client.watcher
}
// NetworkListByLabels returns networks which match by given labels
func (client *Client) NetworkListByLabels(ctx context.Context, networkLabels map[string]string) ([]types.NetworkResource, error) {
args := filters.NewArgs()
for key, value := range networkLabels {
args.Add("label", fmt.Sprintf("%v=%v", key, value))
}
return client.NetworkList(ctx, types.NetworkListOptions{
Filters: args,
})
}
// NetworkListByNames returns networks which match by their names. If a
// network can not be found, the function returns an error
func (client *Client) NetworkListByNames(ctx context.Context, networkNames ...string) ([]types.NetworkResource, error) {
networks, err := client.NetworkList(ctx, types.NetworkListOptions{})
if err != nil {
return nil, err
}
foundNetwork := make(map[string]bool, 0)
for _, networkName := range networkNames {
foundNetwork[networkName] = false
}
filteredNetworks := make([]types.NetworkResource, 0)
for _, networkName := range networkNames {
for _, network := range networks {
if network.Name == networkName {
filteredNetworks = append(filteredNetworks, network)
foundNetwork[networkName] = true
}
}
}
for _, networkName := range networkNames {
if !foundNetwork[networkName] {
return nil, fmt.Errorf("Network %v not found", networkName)
}
}
return filteredNetworks, nil
}
// NetworkRemoveByLabels remove all networks which match by given labels
func (client *Client) NetworkRemoveByLabels(ctx context.Context, containerLabels map[string]string) error {
networks, err := client.NetworkListByLabels(ctx, containerLabels)
if err != nil {
return err
}
for _, network := range networks {
err := client.NetworkRemove(ctx, network.ID)
if err != nil {
return err
}
}
return nil
}
// NetworkRemoveByNames remove all networks match by their names. If a
// network can not be found, the function returns an error
func (client *Client) NetworkRemoveByNames(ctx context.Context, networkNames ...string) error {
networks, err := client.NetworkListByNames(ctx, networkNames...)
if err != nil {
return err
}
for _, network := range networks {
err := client.NetworkRemove(ctx, network.ID)
if err != nil {
return err
}
}
return nil
}
// NetworkRemoveByIDs remove all networks match by their id
func (client *Client) NetworkRemoveByIDs(ctx context.Context, containerIDs ...string) error {
for _, containerID := range containerIDs {
err := client.NetworkRemove(ctx, containerID)
if err != nil {
return err
}
}
return nil
}
// NewBuilder returns a new builder for containers
func (client *Client) NewBuilder(image string) *Builder {
return &Builder{
client: client,
containerConfig: &container.Config{
Image: image,
},
hostConfig: new(container.HostConfig),
networkConfig: &network.NetworkingConfig{
EndpointsConfig: make(map[string]*network.EndpointSettings, 0),
},
networks: make(map[string][]string, 0),
ports: make([]string, 0),
pull: false,
waitForHealthy: false,
}
}
// Pull image
func (client *Client) Pull(ctx context.Context, image string, w io.Writer) error {
parts := strings.Split(image, "/")
switch len(parts) {
case 1:
image = fmt.Sprintf("docker.io/library/%v", parts[0])
case 2:
if strings.Compare(parts[0], "library") == 0 ||
strings.Compare(parts[0], "docker.io") == 0 {
image = fmt.Sprintf("docker.io/library/%v", parts[1])
}
}
readCloser, err := client.ImagePull(ctx, image, types.ImagePullOptions{})
if err != nil {
return err
}
_, err = io.Copy(w, readCloser)
if err != nil {
return err
}
return nil
}
// PullQuiet image
func (client *Client) PullQuiet(ctx context.Context, image string) error {
return client.Pull(ctx, image, ioutil.Discard)
}
// VolumeListByLabels returns volumes which match by given labels
func (client *Client) VolumeListByLabels(ctx context.Context, volumeLabels map[string]string) (volume.VolumesListOKBody, error) {
args := filters.NewArgs()
for key, value := range volumeLabels {
args.Add("label", fmt.Sprintf("%v=%v", key, value))
}
return client.VolumeList(ctx, args)
}
// VolumeListByNames returns volumes which match by their names. If a
// volume can not be found, the function returns an error
func (client *Client) VolumeListByNames(ctx context.Context, volumeNames ...string) (volume.VolumesListOKBody, error) {
args := filters.NewArgs()
foundVolumes := make(map[string]bool, 0)
for _, volumeName := range volumeNames {
foundVolumes[volumeName] = false
args.Add("name", volumeName)
}
volumes, err := client.VolumeList(ctx, args)
if err != nil {
return volume.VolumesListOKBody{}, err
}
for _, volume := range volumes.Volumes {
foundVolumes[volume.Name] = true
}
for _, volumeName := range volumeNames {
if foundVolumes[volumeName] != true {
return volume.VolumesListOKBody{}, fmt.Errorf("Volume %v not found", volumeName)
}
}
return volumes, err
}
// VolumeRemoveByLabels remove all volumes match by their labels
func (client *Client) VolumeRemoveByLabels(ctx context.Context, volumeLabels map[string]string) error {
volumes, err := client.VolumeListByLabels(ctx, volumeLabels)
if err != nil {
return err
}
for _, volume := range volumes.Volumes {
err := client.VolumeRemove(ctx, volume.Name, true)
if err != nil {
return err
}
}
return nil
}
// VolumeRemoveByNames remove all volumes match by their names. If a
// volume can not be found, the function returns an error
func (client *Client) VolumeRemoveByNames(ctx context.Context, volumeNames ...string) error {
volumes, err := client.VolumeListByNames(ctx, volumeNames...)
if err != nil {
return err
}
for _, volume := range volumes.Volumes {
err := client.VolumeRemove(ctx, volume.Name, true)
if err != nil {
return err
}
}
return nil
}
// New returns a new dockerutil client
func New() (*Client, error) {
dockerClient, err := client.NewEnvClient()
if err != nil {
return nil, err
}
return &Client{
dockerClient,
nil,
new(sync.Mutex),
}, nil
}

View File

@ -1,443 +0,0 @@
package dockerutils
import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/volume"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/require"
)
// TestContainerCRUD
// Test the following API functions:
// - ContainerListByLabels
// - ContainerListByNames
// - ContainerRemoveByNames
// - ContainerRemoveByLabels
// - ContainerRemoveByIDs
func TestContainerCRUD(t *testing.T) {
var (
ctx = context.Background()
require = require.New(t)
iterations = 5
cleanupLabels = map[string]string{
uuid.NewV4().String(): uuid.NewV4().String(),
}
)
dockerClient, err := New()
require.NoError(err)
t.Cleanup(func() {
dockerClient.ContainerRemoveByLabels(ctx, cleanupLabels)
})
// Create Containers
containerIDs := make([]string, 0)
containerNames := make([]string, 0)
for i := 0; i < iterations; i++ {
containerName := uuid.NewV4().String()
containerID, err := dockerClient.NewBuilder("nginx:alpine").
Labels(cleanupLabels).
Port("80").
Pull().
WithName(containerName).
Start(ctx)
require.NoError(err)
containerNames = append(containerNames, containerName)
containerIDs = append(containerIDs, containerID)
}
// ListByLabels
containers, err := dockerClient.ContainerListByLabels(ctx, true, cleanupLabels)
require.NoError(err)
require.Len(containers, iterations)
for _, container := range containers {
require.Contains(containerIDs, container.ID)
require.Contains(containerNames, strings.Split(container.Names[0], "/")[1])
}
// ListByNames
containers, err = dockerClient.ContainerListByNames(ctx, true, containerNames...)
require.NoError(err)
require.Len(containers, iterations)
for _, container := range containers {
require.Contains(containerIDs, container.ID)
require.Contains(containerNames, strings.Split(container.Names[0], "/")[1])
}
// RemoveByLabels
err = dockerClient.ContainerRemoveByLabels(ctx, cleanupLabels)
require.NoError(err)
containers, err = dockerClient.ContainerListByLabels(ctx, true, cleanupLabels)
require.NoError(err)
require.Len(containers, 0)
// Create
containerIDs = make([]string, 0)
containerNames = make([]string, 0)
for i := 0; i < iterations; i++ {
containerName := uuid.NewV4().String()
containerID, err := dockerClient.NewBuilder("nginx:alpine").
Labels(cleanupLabels).
Port("80").
Pull().
WithName(containerName).
Start(ctx)
require.NoError(err)
containerNames = append(containerNames, containerName)
containerIDs = append(containerIDs, containerID)
}
// RemoveByNames
err = dockerClient.ContainerRemoveByNames(ctx, containerNames...)
require.NoError(err)
containers, err = dockerClient.ContainerListByNames(ctx, true, containerNames...)
require.NoError(err)
require.Len(containers, 0)
// Create
containerIDs = make([]string, 0)
containerNames = make([]string, 0)
for i := 0; i < iterations; i++ {
containerName := uuid.NewV4().String()
containerID, err := dockerClient.NewBuilder("nginx:alpine").
Labels(cleanupLabels).
Port("80").
Pull().
WithName(containerName).
Start(ctx)
require.NoError(err)
containerNames = append(containerNames, containerName)
containerIDs = append(containerIDs, containerID)
}
// RemoveByID
err = dockerClient.ContainerRemoveByIDs(ctx, containerIDs...)
require.NoError(err)
containers, err = dockerClient.ContainerListByLabels(ctx, true, cleanupLabels)
require.NoError(err)
require.Len(containers, 0)
}
// TestNetworkCRUD
// Test the following API functions:
// - NetworkListByLabels
// - NetworkListByNames
// - NetworkRemoveByLabels
// - NetworkRemoveByNames
// - NetworkRemoveByIDs
func TestNetworkCRUD(t *testing.T) {
var (
ctx = context.Background()
require = require.New(t)
iterations = 5
cleanupLabels = map[string]string{
uuid.NewV4().String(): uuid.NewV4().String(),
}
)
dockerClient, err := New()
require.NoError(err)
t.Cleanup(func() {
dockerClient.NetworkRemoveByLabels(ctx, cleanupLabels)
})
// Create Networks
networkIDs := make([]string, 0)
networkNames := make([]string, 0)
for i := 0; i < iterations; i++ {
networkName := uuid.NewV4().String()
resp, err := dockerClient.NetworkCreate(ctx, networkName, types.NetworkCreate{
Labels: cleanupLabels,
})
require.NoError(err)
networkNames = append(networkNames, networkName)
networkIDs = append(networkIDs, resp.ID)
}
// ListByLabels
networks, err := dockerClient.NetworkListByLabels(ctx, cleanupLabels)
require.NoError(err)
require.Len(networks, iterations)
for _, network := range networks {
require.Contains(networkIDs, network.ID)
require.Contains(networkNames, network.Name)
}
// ListByLabels, network with label does not exist
networks, err = dockerClient.NetworkListByLabels(ctx, map[string]string{uuid.NewV4().String(): uuid.NewV4().String()})
require.NoError(err)
require.Len(networks, 0)
// ListByNames
networks, err = dockerClient.NetworkListByNames(ctx, networkNames...)
require.NoError(err)
require.Len(networks, iterations)
for _, network := range networks {
require.Contains(networkIDs, network.ID)
require.Contains(networkNames, network.Name)
}
// ListByNames, network with names does not exist
networks, err = dockerClient.NetworkListByNames(ctx, uuid.NewV4().String(), uuid.NewV4().String())
require.Error(err)
require.Nil(networks)
// RemoveByLabels
err = dockerClient.NetworkRemoveByLabels(ctx, cleanupLabels)
require.NoError(err)
networks, err = dockerClient.NetworkListByLabels(ctx, cleanupLabels)
require.NoError(err)
require.Len(networks, 0)
// RemoveByLabels, label does not exists
err = dockerClient.NetworkRemoveByLabels(ctx, map[string]string{uuid.NewV4().String(): uuid.NewV4().String()})
require.NoError(err)
// Create Networks
networkIDs = make([]string, 0)
networkNames = make([]string, 0)
for i := 0; i < iterations; i++ {
networkName := uuid.NewV4().String()
resp, err := dockerClient.NetworkCreate(ctx, networkName, types.NetworkCreate{
Labels: cleanupLabels,
})
require.NoError(err)
networkNames = append(networkNames, networkName)
networkIDs = append(networkIDs, resp.ID)
}
// RemoveByNames
err = dockerClient.NetworkRemoveByNames(ctx, networkNames...)
require.NoError(err)
networks, err = dockerClient.NetworkListByNames(ctx, networkNames...)
require.Error(err)
require.Nil(networks)
// RemoveByNames, name does not exists
err = dockerClient.NetworkRemoveByNames(ctx, uuid.NewV4().String())
require.Error(err)
// Create Networks
networkIDs = make([]string, 0)
networkNames = make([]string, 0)
for i := 0; i < iterations; i++ {
networkName := uuid.NewV4().String()
resp, err := dockerClient.NetworkCreate(ctx, networkName, types.NetworkCreate{
Labels: cleanupLabels,
})
require.NoError(err)
networkNames = append(networkNames, networkName)
networkIDs = append(networkIDs, resp.ID)
}
// RemoveByIDs
err = dockerClient.NetworkRemoveByIDs(ctx, networkIDs...)
require.NoError(err)
networks, err = dockerClient.NetworkListByNames(ctx, networkNames...)
require.Error(err)
require.Nil(networks)
// RemoveByID, id does not exists
err = dockerClient.NetworkRemoveByIDs(ctx, uuid.NewV4().String())
require.Error(err)
}
func TestVolumeCRUD(t *testing.T) {
var (
ctx = context.Background()
require = require.New(t)
iterations = 5
cleanupLabels = map[string]string{
uuid.NewV4().String(): uuid.NewV4().String(),
}
)
dockerClient, err := New()
require.NoError(err)
t.Cleanup(func() {
dockerClient.VolumeRemoveByLabels(ctx, cleanupLabels)
})
// Create Volumes
volumeNames := make([]string, 0)
for i := 0; i < iterations; i++ {
volumeName := uuid.NewV4().String()
volume, err := dockerClient.VolumeCreate(ctx, volume.VolumesCreateBody{
Name: volumeName,
Labels: cleanupLabels,
})
require.NoError(err)
volumeNames = append(volumeNames, volume.Name)
}
// ListByLabels
volumes, err := dockerClient.VolumeListByLabels(ctx, cleanupLabels)
require.NoError(err)
require.Len(volumes.Volumes, iterations)
for _, volume := range volumes.Volumes {
require.Contains(volumeNames, volume.Name)
}
// ListByLabels, network with label does not exist
volumes, err = dockerClient.VolumeListByLabels(ctx, map[string]string{uuid.NewV4().String(): uuid.NewV4().String()})
require.NoError(err)
require.Len(volumes.Volumes, 0)
// ListByNames
volumes, err = dockerClient.VolumeListByNames(ctx, volumeNames...)
require.NoError(err)
require.Len(volumes.Volumes, iterations)
for _, volume := range volumes.Volumes {
require.Contains(volumeNames, volume.Name)
}
// ListByNames, network with names does not exist
volumes, err = dockerClient.VolumeListByNames(ctx, uuid.NewV4().String(), uuid.NewV4().String())
require.Error(err)
require.Nil(volumes.Volumes)
// RemoveByLabels
err = dockerClient.VolumeRemoveByLabels(ctx, cleanupLabels)
require.NoError(err)
volumes, err = dockerClient.VolumeListByLabels(ctx, cleanupLabels)
require.NoError(err)
require.Len(volumes.Volumes, 0)
// RemoveByLabels, labels does not exists
err = dockerClient.NetworkRemoveByLabels(ctx, map[string]string{uuid.NewV4().String(): uuid.NewV4().String()})
require.NoError(err)
// Create Volumes
volumeNames = make([]string, 0)
for i := 0; i < iterations; i++ {
volumeName := uuid.NewV4().String()
volume, err := dockerClient.VolumeCreate(ctx, volume.VolumesCreateBody{
Name: volumeName,
Labels: cleanupLabels,
})
require.NoError(err)
volumeNames = append(volumeNames, volume.Name)
}
// RemoveByNames
err = dockerClient.VolumeRemoveByNames(ctx, volumeNames...)
require.NoError(err)
volumes, err = dockerClient.VolumeListByNames(ctx, volumeNames...)
require.Error(err)
require.Nil(volumes.Volumes)
// RemoveByNames, name does not exists
err = dockerClient.NetworkRemoveByNames(ctx, uuid.NewV4().String())
require.Error(err)
}
// TestContainerMultipleNetworks
// Test if a container can be accessed ofer multiple networks/ips.
func TestContainerMultipleNetworks(t *testing.T) {
var (
ctx = context.Background()
require = require.New(t)
iterations = 5
cleanupLabels = map[string]string{
uuid.NewV4().String(): uuid.NewV4().String(),
}
)
dockerClient, err := New()
require.NoError(err)
t.Cleanup(func() {
dockerClient.ContainerRemoveByLabels(ctx, cleanupLabels)
dockerClient.NetworkRemoveByLabels(ctx, cleanupLabels)
})
// Create Containers
containerIDs := make([]string, 0)
containerNames := make([]string, 0)
containersNetworks := make(map[string]map[string][]string, 0)
for i := 0; i < iterations; i++ {
containerName := uuid.NewV4().String()
containerNetworks := map[string][]string{
uuid.NewV4().String(): {
uuid.NewV4().String(),
uuid.NewV4().String(),
},
uuid.NewV4().String(): {
uuid.NewV4().String(),
uuid.NewV4().String(),
},
}
builder := dockerClient.NewBuilder("nginx:alpine").
Labels(cleanupLabels).
Port("80").
Pull().
WithName(containerName)
for networkName, aliasses := range containerNetworks {
_, err := dockerClient.NetworkCreate(ctx, networkName, types.NetworkCreate{
Labels: cleanupLabels,
})
require.NoError(err)
builder.Network(networkName, aliasses...)
}
containerID, err := builder.Start(ctx)
require.NoError(err)
containerNames = append(containerNames, containerName)
containerIDs = append(containerIDs, containerID)
containersNetworks[containerID] = containerNetworks
}
for containerID, containerNetworks := range containersNetworks {
for networkName := range containerNetworks {
networks, err := dockerClient.NetworkListByNames(ctx, networkName)
require.NoError(err)
for _, network := range networks {
if _, present := network.Containers[containerID]; !present {
require.Fail("Container %v not found in network %v", containerID, network.ID)
}
networkIPParts := strings.Split(network.Containers[containerID].IPv4Address, "/")
url := fmt.Sprintf("http://%v", networkIPParts[0])
resp, err := http.Get(url)
require.NoError(err)
defer resp.Body.Close()
require.Equal(http.StatusOK, resp.StatusCode)
}
}
}
}

View File

@ -1,164 +0,0 @@
package dockerutils
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
)
var (
ErrDied = errors.New("died")
ErrUnhealthy = errors.New("went unhealthy")
)
type Watcher struct {
client *Client
errorChannels map[string]chan<- error
doneChannels map[string]chan<- struct{}
errorMapper map[string]ErrorMapper
mutex *sync.RWMutex
lastError error
cancelFunc context.CancelFunc
}
func (watcher *Watcher) AddListenerWithErrorMapper(containerID string, errorChannel chan<- error, doneChannel chan<- struct{}, errorMapper ErrorMapper) error {
watcher.mutex.Lock()
defer watcher.mutex.Unlock()
if watcher.lastError != nil {
return watcher.lastError
}
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(20*time.Second))
defer cancel()
containerJSON, err := watcher.client.ContainerInspect(ctx, containerID)
if err != nil {
return fmt.Errorf("Unable to check if container is already unhealthy: %v", err)
}
if containerJSON.State.Dead || !containerJSON.State.Running {
go func() {
errorChannel <- errorMapper(fmt.Errorf("Container %v: %w", containerID, ErrDied))
doneChannel <- struct{}{}
}()
return nil
}
if containerJSON.State.Health == nil {
go func() {
doneChannel <- struct{}{}
}()
return nil
}
switch containerJSON.State.Health.Status {
case "starting":
watcher.errorChannels[containerID] = errorChannel
watcher.doneChannels[containerID] = doneChannel
watcher.errorMapper[containerID] = errorMapper
case "healthy":
go func() {
doneChannel <- struct{}{}
}()
case "unhealthy":
go func() {
errorChannel <- errorMapper(fmt.Errorf("Container %v: %w", containerID, ErrUnhealthy))
doneChannel <- struct{}{}
}()
default:
go func() {
errorChannel <- errorMapper(fmt.Errorf("Container %v went in an unknown state during startup: %s", containerID, containerJSON.State.Health.Status))
doneChannel <- struct{}{}
}()
}
return nil
}
func (watcher *Watcher) AddListener(containerID string, errorChannel chan<- error, doneChannel chan<- struct{}) error {
return watcher.AddListenerWithErrorMapper(containerID, errorChannel, doneChannel, defaultErrorMapper)
}
func (watcher *Watcher) removeListener(containerID string) {
watcher.mutex.Lock()
defer watcher.mutex.Unlock()
delete(watcher.doneChannels, containerID)
delete(watcher.errorChannels, containerID)
delete(watcher.errorMapper, containerID)
}
func (watcher *Watcher) start() {
ctx, cancel := context.WithCancel(context.Background())
watcher.cancelFunc = cancel
dockerEvents, errors := watcher.client.Events(ctx, types.EventsOptions{})
sendErrorFunc := func() {
watcher.mutex.Lock()
for containerID, errorChannel := range watcher.errorChannels {
go func(errorChannel chan<- error, doneChannel chan<- struct{}, err error, errorMapper ErrorMapper) {
errorChannel <- errorMapper(err)
doneChannel <- struct{}{}
}(errorChannel, watcher.doneChannels[containerID], watcher.lastError, watcher.errorMapper[containerID])
}
watcher.mutex.Unlock()
}
go func() {
for {
select {
case event := <-dockerEvents:
watcher.mutex.RLock()
errorChannel, present := watcher.errorChannels[event.Actor.ID]
if !present || event.Type != events.ContainerEventType {
continue
}
doneChannel := watcher.doneChannels[event.Actor.ID]
errorMapper := watcher.errorMapper[event.Actor.ID]
watcher.mutex.RUnlock()
switch event.Action {
case "health_status: healthy":
go func() {
doneChannel <- struct{}{}
}()
watcher.removeListener(event.Actor.ID)
case "health_status: unhealthy":
go func() {
errorChannel <- errorMapper(fmt.Errorf("Container %v: %w", event.Actor.ID, ErrUnhealthy))
doneChannel <- struct{}{}
}()
watcher.removeListener(event.Actor.ID)
case "die":
go func() {
errorChannel <- errorMapper(fmt.Errorf("Container %v: %w", event.Actor.ID, ErrDied))
doneChannel <- struct{}{}
}()
watcher.removeListener(event.Actor.ID)
}
case err := <-errors:
watcher.lastError = err
sendErrorFunc()
return
case <-ctx.Done():
watcher.lastError = fmt.Errorf("Watcher was canceled")
sendErrorFunc()
return
}
}
}()
}
func (watcher *Watcher) stop() {
watcher.cancelFunc()
}
type ErrorMapper func(error) error
func defaultErrorMapper(err error) error {
return err
}

View File

@ -1,12 +0,0 @@
package types
import "time"
// Device represent a device with all his settings.
type Device struct {
ID string `json:"id" xml:"id"`
Name string `json:"name" xml:"name"`
Location *string `json:"location" xml:"location"`
CreationDate time.Time `json:"creation_date" xml:"creation_date"`
UpdateDate *time.Time `json:"update_date" xml:"update_date"`
}

View File

@ -1,18 +0,0 @@
package types
import (
"time"
)
// MeasuredValue represent a value provided by a measuring instrument. For
// example from a sensor. It can contains different types, for example humidity
// or temperature.
type MeasuredValue struct {
ID string `json:"id" xml:"id"`
Value float64 `json:"value,string" xml:"value,string"`
ValueType string `json:"value_type" xml:"value_type"`
Date time.Time `json:"date" xml:"date"`
SensorID string `json:"sensor_id" xml:"sensor_id"`
CreationDate time.Time `json:"creation_date" xml:"creation_date"`
UpdateDate *time.Time `json:"update_date" xml:"update_date"`
}

View File

@ -1,48 +0,0 @@
package types
import (
"time"
)
// Sensor represents a sensor with all his settings. The struct does not
// contains any read method.
type Sensor struct {
ID string `json:"id" xml:"id"`
Name string `json:"name" xml:"name"`
Location string `json:"location" xml:"location"`
WireID *string `json:"wire_id" xml:"wire_id"`
I2CBus *int `json:"i2c_bus" xml:"i2c_bus"`
I2CAddress *uint8 `json:"i2c_address" xml:"i2c_address"`
GPIONumber string `json:"gpio_number" xml:"gpio_number"`
Model string `json:"model" xml:"model"`
Enabled bool `json:"enabled" xml:"enabled"`
TickDuration string `json:"tick_duration" xml:"tick_duration"`
DeviceID string `json:"device_id" xml:"device_id"`
CreationDate time.Time `json:"creation_date" xml:"creation_date"`
UpdateDate *time.Time `json:"update_date" xml:"update_date"`
}
// GetID returns the UUID of the sensor.
func (s *Sensor) GetID() string {
return s.ID
}
// GetDeviceID returns the UUID of the configured device.
func (s *Sensor) GetDeviceID() string {
return s.DeviceID
}
// GetName returns the name of the sensor.
func (s *Sensor) GetName() string {
return s.Name
}
// GetTicker returns a new ticker, which tick every when the sensor should be
// read
func (s *Sensor) GetTicker() *time.Ticker {
duration, err := time.ParseDuration(s.TickDuration)
if err != nil {
duration = time.Minute
}
return time.NewTicker(duration)
}

View File

@ -1,9 +0,0 @@
[Unit]
Description=Start flucky daemon
[Service]
Type=simple
ExecStart=/usr/bin/flucky daemon
[Install]
WantedBy=multi-user.target