Skip to content

Commit 3e3d121

Browse files
authored
Merge pull request #70 from AliceO2Group/feature/OCONF-184-coconut-conf-history-command
[OCONF-184] coconut conf history command
2 parents 0009161 + e953824 commit 3e3d121

7 files changed

Lines changed: 3229 additions & 2144 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* === This file is part of ALICE O² ===
3+
*
4+
* Copyright 2019 CERN and copyright holders of ALICE O².
5+
* Author: George Raduta <george.raduta@cern.ch>
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*
20+
* In applying this license CERN does not waive the privileges and
21+
* immunities granted to it by virtue of its status as an
22+
* Intergovernmental Organization or submit itself to any jurisdiction.
23+
*/
24+
25+
package cmd
26+
27+
import (
28+
"github.com/AliceO2Group/Control/coconut/configuration"
29+
"github.com/spf13/cobra"
30+
)
31+
32+
var configurationHistoryCmd = &cobra.Command{
33+
Use: "history <query>",
34+
Aliases: []string{"h"},
35+
Example: `coconut conf history <component>
36+
coconut conf history <component> <entry>
37+
coconut conf history <component>/<entry>`,
38+
Short: "List all existing entries with timestamps of a specified component in Consul",
39+
Long: `The configuration history command returns all entries with
40+
all of their associated timestamps or returns all timestamps for a specified component and entry`,
41+
Run: configuration.WrapCall(configuration.History),
42+
Args: cobra.RangeArgs(0, 3),
43+
}
44+
45+
func init() {
46+
configurationCmd.AddCommand(configurationHistoryCmd)
47+
configurationHistoryCmd.Flags().StringP("output", "o", "yaml", "output format for the returned entries (yaml/json)")
48+
}

coconut/cmd/configuration_list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* === This file is part of ALICE O² ===
33
*
4-
* Copyright 2018 CERN and copyright holders of ALICE O².
4+
* Copyright 2019 CERN and copyright holders of ALICE O².
55
* Author: George Raduta <george.raduta@cern.ch>
66
*
77
* This program is free software: you can redistribute it and/or modify

coconut/cmd/configuration_show.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* === This file is part of ALICE O² ===
33
*
4-
* Copyright 2018 CERN and copyright holders of ALICE O².
4+
* Copyright 2019 CERN and copyright holders of ALICE O².
55
* Author: George Raduta <george.raduta@cern.ch>
66
*
77
* This program is free software: you can redistribute it and/or modify

coconut/configuration/configuration.go

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
package configuration
2828

