Skip to content
This repository was archived by the owner on Aug 29, 2023. It is now read-only.
Open
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
83 changes: 83 additions & 0 deletions config_set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"encoding/json"
"fmt"

log "github.com/sirupsen/logrus"

"github.com/weaveworks/footloose/pkg/cluster"

"github.com/spf13/cobra"
"github.com/weaveworks/footloose/pkg/config"
)

var configSetCmd = &cobra.Command{
Use: "set",
Short: "set value to configuration",
RunE: configSet,
}

var setOptions struct {
config string
}

var setDryRun bool

func init() {
configSetCmd.Flags().StringVarP(&setOptions.config, "config", "c", Footloose, "Cluster configuration file")
configSetCmd.Flags().BoolVar(&setDryRun, "dry-run", defaultDryRun, "Dry run changes")
configCmd.AddCommand(configSetCmd)
}

func configSet(cmd *cobra.Command, args []string) error {
conf, cstr := getConfigAndCluster()
checkSetRequirements(args, cstr, conf)
err := config.SetValueToConfig(args[0], conf, config.ClarifyArg(args[1]))
if err != nil {
log.Fatalln(err)
}
handleSetResponse(conf)
return nil
}

func getConfigAndCluster() (*config.Config, *cluster.Cluster) {
conf, err := config.NewConfigFromFile(setOptions.config)
if err != nil {
log.Fatalln(err)
}
cstr, err := cluster.New(*conf)
if err != nil {
log.Fatalln(err)
}
return conf, cstr
}

func checkSetRequirements(args []string, c *cluster.Cluster, conf *config.Config) {
if len(args) != 2 {
log.Fatalln("set command needs 2 args")
}
err := config.IsSetValueValid(args[0], args[1])
if err != nil {
log.Fatalln(err)
}
num, err := c.CountCreatedMachine()
if err != nil {
log.Fatalln(err)
}
if num > 0 {
log.Fatalln("cannot change config, please delete your machines before any change")
}
}

func handleSetResponse(conf *config.Config) {
if setDryRun == true {
res, err := json.MarshalIndent(conf, "", " ")
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%s", res)
} else {
conf.Save(setOptions.config)
}
}
2 changes: 2 additions & 0 deletions defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ func imageTag(v string) string {
return v
}

var defaultDryRun = false

