Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ lint: ${LINT}
${MOCKERY}:
$(VGO) install github.com/vektra/mockery/cmd/mockery@latest
${LINT}:
$(VGO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
$(VGO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.46.2
ffcommon:
$(eval WSCLIENT_PATH := $(shell $(VGO) list -f '{{.Dir}}' github.com/hyperledger/firefly-common/pkg/wsclient))

Expand Down
25 changes: 25 additions & 0 deletions ffconfig/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# FireFly configuration tool

A tool for managing and migrating config files for Hyperledger FireFly.

## Installation

If you have a local Go development environment, and you have included `${GOPATH}/bin` in your path, you can install with:

```sh
go install github.com/hyperledger/firefly/ffconfig@latest
```

## Usage

### Migration

Parse a config file to find any deprecated items that should be updated to the latest config syntax.
The updated config will be written to stdout (or a file specified with `-o`).
```
ffconfig migrate -f firefly.core.yml [-o new.yml]
```

You may optionally specify `--from` and `--to` versions to run a subset of the migrations.

View the source code for all current migrations at [migrate/migrations.go](migrate/migrations.go).
76 changes: 76 additions & 0 deletions ffconfig/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright © 2022 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// 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.

package main

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

"github.com/hyperledger/firefly/ffconfig/migrate"
"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
Use: "ffconfig",
Short: "FireFly configuration tool",
Long: "Tool for managing and migrating config files for Hyperledger FireFly",
RunE: func(cmd *cobra.Command, args []string) error {
return fmt.Errorf("a command is required")
},
}

var migrateCommand = &cobra.Command{
Use: "migrate",
Short: "Migrate a config file to the current version",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := ioutil.ReadFile(cfgFile)
if err != nil {
return err
}
out, err := migrate.Run(cfg, fromVersion, toVersion)
if err != nil {
return err
}
if outFile == "" {
fmt.Print(string(out))
return nil
}
return ioutil.WriteFile(outFile, out, 0600)
},
}

var cfgFile string
var outFile string
var fromVersion string
var toVersion string

func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "firefly.core.yml", "config file")
migrateCommand.PersistentFlags().StringVarP(&outFile, "out", "o", "", "output file (if unspecified, write to stdout)")
migrateCommand.PersistentFlags().StringVar(&fromVersion, "from", "", "from version (optional, such as 1.0.0)")
migrateCommand.PersistentFlags().StringVar(&toVersion, "to", "", "to version (optional, such as 1.1.0)")
rootCmd.AddCommand(migrateCommand)
}

func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
os.Exit(0)
}
201 changes: 201 additions & 0 deletions ffconfig/migrate/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Copyright © 2022 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// 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.

package migrate

import (
"fmt"
"io"
)

type ConfigItem struct {
value interface{}
parent *ConfigItem
name string
writer io.Writer
}

type ConfigItemIterator struct {
items []*ConfigItem
}

func NewConfigItem(value interface{}, writer io.Writer) *ConfigItem {
return &ConfigItem{value: value, writer: writer}
}

func (c *ConfigItem) hasChild(name string) (exists bool) {
if v, ok := c.value.(map[interface{}]interface{}); ok {
_, exists = v[name]
}
return exists
}

func (c *ConfigItem) deleteChild(name string) {
if v, ok := c.value.(map[interface{}]interface{}); ok {
delete(v, name)
}
}

func (c *ConfigItem) setChild(name string, value interface{}) {
if v, ok := c.value.(map[interface{}]interface{}); ok {
v[name] = value
}
}

func (c *ConfigItem) Get(name string) *ConfigItem {
if v, ok := c.value.(map[interface{}]interface{}); ok {
if child, ok := v[name]; ok {
return &ConfigItem{value: child, parent: c, name: name, writer: c.writer}
}
}
return &ConfigItem{value: nil, parent: c, name: name, writer: c.writer}
}

func (c *ConfigItemIterator) Get(name string) *ConfigItemIterator {
items := make([]*ConfigItem, len(c.items))
for i, item := range c.items {
items[i] = item.Get(name)
}
return &ConfigItemIterator{items: items}
}

