Skip to content

Commit

Permalink
Generalize ingress creation
Browse files Browse the repository at this point in the history
  • Loading branch information
ThisIsntTheWay committed Feb 18, 2025
1 parent 522ad04 commit d07552d
Show file tree
Hide file tree
Showing 6 changed files with 327 additions and 285 deletions.
254 changes: 254 additions & 0 deletions pkg/comp-functions/functions/common/ingress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package common

import (
"fmt"
"strings"

"github.com/vshn/appcat/v4/pkg/comp-functions/runtime"
"gopkg.in/yaml.v2"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
)

// IngressConfig contains general information for generating an Ingress obect
type IngressConfig struct {
AdditionalAnnotations map[string]string // Optional
AdditionalIngressNames []string // Optional
FQDNs []string
ServiceConfig IngressRuleConfig
TlsCertBaseName string
}

// IngressRuleConfig describes an ingress rule configuration
type IngressRuleConfig struct {
RelPath string // Optional, defaults to "/"
ServiceNameSuffix string // Optional
ServicePortName string // Has preference over ServicePortNumber
ServicePortNumber int32
}

// Checks if an FQDN is part of a reference FQDN, e.g. an OpenShift Apps domain; "*nextcloud*.apps.cluster.com".
// Returns true if yes and FQDN is not a 2nd level subdomain (i.e. *sub2.sub1*.apps.cluster.com)
func IsSingleSubdomainOfRefDomain(fqdn string, reference string) bool {
if !strings.Contains(fqdn, reference) || reference == "" {
return false
}

noSuffix, _ := strings.CutSuffix(fqdn, reference)
return len(strings.Split(noSuffix, ".")) == 2 // Handles prefixed dot of reference domain
}

// Obtain ingress annotations and optionally extend them using additionalAnnotations
func getIngressAnnotations(svc *runtime.ServiceRuntime, additionalAnnotations map[string]string) map[string]string {
annotations := map[string]string{}
if svc.Config.Data["ingress_annotations"] != "" {
err := yaml.Unmarshal([]byte(svc.Config.Data["ingress_annotations"]), annotations)
if err != nil {
svc.Log.Error(err, "cannot unmarshal ingress annotations from input")
svc.AddResult(runtime.NewWarningResult(fmt.Sprintf("cannot unmarshal ingress annotations from input: %s", err)))
}
} else {
svc.Log.Info("no ingress annotations are defined")
}

for k, v := range additionalAnnotations {
annotations[k] = v
}

return annotations
}

// Creates ingress rules based on a single service name and port. svcNameSuffix is optional and gets appended.
// Will use svcPortName over svcPortNumber (if specified)
func createIngressRule(comp InfoGetter, fqdns []string, ruleConfig IngressRuleConfig) []netv1.IngressRule {
svcNameSuffix := ruleConfig.ServiceNameSuffix
if !strings.HasPrefix(svcNameSuffix, "-") && len(svcNameSuffix) > 0 {
svcNameSuffix = "-" + svcNameSuffix
}

relPath := ruleConfig.RelPath
if relPath == "" {
relPath = "/"
}

ingressRules := []netv1.IngressRule{}

// prefer ServicePortName over ServicePortNumber
serviceBackendPort := netv1.ServiceBackendPort{}
if ruleConfig.ServicePortName != "" {
serviceBackendPort.Name = ruleConfig.ServicePortName
} else {
serviceBackendPort.Number = ruleConfig.ServicePortNumber
}

for _, fqdn := range fqdns {
rule := netv1.IngressRule{
Host: fqdn,
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: []netv1.HTTPIngressPath{
{
Path: relPath,
PathType: ptr.To(netv1.PathType("Prefix")),
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: comp.GetName() + svcNameSuffix,
Port: serviceBackendPort,
},
},
},
},
},
},
}
ingressRules = append(ingressRules, rule)
}

return ingressRules
}

// Generate an ingress TLS secret name as "<baseName>-ingress-cert"
func generateTlsSecretName(baseName string) string {
return strings.Trim(baseName, "-") + "-ingress-cert"
}

// Generate an ingress name. additionalNames will be assembled as "-<n1>-<n2>-<nX>"
func generateIngressName(comp InfoGetter, additionalNames ...string) string {
ingressName := comp.GetName() + "-ingress"
if len(additionalNames) > 0 {
for _, n := range additionalNames {
n = strings.Trim(n, "-")
ingressName = ingressName + "-" + n
}
}

return ingressName
}