var defaultConfig = config.Config{
Cluster: config.Cluster{
Name: "cluster",
Expand Down
19 changes: 17 additions & 2 deletions pkg/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,21 @@ func (c *Cluster) Inspect(hostnames []string) ([]*Machine, error) {
return machines, nil
}

// CountCreatedMachine counts how many machines are created
func (c *Cluster) CountCreatedMachine() (int, error) {
machines, err := c.gatherMachines()
if err != nil {
return 0, err
}
counter := 0
for _, m := range machines {
if m.IsCreated() {
counter++
}
}
return counter, nil
}

func (c *Cluster) machineFilering(machines []*Machine, hostnames []string) []*Machine {
// machinesToKeep map is used to know not found machines
machinesToKeep := make(map[string]bool)
Expand Down Expand Up @@ -341,8 +356,8 @@ func (c *Cluster) gatherMachines() (machines []*Machine, err error) {
}
p := config.PortMapping{}
hostPort, _ := strconv.Atoi(v[0].HostPort)
p.HostPort = uint16(k.Int())
p.ContainerPort = uint16(hostPort)
p.HostPort = k.Int()
p.ContainerPort = hostPort
p.Address = v[0].HostIP
ports = append(ports, p)
}
Expand Down
21 changes: 15 additions & 6 deletions pkg/config/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,37 @@ func NewConfigFromFile(path string) (*Config, error) {
return NewConfigFromYAML(data)
}

// Save writes the Config to a file.
func (c *Config) Save(path string) error {
data, err := yaml.Marshal(c)
if err != nil {
return err
}
return ioutil.WriteFile(path, data, 0666)
}

// MachineReplicas are a number of machine following the same specification.
type MachineReplicas struct {
Spec Machine `json:"spec"`
Count int `json:"count"`
Spec Machine `json:"spec" yaml:"spec,omitempty"`
Count int `json:"count" yaml:"count,omitempty"`
}

// Cluster is a set of Machines.
type Cluster struct {
// Name is the cluster name. Defaults to "cluster".
Name string `json:"name"`
Name string `json:"name" yaml:"name,omitempty"`

// PrivateKey is the path to the private SSH key used to login into the cluster
// machines. Can be expanded to user homedir if ~ is found. Ex. ~/.ssh/id_rsa
PrivateKey string `json:"privateKey"`
PrivateKey string `json:"privateKey" yaml:"privateKey,omitempty"`
}

// Config is the top level config object.
type Config struct {
// Cluster describes cluster-wide configuration.
Cluster Cluster `json:"cluster"`
Cluster Cluster `json:"cluster" yaml:"cluster,omitempty"`
// Machines describe the machines we want created for this cluster.
Machines []MachineReplicas `json:"machines"`
Machines []MachineReplicas `json:"machines" yaml:"machines,omitempty"`
}

// validate checks basic rules for MachineReplicas's fields
Expand Down
55 changes: 39 additions & 16 deletions pkg/config/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,33 @@ import (
// Volume is a volume that can be attached to a Machine.
type Volume struct {
// Type is the volume type. One of "bind" or "volume".
Type string `json:"type"`
Type string `json:"type" yaml:"type,omitempty"`
// Source is the volume source.
// With type=bind, the volume source is a directory or a file in the host
// filesystem.
// With type=volume, source is either the name of a docker volume or "" for
// anonymous volumes.
Source string `json:"source"`
Source string `json:"source" yaml:"source,omitempty"`
// Destination is the mount point inside the container.
Destination string `json:"destination"`
Destination string `json:"destination" yaml:"destination,omitempty"`
// ReadOnly specifies if the volume should be read-only or not.
ReadOnly bool `json:"readOnly"`
ReadOnly bool `json:"readOnly" yaml:"readOnly,omitempty"`
}

// PortMapping describes mapping a port from the machine onto the host.
type PortMapping struct {
// Protocol is the layer 4 protocol for this mapping. One of "tcp" or "udp".
// Defaults to "tcp".
Protocol string `json:"protocol,omitempty"`
Protocol string `json:"protocol,omitempty" yaml:"protocol,omitempty"`
// Address is the host address to bind to. Defaults to "0.0.0.0".
Address string `json:"address,omitempty"`
Address string `json:"address,omitempty" yaml:"address,omitempty"`
// HostPort is the base host port to map the containers ports to. As we
// configure a number of machine replicas, each machine will use HostPort+i
// where i is between 0 and N-1, N being the number of machine replicas. If 0,
// a local port will be automatically allocated.
HostPort uint16 `json:"hostPort,omitempty"`
HostPort int `json:"hostPort,omitempty" yaml:"hostPort,omitempty"`
// ContainerPort is the container port to map.
ContainerPort uint16 `json:"containerPort"`
ContainerPort int `json:"containerPort" yaml:"containerPort,omitempty"`
}

// Machine is the machine configuration.
Expand All @@ -45,26 +45,49 @@ type Machine struct {
// index, a number between 0 and N-1, N being the number of machines in the
// cluster. This name will also be used as the machine hostname. Defaults to
// "node%d".
Name string `json:"name"`
Name string `json:"name" yaml:"name,omitempty"`
// Image is the container image to use for this machine.
Image string `json:"image"`
Image string `json:"image" yaml:"image,omitempty"`
// Privileged controls whether to start the Machine as a privileged container
// or not. Defaults to false.
Privileged bool `json:"privileged,omitempty"`
Privileged bool `json:"privileged,omitempty" yaml:"privileged,omitempty"`
// Volumes is the list of volumes attached to this machine.
Volumes []Volume `json:"volumes,omitempty"`
Volumes []Volume `json:"volumes,omitempty" yaml:"volumes,omitempty"`
// PortMappings is the list of ports to expose to the host.
PortMappings []PortMapping `json:"portMappings,omitempty"`
PortMappings []PortMapping `json:"portMappings,omitempty" yaml:"portMappings,omitempty"`
// Cmd is a cmd which will be run in the container.
Cmd string `json:"cmd,omitempty"`
Cmd string `json:"cmd,omitempty" yaml:"cmd,omitempty"`
}

// validate checks basic rules for Machine's fields
func (conf Machine) validate() error {
func (conf Machine) validate() (rerr error) {
rerr = nil
validName := strings.Contains(conf.Name, "%d")
if validName != true {
log.Warnf("Machine conf validation: machine name %v is not valid, it should contains %%d", conf.Name)
return fmt.Errorf("Machine configuration not valid")
rerr = fmt.Errorf("Machine configuration not valid")
}
for _, pmapping := range conf.PortMappings {
if err := pmapping.validate(); err != nil {
log.Warn(err)
rerr = fmt.Errorf("Machine configuration not valid")
}
}
return rerr
}

func (conf PortMapping) validate() error {
if conf.HostPort > maxPort || conf.HostPort < minPort {
return fmt.Errorf("Machine conf validation: hostPort %v is not valid, it cannot be hight than %v or lesser than %v",
conf.HostPort,
maxPort,
minPort)
}
if conf.ContainerPort > maxPort || conf.ContainerPort < minPort {
return fmt.Errorf("Machine conf validation: containerPort %v is not valid, it cannot be hight than %v or lesser than %v",
conf.ContainerPort,
maxPort,
minPort)
}
return nil
}
96 changes: 96 additions & 0 deletions pkg/config/set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package config

import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
)

const (
machinePattern = "%d"
minPort = 0
maxPort = 65535
machineNameRegex = `^(?:m|M)achines\[[0-9]+\].(?:s|S)pec.(?:n|N)ame$`
portRegex = `^(?:m|M)achines\[[0-9]+\].(?:s|S)pec.(?:p|P)ortMappings\[[0-9]+\].(?:(?:h|H)ostPort|(?:c|C)ontainerPort)$`
)

// IsSetValueValid checks if value is valid for the given path
func IsSetValueValid(stringPath string, value string) (rerr error) {
defer func() {
if r := recover(); r != nil {
rerr = fmt.Errorf(fmt.Sprint(r))
}
}()
v := reflect.ValueOf(ClarifyArg(value))
if v.Kind() == reflect.String {
// check machine name
re := regexp.MustCompile(machineNameRegex)
if re.MatchString(stringPath) == true {
if strings.Contains(v.Interface().(string), machinePattern) == false {
return fmt.Errorf("Machine name is not valid, it should contain %v", machinePattern)
}
}
} else if v.Kind() == reflect.Int {
// check port value
re := regexp.MustCompile(portRegex)
if re.MatchString(stringPath) == true {
if v.Interface().(int) > maxPort || v.Interface().(int) < minPort {
return fmt.Errorf("Port cannot be higher than %v or lesset than %v", maxPort, minPort)
}
}
}
return nil
}

// ClarifyArg converts string to int or bool if possible
func ClarifyArg(v string) interface{} {
intV, err := strconv.Atoi(v)
if err == nil {
return intV
}
boolV, err := strconv.ParseBool(v)
if err == nil {
return boolV
}
return v
}

// SetValueToConfig sets specific value to an object given a string path
func SetValueToConfig(stringPath string, object interface{}, newValue interface{}) (rerr error) {
defer func() {
if r := recover(); r != nil {
rerr = fmt.Errorf(fmt.Sprint(r))
}
}()
keyPath := strings.FieldsFunc(stringPath, pathSplit)
v := reflect.ValueOf(object)
for _, key := range keyPath {
keyUpper := strings.Title(key)
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() == reflect.Struct {
v = v.FieldByName(keyUpper)
if v.IsValid() == false {
return fmt.Errorf("%v key does not exist", keyUpper)
}
} else if v.Kind() == reflect.Slice {
index, errConv := strconv.Atoi(keyUpper)
if errConv != nil {
return fmt.Errorf("%v is not an index", key)
}
v = v.Index(index)
} else {
return fmt.Errorf("%v is neither a slice or a struct", v)
}
}
newV := reflect.ValueOf(newValue)
if v.Kind() == newV.Kind() {
v.Set(newV)
} else {
return fmt.Errorf("%v type and %v type do not correspond", v, newV)
}
return nil
}
Loading