func (c *ConfigItem) Path() string {
if c.parent == nil || c.parent.name == "" {
return c.name
}
return c.parent.Path() + "." + c.name
}

func (c *ConfigItem) Exists() bool {
return c.value != nil
}

func (c *ConfigItem) Length() int {
if v, ok := c.value.([]interface{}); ok {
return len(v)
}
return 0
}

func (c *ConfigItem) Each() *ConfigItemIterator {
list, ok := c.value.([]interface{})
if !ok {
return &ConfigItemIterator{items: make([]*ConfigItem, 0)}
}
items := make([]*ConfigItem, len(list))
for i, val := range list {
items[i] = &ConfigItem{
value: val,
parent: c.parent,
name: c.name,
writer: c.writer,
}
}
return &ConfigItemIterator{items: items}
}

func (c *ConfigItemIterator) Run(fn func(item *ConfigItem)) *ConfigItemIterator {
for _, item := range c.items {
fn(item)
}
return c
}

func (c *ConfigItem) Create() *ConfigItem {
if !c.Exists() {
if c.parent != nil {
c.parent.Create()
}
c.value = make(map[interface{}]interface{})
c.parent.setChild(c.name, c.value)
}
return c
}

func (c *ConfigItem) Set(value interface{}) *ConfigItem {
fmt.Fprintf(c.writer, "Create: %s: %s\n", c.Path(), value)
c.value = value
c.parent.Create()
c.parent.setChild(c.name, c.value)
return c
}

func (c *ConfigItemIterator) Set(value interface{}) *ConfigItemIterator {
for _, item := range c.items {
item.Set(value)
}
return c
}

func (c *ConfigItem) SetIfEmpty(value interface{}) *ConfigItem {
if !c.Exists() {
c.Set(value)
}
return c
}

func (c *ConfigItem) Delete() *ConfigItem {
if c.Exists() {
fmt.Fprintf(c.writer, "Delete: %s\n", c.Path())
c.parent.deleteChild(c.name)
}
return c
}

func (c *ConfigItemIterator) Delete() *ConfigItemIterator {
for _, item := range c.items {
item.Delete()
}
return c
}

func (c *ConfigItem) RenameTo(name string) *ConfigItem {
if c.Exists() {
if c.parent.hasChild(name) {
// Don't overwrite if the new key already exists
c.Delete()
} else {
fmt.Fprintf(c.writer, "Rename: %s -> .%s\n", c.Path(), name)
c.parent.deleteChild(c.name)
c.parent.setChild(name, c.value)
c.name = name
}
}
return c
}

func (c *ConfigItemIterator) RenameTo(name string) *ConfigItemIterator {
for _, item := range c.items {
item.RenameTo(name)
}
return c
}

func (c *ConfigItem) ReplaceValue(old interface{}, new interface{}) *ConfigItem {
if c.value == old {
fmt.Fprintf(c.writer, "Change: %s: %s -> %s\n", c.Path(), old, new)
c.value = new
c.parent.setChild(c.name, new)
}
return c
}

func (c *ConfigItemIterator) ReplaceValue(old interface{}, new interface{}) *ConfigItemIterator {
for _, item := range c.items {
item.ReplaceValue(old, new)
}
return c
}
45 changes: 45 additions & 0 deletions ffconfig/migrate/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright © 2022 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// 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.

package migrate

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func TestDeleteList(t *testing.T) {
value := map[interface{}]interface{}{
"values": []interface{}{"test1", "test2"},
}
config := &ConfigItem{value: value, writer: os.Stdout}
config.Get("values").Each().Delete()
assert.Equal(t, 0, len(value))
}

func TestNoRename(t *testing.T) {
value := map[interface{}]interface{}{
"key1": "val1",
"key2": "val2",
}
config := &ConfigItem{value: value, writer: os.Stdout}
config.Get("key1").RenameTo("key2")
assert.Equal(t, map[interface{}]interface{}{
"key2": "val2",
}, value)
}
Loading