diff --git a/cmd/subcmds/optimize.go b/cmd/subcmds/optimize.go index 877662a9..353a86eb 100644 --- a/cmd/subcmds/optimize.go +++ b/cmd/subcmds/optimize.go @@ -5,7 +5,14 @@ SPDX-License-Identifier: Apache-2.0 package subcmds -import "github.com/spf13/cobra" +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/np-guard/vpc-network-config-synthesis/pkg/ir" + "github.com/np-guard/vpc-network-config-synthesis/pkg/optimize" +) func NewOptimizeCommand(args *inArgs) *cobra.Command { cmd := &cobra.Command{ @@ -14,7 +21,22 @@ func NewOptimizeCommand(args *inArgs) *cobra.Command { Long: `optimization of existing SG (nACLS are not supported yet)`, } + // sub cmds cmd.AddCommand(NewOptimizeSGCommand(args)) return cmd } + +func optimization(cmd *cobra.Command, args *inArgs, newOptimizer func(ir.Collection, string) optimize.Optimizer, isSG bool) error { + cmd.SilenceUsage = true // if we got this far, flags are syntactically correct, so no need to print usage + collection, err := parseCollection(args, isSG) + if err != nil { + return fmt.Errorf("could not parse config file %v: %w", args.configFile, err) + } + optimizer := newOptimizer(collection, args.firewallName) + optimizedCollection, err := optimizer.Optimize() + if err != nil { + return err + } + return writeOutput(args, optimizedCollection, collection.VpcNames(), false) +} diff --git a/cmd/subcmds/optimizeOutput.go b/cmd/subcmds/optimizeOutput.go deleted file mode 100644 index af92e337..00000000 --- a/cmd/subcmds/optimizeOutput.go +++ /dev/null @@ -1,14 +0,0 @@ -/* -Copyright 2023- IBM Inc. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 -*/ - -package subcmds - -import ( - "github.com/np-guard/vpc-network-config-synthesis/pkg/ir" -) - -func writeOptimizeOutput(_ *inArgs, _ ir.Collection) error { - return nil -} diff --git a/cmd/subcmds/optimizeSG.go b/cmd/subcmds/optimizeSG.go index 57ee6c32..7b5fe908 100644 --- a/cmd/subcmds/optimizeSG.go +++ b/cmd/subcmds/optimizeSG.go @@ -6,14 +6,13 @@ SPDX-License-Identifier: Apache-2.0 package subcmds import ( - "fmt" - "github.com/spf13/cobra" - "github.com/np-guard/vpc-network-config-synthesis/pkg/io/confio" "github.com/np-guard/vpc-network-config-synthesis/pkg/optimize" ) +const sgNameFlag = "sg-name" + func NewOptimizeSGCommand(args *inArgs) *cobra.Command { cmd := &cobra.Command{ Use: "sg", @@ -21,24 +20,12 @@ func NewOptimizeSGCommand(args *inArgs) *cobra.Command { Long: `OptimizeSG attempts to reduce the number of security group rules in a SG without changing the semantic.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { - return optimization(cmd, args) + return optimization(cmd, args, optimize.NewSGOptimizer, true) }, } - cmd.Flags().StringVarP(&args.sgName, sgNameFlag, "s", "", "which SG to optimize") - _ = cmd.MarkFlagRequired(sgNameFlag) // Todo: delete this line. if sgName flag is not supplied - optimize all SGs + // flags + cmd.PersistentFlags().StringVarP(&args.firewallName, sgNameFlag, "n", "", "which security group to optimize") return cmd } - -func optimization(cmd *cobra.Command, args *inArgs) error { - cmd.SilenceUsage = true // if we got this far, flags are syntactically correct, so no need to print usage - sgs, err := confio.ReadSGs(args.configFile) - if err != nil { - return fmt.Errorf("could not parse config file %v: %w", args.configFile, err) - } - if optimize.ReduceSGRules(sgs, args.sgName) != nil { - return err - } - return writeOptimizeOutput(args, sgs) -} diff --git a/cmd/subcmds/output.go b/cmd/subcmds/output.go index aa817b7e..b3f71013 100644 --- a/cmd/subcmds/output.go +++ b/cmd/subcmds/output.go @@ -16,33 +16,27 @@ import ( "github.com/np-guard/vpc-network-config-synthesis/pkg/io/csvio" "github.com/np-guard/vpc-network-config-synthesis/pkg/io/mdio" "github.com/np-guard/vpc-network-config-synthesis/pkg/io/tfio" - "github.com/np-guard/vpc-network-config-synthesis/pkg/ir" ) const defaultFilePermission = 0o644 const defaultDirectoryPermission = 0o755 -func writeOutput(args *inArgs, collection ir.Collection, vpcNames []ir.ID) error { - if err := updateOutputFormat(args); err != nil { - return err - } - if args.outputDir != "" && args.outputFmt == apiOutputFormat { - return fmt.Errorf("-d cannot be used with format json") - } +func writeOutput(args *inArgs, collection ir.Collection, vpcNames []string, isSynth bool) error { if args.outputDir != "" { // create the directory if needed if err := os.MkdirAll(args.outputDir, defaultDirectoryPermission); err != nil { return err } } - if err := writeLocals(args, collection, vpcNames); err != nil { + + if err := writeLocals(args, vpcNames, collection); err != nil { return err } var data *bytes.Buffer var err error if args.outputDir == "" { - if data, err = writeCollection(args, collection, ""); err != nil { + if data, err = writeCollection(args, collection, "", isSynth); err != nil { return err } return writeToFile(args.outputFile, data) @@ -55,7 +49,7 @@ func writeOutput(args *inArgs, collection ir.Collection, vpcNames []ir.ID) error if args.prefix != "" { args.outputFile = args.outputDir + "/" + args.prefix + "_" + suffix } - if data, err = writeCollection(args, collection, vpc); err != nil { + if data, err = writeCollection(args, collection, vpc, isSynth); err != nil { return err } if err := writeToFile(args.outputFile, data); err != nil { @@ -65,9 +59,9 @@ func writeOutput(args *inArgs, collection ir.Collection, vpcNames []ir.ID) error return nil } -func writeCollection(args *inArgs, collection ir.Collection, vpc string) (*bytes.Buffer, error) { +func writeCollection(args *inArgs, collection ir.Collection, vpc string, isSynth bool) (*bytes.Buffer, error) { var data bytes.Buffer - writer, err := pickWriter(args, &data) + writer, err := pickWriter(args, &data, isSynth) if err != nil { return nil, err } @@ -77,15 +71,7 @@ func writeCollection(args *inArgs, collection ir.Collection, vpc string) (*bytes return &data, nil } -func writeToFile(outputFile string, data *bytes.Buffer) error { - if outputFile == "" { - fmt.Println(data.String()) - return nil - } - return os.WriteFile(outputFile, data.Bytes(), defaultFilePermission) -} - -func pickWriter(args *inArgs, data *bytes.Buffer) (ir.Writer, error) { +func pickWriter(args *inArgs, data *bytes.Buffer, isSynth bool) (ir.Writer, error) { w := bufio.NewWriter(data) switch args.outputFmt { case tfOutputFormat: @@ -94,24 +80,31 @@ func pickWriter(args *inArgs, data *bytes.Buffer) (ir.Writer, error) { return csvio.NewWriter(w), nil case mdOutputFormat: return mdio.NewWriter(w), nil - case apiOutputFormat: - return confio.NewWriter(w, args.configFile) - default: - return nil, fmt.Errorf("bad output format: %q", args.outputFmt) + case jsonOutputFormat: + if isSynth { + return confio.NewWriter(w, args.configFile) + } } + return nil, fmt.Errorf("bad output format: %q", args.outputFmt) } -func writeLocals(args *inArgs, collection ir.Collection, vpcNames []ir.ID) error { - if !args.locals { +func writeToFile(outputFile string, data *bytes.Buffer) error { + if outputFile == "" { + fmt.Println(data.String()) return nil } - if args.outputFmt != tfOutputFormat { - return fmt.Errorf("--locals flag requires setting the output format to tf") - } + return os.WriteFile(outputFile, data.Bytes(), defaultFilePermission) +} - _, isACLCollection := collection.(*ir.ACLCollection) +func writeLocals(args *inArgs, vpcNames []ir.ID, collection ir.Collection) error { + if !args.locals { + return nil + } var data *bytes.Buffer var err error + + _, isACLCollection := collection.(*ir.ACLCollection) + if data, err = tfio.WriteLocals(vpcNames, isACLCollection); err != nil { return err } diff --git a/cmd/subcmds/outputFormat.go b/cmd/subcmds/outputFormat.go index 51a36c60..f435c43a 100644 --- a/cmd/subcmds/outputFormat.go +++ b/cmd/subcmds/outputFormat.go @@ -14,11 +14,11 @@ const ( tfOutputFormat = "tf" csvOutputFormat = "csv" mdOutputFormat = "md" - apiOutputFormat = "json" + jsonOutputFormat = "json" defaultOutputFormat = csvOutputFormat ) -var outputFormats = []string{tfOutputFormat, csvOutputFormat, mdOutputFormat, apiOutputFormat} +var outputFormats = []string{tfOutputFormat, csvOutputFormat, mdOutputFormat, jsonOutputFormat} func updateOutputFormat(args *inArgs) error { var err error @@ -40,7 +40,7 @@ func inferFormatUsingFilename(filename string) (string, error) { case strings.HasSuffix(filename, ".md"): return mdOutputFormat, nil case strings.HasSuffix(filename, ".json"): - return apiOutputFormat, nil + return jsonOutputFormat, nil default: return "", fmt.Errorf("bad output format") } diff --git a/cmd/subcmds/root.go b/cmd/subcmds/root.go index 1f47e591..98ff7973 100644 --- a/cmd/subcmds/root.go +++ b/cmd/subcmds/root.go @@ -14,56 +14,62 @@ import ( const ( configFlag = "config" - specFlag = "spec" outputFmtFlag = "format" outputFileFlag = "output-file" outputDirFlag = "output-dir" prefixFlag = "prefix" - sgNameFlag = "sg-name" singleACLFlag = "single" localsFlag = "locals" ) type inArgs struct { - configFile string - specFile string - outputFmt string - outputFile string - outputDir string - prefix string - sgName string - singleacl bool - locals bool + configFile string + specFile string + outputFmt string + outputFile string + outputDir string + prefix string + firewallName string + singleacl bool + locals bool } func NewRootCommand() *cobra.Command { args := &inArgs{} + // allow PersistentPreRunE + cobra.EnableTraverseRunHooks = true + rootCmd := &cobra.Command{ Use: "vpcgen", - Short: "Tool for automatic synthesis of VPC network configurations", - Long: `Tool for automatic synthesis of VPC network configurations, namely Network ACLs and Security Groups.`, + Short: "A tool for synthesizing and optimizing VPC network configurations", + Long: `A tool for synthesizing and optimizing VPC network configurations, namely Network ACLs and Security Groups.`, + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + return validateFlags(args) + }, } + // flags rootCmd.PersistentFlags().StringVarP(&args.configFile, configFlag, "c", "", "JSON file containing a configuration object of existing resources") rootCmd.PersistentFlags().StringVarP(&args.outputFmt, outputFmtFlag, "f", "", "Output format; "+mustBeOneOf(outputFormats)) rootCmd.PersistentFlags().StringVarP(&args.outputFile, outputFileFlag, "o", "", "Write all generated resources to the specified file.") - rootCmd.PersistentFlags().StringVarP(&args.outputDir, outputDirFlag, "d", "", - "Write generated resources to files in the specified directory, one file per VPC.") - rootCmd.PersistentFlags().StringVarP(&args.prefix, prefixFlag, "p", "", "The prefix of the files that will be created.") rootCmd.PersistentFlags().BoolVarP(&args.locals, localsFlag, "l", false, "whether to generate a locals.tf file (only possible when the output format is tf)") - rootCmd.PersistentFlags().SortFlags = false + // flags set for all commands + rootCmd.PersistentFlags().SortFlags = false _ = rootCmd.MarkPersistentFlagRequired(configFlag) - rootCmd.MarkFlagsMutuallyExclusive(outputFileFlag, outputDirFlag) + // sub cmds rootCmd.AddCommand(NewSynthCommand(args)) rootCmd.AddCommand(NewOptimizeCommand(args)) - rootCmd.CompletionOptions.HiddenDefaultCmd = true - rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) // disable help command. should use --help flag instead + // prevent Cobra from creating a default 'completion' command + rootCmd.CompletionOptions.DisableDefaultCmd = true + + // disable help command. should use --help flag instead + rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) return rootCmd } diff --git a/cmd/subcmds/synth.go b/cmd/subcmds/synth.go index ef0f81d4..83eb348a 100644 --- a/cmd/subcmds/synth.go +++ b/cmd/subcmds/synth.go @@ -13,6 +13,8 @@ import ( "github.com/np-guard/vpc-network-config-synthesis/pkg/utils" ) +const specFlag = "spec" + func NewSynthCommand(args *inArgs) *cobra.Command { cmd := &cobra.Command{ Use: "synth", @@ -21,9 +23,16 @@ func NewSynthCommand(args *inArgs) *cobra.Command { --config and --spec parameters must be supplied.`, } + // flags cmd.PersistentFlags().StringVarP(&args.specFile, specFlag, "s", "", "JSON file containing spec file") + cmd.PersistentFlags().StringVarP(&args.outputDir, outputDirFlag, "d", "", + "Write generated resources to files in the specified directory, one file per VPC.") + cmd.PersistentFlags().StringVarP(&args.prefix, prefixFlag, "p", "", "The prefix of the files that will be created.") + + // flags settings _ = cmd.MarkPersistentFlagRequired(specFlag) + // sub cmds cmd.AddCommand(NewSynthACLCommand(args)) cmd.AddCommand(NewSynthSGCommand(args)) @@ -41,5 +50,5 @@ func synthesis(cmd *cobra.Command, args *inArgs, newSynthesizer func(*ir.Spec, b if err != nil { return err } - return writeOutput(args, collection, utils.MapKeys(spec.Defs.ConfigDefs.VPCs)) + return writeOutput(args, collection, utils.MapKeys(spec.Defs.ConfigDefs.VPCs), true) } diff --git a/cmd/subcmds/unmarshal.go b/cmd/subcmds/unmarshal.go index 5fa7c720..fcb1f76e 100644 --- a/cmd/subcmds/unmarshal.go +++ b/cmd/subcmds/unmarshal.go @@ -26,3 +26,10 @@ func unmarshal(args *inArgs) (*ir.Spec, error) { return model, nil } + +func parseCollection(args *inArgs, isSG bool) (ir.Collection, error) { + if isSG { + return confio.ReadSGs(args.configFile) + } + return confio.ReadACLs(args.configFile) +} diff --git a/cmd/subcmds/validateFlags.go b/cmd/subcmds/validateFlags.go new file mode 100644 index 00000000..be56f6fd --- /dev/null +++ b/cmd/subcmds/validateFlags.go @@ -0,0 +1,24 @@ +/* +Copyright 2023- IBM Inc. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package subcmds + +import "fmt" + +func validateFlags(args *inArgs) error { + if args.outputDir != "" && args.outputFile != "" { + return fmt.Errorf("specifying both -d and -o is not allowed") + } + if err := updateOutputFormat(args); err != nil { + return err + } + if args.outputDir != "" && args.outputFmt == jsonOutputFormat { + return fmt.Errorf("-d cannot be used with format json") + } + if args.locals && args.outputFmt != tfOutputFormat { + return fmt.Errorf("--locals flag requires setting the output format to tf") + } + return nil +} diff --git a/pkg/io/confio/parse_acls.go b/pkg/io/confio/parse_acls.go new file mode 100644 index 00000000..13481978 --- /dev/null +++ b/pkg/io/confio/parse_acls.go @@ -0,0 +1,12 @@ +/* +Copyright 2023- IBM Inc. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package confio + +import "github.com/np-guard/vpc-network-config-synthesis/pkg/ir" + +func ReadACLs(_ string) (*ir.ACLCollection, error) { + return nil, nil +} diff --git a/pkg/io/csvio/sg.go b/pkg/io/csvio/sg.go index ed91a1ef..35c74f8b 100644 --- a/pkg/io/csvio/sg.go +++ b/pkg/io/csvio/sg.go @@ -20,14 +20,18 @@ func (w *Writer) WriteSG(collection *ir.SGCollection, vpc string) error { if err := w.w.WriteAll(sgHeader()); err != nil { return err } - for _, sgName := range collection.SortedSGNames(vpc) { - vpcName := ir.VpcFromScopedResource(string(sgName)) - sgTable, err := makeSGTable(collection.SGs[vpcName][sgName], sgName) - if err != nil { - return err + for _, vpcName := range collection.VpcNames() { + if vpc != vpcName && vpc != "" { + continue } - if err := w.w.WriteAll(sgTable); err != nil { - return err + for _, sgName := range collection.SortedSGNames(vpcName) { + sgTable, err := makeSGTable(collection.SGs[vpcName][sgName], sgName) + if err != nil { + return err + } + if err := w.w.WriteAll(sgTable); err != nil { + return err + } } } return nil diff --git a/pkg/io/mdio/sg.go b/pkg/io/mdio/sg.go index 3277b3fa..0fcbf734 100644 --- a/pkg/io/mdio/sg.go +++ b/pkg/io/mdio/sg.go @@ -20,14 +20,18 @@ func (w *Writer) WriteSG(collection *ir.SGCollection, vpc string) error { if err := w.writeAll(sgHeader()); err != nil { return err } - for _, sgName := range collection.SortedSGNames(vpc) { - vpcName := ir.VpcFromScopedResource(string(sgName)) - sgTable, err := makeSGTable(collection.SGs[vpcName][sgName], sgName) - if err != nil { - return err + for _, vpcName := range collection.VpcNames() { + if vpc != vpcName && vpc != "" { + continue } - if err := w.writeAll(sgTable); err != nil { - return err + for _, sgName := range collection.SortedSGNames(vpcName) { + sgTable, err := makeSGTable(collection.SGs[vpcName][sgName], sgName) + if err != nil { + return err + } + if err := w.writeAll(sgTable); err != nil { + return err + } } } return nil diff --git a/pkg/io/tfio/acl.go b/pkg/io/tfio/acl.go index b0dd23e8..7f641d3b 100644 --- a/pkg/io/tfio/acl.go +++ b/pkg/io/tfio/acl.go @@ -64,8 +64,14 @@ func aclRule(rule *ir.ACLRule, name string) (tf.Block, error) { {Name: "source", Value: quote(rule.Source.String())}, {Name: "destination", Value: quote(rule.Destination.String())}, } + + comment := "" + if rule.Explanation != "" { + comment = fmt.Sprintf("# %v", rule.Explanation) + } + return tf.Block{Name: "rules", - Comment: fmt.Sprintf("# %v", rule.Explanation), + Comment: comment, Arguments: arguments, Blocks: aclProtocol(rule.Protocol), }, nil diff --git a/pkg/io/tfio/sg.go b/pkg/io/tfio/sg.go index 747be97d..793c5c7a 100644 --- a/pkg/io/tfio/sg.go +++ b/pkg/io/tfio/sg.go @@ -72,10 +72,15 @@ func sgRule(rule *ir.SGRule, sgName ir.SGName, i int) (tf.Block, error) { return tf.Block{}, err } + comment := "" + if rule.Explanation != "" { + comment = fmt.Sprintf("# %v", rule.Explanation) + } + return tf.Block{ Name: "resource", Labels: []string{quote("ibm_is_security_group_rule"), ir.ChangeScoping(quote(ruleName))}, - Comment: fmt.Sprintf("# %v", rule.Explanation), + Comment: comment, Arguments: []tf.Argument{ {Name: "group", Value: group}, {Name: "direction", Value: quote(direction(rule.Direction))}, @@ -85,42 +90,48 @@ func sgRule(rule *ir.SGRule, sgName ir.SGName, i int) (tf.Block, error) { }, nil } -func sg(sgName, comment string) (tf.Block, error) { - vpcName := ir.VpcFromScopedResource(sgName) - sgName = ir.ChangeScoping(sgName) - if err := verifyName(sgName); err != nil { +func sg(sgName, vpcName string) (tf.Block, error) { + tfSGName := ir.ChangeScoping(sgName) + comment := fmt.Sprintf("\n### SG attached to %s", sgName) + if sgName == tfSGName { // its optimization + comment = "" + } + if err := verifyName(tfSGName); err != nil { return tf.Block{}, err } return tf.Block{ Name: "resource", //nolint:revive // obvious false positive - Labels: []string{quote("ibm_is_security_group"), ir.ChangeScoping(quote(sgName))}, + Labels: []string{quote("ibm_is_security_group"), quote(tfSGName)}, Comment: comment, Arguments: []tf.Argument{ - {Name: "name", Value: quote("sg-" + sgName)}, + {Name: "name", Value: quote("sg-" + tfSGName)}, {Name: "resource_group", Value: "local.sg_synth_resource_group_id"}, {Name: "vpc", Value: fmt.Sprintf("local.sg_synth_%s_id", vpcName)}, }, }, nil } -func sgCollection(t *ir.SGCollection, vpc string) (*tf.ConfigFile, error) { - var resources []tf.Block //nolint:prealloc // nontrivial to calculate, and an unlikely performance bottleneck - for _, sgName := range t.SortedSGNames(vpc) { - comment := "" - vpcName := ir.VpcFromScopedResource(string(sgName)) - rules := t.SGs[vpcName][sgName].AllRules() - comment = fmt.Sprintf("\n### SG attached to %v", sgName) - sg, err := sg(sgName.String(), comment) - if err != nil { - return nil, err +func sgCollection(collection *ir.SGCollection, vpc string) (*tf.ConfigFile, error) { + var resources []tf.Block + + for _, vpcName := range collection.VpcNames() { + if vpc != vpcName && vpc != "" { + continue } - resources = append(resources, sg) - for i, rule := range rules { - rule, err := sgRule(rule, sgName, i) + for _, sgName := range collection.SortedSGNames(vpcName) { + rules := collection.SGs[vpcName][sgName].AllRules() + sg, err := sg(sgName.String(), vpcName) if err != nil { return nil, err } - resources = append(resources, rule) + resources = append(resources, sg) + for i, rule := range rules { + rule, err := sgRule(rule, sgName, i) + if err != nil { + return nil, err + } + resources = append(resources, rule) + } } } return &tf.ConfigFile{ diff --git a/pkg/ir/acl.go b/pkg/ir/acl.go index b1de36c3..e13b91bb 100644 --- a/pkg/ir/acl.go +++ b/pkg/ir/acl.go @@ -15,36 +15,38 @@ import ( "github.com/np-guard/vpc-network-config-synthesis/pkg/utils" ) -type Action string +type ( + Action string + + ACLRule struct { + Action Action + Direction Direction + Source *netset.IPBlock + Destination *netset.IPBlock + Protocol netp.Protocol + Explanation string + } + + ACL struct { + Subnet string + Internal []*ACLRule + External []*ACLRule + } + + ACLCollection struct { + ACLs map[ID]map[string]*ACL + } + + ACLWriter interface { + WriteACL(aclColl *ACLCollection, vpc string) error + } +) const ( Allow Action = "allow" Deny Action = "deny" ) -type ACLRule struct { - Action Action - Direction Direction - Source *netset.IPBlock - Destination *netset.IPBlock - Protocol netp.Protocol - Explanation string -} - -type ACL struct { - Subnet string - Internal []*ACLRule - External []*ACLRule -} - -type ACLCollection struct { - ACLs map[ID]map[string]*ACL -} - -type ACLWriter interface { - WriteACL(aclColl *ACLCollection, vpc string) error -} - func (r *ACLRule) isRedundant(rules []*ACLRule) bool { for _, rule := range rules { if rule.mustSupersede(r) { @@ -105,24 +107,27 @@ func NewACLCollection() *ACLCollection { return &ACLCollection{ACLs: map[ID]map[string]*ACL{}} } -func NewACL() *ACL { - return &ACL{Internal: []*ACLRule{}, External: []*ACLRule{}} +func NewACL(subnet string) *ACL { + return &ACL{Subnet: subnet, Internal: []*ACLRule{}, External: []*ACLRule{}} } -func (c *ACLCollection) LookupOrCreate(name string) *ACL { - vpcName := VpcFromScopedResource(name) - if acl, ok := c.ACLs[vpcName][name]; ok { +func (c *ACLCollection) LookupOrCreate(subnet string) *ACL { + vpcName := VpcFromScopedResource(subnet) + if acl, ok := c.ACLs[vpcName][subnet]; ok { return acl } - newACL := NewACL() - newACL.Subnet = name + newACL := NewACL(subnet) if c.ACLs[vpcName] == nil { c.ACLs[vpcName] = make(map[string]*ACL) } - c.ACLs[vpcName][name] = newACL + c.ACLs[vpcName][subnet] = newACL return newACL } +func (c *ACLCollection) VpcNames() []string { + return utils.SortedMapKeys(c.ACLs) +} + func (c *ACLCollection) Write(w Writer, vpc string) error { return w.WriteACL(c, vpc) } diff --git a/pkg/ir/common.go b/pkg/ir/common.go index e10b1265..a89aeae8 100644 --- a/pkg/ir/common.go +++ b/pkg/ir/common.go @@ -5,18 +5,21 @@ SPDX-License-Identifier: Apache-2.0 package ir -type Direction string +type ( + Direction string + + Collection interface { + Write(writer Writer, vpc string) error + VpcNames() []string + } + + Writer interface { + ACLWriter + SGWriter + } +) const ( Outbound Direction = "outbound" Inbound Direction = "inbound" ) - -type Writer interface { - ACLWriter - SGWriter -} - -type Collection interface { - Write(writer Writer, vpc string) error -} diff --git a/pkg/ir/sg.go b/pkg/ir/sg.go index e90538d5..2317a4ec 100644 --- a/pkg/ir/sg.go +++ b/pkg/ir/sg.go @@ -26,37 +26,40 @@ const ( SGResourceFileShareMountTarget SGResource = "fsmt" ) -type SGName string +type ( + SGName string -func (s SGName) String() string { - return string(s) -} + RemoteType interface { + fmt.Stringer + // *netset.IPBlock | SGName + } -type RemoteType interface { - fmt.Stringer - // *netset.IPBlock | SGName -} + SGRule struct { + Direction Direction + Remote RemoteType + Protocol netp.Protocol + Local *netset.IPBlock + Explanation string + } -type SGRule struct { - Direction Direction - Remote RemoteType - Protocol netp.Protocol - Local *netset.IPBlock - Explanation string -} + SG struct { + SGName SGName + InboundRules []*SGRule + OutboundRules []*SGRule + Attached []ID + } -type SG struct { - InboundRules []*SGRule - OutboundRules []*SGRule - Attached []ID -} + SGCollection struct { + SGs map[ID]map[SGName]*SG + } -type SGCollection struct { - SGs map[ID]map[SGName]*SG -} + SGWriter interface { + WriteSG(sgColl *SGCollection, vpc string) error + } +) -type SGWriter interface { - WriteSG(sgColl *SGCollection, vpc string) error +func (s SGName) String() string { + return string(s) } func (r *SGRule) isRedundant(rules []*SGRule) bool { @@ -76,8 +79,8 @@ func (r *SGRule) mustSupersede(other *SGRule) bool { return res } -func NewSG() *SG { - return &SG{InboundRules: []*SGRule{}, OutboundRules: []*SGRule{}, Attached: []ID{}} +func NewSG(sgName SGName) *SG { + return &SG{SGName: sgName, InboundRules: []*SGRule{}, OutboundRules: []*SGRule{}, Attached: []ID{}} } func NewSGCollection() *SGCollection { @@ -89,7 +92,7 @@ func (c *SGCollection) LookupOrCreate(name SGName) *SG { if sg, ok := c.SGs[vpcName][name]; ok { return sg } - newSG := NewSG() + newSG := NewSG(name) if c.SGs[vpcName] == nil { c.SGs[vpcName] = make(map[SGName]*SG) } @@ -110,6 +113,10 @@ func (a *SG) AllRules() []*SGRule { return append(a.InboundRules, a.OutboundRules...) } +func (c *SGCollection) VpcNames() []string { + return utils.SortedMapKeys(c.SGs) +} + func (c *SGCollection) Write(w Writer, vpc string) error { return w.WriteSG(c, vpc) } diff --git a/pkg/optimize/common.go b/pkg/optimize/common.go new file mode 100644 index 00000000..010c2239 --- /dev/null +++ b/pkg/optimize/common.go @@ -0,0 +1,13 @@ +/* +Copyright 2023- IBM Inc. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package optimize + +import "github.com/np-guard/vpc-network-config-synthesis/pkg/ir" + +type Optimizer interface { + // attempts to reduce number of SG/nACL rules + Optimize() (ir.Collection, error) +} diff --git a/pkg/optimize/sg.go b/pkg/optimize/sg.go index f3301962..3288d2a7 100644 --- a/pkg/optimize/sg.go +++ b/pkg/optimize/sg.go @@ -9,6 +9,20 @@ import ( "github.com/np-guard/vpc-network-config-synthesis/pkg/ir" ) -func ReduceSGRules(_ *ir.SGCollection, _ string) error { - return nil +type SGOptimizer struct { + sgCollection *ir.SGCollection + sgName ir.SGName + sgVPC *string +} + +func NewSGOptimizer(collection ir.Collection, sgName string) Optimizer { + components := ir.ScopingComponents(sgName) + if len(components) == 1 { + return &SGOptimizer{sgCollection: collection.(*ir.SGCollection), sgName: ir.SGName(sgName), sgVPC: nil} + } + return &SGOptimizer{sgCollection: collection.(*ir.SGCollection), sgName: ir.SGName(components[1]), sgVPC: &components[0]} +} + +func (s *SGOptimizer) Optimize() (ir.Collection, error) { + return s.sgCollection, nil } diff --git a/pkg/synth/common.go b/pkg/synth/common.go index e6e516f3..a9b7b51f 100644 --- a/pkg/synth/common.go +++ b/pkg/synth/common.go @@ -15,6 +15,7 @@ import ( type ( Synthesizer interface { + // generates SGs/nACLs Synth() (ir.Collection, error) }