Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OCM-6030 | feat: allow to edit component routes of ingress #606

Merged
merged 2 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
OCM-6030 | feat: allow to edit component routes of ingress
  • Loading branch information
gdbranco committed Mar 8, 2024
commit 6d1fd05e6e959dbb93f7868f908b19e138dc5b62
102 changes: 100 additions & 2 deletions cmd/ocm/edit/ingress/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

c "github.com/openshift-online/ocm-cli/pkg/cluster"
"github.com/openshift-online/ocm-cli/pkg/ocm"
"github.com/openshift-online/ocm-cli/pkg/utils"
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
"github.com/spf13/cobra"
)
Expand All @@ -31,6 +32,15 @@ var ValidWildcardPolicies = []string{string(cmv1.WildcardPolicyWildcardsDisallow
string(cmv1.WildcardPolicyWildcardsAllowed)}
var ValidNamespaceOwnershipPolicies = []string{string(cmv1.NamespaceOwnershipPolicyStrict),
string(cmv1.NamespaceOwnershipPolicyInterNamespaceAllowed)}
var expectedComponentRoutes = []string{
string(cmv1.ComponentRouteTypeOauth),
string(cmv1.ComponentRouteTypeConsole),
string(cmv1.ComponentRouteTypeDownloads),
}
var expectedParameters = []string{
hostnameParameter,
tlsSecretRefParameter,
}

var args struct {
clusterKey string
Expand All @@ -43,6 +53,8 @@ var args struct {
namespaceOwnershipPolicy string
clusterRoutesHostname string
clusterRoutesTlsSecretRef string

componentRoutes string
}

const (
Expand All @@ -55,6 +67,12 @@ const (
namespaceOwnershipPolicyFlag = "namespace-ownership-policy"
clusterRoutesHostnameFlag = "cluster-routes-hostname"
clusterRoutesTlsSecretRefFlag = "cluster-routes-tls-secret-ref"
componentRoutesFlag = "component-routes"

expectedLengthOfParsedComponent = 2
hostnameParameter = "hostname"
//nolint:gosec
tlsSecretRefParameter = "tlsSecretRef"
cristianoveiga marked this conversation as resolved.
Show resolved Hide resolved
)

var Cmd = &cobra.Command{
Expand Down Expand Up @@ -139,14 +157,23 @@ func init() {
&args.clusterRoutesHostname,
clusterRoutesHostnameFlag,
"",
"Components route hostname for oauth, console, download.",
"Components route hostname for oauth, console, downloads.",
)

flags.StringVar(
&args.clusterRoutesTlsSecretRef,
clusterRoutesTlsSecretRefFlag,
"",
"Components route TLS secret reference for oauth, console, download.",
"Components route TLS secret reference for oauth, console, downloads.",
)

flags.StringVar(
&args.componentRoutes,
componentRoutesFlag,
"",
//nolint:lll
"Component routes settings. Available keys [oauth, console, downloads]. For each key a pair of hostname and tlsSecretRef is expected to be supplied. "+
"Format should be a comma separate list 'oauth: hostname=example-hostname;tlsSecretRef=example-secret-ref,downloads:...",
)
}

Expand Down Expand Up @@ -284,6 +311,17 @@ func run(cmd *cobra.Command, argv []string) error {
ingressBuilder = ingressBuilder.ClusterRoutesTlsSecretRef(args.clusterRoutesTlsSecretRef)
}

if cmd.Flags().Changed(componentRoutesFlag) {
if cluster.Hypershift().Enabled() {
return fmt.Errorf("Can't edit `%s` for Hosted Control Plane clusters", componentRoutesFlag)
}
componentRoutes, err := parseComponentRoutes(args.componentRoutes)
if err != nil {
return fmt.Errorf("An error occurred whilst parsing the supplied component routes: %s", err)
}
ingressBuilder = ingressBuilder.ComponentRoutes(componentRoutes)
}

ingress, err = ingressBuilder.Build()
if err != nil {
return fmt.Errorf("Failed to edit ingress for cluster '%s': %v", clusterKey, err)
Expand All @@ -302,6 +340,66 @@ func run(cmd *cobra.Command, argv []string) error {
return nil
}

func parseComponentRoutes(input string) (map[string]*cmv1.ComponentRouteBuilder, error) {
result := map[string]*cmv1.ComponentRouteBuilder{}
input = strings.TrimSpace(input)
components := strings.Split(input, ",")
if len(components) != len(expectedComponentRoutes) {
return nil, fmt.Errorf(
"the expected amount of component routes is %d, but %d have been supplied",
len(expectedComponentRoutes),
len(components),
)
}
for _, component := range components {
component = strings.TrimSpace(component)
parsedComponent := strings.Split(component, ":")
if len(parsedComponent) != expectedLengthOfParsedComponent {
return nil, fmt.Errorf(
"only the name of the component should be followed by ':'",
)
}
componentName := strings.TrimSpace(parsedComponent[0])
if !utils.Contains(expectedComponentRoutes, componentName) {
return nil, fmt.Errorf(
"'%s' is not a valid component name. Expected include %s",
componentName,
utils.SliceToSortedString(expectedComponentRoutes),
)
}
parameters := strings.TrimSpace(parsedComponent[1])
componentRouteBuilder := new(cmv1.ComponentRouteBuilder)
parsedParameter := strings.Split(parameters, ";")
if len(parsedParameter) != len(expectedParameters) {
return nil, fmt.Errorf(
"only %d parameters are expected for each component",
len(expectedParameters),
)
}
for _, values := range parsedParameter {
values = strings.TrimSpace(values)
parsedValues := strings.Split(values, "=")
parameterName := strings.TrimSpace(parsedValues[0])
if !utils.Contains(expectedParameters, parameterName) {
return nil, fmt.Errorf(
"'%s' is not a valid parameter for a component route. Expected include %s",
parameterName,
utils.SliceToSortedString(expectedParameters),
)
}
parameterValue := strings.TrimSpace(parsedValues[1])
// TODO: use reflection, couldn't get it to work
if parameterName == hostnameParameter {
componentRouteBuilder.Hostname(parameterValue)
} else if parameterName == tlsSecretRefParameter {
componentRouteBuilder.TlsSecretRef(parameterValue)
}
}
result[componentName] = componentRouteBuilder
}
return result, nil
}

func GetExcludedNamespaces(excludedNamespaces string) []string {
if excludedNamespaces == "" {
return []string{}
Expand Down
78 changes: 78 additions & 0 deletions cmd/ocm/edit/ingress/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package ingress

import (
"fmt"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Parse component routes", func() {
It("Parses input string for component routes", func() {
componentRouteBuilder, err := parseComponentRoutes(
//nolint:lll
"oauth: hostname=oauth-host;tlsSecretRef=oauth-secret,downloads: hostname=downloads-host;tlsSecretRef=downloads-secret,console: hostname=console-host;tlsSecretRef=console-secret",
)
Expect(err).To(BeNil())
for key, builder := range componentRouteBuilder {
expectedHostname := fmt.Sprintf("%s-host", key)
expectedTlsRef := fmt.Sprintf("%s-secret", key)
componentRoute, err := builder.Build()
Expect(err).To(BeNil())
Expect(componentRoute.Hostname()).To(Equal(expectedHostname))
Expect(componentRoute.TlsSecretRef()).To(Equal(expectedTlsRef))
}
})
Context("Fails to parse input string for component routes", func() {
It("fails due to invalid component route", func() {
_, err := parseComponentRoutes(
//nolint:lll
"unknown: hostname=oauth-host;tlsSecretRef=oauth-secret,downloads: hostname=downloads-host;tlsSecretRef=downloads-secret,console: hostname=console-host;tlsSecretRef=console-secret",
)
Expect(err).ToNot(BeNil())
Expect(
err.Error(),
).To(Equal("'unknown' is not a valid component name. Expected include [oauth, console, downloads]"))
})
It("fails due to wrong amount of component routes", func() {
_, err := parseComponentRoutes(
//nolint:lll
"oauth: hostname=oauth-host;tlsSecretRef=oauth-secret,downloads: hostname=downloads-host;tlsSecretRef=downloads-secret",
)
Expect(err).ToNot(BeNil())
Expect(
err.Error(),
).To(Equal("the expected amount of component routes is 3, but 2 have been supplied"))
})
It("fails if it can split ':' in more than they key separation", func() {
_, err := parseComponentRoutes(
//nolint:lll
"oauth: hostname=oauth:-host;tlsSecretRef=oauth-secret,downloads: hostname=downloads-host;tlsSecretRef=downloads-secret,",
)
Expect(err).ToNot(BeNil())
Expect(
err.Error(),
).To(Equal("only the name of the component should be followed by ':'"))
})
It("fails due to invalid parameter", func() {
_, err := parseComponentRoutes(
//nolint:lll
"oauth: unknown=oauth-host;tlsSecretRef=oauth-secret,downloads: hostname=downloads-host;tlsSecretRef=downloads-secret,console: hostname=console-host;tlsSecretRef=console-secret",
)
Expect(err).ToNot(BeNil())
Expect(
err.Error(),
).To(Equal("'unknown' is not a valid parameter for a component route. Expected include [hostname, tlsSecretRef]"))
})
It("fails due to wrong amount of parameters", func() {
_, err := parseComponentRoutes(
//nolint:lll
"oauth: hostname=oauth-host,downloads: hostname=downloads-host;tlsSecretRef=downloads-secret,console: hostname=console-host;tlsSecretRef=console-secret",
)
Expect(err).ToNot(BeNil())
Expect(
err.Error(),
).To(Equal("only 2 parameters are expected for each component"))
})
})
})
13 changes: 13 additions & 0 deletions cmd/ocm/edit/ingress/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ingress

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestEditCluster(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Edit ingress suite")
}
13 changes: 13 additions & 0 deletions pkg/utils/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package utils

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestEditCluster(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Utils suite")
}
13 changes: 13 additions & 0 deletions pkg/utils/slices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package utils

import "reflect"

func Contains[T comparable](slice []T, element T) bool {
for _, sliceElement := range slice {
if reflect.DeepEqual(sliceElement, element) {
return true
}
}

return false
}
22 changes: 22 additions & 0 deletions pkg/utils/slices_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package utils

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Slices", func() {
Context("Validates Contains", func() {
It("Return false when input is empty", func() {
Expect(false).To(Equal(Contains([]string{}, "any")))
})

It("Return true when input is populated and present", func() {
Expect(true).To(Equal(Contains([]string{"test", "any"}, "any")))
})

It("Return false when input is populated and not present", func() {
Expect(false).To(Equal(Contains([]string{"test", "any"}, "none")))
})
})
})
24 changes: 24 additions & 0 deletions pkg/utils/strings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package utils

import (
"sort"
"strings"
)

func SliceToSortedString(s []string) string {
if len(s) == 0 {
return ""
}
SortStringRespectLength(s)
return "[" + strings.Join(s, ", ") + "]"
}

func SortStringRespectLength(s []string) {
sort.Slice(s, func(i, j int) bool {
l1, l2 := len(s[i]), len(s[j])
if l1 != l2 {
return l1 < l2
}
return s[i] < s[j]
})
}
18 changes: 18 additions & 0 deletions pkg/utils/strings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package utils

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Validates SliceToSortedString", func() {
It("Empty when slice is empty", func() {
s := SliceToSortedString([]string{})
Expect("").To(Equal(s))
})

It("Sorted when slice is filled", func() {
s := SliceToSortedString([]string{"b", "a", "c", "a10", "a1", "a20", "a2", "1", "2", "10", "20"})
Expect("[1, 2, a, b, c, 10, 20, a1, a2, a10, a20]").To(Equal(s))
})
})
Loading