2929
import (
30+
"github.com/fatih/color"
31+
"github.com/olekukonko/tablewriter"
3032
"github.com/spf13/cobra"
3133
"github.com/spf13/viper"
3234
"github.com/briandowns/spinner"
@@ -35,6 +37,7 @@ import (
3537
"io"
3638
"github.com/sirupsen/logrus"
3739
"os"
40+
"sort"
3841
"github.com/AliceO2Group/Control/common/logger"
3942
"fmt"
4043
"strings"
@@ -64,6 +67,11 @@ const (
6467
logicError = iota // Logic/Output error
6568
)
6669

70+
var(
71+
blue = color.New(color.FgHiBlue).SprintFunc()
72+
red = color.New(color.FgHiRed).SprintFunc()
73+
)
74+
6775
func WrapCall(call ConfigurationCall) RunFunc {
6876
return func(cmd *cobra.Command, args []string) {
6977
endpoint := viper.GetString("config_endpoint")
@@ -262,6 +270,68 @@ func Show(cfg *configuration.ConsulSource, cmd *cobra.Command, args []string, o
262270
return nil, nonZero
263271
}
264272

273+
func History(cfg *configuration.ConsulSource, cmd *cobra.Command, args []string, o io.Writer)(err error, code int) {
274+
var key, component, entry string
275+
276+
if len(args) < 1 || len(args) > 2 {
277+
err = errors.New(fmt.Sprintf("Accepts between 0 and 3 arg(s), but received %d", len(args)))
278+
return err, invalidArgs
279+
}
280+
switch len(args) {
281+
case 1:
282+
if IsInputSingleValidWord(args[0]) {
283+
component = args[0]
284+
entry = ""
285+
} else if IsInputNameValid(args[0]) && !strings.Contains(args[0], "@"){
286+
splitCom := strings.Split(args[0], "/")
287+
component = splitCom[0]
288+
entry = splitCom[1]
289+
} else {
290+
return errors.New(fmt.Sprintf("Component and Entry name cannot contain `/ or `@`")), invalidArgs
291+
}
292+
case 2:
293+
if IsInputSingleValidWord(args[0]) && IsInputSingleValidWord(args[1]) {
294+
component = args[0]
295+
entry = args[1]
296+
} else {
297+
return errors.New(fmt.Sprintf("Component and Entry name cannot contain `/ or `@`")), invalidArgs
298+
}
299+
}
300+
301+
key = componentsPath + component + "/" + entry
302+
var keys sort.StringSlice
303+
keys , err = cfg.GetKeysByPrefix(key, "")
304+
305+
if len(keys) == 0 {
306+
return errors.New(fmt.Sprintf("No data was found")), emptyData
307+
} else {
308+
if entry != "" {
309+
sort.Sort(sort.Reverse(keys))
310+
drawTableHistoryConfigs([]string{}, keys, 0, o)
311+
} else {
312+
maxLen := GetMaxLenOfKey(keys)
313+
var currentKeys sort.StringSlice
314+
_, entry, _ := GetComponentEntryTimestamp(keys[0])
315+
316+
for _, value := range keys {
317+
_, currentEntry, _ := GetComponentEntryTimestamp(value)
318+
if currentEntry == entry {
319+
currentKeys = append(currentKeys, value)
320+
} else {
321+
fmt.Fprintln(o, "- " + entry)
322+
sort.Sort(sort.Reverse(currentKeys)) //sort in reverse of timestamps
323+
drawTableHistoryConfigs([]string{}, currentKeys,maxLen, o)
324+
currentKeys = []string{value}
325+
entry = currentEntry
326+
}
327+
}
328+
fmt.Fprintln(o, "- " + entry)
329+
drawTableHistoryConfigs([]string{}, currentKeys,maxLen, o)
330+
}
331+
}
332+
return nil, 0
333+
}
334+
265335
func formatListOutput( cmd *cobra.Command, output []string)(parsedOutput []byte, err error) {
266336
format, err := cmd.Flags().GetString("output")
267337
if err != nil {
@@ -289,6 +359,16 @@ func IsInputSingleValidWord(input string) bool {
289359
return !strings.Contains(input, "/") && !strings.Contains(input, "@")
290360
}
291361

362+
// Method to parse a timestamp in the specified format
363+
func GetTimestampInFormat(timestamp string, timeFormat string)(string, error){
364+
timeStampAsInt, err := strconv.ParseInt(timestamp, 10, 64)
365+
if err != nil {
366+
return "", errors.New(fmt.Sprintf("Unable to identify timestamp"))
367+
}
368+
tm := time.Unix(timeStampAsInt, 0)
369+
return tm.Format(timeFormat), nil
370+
}
371+
292372
// Method to return the latest timestamp for a specified component & entry
293373
// If no keys were passed an error and code exit 3 will be returned
294374
func GetLatestTimestamp(keys []string, component string, entry string)(timestamp string, err error, code int) {
@@ -343,4 +423,42 @@ func GetListOfComponentsAndOrWithTimestamps(keys []string, keyPrefix string, use
343423
}
344424
}
345425
return components, nil, nonZero
346-
}
426+
}
427+
428+
func drawTableHistoryConfigs(headers []string, history []string, max int, o io.Writer) {
429+
table := tablewriter.NewWriter(o)
430+
if len(headers) > 0 {
431+
table.SetHeader(headers)
432+
}
433+
table.SetBorder(false)
434+
table.SetColMinWidth(0, max)
435+
436+
for _, value := range history {
437+
component, entry, timestamp := GetComponentEntryTimestamp(value)
438+
prettyTimestamp, err := GetTimestampInFormat(timestamp, time.RFC822)
439+
if err != nil {
440+
prettyTimestamp = timestamp
441+
}
442+
configName := red(component) + "/" + blue(entry) + "@" + timestamp
443+
table.Append([]string{configName, prettyTimestamp})
444+
}
445+
table.Render()
446+
}
447+
448+
func GetComponentEntryTimestamp(key string)(string, string, string) {
449+
key = strings.TrimPrefix(key, componentsPath)
450+
key = strings.TrimPrefix(key, "/'")
451+
key = strings.TrimSuffix(key, "/")
452+
elements := strings.Split(key, "/")
453+
return elements[0], elements[1], elements[2]
454+
}
455+
456+
func GetMaxLenOfKey(keys []string) (maxLen int){
457+
maxLen = 0
458+
for _, value := range keys {
459+
if len(value) - len(componentsPath) >= maxLen {
460+
maxLen = len(value) - len(componentsPath)
461+
}
462+
}
463+
return
464+
}

0 commit comments

Comments
 (0)