Skip to content

Commit

Permalink
feat(namespace): add the host based namespace (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tieske authored Mar 6, 2024
1 parent fb15aca commit a7d5222
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 2 deletions.
24 changes: 23 additions & 1 deletion cmd/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,26 @@ func executeNamespace(cmd *cobra.Command, _ []string) error {
}
}

clearHosts, err := cmd.Flags().GetBool("clear-hosts")
if err != nil {
return fmt.Errorf("failed getting cli argument 'clear-hosts'; %w", err)
}

var hosts []string
{
hosts, err = cmd.Flags().GetStringArray("host")
if err != nil {
return fmt.Errorf("failed to retrieve '--host' entry; %w", err)
}
}

trackInfo := deckformat.HistoryNewEntry("namespace")
trackInfo["input"] = inputFilename
trackInfo["output"] = outputFilename
trackInfo["selectors"] = selectors.GetSources()
trackInfo["path-prefix"] = pathPrefix
trackInfo["clear-host"] = clearHosts
trackInfo["hosts"] = hosts

// do the work; read/prefix/write
data, err := filebasics.DeserializeFile(inputFilename)
Expand All @@ -82,7 +97,11 @@ func executeNamespace(cmd *cobra.Command, _ []string) error {
yamlNode := jsonbasics.ConvertToYamlNode(data)
err = namespace.Apply(yamlNode, selectors, pathPrefix, allowEmptySelectors)
if err != nil {
log.Fatalf("failed to apply the namespace: %s", err)
log.Fatalf("failed to apply the path-based namespace: %s", err)
}
err = namespace.ApplyNamespaceHost(yamlNode, selectors, hosts, clearHosts, allowEmptySelectors)
if err != nil {
log.Fatalf("failed to apply the host-based namespace: %s", err)
}
data = jsonbasics.ConvertToJSONobject(yamlNode)

Expand Down Expand Up @@ -151,4 +170,7 @@ func init() {
"json-pointer identifying routes to update (can be specified more than once)")
namespaceCmd.Flags().StringP("path-prefix", "p", "", "the path based namespace to apply")
namespaceCmd.Flags().BoolP("allow-empty-selectors", "", false, "do not error out if the selectors return empty")
namespaceCmd.Flags().StringArrayP("host", "h", []string{},
"hostname to add to the route.hosts property (can be specified more than once)")
namespaceCmd.Flags().BoolP("clear-hosts", "", false, "clears the route.hosts array (before adding the hosts)")
}
117 changes: 117 additions & 0 deletions namespace/namespace_host.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package namespace

import (
"errors"
"fmt"

"github.com/kong/go-apiops/logbasics"
"github.com/kong/go-apiops/yamlbasics"
"gopkg.in/yaml.v3"
)

// ApplyNamespaceHost applies the namespace to the hosts field of the selected routes
// by adding the listed hosts if they ar not in the list already.
func ApplyNamespaceHost(
deckfile *yaml.Node, // the deckFile to operate on
selectors yamlbasics.SelectorSet, // the selectors to use to select the routes
hosts []string, // the hosts to add to the routes
clear bool, // if true, clear the hosts field before adding the hosts
allowEmptySelection bool, // if true, do not return an error if no routes are selected
) error {
if deckfile == nil {
panic("expected 'deckfile' to be non-nil")
}

allRoutes := getAllRoutes(deckfile)
var targetRoutes yamlbasics.NodeSet
var err error
if selectors.IsEmpty() {
// no selectors, apply to all routes
targetRoutes = make(yamlbasics.NodeSet, len(allRoutes))
copy(targetRoutes, allRoutes)
} else {
targetRoutes, err = selectors.Find(deckfile)
if err != nil {
return err
}
}

var remainder yamlbasics.NodeSet
targetRoutes, remainder = allRoutes.Intersection(targetRoutes) // check for non-routes
if len(remainder) != 0 {
return fmt.Errorf("the selectors returned non-route entities; %d", len(remainder))
}
if len(targetRoutes) == 0 {
if allowEmptySelection {
logbasics.Info("no routes matched the selectors, nothing to do")
return nil
}
return errors.New("no routes matched the selectors")
}

return updateRouteHosts(targetRoutes, hosts, clear)
}

