Compare commits

..

34 Commits

Author SHA1 Message Date
Adrien Marquès 5998bd7b29 Merge branch 'master' of gogs:go/nix-amer
continuous-integration/drone/push Build is passing Details
2019-11-18 14:35:22 +01:00
Adrien Marquès 27ace57523 Update CI and do not use github import path anymore
- use local self-hosted ci : drone instead of circle
- add go modules support
- update import path to be local : git.xdrm.io/go/nix-amer instead of
github.com/xdrm-brackets/nix-amer
2019-11-18 14:27:19 +01:00
Adrien Marquès a3b513a23b update readme 2018-11-18 22:50:20 +01:00
Adrien Marquès b752906d15 add minimal test to test ANY lines in bash decoder/encoder 2018-11-18 21:56:50 +01:00
Adrien Marquès 3331970eb5 fix cnf/parser/bash to restore ANY-typed lines + restore comments at the end of assignment lines properly 2018-11-18 21:50:02 +01:00
Adrien Marquès bca2b145bc fix tests 2018-11-18 19:09:28 +01:00
Adrien Marquès 0059dd90ea add bash (environment variable export) file format 2018-11-18 19:08:18 +01:00
Adrien Marquès c37893c209 lint 2018-11-16 06:57:57 +01:00
Adrien Marquès 5f229709ad circle-ci add go fmt + go vet 2018-11-16 06:55:45 +01:00
Adrien Marquès 3c598ea419 circle-ci upload tests 2018-11-16 06:54:02 +01:00
Adrien Marquès bf391fd5a0 fix: cnf make cnf/* configuration formats more strict and not to include themselves (to really find out the good format) | add tests | TODO: json is a subset of yaml, create my own YAML parser that is strict to the 'well-known' YAML syntax 2018-11-15 17:26:11 +01:00
Adrien Marquès b26dec8576 FIX: find config format from content : seek start of file before each 2018-11-15 14:48:02 +01:00
Adrien Marquès 8c3b300ab5 fix cnf/nginx | udpate tests 2018-11-15 12:04:45 +01:00
Adrien Marquès 2e3700786d fix/upd instruction/copy | add tests 2018-11-14 22:10:31 +01:00
Adrien Marquès fa8a0a5ae5 fix DryRun() for instruction/copy : create/remove file if does not exist, else try to open in WRITE mode 2018-11-14 20:21:34 +01:00
Adrien Marquès 1a7e1c2db0 add DryRun() method for instruction common interface | add DryRun() method to all instructions | TODO: tests + install/delete 2018-11-14 20:11:07 +01:00
Adrien Marquès 2f95acb851 fix tests 2018-11-14 11:38:21 +01:00
Adrien Marquès a6207e0a34 add parallelism [pre] section run before all other | update readme accordingly 2018-11-14 11:34:27 +01:00
Adrien Marquès 5fd41cae67 add parallelism with sections | todo: add [pre] section executed before all 2018-11-14 11:07:46 +01:00
Adrien Marquès 7e8b45d750 fix buildfile/reader to read last line (without NEWLINE) 2018-11-14 09:58:51 +01:00
Adrien Marquès 361c3f6c25 update shields 2018-11-13 21:50:10 +01:00
Adrien Marquès 87bf394536 lint tests 2018-11-13 21:36:19 +01:00
Adrien Marquès a9a69a5728 add cnf/parser/nginx/decoder receiver/syntax/writer error 2018-11-13 21:33:16 +01:00
Adrien Marquès a77923a686 add cnf/parser/nginx/encoder writer error 2018-11-13 21:11:15 +01:00
Adrien Marquès 685da832df test default prefix/indent values + encoder errors 2018-11-13 21:06:39 +01:00
Adrien Marquès 9d2219c3e4 fix readme set coveralls.io MASTER branch 2018-11-13 17:29:16 +01:00
Adrien Marquès 73a0bfdadb fix symbols DisplaySize() + Align() + tests 2018-11-13 17:26:04 +01:00
Adrien Marquès 6cdbf07d37 test clifmt/Title + Warn + Info 2018-11-13 16:22:57 +01:00
Adrien Marquès b4d6b8d8ff add clifmt/colors minimal test 2018-11-13 16:03:57 +01:00
Adrien Marquès d4daa435e5 readme: add 'copy' command 2018-11-13 14:23:36 +01:00
Adrien Marquès 48600584a0 add copy command (instruction) 2018-11-13 14:21:36 +01:00
Adrien Marquès c548e6a5e8 minmod 2018-11-13 14:08:49 +01:00
Adrien Marquès d9bbbfeea3 update readme : add installation instructions 2018-11-13 14:06:33 +01:00
Adrien Marquès 7ddc9a3006 update readme : add toc (Table Of Contents) 2018-11-13 13:50:33 +01:00
53 changed files with 2179 additions and 238 deletions

11
.drone.yml Normal file
View File

@ -0,0 +1,11 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: test
image: golang:1.13
commands:
- go get ./...
- go test -v -race -cover -coverprofile ./coverage.out ./...

170
README.md
View File

@ -1,11 +1,11 @@
# | nix-amer | # | nix-amer |
[![Go version](https://img.shields.io/badge/go_version-1.11-blue.svg)](https://golang.org/doc/go1.11) [![Go version](https://img.shields.io/badge/go_version-1.11-blue.svg)](https://golang.org/doc/go1.11)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/github/license/xdrm-brackets/nix-amer.svg)](https://opensource.org/licenses/MIT)
[![Go Report Card](https://goreportcard.com/badge/github.com/xdrm-brackets/nix-amer)](https://goreportcard.com/report/github.com/xdrm-brackets/nix-amer) [![Go Report Card](https://goreportcard.com/badge/git.xdrm.io/go/nix-amer)](https://goreportcard.com/report/git.xdrm.io/go/nix-amer)
[![Coverage Status](https://coveralls.io/repos/github/xdrm-brackets/nix-amer/badge.svg?branch=meta%2Fcircle-ci)](https://coveralls.io/github/xdrm-brackets/nix-amer?branch=meta%2Fcircle-ci) [![Coverage Status](https://img.shields.io/coveralls/github/xdrm-brackets/nix-amer/master.svg)](https://coveralls.io/github/xdrm-brackets/nix-amer?branch=master)
[![CircleCI Build Status](https://circleci.com/gh/xdrm-brackets/nix-amer.svg?style=shield)](https://circleci.com/gh/xdrm-brackets/nix-amer) [![Build Status](https://drone.xdrm.io/api/badges/go/nix-amer/status.svg)](https://drone.xdrm.io/go/nix-amer)
[![Go doc](https://godoc.org/github.com/xdrm-brackets/nix-amer?status.svg)](https://godoc.org/github.com/xdrm-brackets/nix-amer) [![Go doc](https://godoc.org/git.xdrm.io/go/nix-amer?status.svg)](https://godoc.org/git.xdrm.io/go/nix-amer)
```yaml ```yaml
name: nix-amer name: nix-amer
@ -16,28 +16,102 @@ author: xdrm-brackets
>Need to automate the setup of your linux server or desktop ? This tool is made for you. >Need to automate the setup of your linux server or desktop ? This tool is made for you.
<!-- toc -->
[TOC] * [I. How to use](#i-how-to-use)
+ [1) Requirements](#1-requirements)
+ [2) Installation](#2-installation)
+ [3) Usage](#3-usage)
* [1. Create build file](#1-create-build-file)
* [2. Run on the target](#2-run-on-the-target)
* [II. Commands](#ii-commands)
+ [1) Sections](#1-sections)
+ [2) Comments](#2-comments)
+ [3) Install/remove Packages](#3-installremove-packages)
+ [4) Setup configuration](#4-setup-configuration)
+ [5) Service management](#5-service-management)
+ [6) Custom scripts](#6-custom-scripts)
+ [7) Copy files](#7-copy-files)
+ [8) Aliases](#8-aliases)
* [III. Path Expressions](#iii-path-expressions)
+ [1) Syntax](#1-syntax)
+ [2) File Formats](#2-file-formats)
- [Example](#example)
<!-- tocstop -->
---- ----
### I. How to use
### I. Commands
Your whole setup remains in 1 only build file. Each line contains one instruction, the list of instructions is listed below.
#### 1) Comments #### 1) Requirements
Each line beginning with one of the following characters : `[`, `#` or `;` is considered a comment and is not interpreted. In order to install the `nix-amer` executable, you must have :
- any recent linux system (_has not been tested over other OS_)
- `go` installed (_has not been tested under version **1.11**_)
#### 2) package management #### 2) Installation
Simply launch the following command in any terminal
```bash
$ go get -u git.xdrm.io/go/nix-amer
```
> For those who don't know, it will load the project sources into `$GOPATH/src/git.xdrm.io/go/nix-amer` and compile into the executable at `$GOPATH/bin/nix-amer`.
#### 3) Usage
###### 1. Create build file
The first step is to write your build file according to the installation you want. While writing it you can check the syntax and validate instructions by using the `-dry-run` command-line argument as follows :
```bash
$ nix-amer -p apt-get -dry-run <path/to/build/file>
```
> The `-p` argument (package manager) is mandatory but it will have no effect in `-dry-run` mode. You can use for instance `apt-get` as a default.
###### 2. Run on the target
Once your build file is correct and fulfills your needs, you can log in to the target machine, install nix-amer and run it with your build file. The rich and colorful command-line output will give you a good feedback to rapidly fix problems.
------
### II. Commands
Your whole setup remains in only one file. Each line contains one instruction, the list of instructions is listed below.
#### 1) Sections
Each instruction is enclosed in a section (_cf. ini file format_), a section definition stands on a line where the name of the section is surrounded by `[` and `]`. Each section is executed in parallel ; the special section named `pre` is executed before every other.
#### 2) Comments
Each line beginning with one of the following characters : `#` or `;` is considered a comment and is not interpreted.
#### 3) Install/remove Packages
These instructions allow you to interact with the package system available on your system. These instructions allow you to interact with the package system available on your system.
@ -55,7 +129,7 @@ Remove the listed packages. If more than one, use spaces to separate package nam
#### 3) setup configuration #### 4) Setup configuration
This instruction allow you to set fields of configuration files without the need of an editor and in a developer-readable manner. This instruction allow you to set fields of configuration files without the need of an editor and in a developer-readable manner.
@ -67,7 +141,7 @@ Update a configuration file where \<expr\> is a dot-separated human-readable [pa
#### 4) service management #### 5) Service management
These instructions allow you to interact with the service system (_cf. [systemd](https://github.com/systemd/systemd)_). These instructions allow you to interact with the service system (_cf. [systemd](https://github.com/systemd/systemd)_).
@ -78,9 +152,31 @@ Perform the action on services. If more than one, use spaces to separate service
#### 5) aliases #### 6) Custom scripts
The file format allows you to create aliases to file paths for more readability in the [path expression](#ii-path-expressions) or with the [`run` command](#6-custom-scripts). This instruction allows you to use custom scripts for complex operations.
```
run <script>
```
Execute the executable located at the path \<script\>. If script is an [alias](#8-aliases) it will resolve to its path
#### 7) Copy files
This instruction allows you to copy files.
```
copy <src> <dst>
```
Try to copy the file \<src\> to the path \<dst\>.
#### 8) Aliases
The file format allows you to create aliases to file paths for more readability in the [path expression](#ii-path-expressions) or with the [`run` command](#5-custom-scripts).
``` ```
alias name /path/to.file alias name /path/to.file
@ -92,36 +188,28 @@ Create the alias `name` which resolves to the path `/path/to.file`.
#### 6) custom scripts
These instructions allow you to use custom scripts for complex operations.
```
run <script>
```
Execute the executable located at the path \<script\>. If script is an [alias](#5-aliases) it will resolve to its path
---- ----
### II. Path Expressions ### III. Path Expressions
#### 1) Syntax
The syntax is pretty fast-forward, it uses 2 levels (file, fields) to find your configuration line : `location_or_alias@fields`. The syntax is pretty fast-forward, it uses 2 levels (file, fields) to find your configuration line : `location_or_alias@fields`.
| Field | Description | Example | | Field | Description | Example |
| --------- | :----------------------------------- | -------------------------- | | --------- | :----------------------------------- | -------------------------- |
| `location_or_alias` | Path to the configuration file to edit. The file will be created if not found. If the path is an [alias](#5-aliases) created before in the file, it will resolve to the alias value as a filename. | `/etc/nginx/nginx.conf`, `some-alias` | | `location_or_alias` | Path to the configuration file to edit. The file will be created if not found. If the path is an [alias](#8-aliases) created before in the file, it will resolve to the alias value as a filename. | `/etc/nginx/nginx.conf`, `some-alias` |
| `fields` | Dot-separated chain of strings that match a configuration field. If the field does not point to a raw field but an existing field container, the \<value\> will replace the group with a text value. | `AllowGroups`, `http.gzip` | | `fields` | Dot-separated chain of strings that match a configuration field. If the field does not point to a raw field but an existing field container, the \<value\> will replace the group with a text value. | `AllowGroups`, `http.gzip` |
> The `fields` is processed only for known file formats listed in this [section](#file-formats). > The `fields` is processed only for known file formats listed in this [section](#2-file-formats).
#### File Formats #### 2) File Formats
Configuration files can be written according to some standards or application-specific syntax. This tool uses standard and third-party to parse the following formats : Configuration files can be written according to some standards or application-specific syntax. This tool uses standard and third-party to parse the following formats :
@ -131,8 +219,8 @@ Configuration files can be written according to some standards or application-sp
- [yaml](https://en.wikipedia.org/wiki/YAML) with [go-yaml/yaml](https://github.com/go-yaml/yaml). - [yaml](https://en.wikipedia.org/wiki/YAML) with [go-yaml/yaml](https://github.com/go-yaml/yaml).
- [ini](https://en.wikipedia.org/wiki/INI_file) with [go-ini/ini](https://github.com/go-ini/ini). - [ini](https://en.wikipedia.org/wiki/INI_file) with [go-ini/ini](https://github.com/go-ini/ini).
- [nginx configurations](https://docs.nginx.com/nginx/admin-guide/basic-functionality/managing-configuration-files/) with [my own library](https://godoc.org/git.xdrm.io/go/nix-amer/internal/cnf/parser/nginx).
- [nginx configurations](https://docs.nginx.com/nginx/admin-guide/basic-functionality/managing-configuration-files/) with [my own library](https://godoc.org/github.com/xdrm-brackets/nix-amer/internal/cnf/parser/nginx). - [bash sourced configurations]() with [my own library](https://godoc.org/git.xdrm.io/go/nix-amer/internal/cnf/parser/bash) (_e.g. ~/.bashrc_).
- _and more to come..._ - _and more to come..._
@ -154,17 +242,13 @@ $ nix-amer -p apt-get myserver.build
_myserver.build_ _myserver.build_
``` ```
[ comment starts with opening brackets '[' # [pre] is executed before launching everything else
[pre]
[aliases] install nginx ssh sslh
alias sshd /etc/ssh/sshd_config
alias nginx /etc/nginx/nginx.conf alias nginx /etc/nginx/nginx.conf
alias sshd /etc/ssh/sshd_config
alias sslh /etc/default/sslh alias sslh /etc/default/sslh
[install packages]
install nginx ssh
install sslh
[nginx] [nginx]
set nginx@http.gzip off set nginx@http.gzip off
service enable nginx service enable nginx

View File

@ -3,7 +3,7 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"github.com/xdrm-brackets/nix-amer/internal/instruction" "git.xdrm.io/go/nix-amer/internal/instruction"
) )
// GetArgs manages cli arguments to build executionContext, // GetArgs manages cli arguments to build executionContext,

View File

@ -1,38 +0,0 @@
version: 2
jobs:
build: # runs not using Workflows must have a `build` job as entry point
docker:
- image: circleci/golang
environment: # environment variables for the build itself
GOPATH: /go
TEST_RESULTS: /tmp/test-results
COVER_PROFILE: /tmp/coverage.out
steps: # steps that comprise the `build` job
- checkout # check out source code to working directory
- run: mkdir -p $TEST_RESULTS # create the test results directory
- restore_cache: # restores saved cache if no changes are detected since last run
keys:
- v1-pkg-cache
- run:
name: Load dependencies
command: go get github.com/mattn/goveralls && go get github.com/go-ini/ini && go get gopkg.in/yaml.v2
- run:
name: Load nix-amer
command: go get github.com/xdrm-brackets/nix-amer
- run:
name: Unit tests
command: go test -v -cover -race -coverprofile=$COVER_PROFILE github.com/xdrm-brackets/nix-amer/...
- run:
name: Update coveralls.io
command: /go/bin/goveralls -coverprofile=$COVER_PROFILE -service=circle-ci -repotoken=$COVERALLS_TOKEN
- store_artifacts: # Upload test summary for display in Artifacts
path: /tmp/test-results
destination: raw-test-output
- store_test_results: # Upload test results for display in Test Summary
path: /tmp/test-results

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module git.xdrm.io/go/nix-amer
go 1.12
require (
github.com/go-ini/ini v1.51.0
gopkg.in/yaml.v2 v2.2.5
)

5
go.sum Normal file
View File

@ -0,0 +1,5 @@
github.com/go-ini/ini v1.51.0 h1:VPJKXGzbKlyExUE8f41aV57yxkYx5R49yR6n7flp0M0=
github.com/go-ini/ini v1.51.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -2,7 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/xdrm-brackets/nix-amer/internal/clifmt" "git.xdrm.io/go/nix-amer/internal/clifmt"
) )
func help() { func help() {

View File

@ -2,7 +2,7 @@ package buildfile
import ( import (
"fmt" "fmt"
"github.com/xdrm-brackets/nix-amer/internal/clifmt" "git.xdrm.io/go/nix-amer/internal/clifmt"
) )
// LineError wraps errors with a line index // LineError wraps errors with a line index

View File

@ -4,24 +4,43 @@ import (
"bufio" "bufio"
"errors" "errors"
"fmt" "fmt"
"github.com/xdrm-brackets/nix-amer/internal/clifmt" "git.xdrm.io/go/nix-amer/internal/clifmt"
"github.com/xdrm-brackets/nix-amer/internal/instruction" "git.xdrm.io/go/nix-amer/internal/instruction"
"io" "io"
"regexp"
"strings" "strings"
"sync"
"time" "time"
) )
// ErrNullContext is raised when the given context is nil // ErrNullContext is raised when the given context is nil
var ErrNullContext = errors.New("null context") var ErrNullContext = errors.New("null context")
// ErrNoParent is raised when there is an instruction but has no parent section
var ErrNoParent = errors.New("missing parent section")
// Reader is the buildfile reader // Reader is the buildfile reader
type Reader struct { type Reader struct {
// Context is the linux distribution-specified execution context (package manager, service manager, etc) // Context is the linux distribution-specified execution context (package manager, service manager, etc)
Context *instruction.ExecutionContext Context *instruction.ExecutionContext
// Content is the instruction list // Content is the instruction list
Content []instruction.T Content map[string]*[]instruction.T
} }
type execStatus struct {
name string
start time.Time
stop time.Time
stopped bool
err error
}
type tableSection struct {
name string
instructions []execStatus
}
var reSection = regexp.MustCompile(`(?m)^\[\s*([a-z0-9_-]+)\s*\]$`)
// NewReader creates a new reader for the specified build file and linux distribution // NewReader creates a new reader for the specified build file and linux distribution
func NewReader(ctx *instruction.ExecutionContext, buildfile io.Reader) (*Reader, error) { func NewReader(ctx *instruction.ExecutionContext, buildfile io.Reader) (*Reader, error) {
@ -32,36 +51,60 @@ func NewReader(ctx *instruction.ExecutionContext, buildfile io.Reader) (*Reader,
r := &Reader{ r := &Reader{
Context: ctx, Context: ctx,
Content: make([]instruction.T, 0), Content: make(map[string]*[]instruction.T),
} }
// add each line as instruction // add each line as instruction
l, reader := 0, bufio.NewReader(buildfile) l, reader := 0, bufio.NewReader(buildfile)
eof := false
var section *[]instruction.T // current section
for { for {
l++ l++
if eof {
break
}
// read line until end // 1. read line until end
line, err := reader.ReadString('\n') line, err := reader.ReadString('\n')
if err == io.EOF { if err == io.EOF {
if len(line) > 0 {
eof = true
} else {
break break
}
} else if err != nil { } else if err != nil {
return nil, LineError{l, err} return nil, LineError{l, err}
} }
line = strings.Trim(line, " \t\r\n") line = strings.Trim(line, " \t\r\n")
// ignore newline & comments // 2. ignore newline & comments
if len(line) < 1 || strings.ContainsAny(line[0:1], "[#;") { if len(line) < 1 || strings.ContainsAny(line[0:1], "#;") {
continue continue
} }
// turn into instruction // 3. section
if match := reSection.FindStringSubmatch(line); len(match) > 1 {
// already in section
sec := make([]instruction.T, 0)
section = &sec
r.Content[match[1]] = section
continue
}
// 4. fail if no parent section
if section == nil {
return nil, ErrNoParent
}
// 5. create instruction
inst, err := instruction.Parse(line) inst, err := instruction.Parse(line)
if err != nil { if err != nil {
return nil, LineError{l, err} return nil, LineError{l, err}
} }
// add to list // add to list
r.Content = append(r.Content, inst) *section = append(*section, inst)
} }
@ -69,7 +112,13 @@ func NewReader(ctx *instruction.ExecutionContext, buildfile io.Reader) (*Reader,
} }
// Execute the current buildfile instruction by instruction // Execute the current buildfile instruction by instruction
func (r *Reader) Execute() error { // if <dryRun> is set to TRUE, run on dry-run mode
func (r *Reader) Execute(_dryRun ...bool) error {
dryRun := true
if len(_dryRun) < 1 || !_dryRun[0] {
dryRun = false
}
// 1. update package list // 1. update package list
// err := r.Context.PackageManager.Fetch() // err := r.Context.PackageManager.Fetch()
@ -82,26 +131,132 @@ func (r *Reader) Execute() error {
// if err != nil { // if err != nil {
// return fmt.Errorf("cannot upgrade | %s", err) // return fmt.Errorf("cannot upgrade | %s", err)
// } // }
refresh := make(chan bool, 1)
wg := new(sync.WaitGroup)
wgstatus := new(sync.WaitGroup)
// 3. exec each instruction // 1. create status table + extract [pre] section if one
for i, inst := range r.Content { table := make([]tableSection, 0)
clifmt.Align(fmt.Sprintf("(%d) %s", i, clifmt.Color(0, inst.Raw()))) index := make(map[string]int, 0)
fmt.Printf("%s", clifmt.Color(33, "processing"))
start := time.Now() var pre *[]instruction.T
var preTable *tableSection
_, err := inst.Exec(*r.Context) for secname, sec := range r.Content {
if err != nil {
fmt.Printf("\r") tableSec := tableSection{
clifmt.Align(fmt.Sprintf("(%d) %s", i, clifmt.Color(0, inst.Raw()))) name: secname,
fmt.Printf("%s \n", clifmt.Color(31, err.Error())) instructions: make([]execStatus, len(*sec), len(*sec)+1),
}
// for each instruction
for i, inst := range *sec {
tableSec.instructions[i].name = inst.Raw()
}
table = append(table, tableSec)
index[secname] = len(table) - 1
// [pre] section
if secname == "pre" {
pre = sec
preTable = &tableSec
}
// add one section
wg.Add(len(*sec))
}
// 2. launch status updater
wgstatus.Add(1)
go status(table, refresh, wgstatus)
// 3. launch [pre] (it set)
if pre != nil {
execSection(pre, *r.Context, preTable, dryRun, refresh, wg)
time.Sleep(time.Second * 2)
}
// 4. launch each other section
for secname, sec := range r.Content {
// do not launch pre again
if secname == "pre" {
continue continue
} else {
fmt.Printf("\r")
clifmt.Align(fmt.Sprintf("(%d) %s", i, clifmt.Color(34, inst.Raw())))
fmt.Printf("%ss \n", clifmt.Color(32, fmt.Sprintf("%.2f", time.Now().Sub(start).Seconds())))
} }
i, ok := index[secname]
if !ok {
continue
} }
go execSection(sec, *r.Context, &table[i], dryRun, refresh, wg)
}
wg.Wait()
close(refresh)
wgstatus.Wait()
return nil return nil
} }
func execSection(section *[]instruction.T, ctx instruction.ExecutionContext, tsec *tableSection, dryRun bool, refresher chan<- bool, wg *sync.WaitGroup) {
for i, inst := range *section {
tsec.instructions[i].start = time.Now()
var err error
if dryRun {
_, err = inst.DryRun(ctx)
} else {
_, err = inst.Exec(ctx)
}
tsec.instructions[i].stop = time.Now()
tsec.instructions[i].stopped = true
tsec.instructions[i].err = err
refresher <- true
wg.Done()
}
}
func status(table []tableSection, refresher <-chan bool, wg *sync.WaitGroup) {
for opened := true; true; _, opened = <-refresher {
// 1. clean screen
fmt.Printf("\033[H\033[2J")
// 2. for each section
for _, sec := range table {
fmt.Printf("\n[ %s ]\n", sec.name)
// 3. for each instruction
for i, inst := range sec.instructions {
if !inst.stopped {
clifmt.Align(fmt.Sprintf("(%d) %s", i, clifmt.Color(0, inst.name)))
fmt.Printf("%s\n", clifmt.Color(33, "processing"))
continue
}
if inst.err != nil {
clifmt.Align(fmt.Sprintf("(%d) %s", i, clifmt.Color(0, inst.name)))
fmt.Printf("%s\n", clifmt.Color(31, inst.err.Error()))
continue
} else {
clifmt.Align(fmt.Sprintf("(%d) %s", i, clifmt.Color(34, inst.name)))
fmt.Printf("%ss\n", clifmt.Color(32, fmt.Sprintf("%.2f", inst.stop.Sub(inst.start).Seconds())))
}
}
}
if !opened {
break
}
}
wg.Done()
}

View File

@ -2,7 +2,7 @@ package buildfile
import ( import (
"bytes" "bytes"
"github.com/xdrm-brackets/nix-amer/internal/instruction" "git.xdrm.io/go/nix-amer/internal/instruction"
"testing" "testing"
) )
@ -15,7 +15,7 @@ func TestNullContext(t *testing.T) {
} }
func TestIgnoreCommentsAndEmptyLines(t *testing.T) { func TestIgnoreCommentsAndEmptyLines(t *testing.T) {
ctx, _ := instruction.CreateContext("apt-get", "") ctx, _ := instruction.CreateContext("apt-get", "")
buffer := bytes.NewBufferString("[ some comment ]\n\n \t \n\t \t\n[ other comment after empty lines ]") buffer := bytes.NewBufferString("# some comment\n;other comment\n \t \n\t \t\n; other comment after empty lines")
r, err := NewReader(ctx, buffer) r, err := NewReader(ctx, buffer)
if err != nil { if err != nil {
@ -33,24 +33,24 @@ func TestInstructionSyntax(t *testing.T) {
Line int Line int
Err error Err error
}{ }{
{"install args\ndelete args\n", 0, nil}, {"[pre]\ninstall args\ndelete args\n", 1, nil},
{" install args\ndelete args\n", 0, nil}, {"[pre]\n install args\ndelete args\n", 1, nil},
{"\tinstall args\ndelete args\n", 0, nil}, {"[pre]\n\tinstall args\ndelete args\n", 1, nil},
{" \t install args\ndelete args\n", 0, nil}, {"[pre]\n \t install args\ndelete args\n", 1, nil},
{" \t install args\ndelete args\n", 0, nil}, {"[pre]\n \t install args\ndelete args\n", 1, nil},
{"cmd args\ncmd args\n", 1, instruction.ErrUnknownInstruction}, {"[pre]\ncmd args\ncmd args\n", 2, instruction.ErrUnknownInstruction},
{"install args\ncmd args\n", 2, instruction.ErrUnknownInstruction}, {"[pre]\ninstall args\ncmd args\n", 3, instruction.ErrUnknownInstruction},
{" cmd args\ncmd args\n", 1, instruction.ErrUnknownInstruction}, {"[pre]\n cmd args\ncmd args\n", 2, instruction.ErrUnknownInstruction},
{"\tcmd args\ncmd args\n", 1, instruction.ErrUnknownInstruction}, {"[pre]\n\tcmd args\ncmd args\n", 2, instruction.ErrUnknownInstruction},
{" \t cmd args\ncmd args\n", 1, instruction.ErrUnknownInstruction}, {"[pre]\n \t cmd args\ncmd args\n", 2, instruction.ErrUnknownInstruction},
{" \t cmd args\ncmd args\n", 1, instruction.ErrUnknownInstruction}, {"[pre]\n \t cmd args\ncmd args\n", 2, instruction.ErrUnknownInstruction},
{"cmd args\ncmd\n", 1, instruction.ErrUnknownInstruction}, {"[pre]\ncmd args\ncmd\n", 2, instruction.ErrUnknownInstruction},
{"install\ncmd args\n", 1, instruction.ErrInvalidSyntax}, {"[pre]\ninstall\ncmd args\n", 2, instruction.ErrInvalidSyntax},
{"install args\n cmd args\n", 2, instruction.ErrUnknownInstruction}, {"[pre]\ninstall args\n cmd args\n", 3, instruction.ErrUnknownInstruction},
{"install args\ncmd\n", 2, instruction.ErrInvalidSyntax}, {"[pre]\ninstall args\ncmd\n", 3, instruction.ErrInvalidSyntax},
} }
ctx, _ := instruction.CreateContext("apt-get", "") ctx, _ := instruction.CreateContext("apt-get", "")
@ -64,7 +64,11 @@ func TestInstructionSyntax(t *testing.T) {
// no error expected // no error expected
if test.Err == nil { if test.Err == nil {
if err != nil { if err != nil {
lineerr := err.(LineError) lineerr, ok := err.(LineError)
if !ok {
t.Errorf("[%d] expect error to be of type <LineError>", i)
continue
}
t.Errorf("[%d] expect no error, got <%s>", i, lineerr.Err) t.Errorf("[%d] expect no error, got <%s>", i, lineerr.Err)
} }
continue continue

View File

@ -0,0 +1,56 @@
package clifmt
import (
"testing"
)
func escape(in string) string {
out := make([]rune, 0)
for _, char := range in {
if char == '\\' {
out = append(out, []rune("\\\\")...)
} else if char == '\n' {
out = append(out, []rune("\\n")...)
} else if char == '\r' {
out = append(out, []rune("\\r")...)
} else if char == '\t' {
out = append(out, []rune("\\t")...)
} else if char == '\033' {
out = append(out, []rune("\\033")...)
} else {
out = append(out, char)
}
}
return string(out)
}
func TestColoring(t *testing.T) {
tests := []struct {
Text string
Color byte
Bold bool
Expect string
}{
{"any text", 0, false, "\033[0;0many text\033[0m"},
{"any text", 1, false, "\033[0;1many text\033[0m"},
{"any text", 32, false, "\033[0;32many text\033[0m"},
{"any text", 0, true, "\033[1;0many text\033[0m"},
{"any text", 1, true, "\033[1;1many text\033[0m"},
{"any text", 32, true, "\033[1;32many text\033[0m"},
}
for i, test := range tests {
colored := Color(test.Color, test.Text, test.Bold)
if colored != test.Expect {
t.Errorf("[%d] expected '%s', got '%s'", i, escape(test.Expect), escape(colored))
}
}
}

View File

@ -8,6 +8,7 @@ import (
var titleIndex = 0 var titleIndex = 0
var alignOffset = 40 var alignOffset = 40
var defaultPrinter = fmt.Printf
// Warn returns a red warning ASCII sign. If a string is given // Warn returns a red warning ASCII sign. If a string is given
// as argument, it will print it after the warning sign // as argument, it will print it after the warning sign
@ -32,7 +33,7 @@ func Info(s ...string) string {
// Title prints a formatted title (auto-indexed from local counted) // Title prints a formatted title (auto-indexed from local counted)
func Title(s string) { func Title(s string) {
titleIndex++ titleIndex++
fmt.Printf("\n%s |%d| %s %s\n", Color(33, ">>", false), titleIndex, s, Color(33, "<<", false)) defaultPrinter("\n%s |%d| %s %s\n", Color(33, ">>", false), titleIndex, s, Color(33, "<<", false))
} }
@ -46,40 +47,37 @@ func Align(s string) {
s = strings.Join(tabs, " ") s = strings.Join(tabs, " ")
// 2. get real size // 2. get real size
size := displaySize(s) size := DisplaySize(s)
offset := alignOffset offset := alignOffset
if size > alignOffset-6 { if size > alignOffset-2 {
for i, l := 0, len(s); i < l; i++ { // find when real size is right under for i := len(s) - 1; i > 0; i-- { // find when real size is right under
next := fmt.Sprintf("%s\033[0m… ", s[0:i+1]) next := fmt.Sprintf("%s\033[0m… ", s[0:i])
if displaySize(next) >= alignOffset-5 {
if DisplaySize(next) <= alignOffset {
s = next s = next
size = DisplaySize(s)
break break
} }
} }
size = displaySize(s)
} else {
// fix
offset -= 2
} }
// 3. print string // 3. print string
fmt.Printf("%s", s) defaultPrinter("%s", s)
// 4. print trailing spaces // 4. print trailing spaces
for i := size; i < offset; i++ { for i := size; i < offset; i++ {
fmt.Printf(" ") defaultPrinter(" ")
} }
} }
var re = regexp.MustCompile(`(?m)\[(?:\d+;)*\d+m`) var re = regexp.MustCompile(`(?m)\[(?:\d+;)*\d+m`)
var reDots = regexp.MustCompile(`(?m)…`)
// displaySize returns the real size escaping special characters // DisplaySize returns the real size escaping special characters
func displaySize(s string) int { func DisplaySize(s string) int {
// 1. get actual size // 1. get actual size
size := len(s) size := len(s)
@ -90,26 +88,12 @@ func displaySize(s string) int {
size -= len(m) size -= len(m)
} }
// 3. Remove unicode character (len of 3 instead of 1)
matches = reDots.FindAllString(s, -1)
for _, m := range matches {
size -= len(m)
size++
}
return size return size
} }
func escape(in string) string {
out := make([]rune, 0)
for _, char := range in {
if char == '\n' {
out = append(out, []rune("\\n")...)
} else if char == '\r' {
out = append(out, []rune("\\r")...)
} else if char == '\t' {
out = append(out, []rune("\\t")...)
} else if char == '\033' {
out = append(out, []rune("\\033")...)
} else {
out = append(out, char)
}
}
return string(out)
}

View File

@ -0,0 +1,109 @@
package clifmt
import (
"fmt"
"testing"
)
var lastPrint = ""
func mockupPrinter(format string, args ...interface{}) (int, error) {
lastPrint = fmt.Sprintf("%s%s", lastPrint, fmt.Sprintf(format, args...))
return 0, nil
}
func TestSpecial(t *testing.T) {
defaultPrinter = mockupPrinter
tests := []struct {
Pre func()
Processed string
Expect string
}{
{nil, Warn(), "\033[0;31m/!\\\033[0m"},
{nil, Warn("any text"), "\033[0;31m/!\\\033[0m any text"},
{nil, Info(), "\033[0;34m(!)\033[0m"},
{nil, Info("any text"), "\033[0;34m(!)\033[0m any text"},
{func() { Title("any text") }, "", "\n\033[0;33m>>\033[0m |1| any text \033[0;33m<<\033[0m\n"},
{func() { Title("any text") }, "", "\n\033[0;33m>>\033[0m |2| any text \033[0;33m<<\033[0m\n"},
}
for i, test := range tests {
if test.Pre != nil {
lastPrint = ""
test.Pre()
test.Processed = lastPrint
}
if test.Processed != test.Expect {
t.Errorf("[%d] expected '%s', got '%s'", i, escape(test.Expect), escape(test.Processed))
}
}
}
func TestAlign(t *testing.T) {
defaultPrinter = mockupPrinter
tests := []struct {
Offset int
Text string
Expect string
}{
{12, "1234567890 ", "1234567890 "},
{12, "12345678901 ", "1234567890\033[0m… "},
{12, "123456789012", "1234567890\033[0m… "},
{12, "1234567890123", "1234567890\033[0m… "},
}
for i, test := range tests {
lastPrint = ""
alignOffset = test.Offset
Align(test.Text)
if DisplaySize(lastPrint) != alignOffset {
t.Errorf("[%d] expected output to be %d chars, got %d (%s)", i, alignOffset, DisplaySize(lastPrint), escape(lastPrint))
}
if lastPrint != test.Expect {
t.Errorf("[%d] expected '%s', got '%s'", i, escape(test.Expect), escape(lastPrint))
}
}
}
func TestDisplaySize(t *testing.T) {
tests := []struct {
Text string
Expect int
}{
{"", 0},
{"\033[32m\033[0m", 0},
{"\033[0;32m\033[0m", 0},
{"1", 1},
{"\033[32m1\033[0m", 1},
{"\033[0;32m1\033[0m", 1},
{"\033[0;32m1\033[0m\033[0;32m\033[1;31m\033[0m", 1},
{"123", 3},
{"…123", 4},
{"123…", 4},
{"…123…", 5},
{"123456789", 9},
{"1234567890", 10},
{"1234567890\033[0m… ", 12},
}
for i, test := range tests {
if DisplaySize(test.Text) != test.Expect {
t.Errorf("[%d] expected output to be %d chars, got %d (%s)", i, test.Expect, DisplaySize(test.Text), escape(lastPrint))
}
}
}

99
internal/cnf/bash.go Normal file
View File

@ -0,0 +1,99 @@
package cnf
import (
lib "git.xdrm.io/go/nix-amer/internal/cnf/parser/bash"
"io"
"strings"
)
type bash struct {
data *lib.File
parsed bool
}
// ReadFrom implements io.ReaderFrom
func (d *bash) ReadFrom(_reader io.Reader) (int64, error) {
d.data = new(lib.File)
// 1. get bash decoder
decoder := lib.NewDecoder(_reader)
err := decoder.Decode(d.data)
if err != nil {
return 0, err
}
d.parsed = true
return 0, nil
}
// WriteTo implements io.WriterTo
func (d *bash) WriteTo(_writer io.Writer) (int64, error) {
encoder := lib.NewEncoder(_writer)
return 0, encoder.Encode(d.data)
}
// browse returns the target of a dot-separated path (as an interface{} chain where the last is the target if found)
// if <create> is true, create if does not exist
func (d *bash) browse(_path string, create ...bool) (*lib.Line, bool) {
mustCreate := len(create) > 0 && create[0]
// 1. extract path
path := strings.Split(_path, ".")
field := path[len(path)-1]
// 2. nothing
if len(path) < 1 {
return &lib.Line{}, true
}
// 3. iterate over path / nested fields
for _, line := range d.data.Lines {
if line.Type == lib.ASSIGNMENT && len(line.Components) > 1 && line.Components[1] == field {
return line, true
}
}
// 4. create assignment
if mustCreate {
assignment := &lib.Line{
Type: lib.ASSIGNMENT,
Components: []string{"", field, "", ""},
}
d.data.Lines = append(d.data.Lines, assignment)
return assignment, true
}
return nil, false
}
// Get returns the value of a dot-separated path, and if it exists
func (d *bash) Get(_path string) (string, bool) {
// 1. browse path
last, found := d.browse(_path, true)
if !found {
return "", false
}
// 2. Get last field
return last.Components[2], true
}
// Set the value of a dot-separated path, and creates it if not found
func (d *bash) Set(_path, _value string) bool {
// 1. browse path
last, found := d.browse(_path, true)
if !found {
return false
}
// 2. Set value
last.Components[2] = _value
return true
}

220
internal/cnf/bash_test.go Normal file
View File

@ -0,0 +1,220 @@
package cnf
import (
"bytes"
"testing"
)
func TestBashGet(t *testing.T) {
tests := []struct {
raw string
key string
}{
{"key=value;\n", "key"},
{" \t key=value;\n", "key"},
{"key=value; #comment\n", "key"},
{"\t key=value; #comment\n", "key"},
{"key=value; # comment\n", "key"},
{"\t \tkey=value; # comment\n", "key"},
}
for i, test := range tests {
parser := new(bash)
reader := bytes.NewBufferString(test.raw)
// try to extract value
_, err := parser.ReadFrom(reader)
if err != nil {
t.Errorf("[%d] parse error: %s", i, err)
continue
}
// extract value
value, found := parser.Get(test.key)
if !found {
t.Errorf("[%d] expected a result, got none", i)
continue
}
// check value
if value != "value" {
t.Errorf("[%d] expected 'value' got '%s'", i, value)
}
}
}
func TestBashSetPathExists(t *testing.T) {
tests := []struct {
raw string
key string
value string
}{
{"key=value;\n", "key", "newvalue"},
{" \t key=value;\n", "key", "newvalue"},
{"key=value; #comment\n", "key", "newvalue"},
{"\t key=value; #comment\n", "key", "newvalue"},
{"key=value; # comment\n", "key", "newvalue"},
{"\t \tkey=value; # comment\n", "key", "newvalue"},
}
for i, test := range tests {
parser := new(bash)
reader := bytes.NewBufferString(test.raw)
// try to extract value
_, err := parser.ReadFrom(reader)
if err != nil {
t.Errorf("[%d] parse error: %s", i, err)
continue
}
// update value
if !parser.Set(test.key, test.value) {
t.Errorf("[%d] cannot set '%s' to '%s'", i, test.key, test.value)
continue
}
// check new value
value, found := parser.Get(test.key)
if !found {
t.Errorf("[%d] expected a result, got none", i)
continue
}
// check value
if value != test.value {
t.Errorf("[%d] expected '%s' got '%s'", i, test.value, value)
}
}
}
func TestBashSetCreatePath(t *testing.T) {
tests := []struct {
raw string
key string
ignore string // path to field that must be present after transformation
value string
}{
{"ignore=xxx;\n", "key", "ignore", "newvalue"},
{"unknown-line\nignore=xxx;\n", "key", "ignore", "newvalue"},
}
for i, test := range tests {
parser := new(bash)
reader := bytes.NewBufferString(test.raw)
// try to extract value
_, err := parser.ReadFrom(reader)
if err != nil {
t.Errorf("[%d] parse error: %s", i, err)
continue
}
// update value
if !parser.Set(test.key, test.value) {
t.Errorf("[%d] cannot set '%s' to '%s'", i, test.key, test.value)
continue
}
// check new value
value, found := parser.Get(test.key)
if !found {
t.Errorf("[%d] expected a result, got none", i)
continue
}
// check value
if value != test.value {
t.Errorf("[%d] expected '%s' got '%s'", i, test.value, value)
continue
}
// check that ignore field is still there
value, found = parser.Get(test.ignore)
if !found {
t.Errorf("[%d] expected ignore field, got none", i)
continue
}
// check value
if value != "xxx" {
t.Errorf("[%d] expected ignore value to be '%s' got '%s'", i, "xxx", value)
continue
}
}
}
func TestBashSetCreateEncode(t *testing.T) {
tests := []struct {
raw string
key string
value string
encoded string
}{
{"ignore=xxx;\n", "key", `"newvalue"`, "ignore=xxx;\nkey=\"newvalue\";\n"},
{"#!/bin/bash\n\nfunc(){\n\techo \"something\";\n}\nignore=xxx;\n", "key", `"newvalue"`, "#!/bin/bash\nfunc(){\n\techo \"something\";\n}\nignore=xxx;\nkey=\"newvalue\";\n"},
}
for i, test := range tests {
parser := new(bash)
r, w := bytes.NewBufferString(test.raw), new(bytes.Buffer)
// try to extract value
_, err := parser.ReadFrom(r)
if err != nil {
t.Errorf("[%d] parse error: %s", i, err)
continue
}
// update value
if !parser.Set(test.key, test.value) {
t.Errorf("[%d] cannot set '%s' to '%s'", i, test.key, test.value)
continue
}
// check new value
value, found := parser.Get(test.key)
if !found {
t.Errorf("[%d] expected a result, got none", i)
continue
}
// check value
if value != test.value {
t.Errorf("[%d] expected '%s' got '%s'", i, test.value, value)
continue
}
// writeToBuffer
_, err = parser.WriteTo(w)
if err != nil {
t.Errorf("[%d] unexpected write error <%s>", i, err)
continue
}
encoded := w.String()
// check value
if encoded != test.encoded {
t.Errorf("[%d] wrong encoded value \n-=-=-= HAVE =-=-=-\n%s\n-=-=-= WANT =-=-=-\n%s\n-=-=-=-=-=\n", i, escape(test.encoded), escape(encoded))
continue
}
// try to write
}
}

View File

@ -16,8 +16,15 @@ type ini struct {
// ReadFrom implements io.ReaderFrom // ReadFrom implements io.ReaderFrom
func (d *ini) ReadFrom(_reader io.Reader) (int64, error) { func (d *ini) ReadFrom(_reader io.Reader) (int64, error) {
// disallow "key: value"
// when trying to find out the format from content
// and avoids conflicts with YAML
opts := lib.LoadOptions{
KeyValueDelimiters: "=",
}
file, err := lib.LoadSources(opts, ioutil.NopCloser(_reader))
// 1. get json decoder // 1. get json decoder
file, err := lib.Load(ioutil.NopCloser(_reader))
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -2,8 +2,6 @@ package cnf
import ( import (
"errors" "errors"
"fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
) )
@ -14,51 +12,45 @@ var ErrUnknownExtension = errors.New("unknown extension format")
// ErrUnknownFormat is raised when the format cannot be guessed from the content of the file // ErrUnknownFormat is raised when the format cannot be guessed from the content of the file
var ErrUnknownFormat = errors.New("cannot infer format from content") var ErrUnknownFormat = errors.New("cannot infer format from content")
// ErrFileNotExist is raised when required file does not exist
var ErrFileNotExist = errors.New("cannot find file")
// Load the current file and create the configuration format accordingly // Load the current file and create the configuration format accordingly
func Load(path string) (ConfigurationFormat, error) { func Load(path string) (ConfigurationFormat, error) {
var confFormat ConfigurationFormat
// 1. check file // 1. check file
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("cannot find file '%s'", path) return nil, ErrFileNotExist
} }
// 2. Try to load from extension // 2. Try to load from extension
extension := filepath.Ext(path) extension := filepath.Ext(path)
if len(extension) > 0 { if len(extension) > 0 {
confFormat = loadFromExtension(extension) if confFormat := loadFromExtension(extension); confFormat != nil {
if confFormat == nil {
return nil, ErrUnknownExtension
}
// open file // open file
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer file.Close()
// parse // parse
_, err = confFormat.ReadFrom(file) _, err = confFormat.ReadFrom(file)
file.Close()
if err == nil { if err == nil {
return confFormat, nil return confFormat, nil
} }
// return nil, fmt.Errorf("cannot parse file as '%s' | %s", extension, err) }
} }
// 3. open file // 4. Try to guess from the content
file, err := os.Open(path) confFormat, err := loadFromContent(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer file.Close()
// 4. Try to guess from the content
confFormat = loadFromContent(file)
if confFormat == nil { if confFormat == nil {
return nil, ErrUnknownFormat return nil, ErrUnknownFormat
} }
@ -78,19 +70,29 @@ func loadFromExtension(ext string) ConfigurationFormat {
return new(yaml) return new(yaml)
case ".nginx": case ".nginx":
return new(nginx) return new(nginx)
case ".sh":
return new(bash)
default: default:
return nil return nil
} }
} }
func loadFromContent(file io.Reader) ConfigurationFormat { func loadFromContent(path string) (ConfigurationFormat, error) {
// extensions ordered by unicity of the language's syntax // 3. open file
extensions := []string{".json", ".yaml", ".nginx", ".ini"} file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
// extensions ordered by strictness of the language's syntax
extensions := []string{".json", ".nginx", ".ini", ".yaml", ".sh"}
// try to load each available extension // try to load each available extension
for _, ext := range extensions { for _, ext := range extensions {
file.Seek(0, 0)
// load parser // load parser
c := loadFromExtension(ext) c := loadFromExtension(ext)
@ -101,10 +103,10 @@ func loadFromContent(file io.Reader) ConfigurationFormat {
// parse // parse
_, err := c.ReadFrom(file) _, err := c.ReadFrom(file)
if err == nil { if err == nil {
return c return c, nil
} }
} }
return nil return nil, ErrUnknownFormat
} }

250
internal/cnf/loader_test.go Normal file
View File

@ -0,0 +1,250 @@
package cnf
import (
"fmt"
"os"
"testing"
)
func TestLoadFileNotExist(t *testing.T) {
_, err := Load("/tmp/not-existing/file")
if err == nil {
t.Fatalf("expected error")
}
if err != ErrFileNotExist {
t.Fatalf("expected error <%s>, got <%s>", ErrFileNotExist, err)
}
}
func TestLoadFromExtensionWithContentMatch(t *testing.T) {
tests := []struct {
File string
Format ConfigurationFormat
Content string
Field string
}{
// key = value
{"/tmp/load-test.json", new(json), `{ "key": "value" }`, "key"},
{"/tmp/load-test.nginx", new(nginx), "key value;", "key"},
{"/tmp/load-test.ini", new(ini), `key = value`, "key"},
{"/tmp/load-test.yaml", new(yaml), "key: value", "key"},
// parent.key = value
{"/tmp/load-test.json", new(json), `{ "parent": { "key": "value" } }`, "parent.key"},
{"/tmp/load-test.nginx", new(nginx), "parent {\nkey value;\n}", "parent.key"},
{"/tmp/load-test.ini", new(ini), "[parent]\nkey = value", "parent.key"},
{"/tmp/load-test.yaml", new(yaml), "parent:\n key: value", "parent.key"},
// comments
// {"/tmp/load-test.json", new(json), "{ \"parent\": { \"key\": \"value\" } }", "parent.key"},
{"/tmp/load-test.nginx", new(nginx), ";comment\nparent {\nkey value;\n}", "parent.key"},
{"/tmp/load-test.nginx", new(nginx), "#comment\nparent {\nkey value;\n}", "parent.key"},
{"/tmp/load-test.ini", new(ini), ";comment\n[parent]\nkey = value", "parent.key"},
{"/tmp/load-test.ini", new(ini), "#comment\n[parent]\nkey = value", "parent.key"},
{"/tmp/load-test.yaml", new(yaml), "#comment\nparent:\n key: value", "parent.key"},
}
for i, test := range tests {
f, err := os.Create(test.File)
if err != nil {
t.Errorf("[%d] cannot create file '%s'", i, test.File)
continue
}
f.Write([]byte(test.Content))
f.Close()
format, err := Load(test.File)
if err != nil {
t.Errorf("[%d] unexpected error <%s>", i, err)
continue
}
if format == nil {
t.Errorf("[%d] expected format not to be null", i)
continue
}
// check type
have, want := fmt.Sprintf("%T", format), fmt.Sprintf("%T", test.Format)
if have != want {
t.Errorf("[%d] expected format <%s>, got <%s>", i, want, have)
continue
}
// try to get value
val, found := format.Get(test.Field)
if !found {
t.Errorf("[%d] expected to find '%s'", i, test.Field)
continue
}
if val != "value" {
t.Errorf("[%d] expected value '%s', got '%s'", i, "value", val)
continue
}
}
for _, test := range tests {
os.RemoveAll(test.File)
}
}
func TestLoadFromExtensionWithContentNotMatch(t *testing.T) {
tests := []struct {
File string
Format ConfigurationFormat
Content string
Field string
}{
// [JSON] key = value
{"/tmp/load-test.json", new(json), `{ "key": "value" }`, "key"},
{"/tmp/load-test.nginx", new(json), `{ "key": "value" }`, "key"},
{"/tmp/load-test.ini", new(json), `{ "key": "value" }`, "key"},
// {"/tmp/load-test.yaml", new(json), `{ "key": "value" }`, "key"},
// [NGINX] key = value
{"/tmp/load-test.json", new(nginx), "key value;", "key"},
{"/tmp/load-test.nginx", new(nginx), "key value;", "key"},
{"/tmp/load-test.ini", new(nginx), "key value;", "key"},
// {"/tmp/load-test.yaml", new(nginx), "key value;", "key"},
// [INI] key = value
{"/tmp/load-test.json", new(ini), `key = value`, "key"},
{"/tmp/load-test.nginx", new(ini), `key = value`, "key"},
{"/tmp/load-test.ini", new(ini), `key = value`, "key"},
// {"/tmp/load-test.yaml", new(ini), `key = value`, "key"},
// [YAML] key = value
{"/tmp/load-test.json", new(yaml), "key: value", "key"},
{"/tmp/load-test.nginx", new(yaml), "key: value", "key"},
{"/tmp/load-test.yaml", new(yaml), "key: value", "key"},
{"/tmp/load-test.yaml", new(yaml), "key: value", "key"},
}
for i, test := range tests {
f, err := os.Create(test.File)
if err != nil {
t.Errorf("[%d] cannot create file '%s'", i, test.File)
continue
}
f.Write([]byte(test.Content))
f.Close()
format, err := Load(test.File)
if err != nil {
t.Errorf("[%d] unexpected error <%s>", i, err)
continue
}
if format == nil {
t.Errorf("[%d] expected format not to be null", i)
continue
}
// check type
have, want := fmt.Sprintf("%T", format), fmt.Sprintf("%T", test.Format)
if have != want {
t.Errorf("[%d] expected format <%s>, got <%s>", i, want, have)
continue
}
// try to get value
val, found := format.Get(test.Field)
if !found {
t.Errorf("[%d] expected to find '%s'", i, test.Field)
continue
}
if val != "value" {
t.Errorf("[%d] expected value '%s', got '%s'", i, "value", val)
continue
}
}
for _, test := range tests {
os.RemoveAll(test.File)
}
}
func TestLoadContent(t *testing.T) {
file := "/tmp/no-extension-file"
defer os.RemoveAll(file)
tests := []struct {
Format ConfigurationFormat
Content string
Field string
}{
// key = value
{new(json), `{ "key": "value" }`, "key"},
{new(nginx), "key value;", "key"},
{new(ini), `key = value`, "key"},
{new(yaml), "key: value", "key"},
// parent.key = value
{new(json), `{ "parent": { "key": "value" } }`, "parent.key"},
{new(nginx), "parent {\nkey value;\n}", "parent.key"},
{new(ini), "[parent]\nkey = value", "parent.key"},
{new(yaml), "parent:\n key: value", "parent.key"},
// comments
{new(json), "{ \"parent\": { \"key\": \"value\" } }", "parent.key"},
{new(nginx), ";comment\nparent {\nkey value;\n}", "parent.key"},
{new(nginx), "#comment\nparent {\nkey value;\n}", "parent.key"},
{new(ini), ";comment\n[parent]\nkey = value", "parent.key"},
{new(ini), "#comment\n[parent]\nkey = value", "parent.key"},
{new(yaml), "#comment\nparent:\n key: value", "parent.key"},
}
for i, test := range tests {
os.RemoveAll(file)
f, err := os.Create(file)
if err != nil {
t.Errorf("[%d] cannot create file '%s'", i, file)
continue
}
f.Write([]byte(test.Content))
f.Close()
format, err := Load(file)
if err != nil {
t.Errorf("[%d] unexpected error <%s>", i, err)
continue
}
if format == nil {
t.Errorf("[%d] expected format not to be null", i)
continue
}
// check type
have, want := fmt.Sprintf("%T", format), fmt.Sprintf("%T", test.Format)
if have != want {
t.Errorf("[%d] expected format <%s>, got <%s>", i, want, have)
continue
}
// try to get value
val, found := format.Get(test.Field)
if !found {
t.Errorf("[%d] expected to find '%s'", i, test.Field)
continue
}
if val != "value" {
t.Errorf("[%d] expected value '%s', got '%s'", i, "value", val)
continue
}
}
}

View File

@ -1,7 +1,7 @@
package cnf package cnf
import ( import (
lib "github.com/xdrm-brackets/nix-amer/internal/cnf/parser/nginx" lib "git.xdrm.io/go/nix-amer/internal/cnf/parser/nginx"
"io" "io"
"strings" "strings"
) )
@ -78,6 +78,7 @@ func (d *nginx) browse(_path string, create ...bool) (*lib.Line, bool) {
Lines: make([]*lib.Line, 0), Lines: make([]*lib.Line, 0),
} }
current.Lines = append(current.Lines, sec) current.Lines = append(current.Lines, sec)
current = sec
continue continue
} }

View File

@ -102,6 +102,7 @@ func TestNginxSetCreatePath(t *testing.T) {
{"ignore xxx;\n", "key", "ignore", "newvalue"}, {"ignore xxx;\n", "key", "ignore", "newvalue"},
{"ignore xxx;\nsection {\n\tkey value;\n}\n", "section.key", "ignore", "newvalue"}, {"ignore xxx;\nsection {\n\tkey value;\n}\n", "section.key", "ignore", "newvalue"},
{"section {\n\tkey value;\n\tignore xxx;\n}\n", "section.key", "section.ignore", "newvalue"}, {"section {\n\tkey value;\n\tignore xxx;\n}\n", "section.key", "section.ignore", "newvalue"},
{"ignoresec {\n\tignore xxx;\n}\n\nsection {\n}\n", "section.key", "ignoresec.ignore", "newvalue"},
{"ignoresec {\n\tignore xxx;\n}\n\nsection {\n\tkey value;\n}\n", "section.key", "ignoresec.ignore", "newvalue"}, {"ignoresec {\n\tignore xxx;\n}\n\nsection {\n\tkey value;\n}\n", "section.key", "ignoresec.ignore", "newvalue"},
} }
@ -151,3 +152,103 @@ func TestNginxSetCreatePath(t *testing.T) {
} }
} }
func TestNginxSetCreateEncode(t *testing.T) {
tests := []struct {
raw string
key string
value string
encoded string
}{
{"ignore xxx;\n", "key", "newvalue", "ignore\t\txxx;\nkey\t\tnewvalue;\n"},
{"ignore xxx;\nsection {\n\tkey value;\n}\n", "section.key", "newvalue", "ignore\t\txxx;\nsection {\n\tkey\t\tnewvalue;\n}\n\n"},
{"section {\n\tkey value;\n\tignore xxx;\n}\n", "section.key", "newvalue", "section {\n\tkey\t\tnewvalue;\n\tignore\t\txxx;\n}\n\n"},
{"ignoresec {\n\tignore xxx;\n}\n\nsection {\n}\n", "section.key", "newvalue", "ignoresec {\n\tignore\t\txxx;\n}\n\nsection {\n\tkey\t\tnewvalue;\n}\n\n"},
{"ignoresec {\n\tignore xxx;\n}\n\nsection {\n\tkey value;\n}\n", "section.key", "newvalue", "ignoresec {\n\tignore\t\txxx;\n}\n\nsection {\n\tkey\t\tnewvalue;\n}\n\n"},
{
"; comment 1\n\nk1 v1;\n#comment 2\nsec1 {\n}\nsec2 {\nignore xxx;\n sec3 {\nignore2 yyy;\n}\n\n}\n\n",
"key",
"newvalue",
"; comment 1\nk1\t\tv1;\n#comment 2\nsec1 {\n}\n\nsec2 {\n\tignore\t\txxx;\n\tsec3 {\n\t\tignore2\t\tyyy;\n\t}\n\n}\n\nkey\t\tnewvalue;\n"},
{
"; comment 1\n\nk1 v1;\n#comment 2\nsec1 {\n}\nsec2 {\nignore xxx;\n sec3 {\nignore2 yyy;\n}\n\n}\n\n",
"section.key",
"newvalue",
"; comment 1\nk1\t\tv1;\n#comment 2\nsec1 {\n}\n\nsec2 {\n\tignore\t\txxx;\n\tsec3 {\n\t\tignore2\t\tyyy;\n\t}\n\n}\n\nsection {\n\tkey\t\tnewvalue;\n}\n\n"},
}
for i, test := range tests {
parser := new(nginx)
r, w := bytes.NewBufferString(test.raw), new(bytes.Buffer)
// try to extract value
_, err := parser.ReadFrom(r)
if err != nil {
t.Errorf("[%d] parse error: %s", i, err)
continue
}
// update value
if !parser.Set(test.key, test.value) {
t.Errorf("[%d] cannot set '%s' to '%s'", i, test.key, test.value)
continue
}
// check new value
value, found := parser.Get(test.key)
if !found {
t.Errorf("[%d] expected a result, got none", i)
continue
}
// check value
if value != test.value {
t.Errorf("[%d] expected '%s' got '%s'", i, test.value, value)
continue
}
// writeToBuffer
_, err = parser.WriteTo(w)
if err != nil {
t.Errorf("[%d] unexpected write error <%s>", i, err)
continue
}
encoded := w.String()
// check value
if encoded != test.encoded {
t.Errorf("[%d] wrong encoded value \n-=-=-= HAVE =-=-=-\n%s\n-=-=-= WANT =-=-=-\n%s\n-=-=-=-=-=\n", i, escape(test.encoded), escape(encoded))
continue
}
// try to write
}
}
func escape(in string) string {
out := make([]rune, 0)
for _, char := range in {
if char == '\\' {
out = append(out, []rune("\\\\")...)
} else if char == '\n' {
out = append(out, []rune("\\n")...)
} else if char == '\r' {
out = append(out, []rune("\\r")...)
} else if char == '\t' {
out = append(out, []rune("\\t")...)
} else if char == '\033' {
out = append(out, []rune("\\033")...)
} else {
out = append(out, char)
}
}
return string(out)
}

View File

@ -0,0 +1,20 @@
package bash
import (
"io"
)
// File represents a bash file
type File struct {
Lines []*Line
}
// NewDecoder implements parser.T
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{reader: r}
}
// NewEncoder implements parser.T
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{writer: w}
}

View File

@ -0,0 +1,74 @@
package bash
import (
"bufio"
"io"
"regexp"
"strings"
)
// Decoder implements parser.Decoder
type Decoder struct{ reader io.Reader }
// Decode is the main function of the parser.Decoder interface
func (d *Decoder) Decode(v interface{}) error {
// check 'v'
if v == nil {
return ErrNullReceiver
}
vcast, ok := v.(*File)
if !ok {
return ErrInvalidReceiver
}
vcast.Lines = make([]*Line, 0)
r := bufio.NewReader(d.reader)
n := -1 // line number
// regexes
reAssign := regexp.MustCompile(`(?m)^(\s*)([A-Za-z0-9_]+)=([^;]+);?\s*(#.+)?$`)
eof := false
for {
n++
if eof {
break
}
l := &Line{Type: ANY}
// 1. read line
line, err := r.ReadString('\n')
if err == io.EOF {
if len(line) > 0 {
eof = true
} else {
break
}
} else if err != nil {
return &LineError{n, err}
}
line = strings.Trim(line, "\r\n")
// 2. ignore empty
if len(strings.Trim(line, " \t\r\n")) < 1 {
continue
}
// 3. assignment
match := reAssign.FindStringSubmatch(line)
if match != nil {
l.Type = ASSIGNMENT
l.Components = match[1:]
} else {
l.Components = []string{line}
}
// 4. add to file
vcast.Lines = append(vcast.Lines, l)
}
return nil
}

View File

@ -0,0 +1,59 @@
package bash
import (
"fmt"
"io"
"strings"
)
// Encoder implements parser.Encoder
type Encoder struct {
writer io.Writer
prefix []byte
indent []byte
}
// Encode is the main function of the parser.Encoder interface
func (e *Encoder) Encode(v interface{}) error {
// check 'v'
vcast, ok := v.(*File)
if !ok {
return ErrInvalidReceiver
}
// empty config
if len(vcast.Lines) < 1 {
return nil
}
for _, line := range vcast.Lines {
// line representation
repr := ""
// ASSIGNMENT
if line.Type == ASSIGNMENT {
repr = fmt.Sprintf("%s%s=%s;", line.Components[0], line.Components[1], line.Components[2])
// optional comment
if len(line.Components[3]) > 0 {
repr += fmt.Sprintf(" %s", line.Components[3])
}
// ANY
} else {
repr = strings.Join(line.Components, "")
}
repr += "\n"
_, err := e.writer.Write([]byte(repr))
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,23 @@
package bash
import (
"fmt"
"git.xdrm.io/go/nix-amer/internal/clifmt"
)
// ErrNullReceiver is raised when a null receiver is provided
var ErrNullReceiver = fmt.Errorf("receiver must not be null")
// ErrInvalidReceiver is raised when an invalid receiver is provided
var ErrInvalidReceiver = fmt.Errorf("receiver must be compatible with *File")
// LineError wraps errors with a line index
type LineError struct {
Line int
Err error
}
// Error implements Error
func (le LineError) Error() string {
return fmt.Sprintf(":%d %s", le.Line, clifmt.Color(31, le.Err.Error()))
}

View File

@ -0,0 +1,26 @@
package bash
// LineType enumerates available line types
type LineType byte
const (
// ANY is the default line type (all except assignments)
ANY LineType = iota
// ASSIGNMENT line
ASSIGNMENT
)
// Line represents a meaningful line
type Line struct {
// Type of line
Type LineType
// Components of the line
//
// When Type = ASSIGNMENT :
// [0] = indentation (spaces and tabs)
// [1] = variable name
// [2] = variable value
// [3] = comment (optional)
Components []string
}

View File

@ -153,14 +153,4 @@ func (d *Decoder) Decode(v interface{}) error {
} }
return nil return nil
}
func inArray(haystack string, needle byte) bool {
for i, l := 0, len(haystack); i < l; i++ {
if haystack[i] == needle {
return true
}
}
return false
} }

View File

@ -1,6 +1,7 @@
package nginx package nginx
import ( import (
"errors"
"strings" "strings"
"testing" "testing"
) )
@ -160,3 +161,68 @@ func TestNestedSections(t *testing.T) {
} }
} }
func TestReceiverAndSyntaxErrors(t *testing.T) {
tests := []struct {
Receiver interface{}
Input string
Err error
}{
{new(Line), "", nil},
{nil, "", ErrNullReceiver},
{[]byte{}, "", ErrInvalidReceiver},
{new(Line), "}", &LineError{0, ErrUnexpectedSectionClose}},
{new(Line), "key valuewithoutsemicolon", &LineError{0, ErrInvalidSyntax}},
{new(Line), "section {\nkey value;", ErrUnclosedSection},
}
for i, test := range tests {
// create reader
r := strings.NewReader(test.Input)
// parse input
var receiver interface{} = test.Receiver
decoder := NewDecoder(r)
err := decoder.Decode(receiver)
if err == nil {
if test.Err != nil {
t.Errorf("[%d] expected error", i)
}
continue
}
if err.Error() != test.Err.Error() {
t.Errorf("[%d] expected error <%s>, got <%s>", i, test.Err, err)
continue
}
}
}
var errReader = errors.New("error")
type defectiveReader struct{}
func (d defectiveReader) Read(buf []byte) (int, error) {
return 0, errReader
}
func TestReadErrors(t *testing.T) {
// create reader
r := &defectiveReader{}
// parse input
receiver := new(Line)
decoder := NewDecoder(r)
err := decoder.Decode(receiver)
if err == nil {
t.Fatalf("expected error")
}
if err.Error() != (&LineError{0, errReader}).Error() {
t.Fatalf("expected error <%s>, got <%s>", &LineError{0, errReader}, err)
}
}

View File

@ -2,6 +2,7 @@ package nginx
import ( import (
"bytes" "bytes"
"errors"
"strings" "strings"
"testing" "testing"
) )
@ -89,6 +90,113 @@ func TestDecodeEncode(t *testing.T) {
} }
} }
func TestErrors(t *testing.T) {
tests := []struct {
Receiver interface{}
Input string
Err error
}{
{new(Line), "", nil},
{nil, "", ErrInvalidReceiver},
{[]byte{}, "", ErrInvalidReceiver},
}
for i, test := range tests {
// create reader/writer
r, w := strings.NewReader(test.Input), &bytes.Buffer{}
// parse input
var receiver interface{} = new(Line)
decoder := NewDecoder(r)
if err := decoder.Decode(receiver); err != nil {
t.Errorf("[%d] unexpected error <%s>", i, err)
continue
}
// encode back to writer
receiver = test.Receiver
encoder := NewEncoder(w)
encoder.SetIndent("", "\t")
if err := encoder.Encode(receiver); err != test.Err {
t.Errorf("[%d] expected error <%s>, got <%s>", i, test.Err, err)
continue
}
}
}
func TestDefaultIndent(t *testing.T) {
tests := []struct {
Input string
SetIndent []string
Output string
}{
{"section {\nkey value;\n}\n", []string{"*prefix*", "*indent*"}, "*prefix*section {\n*prefix**indent*key\t\tvalue;\n*prefix*}\n\n"},
{"section {\nkey value;\n}\n", nil, "section {\n\tkey\t\tvalue;\n}\n\n"},
}
for i, test := range tests {
// create reader/writer
r, w := strings.NewReader(test.Input), &bytes.Buffer{}
// parse input
receiver := new(Line)
decoder := NewDecoder(r)
if err := decoder.Decode(receiver); err != nil {
t.Errorf("[%d] unexpected error <%s>", i, err)
continue
}
// encode back to writer
encoder := NewEncoder(w)
if test.SetIndent != nil && len(test.SetIndent) >= 2 {
encoder.SetIndent(test.SetIndent[0], test.SetIndent[1])
}
if err := encoder.Encode(receiver); err != nil {
t.Errorf("[%d] unexpected error <%s>", i, err)
continue
}
// check equality
if w.String() != test.Output {
t.Errorf("[%d] expected '%s', got '%s'", i, escape(test.Output), escape(w.String()))
}
}
}
var errWriter = errors.New("error")
type defectiveWriter struct{}
func (d defectiveWriter) Write(buf []byte) (int, error) {
return 0, errWriter
}
func TestWriteError(t *testing.T) {
input := "section {\nkey value;\n}\n"
// create reader/writer
r, w := strings.NewReader(input), &defectiveWriter{}
// parse input
receiver := new(Line)
decoder := NewDecoder(r)
if err := decoder.Decode(receiver); err != nil {
t.Fatalf("unexpected error <%s>", err)
}
// encode back to writer
encoder := NewEncoder(w)
if err := encoder.Encode(receiver); err != errWriter {
t.Fatalf("expected error <%s>, got <%s>", errWriter, err)
}
}
func escape(raw string) string { func escape(raw string) string {
escaped := make([]rune, 0) escaped := make([]rune, 0)

View File

@ -2,7 +2,7 @@ package nginx
import ( import (
"fmt" "fmt"
"github.com/xdrm-brackets/nix-amer/internal/clifmt" "git.xdrm.io/go/nix-amer/internal/clifmt"
) )
// ErrNullReceiver is raised when a null receiver is provided // ErrNullReceiver is raised when a null receiver is provided

View File

@ -39,3 +39,7 @@ func (d alias) Exec(ctx ExecutionContext) ([]byte, error) {
ctx.Alias[d.Name] = d.Value ctx.Alias[d.Name] = d.Value
return nil, nil return nil, nil
} }
func (d alias) DryRun(ctx ExecutionContext) ([]byte, error) {
return d.Exec(ctx)
}

View File

@ -2,9 +2,9 @@ package instruction
import ( import (
"fmt" "fmt"
"github.com/xdrm-brackets/nix-amer/internal/exec" "git.xdrm.io/go/nix-amer/internal/exec"
"github.com/xdrm-brackets/nix-amer/internal/pkg" "git.xdrm.io/go/nix-amer/internal/pkg"
"github.com/xdrm-brackets/nix-amer/internal/ser" "git.xdrm.io/go/nix-amer/internal/ser"
) )
// T is the instruction common interface // T is the instruction common interface
@ -15,6 +15,8 @@ type T interface {
Build(string) error Build(string) error
// Exec the given instruction // Exec the given instruction
Exec(ExecutionContext) ([]byte, error) Exec(ExecutionContext) ([]byte, error)
// DryRun checks the success of the given instruction without actually running it (non-destructive)
DryRun(ExecutionContext) ([]byte, error)
} }
// ExecutionContext contains system-specific drivers to manage the host // ExecutionContext contains system-specific drivers to manage the host

View File

@ -0,0 +1,84 @@
package instruction
import (
"os"
"strings"
)
type copy struct {
raw string
Src string
Dst string
}
func (d *copy) Raw() string { return strings.Join([]string{"copy", d.raw}, " ") }
func (d *copy) Build(_args string) error {
// 1. extract action (sub command)
split := strings.Fields(_args)
// 2. check syntax
if len(split) != 2 {
return ErrInvalidSyntax
}
d.Src = strings.Trim(split[0], " \t")
d.Dst = strings.Trim(split[1], " \t")
d.raw = _args
return nil
}
func (d copy) Exec(ctx ExecutionContext) ([]byte, error) {
// 1. fail if source file not found
if _, err := os.Stat(d.Src); os.IsNotExist(err) {
return nil, &FileError{"cannot find file", d.Src, err}
}
// 2. execute script
if err := ctx.Executor.Command("cp", "-r", d.Src, d.Dst).Run(); err != nil {
return nil, &FileError{"cannot copy to", d.Dst, err}
}
return nil, nil
}
func (d copy) DryRun(ctx ExecutionContext) ([]byte, error) {
// 1. fail if source file not found
if _, err := os.Stat(d.Src); os.IsNotExist(err) {
return nil, &FileError{"cannot find file", d.Src, err}
}
// 2. if destination to create : try to create (then remove)
fi, err := os.Stat(d.Dst)
if os.IsNotExist(err) {
file, err2 := os.OpenFile(d.Dst, os.O_APPEND|os.O_WRONLY|os.O_CREATE, os.FileMode(0777))
if err2 != nil {
return nil, &FileError{"cannot copy to", d.Dst, err2}
}
file.Close()
if err2 := os.Remove(d.Dst); err2 != nil {
return nil, &FileError{"cannot remove dry-run file", d.Dst, err2}
}
return nil, nil
} else if fi != nil && fi.IsDir() {
return nil, nil // no error if dir
}
// 3. if destination exists : check write permission
file, err := os.OpenFile(d.Dst, os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
return nil, &FileError{"cannot copy to", d.Dst, err}
}
file.Close()
return nil, nil
}

View File

@ -0,0 +1,308 @@
package instruction
import (
"fmt"
"os"
"testing"
)
func TestCopyInvalidSyntax(t *testing.T) {
tests := []string{
"one-arg",
"src dst extra-arg",
}
for i, test := range tests {
inst := new(copy)
err := inst.Build(test)
if err != ErrInvalidSyntax {
t.Errorf("[%d] expected error <%s>, got <%s>", i, ErrInvalidSyntax, err)
}
}
}
func TestCopyBuildArgs(t *testing.T) {
tests := []string{
"source destination",
"\tsource destination",
" source destination",
"source\t destination",
"source destination",
"source \tdestination",
"source destination",
"source destination\t",
"source\t\tdestination\t",
"source \t\t destination\t",
"source\t \tdestination\t",
}
for i, test := range tests {
inst := new(copy)
err := inst.Build(test)
if err != nil {
t.Errorf("[%d] unexpected error <%s>", i, err)
continue
}
if inst.Src != "source" {
t.Errorf("[%d] expected 'source', got '%s'", i, inst.Src)
continue
}
if inst.Dst != "destination" {
t.Errorf("[%d] expected 'source', got '%s'", i, inst.Dst)
continue
}
}
}
func TestCopySourceNotExist(t *testing.T) {
defer os.RemoveAll("/tmp/destination")
raw := "/tmp/source /tmp/destination"
ctx, err := CreateContext("apt-get", "")
if err != nil {
t.Fatalf("cannot create context")
}
for i := 0; i < 2; i++ {
inst := new(copy)
err := inst.Build(raw)
if err != nil {
t.Fatalf("[%d] unexpected error <%s>", i, err)
}
if i == 0 {
_, err = inst.Exec(*ctx)
} else {
_, err = inst.DryRun(*ctx)
}
if err == nil {
t.Fatalf("[%d] expected error", i)
}
ce, ok := err.(*FileError)
if !ok {
t.Fatalf("[%d] expected error of type <*FileError>", i)
}
if ce.Reason != "cannot find file" || ce.File != "/tmp/source" {
t.Fatalf("[%d] expected error <%s '%s'> got <%s '%s'>", i, "cannot find file", "/tmp/source", ce.Reason, ce.File)
}
}
}
func TestCopySourceIsDir(t *testing.T) {
src, dst := "/tmp/sourcedir", "/tmp/destinationdir"
raw := fmt.Sprintf("%s %s", src, dst)
defer os.RemoveAll(src)
defer os.RemoveAll(dst)
ctx, err := CreateContext("apt-get", "")
if err != nil {
t.Fatalf("cannot create context")
}
for i := 0; i < 2; i++ {
// 1. create directory
os.RemoveAll(src)
if err := os.MkdirAll(src, os.FileMode(0777)); err != nil {
t.Fatalf("[%d] cannot create test directory | %s", i, err)
}
inst := new(copy)
err := inst.Build(raw)
if err != nil {
t.Fatalf("[%d] unexpected error <%s>", i, err)
}
if i == 0 {
_, err = inst.Exec(*ctx)
} else {
_, err = inst.DryRun(*ctx)
}
if err != nil {
t.Fatalf("[%d] unexpected error <%s>", i, err)
}
}
}
func TestCopyInvalidDestination(t *testing.T) {
src, dst := "/tmp/source", "/tmp/missing-directory/invalid-destination"
raw := fmt.Sprintf("%s %s", src, dst)
defer os.RemoveAll(src)
defer os.RemoveAll(dst)
ctx, err := CreateContext("apt-get", "")
if err != nil {
t.Fatalf("cannot create context")
}
for i := 0; i < 2; i++ {
os.RemoveAll(src)
// 1. create directory
if err := os.MkdirAll(src, os.FileMode(0777)); err != nil {
t.Fatalf("[%d] cannot create test directory | %s", i, err)
}
inst := new(copy)
err := inst.Build(raw)
if err != nil {
t.Fatalf("[%d] unexpected error <%s>", i, err)
}
if i == 0 {
_, err = inst.Exec(*ctx)
} else {
_, err = inst.DryRun(*ctx)
}
if err == nil {
t.Fatalf("[%d] expected error", i)
}
ce, ok := err.(*FileError)
if !ok {
t.Fatalf("[%d] expected error of type <*FileError>", i)
}
if ce.Reason != "cannot copy to" || ce.File != dst {
t.Fatalf("[%d] expected error <%s '%s'> got <%s '%s'>", i, "cannot copy to", dst, ce.Reason, ce.File)
}
}
}
func TestCopySourceIsFile(t *testing.T) {
src, dst := "/tmp/source", "/tmp/destination-file"
raw := fmt.Sprintf("%s %s", src, dst)
defer os.RemoveAll(src)
defer os.RemoveAll(dst)
ctx, err := CreateContext("apt-get", "")
if err != nil {
t.Fatalf("cannot create context")
}
for i := 0; i < 2; i++ {
os.RemoveAll(src)
// 1. create directory
fd, err := os.Create(src)
if err != nil {
t.Fatalf("[%d] cannot create test file | %s", i, err)
}
fd.Close()
inst := new(copy)
err = inst.Build(raw)
if err != nil {
t.Fatalf("[%d] unexpected error <%s>", i, err)
}
if i == 0 {
_, err = inst.Exec(*ctx)
} else {
_, err = inst.DryRun(*ctx)
}
if err != nil {
t.Fatalf("[%d] unexpected error <%s>", i, err)
}
}
}
func TestCopySourceIsFileDestinationExists(t *testing.T) {
src, dst := "/tmp/source", "/tmp/destination-file"
raw := fmt.Sprintf("%s %s", src, dst)
defer os.RemoveAll(src)
defer os.RemoveAll(dst)
ctx, err := CreateContext("apt-get", "")
if err != nil {
t.Fatalf("cannot create context")
}
for i := 0; i < 2; i++ {
os.RemoveAll(src)
os.RemoveAll(dst)
// 1. create source
fd, err := os.Create(src)
if err != nil {
t.Fatalf("[%d] cannot create test file | %s", i, err)
}
fd.Close()
// 1. create destination
fd, err = os.Create(src)
if err != nil {
t.Fatalf("[%d] cannot create test file | %s", i, err)
}
fd.Close()
inst := new(copy)
err = inst.Build(raw)
if err != nil {
t.Fatalf("[%d] unexpected error <%s>", i, err)
}
if i == 0 {
_, err = inst.Exec(*ctx)
} else {
_, err = inst.DryRun(*ctx)
}
if err != nil {
t.Fatalf("[%d] unexpected error <%s>", i, err)
}
}
}
func TestCopyCannotWriteDestination(t *testing.T) {
src, dst := "/tmp/source", "/tmp/destination-perm"
raw := fmt.Sprintf("%s %s", src, dst)
defer os.RemoveAll(src)
defer os.RemoveAll(dst)
ctx, err := CreateContext("apt-get", "")
if err != nil {
t.Fatalf("cannot create context")
}
for i := 0; i < 2; i++ {
os.RemoveAll(src)
if err := os.RemoveAll(dst); err != nil {
t.Fatalf("[%d] cannot remove destination file\n", i)
}
// 1. create source
fd, err := os.Create(src)
if err != nil {
t.Fatalf("[%d] cannot create test file | %s", i, err)
}
fd.Close()
// 1. create destination
fd, err = os.Create(src)
if err != nil {
t.Fatalf("[%d] cannot create test file | %s", i, err)
}
err = fd.Chmod(os.FileMode(0555))
if err != nil {
t.Fatalf("[%d] cannot set permissions | %s", i, err)
}
fd.Close()
inst := new(copy)
err = inst.Build(raw)
if err != nil {
t.Fatalf("[%d] unexpected error <%s>", i, err)
}
if i == 0 {
_, err = inst.Exec(*ctx)
} else {
_, err = inst.DryRun(*ctx)
}
if err != nil {
t.Fatalf("[%d] unexpected error <%s>", i, err)
}
}
}

View File

@ -33,3 +33,7 @@ func (d delete) Exec(ctx ExecutionContext) ([]byte, error) {
return nil, nil return nil, nil
} }
func (d delete) DryRun(ctx ExecutionContext) ([]byte, error) {
return nil, nil
}

View File

@ -1,15 +1,29 @@
package instruction package instruction
import ( import (
"errors" "fmt"
) )
// ErrInvalidAlias is raised when encountering an invalid token in an alias name // ErrInvalidAlias is raised when encountering an invalid token in an alias name
var ErrInvalidAlias = errors.New("invalid alias name (contains '/')") var ErrInvalidAlias = fmt.Errorf("invalid alias name (contains '/')")
// ErrInvalidSyntax is raised when encountering an invalid token // ErrInvalidSyntax is raised when encountering an invalid token
var ErrInvalidSyntax = errors.New("invalid instruction format") var ErrInvalidSyntax = fmt.Errorf("invalid instruction format")
// ErrUnknownInstruction is raised when encountering an unknown instruction // ErrUnknownInstruction is raised when encountering an unknown instruction
// it can mean that you're not using the right version or that you've misspelled it // it can mean that you're not using the right version or that you've misspelled it
var ErrUnknownInstruction = errors.New("unknown instruction") var ErrUnknownInstruction = fmt.Errorf("unknown instruction")
// FileError is used for file-specific errors
type FileError struct {
Reason string
File string
Err error
}
func (ce FileError) Error() string {
if ce.Err == nil {
return fmt.Sprintf("%s '%s'", ce.Reason, ce.File)
}
return fmt.Sprintf("%s '%s' | %s", ce.Reason, ce.File, ce.Err)
}

View File

@ -33,3 +33,7 @@ func (d install) Exec(ctx ExecutionContext) ([]byte, error) {
return nil, nil return nil, nil
} }
func (d install) DryRun(ctx ExecutionContext) ([]byte, error) {
return nil, nil
}

View File

@ -40,6 +40,10 @@ func Parse(raw string) (T, error) {
i := &set{} i := &set{}
err := i.Build(split[1]) err := i.Build(split[1])
return i, err return i, err
case "copy":
i := &copy{}
err := i.Build(split[1])
return i, err
case "alias": case "alias":
i := &alias{} i := &alias{}
err := i.Build(split[1]) err := i.Build(split[1])

View File

@ -40,3 +40,21 @@ func (d run) Exec(ctx ExecutionContext) ([]byte, error) {
return nil, nil return nil, nil
} }
func (d run) DryRun(ctx ExecutionContext) ([]byte, error) {
// 1. get file / alias
path := d.raw
if !strings.Contains(path, "/") {
if p, exists := ctx.Alias[path]; exists {
path = p
}
}
// 1. fail if file not found
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("cannot find script '%s'", path)
}
return nil, nil
}

View File

@ -60,3 +60,17 @@ func (d service) Exec(ctx ExecutionContext) ([]byte, error) {
return nil, nil return nil, nil
} }
func (d service) DryRun(ctx ExecutionContext) ([]byte, error) {
// fail if a service does not exist
for _, service := range d.Services {
if err := ctx.ServiceManager.Exec("status", service); err != nil {
return nil, fmt.Errorf("cannot find service '%s' | %s", service, err)
}
}
return nil, nil
}

View File

@ -2,8 +2,9 @@ package instruction
import ( import (
"fmt" "fmt"
"github.com/xdrm-brackets/nix-amer/internal/cnf" "git.xdrm.io/go/nix-amer/internal/cnf"
"os" "os"
"path/filepath"
"strings" "strings"
) )
@ -94,3 +95,57 @@ func (d set) Exec(ctx ExecutionContext) ([]byte, error) {
return nil, nil return nil, nil
} }
func (d set) DryRun(ctx ExecutionContext) ([]byte, error) {
// 1. get file / alias
path := d.File
if !strings.Contains(path, "/") {
if p, exists := ctx.Alias[path]; exists {
path = p
}
}
// 2. fail if file not found
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("cannot find file '%s'", path)
}
// 3. try to load format
format, err := cnf.Load(path)
if err != nil {
return nil, err
}
// 4. try to update value
if !format.Set(d.Path, d.Value) {
return nil, ErrCannotSet
}
// 5. fail on missing write permission
file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, os.FileMode(0775))
if err != nil {
return nil, fmt.Errorf("cannot update '%s' | %s", path, err)
}
file.Close()
// 6. create non-destructive dry-run folder
dryRunFolder := "/tmp/dry-run"
if err := os.MkdirAll(dryRunFolder, os.FileMode(0777)); err != nil {
return nil, fmt.Errorf("cannot create dry-run folder | %s", err)
}
// 7. create updated file inside .dry-run
tmpout := filepath.Join(dryRunFolder, strings.Replace(path, "/", "-", -1))
file, err = os.Create(tmpout)
if err != nil {
return nil, fmt.Errorf("cannot create dry-run file '%s' | %s", tmpout, err)
}
defer file.Close()
if _, err = format.WriteTo(file); err != nil {
return nil, fmt.Errorf("cannot write dry-run file '%s' | %s", tmpout, err)
}
return nil, nil
}

View File

@ -1,6 +1,6 @@
package pkg package pkg
import "github.com/xdrm-brackets/nix-amer/internal/exec" import "git.xdrm.io/go/nix-amer/internal/exec"
type apk struct{ exec exec.Executor } type apk struct{ exec exec.Executor }

View File

@ -1,7 +1,7 @@
package pkg package pkg
import ( import (
"github.com/xdrm-brackets/nix-amer/internal/exec" "git.xdrm.io/go/nix-amer/internal/exec"
) )
type aptGet struct{ exec exec.Executor } type aptGet struct{ exec exec.Executor }

View File

@ -1,7 +1,7 @@
package pkg package pkg
import ( import (
"github.com/xdrm-brackets/nix-amer/internal/exec" "git.xdrm.io/go/nix-amer/internal/exec"
) )
// DefaultManager if not empty is the default package-manager to use when missing // DefaultManager if not empty is the default package-manager to use when missing

View File

@ -1,6 +1,6 @@
package pkg package pkg
import "github.com/xdrm-brackets/nix-amer/internal/exec" import "git.xdrm.io/go/nix-amer/internal/exec"
type dnf struct{ exec exec.Executor } type dnf struct{ exec exec.Executor }

View File

@ -1,6 +1,6 @@
package pkg package pkg
import "github.com/xdrm-brackets/nix-amer/internal/exec" import "git.xdrm.io/go/nix-amer/internal/exec"
type eopkg struct{ exec exec.Executor } type eopkg struct{ exec exec.Executor }

View File

@ -2,7 +2,7 @@ package pkg
import ( import (
"errors" "errors"
"github.com/xdrm-brackets/nix-amer/internal/exec" "git.xdrm.io/go/nix-amer/internal/exec"
) )
// ErrUnknownManager is raised when the asked manager does not exist // ErrUnknownManager is raised when the asked manager does not exist

View File

@ -1,7 +1,7 @@
package pkg package pkg
import ( import (
"github.com/xdrm-brackets/nix-amer/internal/exec" "git.xdrm.io/go/nix-amer/internal/exec"
"testing" "testing"
) )

View File

@ -1,6 +1,6 @@
package pkg package pkg
import "github.com/xdrm-brackets/nix-amer/internal/exec" import "git.xdrm.io/go/nix-amer/internal/exec"
type pacman struct{ exec exec.Executor } type pacman struct{ exec exec.Executor }

View File

@ -1,6 +1,6 @@
package pkg package pkg
import "github.com/xdrm-brackets/nix-amer/internal/exec" import "git.xdrm.io/go/nix-amer/internal/exec"
type yum struct{ exec exec.Executor } type yum struct{ exec exec.Executor }

View File

@ -1,7 +1,7 @@
package ser package ser
import ( import (
"github.com/xdrm-brackets/nix-amer/internal/exec" "git.xdrm.io/go/nix-amer/internal/exec"
) )
// DefaultManager if not empty is the default service-manager to use when missing // DefaultManager if not empty is the default service-manager to use when missing

View File

@ -2,7 +2,7 @@ package ser
import ( import (
"errors" "errors"
"github.com/xdrm-brackets/nix-amer/internal/exec" "git.xdrm.io/go/nix-amer/internal/exec"
) )
// ErrUnknownManager is raised when the asked manager does not exist // ErrUnknownManager is raised when the asked manager does not exist

View File

@ -1,13 +1,13 @@
package ser package ser
import ( import (
"github.com/xdrm-brackets/nix-amer/internal/exec" "git.xdrm.io/go/nix-amer/internal/exec"
) )
type systemd struct{ exec exec.Executor } type systemd struct{ exec exec.Executor }
// available actions // available actions
var actions = []string{"enable", "disable", "start", "stop", "reload", "restart"} var actions = []string{"enable", "disable", "status", "start", "stop", "reload", "restart"}
func (d *systemd) SetExecutor(_exec exec.Executor) { func (d *systemd) SetExecutor(_exec exec.Executor) {
d.exec = _exec d.exec = _exec

20
main.go
View File

@ -2,8 +2,8 @@ package main
import ( import (
"fmt" "fmt"
"github.com/xdrm-brackets/nix-amer/internal/buildfile" "git.xdrm.io/go/nix-amer/internal/buildfile"
"github.com/xdrm-brackets/nix-amer/internal/clifmt" "git.xdrm.io/go/nix-amer/internal/clifmt"
"os" "os"
"time" "time"
) )
@ -30,7 +30,12 @@ func main() {
// 2. parse buildfile // 2. parse buildfile
instructions, err := buildfile.NewReader(ctx, bfreader) instructions, err := buildfile.NewReader(ctx, bfreader)
if err != nil { if err != nil {
fmt.Printf("%s%s\n", bf, err) if _, ok := err.(buildfile.LineError); ok {
fmt.Printf("line error\n")
fmt.Printf("%s%s\n", bf, err.Error())
} else {
fmt.Printf("%s\n", clifmt.Warn(err.Error()))
}
return return
} }
@ -38,13 +43,10 @@ func main() {
fmt.Printf("%s\n", clifmt.Color(32, "valid")) fmt.Printf("%s\n", clifmt.Color(32, "valid"))
// stop here if dry run // stop here if dry run
if dryRun {
return
}
// 3. Execute // 3. Execute
fmt.Printf("------\n") fmt.Printf("------\n")
err = instructions.Execute() err = instructions.Execute(dryRun)
if err != nil { if err != nil {
fmt.Printf("%s\n", clifmt.Warn(err.Error())) fmt.Printf("%s\n", clifmt.Warn(err.Error()))
return return
@ -53,4 +55,8 @@ func main() {
clifmt.Align("finished in") clifmt.Align("finished in")
fmt.Printf("%ss\n", clifmt.Color(32, fmt.Sprintf("%.2f", time.Now().Sub(start).Seconds()))) fmt.Printf("%ss\n", clifmt.Color(32, fmt.Sprintf("%.2f", time.Now().Sub(start).Seconds())))
if dryRun {
fmt.Printf("\n%s %s\n", clifmt.Info("updated configurations are inside"), clifmt.Color(32, "/tmp/dry-run"))
}
} }