mirror of
https://gitea.elkins.co/Networking/ccl.git
synced 2024-12-04 05:27:51 -06:00
Convert project structure to cobra; many other changes. WIP.
- Factor out various components to different packages - Add abstract command package to allow for calling go funcs etc. at runtime.
This commit is contained in:
parent
598ed0d427
commit
61b7fe55a0
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
ccl
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2022 Joel D. Elkins <joel@elkins.co>
|
||||
|
||||
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.
|
237
cmd/ccl/ccl.go
237
cmd/ccl/ccl.go
@ -1,237 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/emirpasic/gods/sets/hashset"
|
||||
toml "github.com/pelletier/go-toml"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
CONFIG_FILE = "/etc/containerdefs.toml"
|
||||
)
|
||||
|
||||
type Network struct {
|
||||
Name string
|
||||
DNS []string
|
||||
IPv6 bool `default:"true"`
|
||||
IPv4Address string `toml:"ipv4_address"`
|
||||
IPv6Address string `toml:"ipv6_address"`
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
Category string
|
||||
Name string
|
||||
Image string
|
||||
Hostname string
|
||||
Command string
|
||||
Arguments string
|
||||
Networks []Network
|
||||
createCommands []string
|
||||
upCommands []string
|
||||
}
|
||||
|
||||
type command string
|
||||
|
||||
const (
|
||||
CREATE command = "create"
|
||||
START command = "start"
|
||||
)
|
||||
|
||||
var (
|
||||
networks *[]Network
|
||||
containers *[]Container
|
||||
categories *[]string
|
||||
commands = map[command]func(*Container){
|
||||
CREATE: PrintCreate,
|
||||
START: PrintStart,
|
||||
}
|
||||
)
|
||||
|
||||
// A parsing convenience
|
||||
type parse struct {
|
||||
Networks []Network
|
||||
Containers []Container
|
||||
}
|
||||
|
||||
func Categories() []string {
|
||||
if categories != nil {
|
||||
return *categories
|
||||
}
|
||||
categories = &[]string{"all"}
|
||||
gs := hashset.New()
|
||||
for _, c := range *containers {
|
||||
gs.Add(c.Category)
|
||||
}
|
||||
for _, c := range gs.Values() {
|
||||
*categories = append(*categories, c.(string))
|
||||
}
|
||||
return *categories
|
||||
}
|
||||
|
||||
func Union(ids []string) (conts []Container) {
|
||||
h := hashset.New()
|
||||
for _, id := range ids {
|
||||
for _, c := range *containers {
|
||||
if id == "all" || c.Name == id || c.Category == id {
|
||||
h.Add(c.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, c := range h.Values() {
|
||||
name := c.(string)
|
||||
match := slices.IndexFunc(*containers, func(c Container) bool { return c.Name == name })
|
||||
conts = append(conts, (*containers)[match])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Init(configFile string) {
|
||||
f, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p := parse{}
|
||||
err = toml.Unmarshal(f, &p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
containers, networks = &p.Containers, &p.Networks
|
||||
for i, c := range p.Containers {
|
||||
p.Containers[i].createCommands, p.Containers[i].upCommands = c.MakeCommands()
|
||||
}
|
||||
}
|
||||
|
||||
func netToArgs(c *Network) string {
|
||||
net := "%s%s%s"
|
||||
ipv4 := ""
|
||||
if c.IPv4Address != "" {
|
||||
ipv4 = fmt.Sprintf(" --ip %s", c.IPv4Address)
|
||||
}
|
||||
ipv6 := ""
|
||||
if c.IPv6Address != "" {
|
||||
ipv6 = fmt.Sprintf(" --ip6 %s", c.IPv6Address)
|
||||
}
|
||||
net = fmt.Sprintf(net, c.Name, ipv4, ipv6)
|
||||
return net
|
||||
}
|
||||
|
||||
func NetworkDefaults(name string) (net *Network) {
|
||||
for _, n := range *networks {
|
||||
if n.Name == name {
|
||||
net = &n
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ClearRACommands(c *Container) *[]string {
|
||||
cmds := []string{}
|
||||
for _, n := range c.Networks {
|
||||
nw := NetworkDefaults(n.Name)
|
||||
if !nw.IPv6 {
|
||||
cmds = append(cmds, fmt.Sprintf("ip netns exec %s sysctl -w net.ipv6.conf.default.accept_ra=0", c.Name))
|
||||
cmds = append(cmds, 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
|
||||
return &cmds
|
||||
}
|
||||
}
|
||||
return &cmds
|
||||
}
|
||||
|
||||
func (c *Container) MakeCommands() ([]string, []string) {
|
||||
cmd := []string{
|
||||
"podman create --name %s%s%s%s%s%s",
|
||||
}
|
||||
hostname := ""
|
||||
if c.Hostname != "" {
|
||||
hostname = fmt.Sprintf(" --hostname %s", c.Hostname)
|
||||
}
|
||||
net := ""
|
||||
dns := ""
|
||||
if len(c.Networks) > 0 {
|
||||
net = " --net " + netToArgs(&c.Networks[0])
|
||||
if len(c.Networks[0].DNS) > 0 {
|
||||
dns = " --dns " + strings.Join(c.Networks[0].DNS, ",")
|
||||
} else if len(NetworkDefaults(c.Networks[0].Name).DNS) > 0 {
|
||||
dns = " --dns " + strings.Join(NetworkDefaults(c.Networks[0].Name).DNS, ",")
|
||||
}
|
||||
}
|
||||
args := ""
|
||||
if c.Arguments != "" {
|
||||
args = " " + c.Arguments
|
||||
}
|
||||
entry := ""
|
||||
if c.Command != "" {
|
||||
entry = " " + c.Command
|
||||
}
|
||||
cmd[0] = fmt.Sprintf(cmd[0], c.Name, hostname, net, dns, args, entry)
|
||||
|
||||
if len(c.Networks) > 1 {
|
||||
for i := 1; i < len(c.Networks); i++ {
|
||||
n := c.Networks[i]
|
||||
s := fmt.Sprintf("podman network connect %s %s", c.Name, netToArgs(&n))
|
||||
cmd = append(cmd, s)
|
||||
}
|
||||
}
|
||||
|
||||
up := []string{}
|
||||
if len(c.Networks) > 0 {
|
||||
if !c.Networks[0].IPv6 || !NetworkDefaults(c.Networks[0].Name).IPv6 {
|
||||
for _, k := range *ClearRACommands(c) {
|
||||
up = append(up, k)
|
||||
}
|
||||
up = append(up, fmt.Sprintf("ip netns exec %s ip -6 address flush scope global", c.Name))
|
||||
up = append(up, fmt.Sprintf("ip netns exec %s ip -6 route flush proto ra", c.Name))
|
||||
}
|
||||
}
|
||||
|
||||
return cmd, up
|
||||
}
|
||||
|
||||
func main() {
|
||||
Init(CONFIG_FILE)
|
||||
containers := Union([]string{"all"})
|
||||
if len(os.Args) > 1 {
|
||||
containers = Union(os.Args[1:])
|
||||
}
|
||||
for _, c := range containers {
|
||||
commands[CREATE](&c)
|
||||
// commands[START](&c)
|
||||
}
|
||||
log.Printf("CATEGORIES: %v", Categories())
|
||||
}
|
||||
|
||||
func PrintCreate(ct *Container) {
|
||||
for _, c := range ct.createCommands {
|
||||
fmt.Println(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) Pid() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *Container) AssureNetNS() {
|
||||
// stat, _ := file.Stat("/var/run/netns")
|
||||
// if !stat.IsDir() {
|
||||
// os.MkdirAll("/var/run/netns", os.ModePerm)
|
||||
// }
|
||||
}
|
||||
|
||||
func PrintStart(ct *Container) {
|
||||
start_commands := []string{
|
||||
"podman start " + ct.Name,
|
||||
"sleep 1",
|
||||
}
|
||||
for _, c := range ct.upCommands {
|
||||
start_commands = append(start_commands, c)
|
||||
}
|
||||
for _, c := range start_commands {
|
||||
fmt.Println(c)
|
||||
}
|
||||
}
|
47
cmd/create.go
Normal file
47
cmd/create.go
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright © 2022 Joel D. Elkins <joel@elkins.co>
|
||||
|
||||
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 (
|
||||
"gitea.elkins.co/Networking/ccl/internal/pkg/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// createCmd represents the create command
|
||||
var createCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create containers",
|
||||
Args: cobra.OnlyValidArgs,
|
||||
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) {
|
||||
conts := config.Union(args)
|
||||
for _, c := range conts {
|
||||
config.PrintCreate(&c)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(createCmd)
|
||||
}
|
59
cmd/ls.go
Normal file
59
cmd/ls.go
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright © 2022 Joel D. Elkins <joel@elkins.co>
|
||||
|
||||
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"
|
||||
"text/tabwriter"
|
||||
|
||||
"gitea.elkins.co/Networking/ccl/internal/pkg/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// lsCmd represents the ls command
|
||||
var lsCmd = &cobra.Command{
|
||||
Use: "ls",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List configured containers",
|
||||
Args: cobra.OnlyValidArgs,
|
||||
ValidArgsFunction: validNouns,
|
||||
Long: `List configured containers. You can provide one or more names
|
||||
or categories to limit the list.`,
|
||||
Example: `ccl ls # same as below
|
||||
ccl ls all # same as above
|
||||
ccl ls default sub # multiple ok
|
||||
ccl ls squid`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
conts := config.Union(args)
|
||||
w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 4, ' ', 0)
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintf(w, "CATEGORY\tNAME\tIMAGE\n")
|
||||
for _, c := range conts {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", c.Category, c.Name, c.Image)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(lsCmd)
|
||||
}
|
38
cmd/nouns.go
Normal file
38
cmd/nouns.go
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright © 2022 Joel D. Elkins <joel@elkins.co>
|
||||
|
||||
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 (
|
||||
"gitea.elkins.co/Networking/ccl/internal/pkg/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func validNouns(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
validArgs := []string{}
|
||||
for _, c := range config.Union([]string{}) {
|
||||
validArgs = append(validArgs, c.Name)
|
||||
}
|
||||
for _, c := range config.Categories() {
|
||||
validArgs = append(validArgs, c)
|
||||
}
|
||||
return validArgs, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
67
cmd/root.go
Normal file
67
cmd/root.go
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
Copyright © 2022 Joel D. Elkins <joel@elkins.co>
|
||||
|
||||
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 (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"gitea.elkins.co/Networking/ccl/internal/pkg/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "ccl",
|
||||
Short: "Manage a set of pre-configured of podman containers",
|
||||
Long: `ccl is a utility to manage a set of podman containers. Use with various subcommands
|
||||
to define, start, stop, or update the container images. Configuration is read
|
||||
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
|
||||
)
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(config.Init)
|
||||
cobra.OnInitialize(func() {
|
||||
if verbose {
|
||||
output = os.Stderr
|
||||
} else {
|
||||
output = io.Discard
|
||||
}
|
||||
})
|
||||
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")
|
||||
}
|
45
cmd/start.go
Normal file
45
cmd/start.go
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright © 2022 Joel D. Elkins <joel@elkins.co>
|
||||
|
||||
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 (
|
||||
"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",
|
||||
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) {
|
||||
conts := config.Union(args)
|
||||
for _, c := range conts {
|
||||
config.PrintStart(&c)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(startCmd)
|
||||
}
|
6
go.mod
6
go.mod
@ -5,5 +5,11 @@ go 1.18
|
||||
require (
|
||||
github.com/emirpasic/gods v1.18.1
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/spf13/cobra v1.5.0
|
||||
golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
)
|
||||
|
10
go.sum
10
go.sum
@ -1,6 +1,16 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
|
||||
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75 h1:x03zeu7B2B11ySp+daztnwM5oBJ/8wGUSqrwcw9L0RA=
|
||||
golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
132
internal/pkg/command/command.go
Normal file
132
internal/pkg/command/command.go
Normal file
@ -0,0 +1,132 @@
|
||||
/*
|
||||
Copyright © 2022 Joel D. Elkins <joel@elkins.co>
|
||||
|
||||
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 command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type CommandType int
|
||||
|
||||
func (ct CommandType) String() string {
|
||||
switch ct {
|
||||
case CT_SH:
|
||||
return "SHELL"
|
||||
case CT_REF:
|
||||
return "FUNC"
|
||||
case CT_INDIRECT:
|
||||
return "INDIRECT"
|
||||
case CT_SET:
|
||||
return "SET"
|
||||
case CT_DEBUG:
|
||||
return "DEBUG"
|
||||
default:
|
||||
return "UNKOWN"
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
CT_SH CommandType = iota
|
||||
CT_REF
|
||||
CT_INDIRECT
|
||||
CT_SET
|
||||
CT_DEBUG
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
Type CommandType
|
||||
Command interface{}
|
||||
}
|
||||
|
||||
func NewShell(cmd string) Command {
|
||||
return Command{CT_SH, cmd}
|
||||
}
|
||||
|
||||
func NewFunc(f func() string) Command {
|
||||
return Command{CT_REF, f}
|
||||
}
|
||||
|
||||
func NewIndirect(c Command) Command {
|
||||
return Command{CT_INDIRECT, c}
|
||||
}
|
||||
|
||||
func NewSet(cs []Command) Command {
|
||||
return Command{CT_SET, cs}
|
||||
}
|
||||
|
||||
func NewDebug(msg string) Command {
|
||||
return Command{CT_DEBUG, msg}
|
||||
}
|
||||
|
||||
func (c Command) GetShell() (string, error) {
|
||||
s, ok := c.Command.(string)
|
||||
if ok {
|
||||
return s, nil
|
||||
}
|
||||
return s, fmt.Errorf("Problem extracting shell command: CommandType = %d", c.Type)
|
||||
}
|
||||
|
||||
func (c Command) CallRef() (string, error) {
|
||||
f, ok := c.Command.(func() string)
|
||||
if ok {
|
||||
s := f()
|
||||
return s, nil
|
||||
}
|
||||
return "", fmt.Errorf("Type error. Requested = %d, Type = %d", CT_REF, c.Type)
|
||||
}
|
||||
|
||||
func (c Command) Execute(output io.Writer) error {
|
||||
switch 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
|
||||
case CT_REF:
|
||||
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)
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
103
internal/pkg/config/config.go
Normal file
103
internal/pkg/config/config.go
Normal file
@ -0,0 +1,103 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.elkins.co/Networking/ccl/internal/pkg/container"
|
||||
"gitea.elkins.co/Networking/ccl/internal/pkg/network"
|
||||
"github.com/emirpasic/gods/sets/hashset"
|
||||
toml "github.com/pelletier/go-toml"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
CONFIG_FILE_DEFAULT = "/etc/containerdefs.toml"
|
||||
)
|
||||
|
||||
type command string
|
||||
|
||||
var (
|
||||
ConfigFile string = CONFIG_FILE_DEFAULT
|
||||
networks *[]network.Network
|
||||
containers *[]container.Container
|
||||
categories *[]string
|
||||
)
|
||||
|
||||
// A parsing convenience
|
||||
type parse struct {
|
||||
Networks []network.Network
|
||||
Containers []container.Container
|
||||
}
|
||||
|
||||
func Categories() []string {
|
||||
if categories != nil {
|
||||
return *categories
|
||||
}
|
||||
categories = &[]string{"all"}
|
||||
gs := hashset.New()
|
||||
for _, c := range *containers {
|
||||
gs.Add(c.Category)
|
||||
}
|
||||
for _, c := range gs.Values() {
|
||||
*categories = append(*categories, c.(string))
|
||||
}
|
||||
return *categories
|
||||
}
|
||||
|
||||
func Union(ids []string) (conts []container.Container) {
|
||||
if len(ids) == 0 {
|
||||
return *containers
|
||||
}
|
||||
h := hashset.New()
|
||||
for _, id := range ids {
|
||||
for _, c := range *containers {
|
||||
if id == "all" || c.Name == id || c.Category == id {
|
||||
h.Add(c.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, c := range h.Values() {
|
||||
name := c.(string)
|
||||
match := slices.IndexFunc(*containers, func(c container.Container) bool { return c.Name == name })
|
||||
conts = append(conts, (*containers)[match])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Init() {
|
||||
f, err := os.ReadFile(ConfigFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p := parse{}
|
||||
err = toml.Unmarshal(f, &p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
containers, networks = &p.Containers, &p.Networks
|
||||
for i := range p.Containers {
|
||||
p.Containers[i].Init(networks)
|
||||
}
|
||||
}
|
||||
|
||||
func NetworkDefaults(name string) (net *network.Network) {
|
||||
for _, n := range *networks {
|
||||
if n.Name == name {
|
||||
net = &n
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func PrintCreate(ct *container.Container) {
|
||||
for _, c := range ct.CreateCommands() {
|
||||
fmt.Println(c)
|
||||
}
|
||||
}
|
||||
|
||||
func PrintStart(ct *container.Container) {
|
||||
for _, c := range ct.StartCommands() {
|
||||
fmt.Println(c)
|
||||
}
|
||||
}
|
197
internal/pkg/container/container.go
Normal file
197
internal/pkg/container/container.go
Normal file
@ -0,0 +1,197 @@
|
||||
/*
|
||||
Copyright © 2022 Joel D. Elkins <joel@elkins.co>
|
||||
|
||||
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 container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.elkins.co/Networking/ccl/internal/pkg/command"
|
||||
"gitea.elkins.co/Networking/ccl/internal/pkg/network"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
Category string
|
||||
Name string
|
||||
Image string
|
||||
Hostname string
|
||||
Command string
|
||||
Arguments string
|
||||
Networks []network.Network
|
||||
createCommands []command.Command
|
||||
upCommands []command.Command
|
||||
pid int
|
||||
}
|
||||
|
||||
func (c *Container) ClearRACommands() []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
|
||||
return cmds
|
||||
}
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
func (c *Container) CreateCommands() []command.Command {
|
||||
if len(c.createCommands) == 0 {
|
||||
c.initCommands()
|
||||
}
|
||||
return c.createCommands
|
||||
}
|
||||
|
||||
func (c *Container) RecreateCommands() []command.Command {
|
||||
cmds := c.DestroyCommands()
|
||||
create := c.CreateCommands()
|
||||
for _, c := range create {
|
||||
cmds = append(cmds, c)
|
||||
}
|
||||
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 {
|
||||
cmds := c.StopCommands()
|
||||
cmds = append(cmds, command.NewShell("podman rm "+c.Name))
|
||||
return cmds
|
||||
}
|
||||
|
||||
func (c *Container) StartCommands() []command.Command {
|
||||
_, err := c.Pid()
|
||||
if err != nil {
|
||||
if len(c.upCommands) == 0 {
|
||||
c.initCommands()
|
||||
}
|
||||
return c.upCommands
|
||||
}
|
||||
return []command.Command{}
|
||||
}
|
||||
|
||||
func (c *Container) Init(nets *[]network.Network) {
|
||||
for i := range c.Networks {
|
||||
var n *network.Network
|
||||
for j := range *nets {
|
||||
if (*nets)[j].Name == c.Networks[i].Name {
|
||||
n = &(*nets)[j]
|
||||
}
|
||||
}
|
||||
if n == nil {
|
||||
continue
|
||||
}
|
||||
if len(c.Networks[i].DNS) == 0 {
|
||||
c.Networks[i].DNS = n.DNS
|
||||
}
|
||||
if c.Networks[i].IPv6 == nil {
|
||||
if n.IPv6 != nil {
|
||||
c.Networks[i].IPv6 = n.IPv6
|
||||
} else {
|
||||
yes := true
|
||||
c.Networks[i].IPv6 = &yes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) Pid() (pid int, err error) {
|
||||
pid_s, err := exec.Command("podman", "inspect", "-f", "{{.State.Pid}}").CombinedOutput()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.pid, err = strconv.Atoi(string(pid_s))
|
||||
return c.pid, err
|
||||
}
|
||||
|
||||
func (c *Container) initCommands() {
|
||||
c.createCommands = []command.Command{
|
||||
command.NewShell("podman create --name %s%s%s%s%s%s"),
|
||||
}
|
||||
hostname := ""
|
||||
if c.Hostname != "" {
|
||||
hostname = fmt.Sprintf(" --hostname %s", c.Hostname)
|
||||
}
|
||||
net := ""
|
||||
dns := ""
|
||||
if len(c.Networks) > 0 {
|
||||
net = " --net " + c.Networks[0].ToArgs()
|
||||
if len(c.Networks[0].DNS) > 0 {
|
||||
dns = " --dns " + strings.Join(c.Networks[0].DNS, ",")
|
||||
}
|
||||
}
|
||||
args := ""
|
||||
if c.Arguments != "" {
|
||||
args = " " + c.Arguments
|
||||
}
|
||||
entry := ""
|
||||
if c.Command != "" {
|
||||
entry = " " + c.Command
|
||||
}
|
||||
t, _ := c.createCommands[0].GetShell()
|
||||
c.createCommands[0] = command.NewShell(fmt.Sprintf(t, c.Name, hostname, net, dns, args, entry))
|
||||
|
||||
if len(c.Networks) > 1 {
|
||||
for i := 1; i < len(c.Networks); i++ {
|
||||
n := c.Networks[i]
|
||||
s := fmt.Sprintf("podman network connect %s %s", c.Name, n.ToArgs())
|
||||
c.createCommands = append(c.createCommands, command.NewShell(s))
|
||||
}
|
||||
}
|
||||
|
||||
c.upCommands = []command.Command{
|
||||
command.NewShell("podman start " + c.Name),
|
||||
command.NewFunc(func() string {
|
||||
var err error
|
||||
pid, err := c.Pid()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%s is not running\n", c.Name)
|
||||
}
|
||||
commands := command.NewSet([]command.Command{
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Sprintln("Error:", err)
|
||||
}
|
||||
return fmt.Sprintln("Pid =", pid)
|
||||
}),
|
||||
}
|
||||
if len(c.Networks) > 0 && !*c.Networks[0].IPv6 {
|
||||
c.upCommands = append(c.upCommands, command.NewShell("sleep 1"))
|
||||
for _, k := range c.ClearRACommands() {
|
||||
c.upCommands = append(c.upCommands, k)
|
||||
}
|
||||
}
|
||||
}
|
46
internal/pkg/network/network.go
Normal file
46
internal/pkg/network/network.go
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright © 2022 Joel D. Elkins <joel@elkins.co>
|
||||
|
||||
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 network
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Network struct {
|
||||
Name string
|
||||
DNS []string
|
||||
IPv6 *bool `toml:"ipv6"`
|
||||
IPv4Address string `toml:"ipv4_address"`
|
||||
IPv6Address string `toml:"ipv6_address"`
|
||||
}
|
||||
|
||||
func (n *Network) ToArgs() string {
|
||||
net := "%s%s%s"
|
||||
ipv4 := ""
|
||||
if n.IPv4Address != "" {
|
||||
ipv4 = fmt.Sprintf(" --ip %s", n.IPv4Address)
|
||||
}
|
||||
ipv6 := ""
|
||||
if n.IPv6Address != "" {
|
||||
ipv6 = fmt.Sprintf(" --ip6 %s", n.IPv6Address)
|
||||
}
|
||||
net = fmt.Sprintf(net, n.Name, ipv4, ipv6)
|
||||
return net
|
||||
}
|
28
main.go
Normal file
28
main.go
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright © 2022 Joel D. Elkins <joel@elkins.co>
|
||||
|
||||
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 main
|
||||
|
||||
import "gitea.elkins.co/Networking/ccl/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
Loading…
Reference in New Issue
Block a user