// updateRouteHosts updates the hosts field of the provided routes.
// If clear is true, the hosts field is cleared before adding the hosts.
func updateRouteHosts(routes yamlbasics.NodeSet, hosts []string, clear bool) error {
for _, route := range routes {
if err := yamlbasics.CheckType(route, yamlbasics.TypeObject); err != nil {
logbasics.Info("ignoring route: " + err.Error())
continue
}

hostsValueNode := yamlbasics.GetFieldValue(route, "hosts")
if hostsValueNode == nil {
// the 'hosts' array doesn't exist
if len(hosts) == 0 {
// nothing to do since we're not adding anything
continue
}
// create an empty 'hosts' array, so we can add to it
hostsValueNode = yamlbasics.NewArray()
yamlbasics.SetFieldValue(route, "hosts", hostsValueNode)
} else {
// the 'hosts' array exists, check the type
if err := yamlbasics.CheckType(hostsValueNode, yamlbasics.TypeArray); err != nil {
logbasics.Info("ignoring route.hosts property: " + err.Error())
continue
}
}

if clear && len(hostsValueNode.Content) > 0 {
hostsValueNode.Content = make([]*yaml.Node, 0)
}

if len(hosts) > 0 {
appendHosts(hostsValueNode, hosts)
}
}

return nil
}

// appendHosts appends the provided hosts to the hosts array, without duplicates.
func appendHosts(hostsValueNode *yaml.Node, hosts []string) {
if hostsValueNode == nil || hostsValueNode.Kind != yaml.SequenceNode {
panic("expected 'hostsValueNode' to be a sequence node")
}
if len(hosts) == 0 {
panic("expected 'hosts' to be non-nil and non-empty")
}

for _, hostname := range hosts {
exists := false
for _, hostNameNode := range hostsValueNode.Content {
if hostNameNode.Value == hostname {
// already exists, skip
exists = true
break
}
}
if !exists {
// add the hostname to the array
hostsValueNode.Content = append(hostsValueNode.Content, yamlbasics.NewString(hostname))
}
}
}
143 changes: 143 additions & 0 deletions namespace/namespace_host_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package namespace_test

import (
"github.com/kong/go-apiops/namespace"
"github.com/kong/go-apiops/yamlbasics"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Host-Namespace", func() {
Describe("ApplyNamespaceHost", func() {
// clear == fasle/true
// hosts exists/not exists
// hosts has a name, has no name
// hosts has the namespace (eg adding duplicate)
Describe("clear hosts", func() {
It("clears the hosts", func() {
data := `{
"routes": [
{
"hosts": ["one", "two"]
}
]
}`
deckfile := toYaml(data)
hosts := []string{"three"}
err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, true, false)
Expect(err).To(BeNil())
Expect(toString(deckfile)).To(MatchJSON(`{
"routes": [
{
"hosts": ["three"]
}
]
}`))
})
It("clears the hosts, no hosts", func() {
data := `{
"routes": [
{
"paths": []
}
]
}`
deckfile := toYaml(data)
hosts := []string{"three"}
err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, true, false)
Expect(err).To(BeNil())
Expect(toString(deckfile)).To(MatchJSON(`{
"routes": [
{
"paths": [],
"hosts": ["three"]
}
]
}`))
})
})
})
Describe("appends hosts", func() {
It("Route without hosts array", func() {
data := `{
"routes": [
{
"paths": []
}
]
}`
deckfile := toYaml(data)
hosts := []string{"three"}
err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, false, false)
Expect(err).To(BeNil())
Expect(toString(deckfile)).To(MatchJSON(`{
"routes": [
{
"paths": [],
"hosts": ["three"]
}
]
}`))
})
It("Route with empty hosts array", func() {
data := `{
"routes": [
{
"hosts": []
}
]
}`
deckfile := toYaml(data)
hosts := []string{"three"}
err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, false, false)
Expect(err).To(BeNil())
Expect(toString(deckfile)).To(MatchJSON(`{
"routes": [
{
"hosts": ["three"]
}
]
}`))
})
It("adds hosts", func() {
data := `{
"routes": [
{
"hosts": ["one", "two"]
}
]
}`
deckfile := toYaml(data)
hosts := []string{"three"}
err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, false, false)
Expect(err).To(BeNil())
Expect(toString(deckfile)).To(MatchJSON(`{
"routes": [
{
"hosts": ["one", "two", "three"]
}
]
}`))
})
It("doesn't add duplicate hosts", func() {
data := `{
"routes": [
{
"hosts": ["one", "two"]
}
]
}`
deckfile := toYaml(data)
hosts := []string{"one", "two", "three"}
err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, false, false)
Expect(err).To(BeNil())
Expect(toString(deckfile)).To(MatchJSON(`{
"routes": [
{
"hosts": ["one", "two", "three"]
}
]
}`))
})
})
})
2 changes: 1 addition & 1 deletion namespace/namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func toString(data *yaml.Node) string {
return string(out)
}

var _ = Describe("Namespace", func() {
var _ = Describe("Path-Namespace", func() {
Describe("CheckNamespace", func() {
It("validates a plain namespace", func() {
err := namespace.CheckNamespace("/prefix/")
Expand Down

0 comments on commit a7d5222

Please sign in to comment.