// Generate up to 2 ingresses that bundle FQDNs depending on the following:
// FQDNs that are one subdomain ON defaultAppsDomain (e.g. sub1.apps.cluster.com) -> Empty TLS config (uses wildcard cert on OCP).
// FQDNs that do not statisfy the former -> TLS config using a Let's Encrypt certificate.
func GenerateBundledIngresses(comp InfoGetter, svc *runtime.ServiceRuntime, ingressConfig IngressConfig) ([]*netv1.Ingress, error) {
if len(ingressConfig.FQDNs) == 0 {
return nil, fmt.Errorf("no FQDNs")
}

// bool: true -> Use wildcard, false -> Use LE, []string: List of FQDNs
ingressMap := map[bool][]string{}

ocpDefaultAppsDomain := svc.Config.Data["ocpDefaultAppsDomain"]
if ocpDefaultAppsDomain != "" {
for _, fqdn := range ingressConfig.FQDNs {
useWildcard := IsSingleSubdomainOfRefDomain(fqdn, ocpDefaultAppsDomain)
ingressMap[useWildcard] = append(ingressMap[useWildcard], fqdn)
}
} else {
ingressMap[false] = ingressConfig.FQDNs
}

// Create ingresses that bundle FQDNs depending on their certificate requirements (LE/Wildcard)
var ingresses []*netv1.Ingress
fqdnsLetsEncrypt, fqdnsWildcard := ingressMap[false], ingressMap[true]

tlsName := generateTlsSecretName(ingressConfig.TlsCertBaseName)
if tlsName == "" {
return nil, fmt.Errorf("a TLS cert base name must be defined")
}

ingressMetadata := metav1.ObjectMeta{
Namespace: comp.GetInstanceNamespace(),
Annotations: getIngressAnnotations(svc, ingressConfig.AdditionalAnnotations),
}

// Ingress using Let's Encrypt
if len(fqdnsLetsEncrypt) > 0 {
ingressMetadata.Name = generateIngressName(comp, "letsencrypt")
ingresses = append(ingresses, &netv1.Ingress{
ObjectMeta: ingressMetadata,
Spec: netv1.IngressSpec{
Rules: createIngressRule(comp, fqdnsLetsEncrypt, ingressConfig.ServiceConfig),
TLS: []netv1.IngressTLS{
{
Hosts: fqdnsLetsEncrypt,
SecretName: tlsName,
},
},
},
})
}

// Ingress using apps domain wildcard
if len(fqdnsWildcard) > 0 {
ingressMetadata.Name = generateIngressName(comp, "wildcard")
ingresses = append(ingresses, &netv1.Ingress{
ObjectMeta: ingressMetadata,
Spec: netv1.IngressSpec{
Rules: createIngressRule(comp, fqdnsWildcard, ingressConfig.ServiceConfig),
TLS: []netv1.IngressTLS{{}},
},
})
}

return ingresses, nil
}

// Generate an Ingress containing a single FQDN using a TLS config as such:
// FQDN is one subdomain ON defaultAppsDomain (e.g. sub1.apps.cluster.com) -> Empty TLS config (uses wildcard cert on OCP).
// FQDN does not statisfy the former -> TLS config using a Let's Encrypt certificate.
func GenerateIngress(comp InfoGetter, svc *runtime.ServiceRuntime, ingressConfig IngressConfig) (*netv1.Ingress, error) {
if len(ingressConfig.FQDNs) == 0 {
return nil, fmt.Errorf("empty FQDN")
}

fqdn := ingressConfig.FQDNs[0]
if len(ingressConfig.FQDNs) > 1 {
svc.AddResult(
runtime.NewWarningResult(
fmt.Sprintf("More than 1 FQDN has been passed to a singleton ingress object, using first available: %s", fqdn),
),
)
}

annotations := getIngressAnnotations(svc, ingressConfig.AdditionalAnnotations)

tlsName := ingressConfig.TlsCertBaseName
if tlsName == "" {
return nil, fmt.Errorf("a TLS cert base name must be defined")
}
for _, an := range ingressConfig.AdditionalIngressNames {
tlsName = tlsName + "-" + strings.Trim(an, "-")
}

tlsConfig := netv1.IngressTLS{}
if !IsSingleSubdomainOfRefDomain(fqdn, svc.Config.Data["ocpDefaultAppsDomain"]) {
tlsConfig.Hosts = []string{fqdn}
tlsConfig.SecretName = generateTlsSecretName(tlsName)
}

ingress := &netv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: generateIngressName(comp, ingressConfig.AdditionalIngressNames...),
Namespace: comp.GetInstanceNamespace(),
Annotations: annotations,
},
Spec: netv1.IngressSpec{
Rules: createIngressRule(comp, []string{fqdn}, ingressConfig.ServiceConfig),
TLS: []netv1.IngressTLS{tlsConfig},
},
}

return ingress, nil
}

// Apply generated ingresses using svc.SetDesiredKubeObject()
func CreateIngresses(comp InfoGetter, svc *runtime.ServiceRuntime, ingresses []*netv1.Ingress, opts ...runtime.KubeObjectOption) error {
for _, ingress := range ingresses {
err := svc.SetDesiredKubeObject(ingress, ingress.Name, opts...)
if err != nil {
return err
}
}

return nil
}
12 changes: 0 additions & 12 deletions pkg/comp-functions/functions/common/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package common

