From f74dc503925072063515bc1ea23bb37fd4701358 Mon Sep 17 00:00:00 2001 From: "Joel D. Elkins" Date: Mon, 18 Jul 2022 19:47:05 -0500 Subject: [PATCH] Rest of commands; tooling including conditional command types --- cmd/create.go | 12 ++- cmd/recreate.go | 55 ++++++++++++++ cmd/restart.go | 65 ++++++++++++++++ cmd/root.go | 12 ++- cmd/start.go | 18 ++++- cmd/update.go | 55 ++++++++++++++ internal/pkg/command/command.go | 111 ++++++++++++++++++++++------ internal/pkg/config/config.go | 7 +- internal/pkg/container/container.go | 109 ++++++++++++++++++++------- 9 files changed, 385 insertions(+), 59 deletions(-) create mode 100644 cmd/recreate.go create mode 100644 cmd/restart.go create mode 100644 cmd/update.go diff --git a/cmd/create.go b/cmd/create.go index 02207b0..2c9657b 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -22,6 +22,8 @@ THE SOFTWARE. package cmd import ( + "fmt" + "gitea.elkins.co/Networking/ccl/internal/pkg/config" "github.com/spf13/cobra" ) @@ -34,11 +36,17 @@ var createCmd = &cobra.Command{ ValidArgsFunction: validNouns, Long: `Create containers specified by the arguments. Arguments can be either container names or categories. Multiple arguments are supported.`, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { conts := config.Union(args) for _, c := range conts { - config.PrintCreate(&c) + fmt.Fprintln(output, "CREATE", c.Name) + for _, c := range c.CreateCommands() { + if err := c.Execute(output, fake); err != nil { + return err + } + } } + return nil }, } diff --git a/cmd/recreate.go b/cmd/recreate.go new file mode 100644 index 0000000..3ae1b02 --- /dev/null +++ b/cmd/recreate.go @@ -0,0 +1,55 @@ +/* +Copyright © 2022 Joel D. Elkins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "fmt" + + "gitea.elkins.co/Networking/ccl/internal/pkg/config" + "github.com/spf13/cobra" +) + +// recreateCmd represents the recreate command +var recreateCmd = &cobra.Command{ + Use: "recreate", + Short: "Recreate container images", + Args: cobra.OnlyValidArgs, + ValidArgsFunction: validNouns, + Long: `Recreate container images, stopping and restarting if necessary. Arguments can be +one or more container names or categories. If empty, "all" is assumed.`, + RunE: func(cmd *cobra.Command, args []string) error { + conts := config.Union(args) + for _, c := range conts { + fmt.Fprintln(output, "RECREATE", c.Name) + for _, c := range c.RecreateCommands() { + if err := c.Execute(output, fake); err != nil { + return err + } + } + } + return nil + }, +} + +func init() { + rootCmd.AddCommand(recreateCmd) +} diff --git a/cmd/restart.go b/cmd/restart.go new file mode 100644 index 0000000..3d389f9 --- /dev/null +++ b/cmd/restart.go @@ -0,0 +1,65 @@ +/* +Copyright © 2022 Joel D. Elkins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "fmt" + + "gitea.elkins.co/Networking/ccl/internal/pkg/config" + "github.com/spf13/cobra" +) + +// restartCmd represents the restart command +var restartCmd = &cobra.Command{ + Use: "restart", + Short: "Restart containers", + Args: cobra.OnlyValidArgs, + ValidArgsFunction: validNouns, + Long: `Stop configured containers (if running), then restart them. Arguments can be +one or more container names or categories. If empty, "all" is assumed.`, + RunE: func(cmd *cobra.Command, args []string) error { + conts := config.Union(args) + for _, c := range conts { + fmt.Fprintln(output, "RESTART", c.Name) + for _, c := range c.RestartCommands() { + if err := c.Execute(output, fake); err != nil { + return err + } + } + } + return nil + }, +} + +func init() { + rootCmd.AddCommand(restartCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // restartCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // restartCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/root.go b/cmd/root.go index 0e00a06..4bbd87b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,6 +22,7 @@ THE SOFTWARE. package cmd import ( + "fmt" "io" "os" @@ -39,9 +40,11 @@ from a toml configuration file, and the utility uses this information to execute the necessary podman commands.`, ValidArgsFunction: cobra.NoFileCompletions, } + var ( output io.Writer verbose bool + fake bool ) // Execute adds all child commands to the root command and sets flags appropriately. @@ -54,7 +57,13 @@ func Execute() { } func init() { - cobra.OnInitialize(config.Init) + cobra.OnInitialize(func() { + err := config.Init() + if err != nil { + fmt.Fprintln(os.Stderr, "Could not initialize configuration:", err) + os.Exit(1) + } + }) cobra.OnInitialize(func() { if verbose { output = os.Stderr @@ -64,4 +73,5 @@ func init() { }) rootCmd.PersistentFlags().StringVarP(&config.ConfigFile, "config", "c", config.CONFIG_FILE_DEFAULT, "pathname of config file") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show additional info from command execution") + rootCmd.PersistentFlags().BoolVarP(&fake, "no-action", "n", false, "do not actually execute commands") } diff --git a/cmd/start.go b/cmd/start.go index 3d370f5..8fcc095 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -22,21 +22,31 @@ THE SOFTWARE. package cmd import ( + "fmt" + "gitea.elkins.co/Networking/ccl/internal/pkg/config" "github.com/spf13/cobra" ) // startCmd represents the start command var startCmd = &cobra.Command{ - Use: "start", - Short: "Start containers", + Use: "start", + Short: "Start containers", + Args: cobra.OnlyValidArgs, + ValidArgsFunction: validNouns, Long: `Start configured containers. They must be created first. Arguments can be one or more container names or categories. If empty, "all" is assumed.`, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { conts := config.Union(args) for _, c := range conts { - config.PrintStart(&c) + fmt.Fprintln(output, "START", c.Name) + for _, c := range c.StartCommands() { + if err := c.Execute(output, fake); err != nil { + return err + } + } } + return nil }, } diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 0000000..3ca6203 --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,55 @@ +/* +Copyright © 2022 Joel D. Elkins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "fmt" + + "gitea.elkins.co/Networking/ccl/internal/pkg/config" + "github.com/spf13/cobra" +) + +// updateCmd represents the update command +var updateCmd = &cobra.Command{ + Use: "update", + Short: "Update container images", + Args: cobra.OnlyValidArgs, + ValidArgsFunction: validNouns, + Long: `Update container images, stopping and restarting if necessary. Arguments can be +one or more container names or categories. If empty, "all" is assumed.`, + RunE: func(cmd *cobra.Command, args []string) error { + conts := config.Union(args) + for _, c := range conts { + fmt.Fprintln(output, "UPDATE", c.Name) + for _, c := range c.UpdateCommands() { + if err := c.Execute(output, fake); err != nil { + return err + } + } + } + return nil + }, +} + +func init() { + rootCmd.AddCommand(updateCmd) +} diff --git a/internal/pkg/command/command.go b/internal/pkg/command/command.go index b90b178..0bb759e 100644 --- a/internal/pkg/command/command.go +++ b/internal/pkg/command/command.go @@ -29,8 +29,20 @@ import ( type CommandType int +const ( + CT_NOP CommandType = iota + CT_SH + CT_REF + CT_INDIRECT + CT_SET + CT_DEBUG + CT_CONDITIONAL +) + func (ct CommandType) String() string { switch ct { + case CT_NOP: + return "NOP" case CT_SH: return "SHELL" case CT_REF: @@ -41,30 +53,40 @@ func (ct CommandType) String() string { return "SET" case CT_DEBUG: return "DEBUG" + case CT_CONDITIONAL: + return "CONDITIONAL" default: return "UNKOWN" } } -const ( - CT_SH CommandType = iota - CT_REF - CT_INDIRECT - CT_SET - CT_DEBUG -) - type Command struct { Type CommandType Command interface{} } +type namedFunc struct { + Name string + Func func() string +} + +type conditional struct { + Name string + Condition func() bool + ThenCmd Command + ElseCmd Command +} + +func (f namedFunc) String() string { + return f.Name + "()" +} + func NewShell(cmd string) Command { return Command{CT_SH, cmd} } -func NewFunc(f func() string) Command { - return Command{CT_REF, f} +func NewFunc(name string, f func() string) Command { + return Command{CT_REF, namedFunc{name, f}} } func NewIndirect(c Command) Command { @@ -79,6 +101,19 @@ func NewDebug(msg string) Command { return Command{CT_DEBUG, msg} } +func NewNop() Command { + return Command{CT_NOP, nil} +} + +func NewConditional(name string, ifPart func() bool, thenPart Command, elsePart Command) Command { + return Command{CT_CONDITIONAL, conditional{ + Name: name, + Condition: ifPart, + ThenCmd: thenPart, + ElseCmd: elsePart, + }} +} + func (c Command) GetShell() (string, error) { s, ok := c.Command.(string) if ok { @@ -88,45 +123,79 @@ func (c Command) GetShell() (string, error) { } func (c Command) CallRef() (string, error) { - f, ok := c.Command.(func() string) + f, ok := c.Command.(namedFunc) if ok { - s := f() + s := f.Func() return s, nil } return "", fmt.Errorf("Type error. Requested = %d, Type = %d", CT_REF, c.Type) } -func (c Command) Execute(output io.Writer) error { +func (c Command) Execute(output io.Writer, fake bool) error { switch c.Type { + case CT_NOP: + fmt.Fprintln(output, c.Type) case CT_SH: cmd, err := c.GetShell() if err != nil { return err } - out, err := exec.Command("sh", "-c", cmd).CombinedOutput() - fmt.Fprintln(output, out) - return err + if fake { + fmt.Fprintln(output, c.Type, "sh", "-c", cmd) + return nil + } else { + out, err := exec.Command("sh", "-c", cmd).CombinedOutput() + fmt.Fprintln(output, out) + return err + } case CT_REF: - s, err := c.CallRef() - fmt.Println(s) - return err + if fake { + fmt.Fprintln(output, c.Type, c.Command) + return nil + } else { + s, err := c.CallRef() + fmt.Println(s) + return err + } case CT_INDIRECT: ct, ok := c.Command.(Command) if !ok { return fmt.Errorf("Type error: Requested = %d, Type = %d", CT_INDIRECT, c.Type) } - return ct.Execute(output) + return ct.Execute(output, fake) case CT_SET: cs, ok := c.Command.([]Command) if !ok { return fmt.Errorf("Type error: Requested = %d, Type = %d", CT_SET, c.Type) } for i := range cs { - err := cs[i].Execute(output) + err := cs[i].Execute(output, fake) if err != nil { return err } } + case CT_CONDITIONAL: + cond, ok := c.Command.(conditional) + if !ok { + return fmt.Errorf("Type error: Requested = %d, Type = %d", CT_CONDITIONAL, c.Type) + } + if fake { + fmt.Fprintln(output, c.Type, cond.Name) + fmt.Fprintln(output, "-- True branch") + cond.ThenCmd.Execute(output, fake) + fmt.Fprintln(output, "-- False branch") + cond.ElseCmd.Execute(output, fake) + fmt.Fprintln(output, "-- End conditional") + return nil + } else { + branch := cond.Condition() + fmt.Fprintln(output, c.Type, cond.Name, ":", branch) + if branch { + return cond.ThenCmd.Execute(output, fake) + } else { + return cond.ElseCmd.Execute(output, fake) + } + } } return nil } diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index f179391..5d70051 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -65,20 +65,21 @@ func Union(ids []string) (conts []container.Container) { return } -func Init() { +func Init() error { f, err := os.ReadFile(ConfigFile) if err != nil { - return + return err } p := parse{} err = toml.Unmarshal(f, &p) if err != nil { - return + return err } containers, networks = &p.Containers, &p.Networks for i := range p.Containers { p.Containers[i].Init(networks) } + return nil } func NetworkDefaults(name string) (net *network.Network) { diff --git a/internal/pkg/container/container.go b/internal/pkg/container/container.go index 27a3ced..a89bb60 100644 --- a/internal/pkg/container/container.go +++ b/internal/pkg/container/container.go @@ -45,13 +45,15 @@ type Container struct { pid int } -func (c *Container) ClearRACommands() []command.Command { +func (c *Container) flushIPv6Commands() []command.Command { cmds := []command.Command{} for _, n := range c.Networks { if n.IPv6 != nil && !*n.IPv6 { cmds = append(cmds, command.NewShell(fmt.Sprintf("ip netns exec %s sysctl -w net.ipv6.conf.default.accept_ra=0", c.Name))) cmds = append(cmds, command.NewShell(fmt.Sprintf("ip netns exec %s sysctl -w net.ipv6.conf.all.accept_ra=0", c.Name))) - // TODO: iterate through invoices and set the accpet_ra parameter to zero for each + cmds = append(cmds, command.NewShell(fmt.Sprintf("ip netns exec %s ip -6 address flush scope global", c.Name))) + cmds = append(cmds, command.NewShell(fmt.Sprintf("ip netns exec %s ip -6 route flush proto ra", c.Name))) + // TODO: test, and, if necessary, iterate through interfaces and set the accpet_ra parameter to zero for each return cmds } } @@ -66,21 +68,24 @@ func (c *Container) CreateCommands() []command.Command { } func (c *Container) RecreateCommands() []command.Command { - cmds := c.DestroyCommands() - create := c.CreateCommands() - for _, c := range create { - cmds = append(cmds, c) + wasRunning := false + return []command.Command{ + command.NewFunc("stash_run_state", func() string { + wasRunning = c.isRunning() + runMsg := "not running" + if wasRunning { + runMsg = "running" + } + return "Container " + c.Name + " is " + runMsg + }), + command.NewSet(c.DestroyCommands()), + command.NewSet(c.CreateCommands()), + command.NewConditional("start_if_was_running", + func() bool { return wasRunning }, + command.NewSet(c.StartCommands()), + command.NewNop(), + ), } - return cmds -} - -func (c *Container) StopCommands() []command.Command { - cmds := []command.Command{} - if _, err := c.Pid(); err != nil { - cmds = append(cmds, command.NewShell("podman stop "+c.Name)) - } - cmds = append(cmds, command.NewShell("podman rm "+c.Name)) - return cmds } func (c *Container) DestroyCommands() []command.Command { @@ -90,14 +95,62 @@ func (c *Container) DestroyCommands() []command.Command { } func (c *Container) StartCommands() []command.Command { - _, err := c.Pid() - if err != nil { - if len(c.upCommands) == 0 { - c.initCommands() - } - return c.upCommands + if len(c.upCommands) == 0 { + c.initCommands() + } + return []command.Command{ + command.NewConditional("start_unless_running", + c.isRunning, + command.NewNop(), + command.NewSet(c.upCommands), + ), + } +} + +func (c *Container) RestartCommands() []command.Command { + return []command.Command{ + command.NewSet(c.StopCommands()), + command.NewShell("sleep 1"), + command.NewSet(c.StartCommands()), + } +} + +func (c *Container) isRunning() bool { + if _, err := c.Pid(); err != nil { + return false + } + return true +} + +func (c *Container) UpdateCommands() []command.Command { + wasRunning := false + return []command.Command{ + command.NewFunc("stash_run_state", func() string { + wasRunning = c.isRunning() + runMsg := "not running" + if wasRunning { + runMsg = "running" + } + return "Container " + c.Name + " is " + runMsg + }), + command.NewShell("podman pull " + c.Image), + command.NewSet(c.RecreateCommands()), + command.NewConditional("restart_if_was_running", + func() bool { return wasRunning }, + command.NewSet(c.StartCommands()), + command.NewNop(), + ), + } +} + +func (c *Container) StopCommands() []command.Command { + return []command.Command{ + command.NewConditional("stop_if_running", + c.isRunning, + command.NewShell("podman stop "+c.Name), + command.NewNop(), + ), } - return []command.Command{} } func (c *Container) Init(nets *[]network.Network) { @@ -136,7 +189,7 @@ func (c *Container) Pid() (pid int, err error) { func (c *Container) initCommands() { c.createCommands = []command.Command{ - command.NewShell("podman create --name %s%s%s%s%s%s"), + command.NewShell("podman create --name %s%s%s%s%s %s%s"), } hostname := "" if c.Hostname != "" { @@ -159,7 +212,7 @@ func (c *Container) initCommands() { entry = " " + c.Command } t, _ := c.createCommands[0].GetShell() - c.createCommands[0] = command.NewShell(fmt.Sprintf(t, c.Name, hostname, net, dns, args, entry)) + c.createCommands[0] = command.NewShell(fmt.Sprintf(t, c.Name, hostname, net, dns, args, c.Image, entry)) if len(c.Networks) > 1 { for i := 1; i < len(c.Networks); i++ { @@ -171,7 +224,7 @@ func (c *Container) initCommands() { c.upCommands = []command.Command{ command.NewShell("podman start " + c.Name), - command.NewFunc(func() string { + command.NewFunc("assure_netns", func() string { var err error pid, err := c.Pid() if err != nil { @@ -181,7 +234,7 @@ func (c *Container) initCommands() { command.NewShell(fmt.Sprintf("rm -f /var/run/netns/%s", c.Name)), command.NewShell(fmt.Sprintf("ln -s /proc/%d/ns/net /var/run/netns/%s", pid, c.Name)), }) - err = commands.Execute(os.Stderr) + err = commands.Execute(os.Stderr, false) if err != nil { return fmt.Sprintln("Error:", err) } @@ -190,7 +243,7 @@ func (c *Container) initCommands() { } if len(c.Networks) > 0 && !*c.Networks[0].IPv6 { c.upCommands = append(c.upCommands, command.NewShell("sleep 1")) - for _, k := range c.ClearRACommands() { + for _, k := range c.flushIPv6Commands() { c.upCommands = append(c.upCommands, k) } }