import (
"fmt"
"strings"
)

// SetNestedObjectValue is necessary as unstructured can't handle anything except basic values and maps.
Expand Down Expand Up @@ -32,14 +31,3 @@ func SetNestedObjectValue(values map[string]interface{}, path []string, val inte

return SetNestedObjectValue(tmpVals, path[1:], val)
}

// Checks if an FQDN is part of a reference FQDN, e.g. an OpenShift Apps domain; "*nextcloud*.apps.cluster.com".
// Returns true if yes and FQDN is not a 2nd level subdomain (i.e. *sub2.sub1*.apps.cluster.com)
func IsSingleSubdomainOfRefDomain(fqdn string, reference string) bool {
if !strings.Contains(fqdn, reference) || reference == "" {
return false
}

noSuffix, _ := strings.CutSuffix(fqdn, reference)
return len(strings.Split(noSuffix, ".")) == 2 // Handles prefixed dot of reference domain
}
77 changes: 16 additions & 61 deletions pkg/comp-functions/functions/vshnforgejo/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"strings"

xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1"
"github.com/ghodss/yaml"
vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1"
"github.com/vshn/appcat/v4/pkg/comp-functions/functions/common"
"github.com/vshn/appcat/v4/pkg/comp-functions/runtime"
Expand Down Expand Up @@ -50,6 +49,8 @@ func DeployForgejo(ctx context.Context, comp *vshnv1.VSHNForgejo, svc *runtime.S
return runtime.NewWarningResult(fmt.Sprintf("cannot add forgejo release: %s", err))
}

svc.Log.Info("Have added forgejo release!")

return nil
}

Expand Down Expand Up @@ -143,19 +144,6 @@ func addForgejo(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.V
"enabled": true,
},
},
"ingress": map[string]any{
"annotations": map[string]string{
"cert-manager.io/cluster-issuer": "letsencrypt-staging",
},
"enabled": true,
"hosts": []map[string]any{},
"tls": []map[string]any{
{
"hosts": []string{},
"secretName": "forgejo-tls",
},
},
},
"persistance": map[string]any{
"enabled": true,
},
Expand All @@ -175,40 +163,22 @@ func addForgejo(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.V
"type": "Recreate",
},
}
/*
hosts:
- host: forgejo213.apps.whatever.cloud
paths:
- path: /
pathType: Prefix
*/
nonTLShosts := []map[string]any{}

for _, host := range comp.Spec.Parameters.Service.FQDN {
// building an array of maps with the host and the paths
nonTLShosts = append(nonTLShosts, map[string]any{
"host": host,
"paths": []map[string]any{
{
"path": "/",
"pathType": "Prefix",
},
},
})

tlsConfig := map[string]any{}
if !common.IsSingleSubdomainOfRefDomain(host, svc.Config.Data["ocpDefaultAppsDomain"]) {
tlsConfig["hosts"] = comp.Spec.Parameters.Service.FQDN
tlsConfig["secretName"] = "forgejo-tls"
}

err := common.SetNestedObjectValue(values, []string{"ingress", "tls"}, []map[string]any{tlsConfig})
if err != nil {
return err
}
svc.Log.Info("Adding ingress")
ingressConfig := common.IngressConfig{
FQDNs: comp.Spec.Parameters.Service.FQDN,
ServiceConfig: common.IngressRuleConfig{
ServiceNameSuffix: "http",
ServicePortNumber: 3000,
},
TlsCertBaseName: "forgejo",
}
ingresses, err := common.GenerateBundledIngresses(comp, svc, ingressConfig)
if err != nil {
return err
}

err := common.SetNestedObjectValue(values, []string{"ingress", "hosts"}, nonTLShosts)
err = common.CreateIngresses(comp, svc, ingresses)
if err != nil {
return err
}
Expand All @@ -218,23 +188,8 @@ func addForgejo(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.V
values["podSecurityContext"] = securityContext["podSecurityContext"]
}

if svc.Config.Data["ingress_annotations"] != "" {
annotations := map[string]string{}

err := yaml.Unmarshal([]byte(svc.Config.Data["ingress_annotations"]), &annotations)
if err != nil {
// do nothing and use default annotation which sets issuer to staging
svc.Log.Error(fmt.Errorf("cannot unmarshal ingress annotations"), "error", err)
}

err = common.SetNestedObjectValue(values, []string{"ingress", "annotations"}, annotations)
if err != nil {
return err
}
}

if comp.Spec.Parameters.Service.AdminEmail != "" {
err = common.SetNestedObjectValue(values, []string{"gitea", "config", "admin", "ADMIN_EMAIL"}, comp.Spec.Parameters.Service.AdminEmail)
err := common.SetNestedObjectValue(values, []string{"gitea", "config", "admin", "ADMIN_EMAIL"}, comp.Spec.Parameters.Service.AdminEmail)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit d07552d

Please sign in to comment.