[Breaking Change!] Remove Deprecated CRDs (#664)

* chore!: Remove TCPEdge CRD

* chore!: Remove TLSEdge CRD

* chore!: Remove HTTPSEdge, Tunnel, and NgrokModuleSet CRDs
This commit is contained in:
Jonathan Stacks
2025-07-08 11:12:32 -05:00
committed by GitHub
parent 1c316e0630
commit 981805a7aa
111 changed files with 328 additions and 15863 deletions
@@ -58,9 +58,6 @@ jobs:
run: |
kubectl get tunnels.ingress.k8s.ngrok.com
kubectl get domains.ingress.k8s.ngrok.com
kubectl get httpsedges.ingress.k8s.ngrok.com
kubectl get tlsedges.ingress.k8s.ngrok.com
kubectl get tcpedges.ingress.k8s.ngrok.com
kubectl get cloudendpoints.ngrok.k8s.ngrok.com -A
kubectl get agentendpoints.ngrok.k8s.ngrok.com -A
- name: Show logs of the ngrok-operator
Generated
-43
View File
@@ -18,33 +18,6 @@ resources:
kind: Domain
path: github.com/ngrok/ngrok-operator/api/ingress/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: k8s.ngrok.com
group: ingress
kind: Tunnel
path: github.com/ngrok/ngrok-operator/api/ingress/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: k8s.ngrok.com
group: ingress
kind: TCPEdge
path: github.com/ngrok/ngrok-operator/api/ingress/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: k8s.ngrok.com
group: ingress
kind: HTTPSEdge
path: github.com/ngrok/ngrok-operator/api/ingress/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
@@ -54,22 +27,6 @@ resources:
kind: IPPolicy
path: github.com/ngrok/ngrok-operator/api/ingress/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
domain: k8s.ngrok.com
group: ingress
kind: NgrokModule
path: github.com/ngrok/ngrok-operator/api/ingress/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
domain: k8s.ngrok.com
group: ingress
kind: NgrokModuleSet
path: github.com/ngrok/ngrok-operator/api/ingress/v1alpha1
version: v1alpha1
- controller: true
domain: k8s.ngrok.com
group: gateway
-210
View File
@@ -1,210 +0,0 @@
/*
MIT License
Copyright (c) 2022 ngrok, Inc.
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 v1alpha1
import (
"encoding/json"
"slices"
"github.com/ngrok/ngrok-api-go/v7"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
type HTTPSEdgeRouteSpec struct {
ngrokAPICommon `json:",inline"`
// MatchType is the type of match to use for this route. Valid values are:
// +kubebuilder:validation:Required
// +kubebuilder:validation:Enum=exact_path;path_prefix
MatchType string `json:"matchType"`
// Match is the value to match against the request path
// +kubebuilder:validation:Required
Match string `json:"match"`
// Backend is the definition for the tunnel group backend
// that serves traffic for this edge
// +kubebuilder:validation:Required
Backend TunnelGroupBackend `json:"backend,omitempty"`
// CircuitBreaker is a circuit breaker configuration to apply to this route
CircuitBreaker *EndpointCircuitBreaker `json:"circuitBreaker,omitempty"`
// Compression is whether or not to enable compression for this route
Compression *EndpointCompression `json:"compression,omitempty"`
// IPRestriction is an IPRestriction to apply to this route
IPRestriction *EndpointIPPolicy `json:"ipRestriction,omitempty"`
// Headers are request/response headers to apply to this route
Headers *EndpointHeaders `json:"headers,omitempty"`
// OAuth configuration to apply to this route
OAuth *EndpointOAuth `json:"oauth,omitempty"`
// OIDC is the OpenID Connect configuration to apply to this route
OIDC *EndpointOIDC `json:"oidc,omitempty"`
// SAML is the SAML configuration to apply to this route
SAML *EndpointSAML `json:"saml,omitempty"`
// WebhookVerification is webhook verification configuration to apply to this route
WebhookVerification *EndpointWebhookVerification `json:"webhookVerification,omitempty"`
// TrafficPolicy is the raw json policy string that was applied to the ngrok API
// +kubebuilder:validation:Schemaless
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Type=object
Policy json.RawMessage `json:"policy,omitempty"`
}
// HTTPSEdgeSpec defines the desired state of HTTPSEdge
type HTTPSEdgeSpec struct {
ngrokAPICommon `json:",inline"`
// Hostports is a list of hostports served by this edge
// +kubebuilder:validation:Required
Hostports []string `json:"hostports,omitempty"`
// Routes is a list of routes served by this edge
Routes []HTTPSEdgeRouteSpec `json:"routes,omitempty"`
// TLSTermination is the TLS termination configuration for this edge
TLSTermination *EndpointTLSTerminationAtEdge `json:"tlsTermination,omitempty"`
MutualTLS *EndpointMutualTLS `json:"mutualTLS,omitempty"`
}
type HTTPSEdgeRouteStatus struct {
// ID is the unique identifier for this route
ID string `json:"id,omitempty"`
// URI is the URI for this route
URI string `json:"uri,omitempty"`
Match string `json:"match,omitempty"`
MatchType string `json:"matchType,omitempty"`
// Backend stores the status of the tunnel group backend,
// mainly the ID of the backend
Backend TunnelGroupBackendStatus `json:"backend,omitempty"`
}
// HTTPSEdgeStatus defines the observed state of HTTPSEdge
type HTTPSEdgeStatus struct {
// ID is the unique identifier for this edge
ID string `json:"id,omitempty"`
// URI is the URI for this edge
URI string `json:"uri,omitempty"`
Routes []HTTPSEdgeRouteStatus `json:"routes,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:deprecatedversion:warning="HTTPSEdge is deprecated and will be removed in a future release. Please migrate to CloudEndpoint or AgentEndpoint instead. See https://github.com/ngrok/ngrok-operator/discussions/654 for more information."
// HTTPSEdge is the Schema for the httpsedges API
type HTTPSEdge struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec HTTPSEdgeSpec `json:"spec,omitempty"`
Status HTTPSEdgeStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// HTTPSEdgeList contains a list of HTTPSEdge
type HTTPSEdgeList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []HTTPSEdge `json:"items"`
}
func init() {
SchemeBuilder.Register(&HTTPSEdge{}, &HTTPSEdgeList{})
}
// Equal returns true if the two HTTPSEdge objects are equal
// It only checks the top level attributes like hostports and metadata
// It does not check the routes or the tunnel group backend
func (e *HTTPSEdge) Equal(edge *ngrok.HTTPSEdge) bool {
if e == nil && edge == nil {
return true
}
if e == nil || edge == nil {
return false
}
// check if the metadata matches
if e.Spec.Metadata != edge.Metadata {
return false
}
// check if the hostports match
if !slices.Equal(e.Spec.Hostports, edge.Hostports) {
return false
}
// check if TLSTermination & mutualTLS matches
return e.tlsTerminationEqual(edge) &&
e.mutualTLSEqual(edge)
}
func (e *HTTPSEdge) tlsTerminationEqual(edge *ngrok.HTTPSEdge) bool {
if e.Spec.TLSTermination == nil && edge.TlsTermination == nil {
return true
}
if (e.Spec.TLSTermination == nil && edge.TlsTermination != nil) || (e.Spec.TLSTermination != nil && edge.TlsTermination == nil) {
// one is nil and the other is not so they don't match
return false
}
if e.Spec.TLSTermination.MinVersion != *edge.TlsTermination.MinVersion {
return false
}
return true
}
func (e *HTTPSEdge) mutualTLSEqual(edge *ngrok.HTTPSEdge) bool {
if e.Spec.MutualTLS == nil && edge.MutualTls == nil {
return true
}
if (e.Spec.MutualTLS == nil && edge.MutualTls != nil) || (e.Spec.MutualTLS != nil && edge.MutualTls == nil) {
// one is nil and the other is not so they don't match
return false
}
edgeCAIDs := make([]string, len(e.Spec.MutualTLS.CertificateAuthorities))
for i, ca := range edge.MutualTls.CertificateAuthorities {
edgeCAIDs[i] = ca.ID
}
return slices.Equal(e.Spec.MutualTLS.CertificateAuthorities, edgeCAIDs)
}
@@ -1,244 +0,0 @@
package v1alpha1
import (
"testing"
"github.com/ngrok/ngrok-api-go/v7"
)
func TestHTTPSEdgeEqual(t *testing.T) {
cases := []struct {
name string
a *HTTPSEdge
b *ngrok.HTTPSEdge
expected bool
}{
{
name: "nils",
a: nil,
b: nil,
expected: true,
},
{
name: "a nil",
a: nil,
b: &ngrok.HTTPSEdge{},
expected: false,
},
{
name: "b nil",
a: &HTTPSEdge{},
b: nil,
expected: false,
},
{
name: "metadata different",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
ngrokAPICommon: ngrokAPICommon{
Metadata: "a",
},
},
},
b: &ngrok.HTTPSEdge{
Metadata: "b",
},
expected: false,
},
{
name: "metadata same",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
ngrokAPICommon: ngrokAPICommon{
Metadata: "a",
},
},
},
b: &ngrok.HTTPSEdge{
Metadata: "a",
},
expected: true,
},
{
name: "hostports different",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
Hostports: []string{"a"},
},
},
b: &ngrok.HTTPSEdge{
Hostports: []string{"b"},
},
expected: false,
},
{
name: "hostports same",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
Hostports: []string{"a"},
},
},
b: &ngrok.HTTPSEdge{
Hostports: []string{"a"},
},
expected: true,
},
{
name: "tls termination both nil",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
TLSTermination: nil,
},
},
b: &ngrok.HTTPSEdge{
TlsTermination: nil,
},
expected: true,
},
{
name: "tls termination a nil",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
TLSTermination: nil,
},
},
b: &ngrok.HTTPSEdge{
TlsTermination: &ngrok.EndpointTLSTermination{},
},
expected: false,
},
{
name: "tls termination b nil",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
TLSTermination: &EndpointTLSTerminationAtEdge{},
},
},
b: &ngrok.HTTPSEdge{
TlsTermination: nil,
},
expected: false,
},
{
name: "tls termination both empty",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
TLSTermination: nil,
},
},
b: &ngrok.HTTPSEdge{
TlsTermination: nil,
},
expected: true,
},
{
name: "tls termination different",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
TLSTermination: &EndpointTLSTerminationAtEdge{
MinVersion: "a",
},
},
},
b: &ngrok.HTTPSEdge{
TlsTermination: &ngrok.EndpointTLSTermination{
MinVersion: ngrok.String("b"),
},
},
expected: false,
},
{
name: "tls termination same",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
TLSTermination: &EndpointTLSTerminationAtEdge{
MinVersion: "a",
},
},
},
b: &ngrok.HTTPSEdge{
TlsTermination: &ngrok.EndpointTLSTermination{
MinVersion: ngrok.String("a"),
},
},
expected: true,
},
{
name: "mtls both nil",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
MutualTLS: nil,
},
},
b: &ngrok.HTTPSEdge{
MutualTls: nil,
},
expected: true,
},
{
name: "mtls a nil",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
MutualTLS: nil,
},
},
b: &ngrok.HTTPSEdge{
MutualTls: &ngrok.EndpointMutualTLS{},
},
expected: false,
},
{
name: "mtls b nil",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
MutualTLS: &EndpointMutualTLS{},
},
},
b: &ngrok.HTTPSEdge{MutualTls: nil},
},
{
name: "tls termination different",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
MutualTLS: &EndpointMutualTLS{
CertificateAuthorities: []string{"a"},
},
},
},
b: &ngrok.HTTPSEdge{
MutualTls: &ngrok.EndpointMutualTLS{
CertificateAuthorities: []ngrok.Ref{{
ID: "b",
}},
},
},
expected: false,
},
{
name: "tls termination same",
a: &HTTPSEdge{
Spec: HTTPSEdgeSpec{
MutualTLS: &EndpointMutualTLS{
CertificateAuthorities: []string{"a123"},
},
},
},
b: &ngrok.HTTPSEdge{
MutualTls: &ngrok.EndpointMutualTLS{
CertificateAuthorities: []ngrok.Ref{{
ID: "a123",
}},
},
},
expected: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if c.a.Equal(c.b) != c.expected {
t.Errorf("expected %v, got %v", c.expected, !c.expected)
}
})
}
}
-451
View File
@@ -2,10 +2,6 @@ package v1alpha1
import (
"encoding/json"
"github.com/ngrok/ngrok-api-go/v7"
"k8s.io/apimachinery/pkg/api/resource"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// common ngrok API/Dashboard fields
@@ -18,453 +14,6 @@ type ngrokAPICommon struct {
Metadata string `json:"metadata,omitempty"`
}
// Route Module Types
type EndpointCompression struct {
// Enabled is whether or not to enable compression for this endpoint
Enabled bool `json:"enabled,omitempty"`
}
type EndpointIPPolicy struct {
IPPolicies []string `json:"policies,omitempty"`
}
// EndpointRequestHeaders is the configuration for a HTTPSEdgeRoute's request headers
// to be added or removed from the request before it is sent to the backend service.
type EndpointRequestHeaders struct {
// a map of header key to header value that will be injected into the HTTP Request
// before being sent to the upstream application server
Add map[string]string `json:"add,omitempty"`
// a list of header names that will be removed from the HTTP Request before being
// sent to the upstream application server
Remove []string `json:"remove,omitempty"`
}
// EndpointResponseHeaders is the configuration for a HTTPSEdgeRoute's response headers
// to be added or removed from the response before it is sent to the client.
type EndpointResponseHeaders struct {
// a map of header key to header value that will be injected into the HTTP Response
// returned to the HTTP client
Add map[string]string `json:"add,omitempty"`
// a list of header names that will be removed from the HTTP Response returned to
// the HTTP client
Remove []string `json:"remove,omitempty"`
}
type EndpointHeaders struct {
// Request headers are the request headers module configuration or null
Request *EndpointRequestHeaders `json:"request,omitempty"`
// Response headers are the response headers module configuration or null
Response *EndpointResponseHeaders `json:"response,omitempty"`
}
type EndpointMutualTLS struct {
// List of CA IDs that will be used to validate incoming connections to the
// edge.
CertificateAuthorities []string `json:"certificateAuthorities,omitempty"`
}
type EndpointTLSTermination struct {
// TerminateAt determines where the TLS connection should be terminated.
// "edge" if the ngrok edge should terminate TLS traffic, "upstream" if TLS
// traffic should be passed through to the upstream ngrok agent /
// application server for termination.
// +kubebuilder:validation:Enum=edge;agent;upstream
// +kubebuilder:default:=edge
TerminateAt string `json:"terminateAt,omitempty"`
// MinVersion is the minimum TLS version to allow for connections to the edge
MinVersion *string `json:"minVersion,omitempty"`
}
type EndpointTLSTerminationAtEdge struct {
// MinVersion is the minimum TLS version to allow for connections to the edge
MinVersion string `json:"minVersion,omitempty"`
}
type SecretKeyRef struct {
// Name of the Kubernetes secret
Name string `json:"name,omitempty"`
// Key in the secret to use
Key string `json:"key,omitempty"`
}
type EndpointWebhookVerification struct {
// a string indicating which webhook provider will be sending webhooks to this
// endpoint. Value must be one of the supported providers defined at
// https://ngrok.com/docs/http/webhook-verification/#supported-providers
Provider string `json:"provider,omitempty"`
// SecretRef is a reference to a secret containing the secret used to validate
// requests from the given provider. All providers except AWS SNS require a secret
SecretRef *SecretKeyRef `json:"secret,omitempty"`
}
type EndpointCircuitBreaker struct {
// Duration after which the circuit is tripped to wait before re-evaluating upstream health
// +kubebuilder:validation:Format=duration
TrippedDuration v1.Duration `json:"trippedDuration,omitempty"`
// Statistical rolling window duration that metrics are retained for.
// +kubebuilder:validation:Format=duration
RollingWindow v1.Duration `json:"rollingWindow,omitempty"`
// Integer number of buckets into which metrics are retained. Max 128.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=128
NumBuckets uint32 `json:"numBuckets,omitempty"`
// Integer number of requests in a rolling window that will trip the circuit.
// Helpful if traffic volume is low.
VolumeThreshold uint32 `json:"volumeThreshold,omitempty"`
// Error threshold percentage should be between 0 - 1.0, not 0-100.0
ErrorThresholdPercentage resource.Quantity `json:"errorThresholdPercentage,omitempty"`
}
type EndpointOIDC struct {
// Do not enforce authentication on HTTP OPTIONS requests. necessary if you are
// supporting CORS.
OptionsPassthrough bool `json:"optionsPassthrough,omitempty"`
// the prefix of the session cookie that ngrok sets on the http client to cache
// authentication. default is 'ngrok.'
CookiePrefix string `json:"cookiePrefix,omitempty"`
// Duration of inactivity after which if the user has not accessed
// the endpoint, their session will time out and they will be forced to
// reauthenticate.
// +kubebuilder:validation:Format=duration
InactivityTimeout v1.Duration `json:"inactivityTimeout,omitempty"`
// The maximum duration of an authenticated session.
// After this period is exceeded, a user must reauthenticate.
// +kubebuilder:validation:Format=duration
MaximumDuration v1.Duration `json:"maximumDuration,omitempty"`
// URL of the OIDC "OpenID provider". This is the base URL used for discovery.
Issuer string `json:"issuer,omitempty"`
// The OIDC app's client ID and OIDC audience.
ClientID string `json:"clientId,omitempty"`
// The OIDC app's client secret.
ClientSecret SecretKeyRef `json:"clientSecret,omitempty"`
// The set of scopes to request from the OIDC identity provider.
Scopes []string `json:"scopes,omitempty"`
}
type EndpointSAML struct {
// Do not enforce authentication on HTTP OPTIONS requests. necessary if you are
// supporting CORS.
OptionsPassthrough bool `json:"optionsPassthrough,omitempty"`
// the prefix of the session cookie that ngrok sets on the http client to cache
// authentication. default is 'ngrok.'
CookiePrefix string `json:"cookiePrefix,omitempty"`
// Duration of inactivity after which if the user has not accessed
// the endpoint, their session will time out and they will be forced to
// reauthenticate.
// +kubebuilder:validation:Format=duration
InactivityTimeout v1.Duration `json:"inactivityTimeout,omitempty"`
// The maximum duration of an authenticated session.
// After this period is exceeded, a user must reauthenticate.
// +kubebuilder:validation:Format=duration
MaximumDuration v1.Duration `json:"maximumDuration,omitempty"`
// The full XML IdP EntityDescriptor. Your IdP may provide this to you as a a file
// to download or as a URL.
IdPMetadata string `json:"idpMetadata,omitempty"`
// If true, indicates that whenever we redirect a user to the IdP for
// authentication that the IdP must prompt the user for authentication credentials
// even if the user already has a valid session with the IdP.
ForceAuthn bool `json:"forceAuthn,omitempty"`
// If true, the IdP may initiate a login directly (e.g. the user does not need to
// visit the endpoint first and then be redirected). The IdP should set the
// RelayState parameter to the target URL of the resource they want the user to be
// redirected to after the SAML login assertion has been processed.
AllowIdPInitiated *bool `json:"allowIdpInitiated,omitempty"`
// If present, only users who are a member of one of the listed groups may access
// the target endpoint.
AuthorizedGroups []string `json:"authorizedGroups,omitempty"`
// Defines the name identifier format the SP expects the IdP to use in its
// assertions to identify subjects. If unspecified, a default value of
// urn:oasis:names:tc:SAML:2.0:nameid-format:persistent will be used. A subset of
// the allowed values enumerated by the SAML specification are supported.
NameIDFormat string `json:"nameidFormat,omitempty"`
}
type OAuthProviderCommon struct {
// Do not enforce authentication on HTTP OPTIONS requests. necessary if you are
// supporting CORS.
OptionsPassthrough bool `json:"optionsPassthrough,omitempty"`
// the prefix of the session cookie that ngrok sets on the http client to cache
// authentication. default is 'ngrok.'
CookiePrefix string `json:"cookiePrefix,omitempty"`
// Duration of inactivity after which if the user has not accessed
// the endpoint, their session will time out and they will be forced to
// reauthenticate.
// +kubebuilder:validation:Format=duration
InactivityTimeout v1.Duration `json:"inactivityTimeout,omitempty"`
// Integer number of seconds of the maximum duration of an authenticated session.
// After this period is exceeded, a user must reauthenticate.
// +kubebuilder:validation:Format=duration
MaximumDuration v1.Duration `json:"maximumDuration,omitempty"`
// Duration after which ngrok guarantees it will refresh user
// state from the identity provider and recheck whether the user is still
// authorized to access the endpoint. This is the preferred tunable to use to
// enforce a minimum amount of time after which a revoked user will no longer be
// able to access the resource.
// +kubebuilder:validation:Format=duration
AuthCheckInterval v1.Duration `json:"authCheckInterval,omitempty"`
// the OAuth app client ID. retrieve it from the identity provider's dashboard
// where you created your own OAuth app. optional. if unspecified, ngrok will use
// its own managed oauth application which has additional restrictions. see the
// OAuth module docs for more details. if present, clientSecret must be present as
// well.
ClientID *string `json:"clientId,omitempty"`
// the OAuth app client secret. retrieve if from the identity provider's dashboard
// where you created your own OAuth app. optional, see all of the caveats in the
// docs for clientId.
ClientSecret *SecretKeyRef `json:"clientSecret,omitempty"`
// a list of provider-specific OAuth scopes with the permissions your OAuth app
// would like to ask for. these may not be set if you are using the ngrok-managed
// oauth app (i.e. you must pass both client_id and client_secret to set scopes)
Scopes []string `json:"scopes,omitempty"`
// a list of email addresses of users authenticated by identity provider who are
// allowed access to the endpoint
EmailAddresses []string `json:"emailAddresses,omitempty"`
// a list of email domains of users authenticated by identity provider who are
// allowed access to the endpoint
EmailDomains []string `json:"emailDomains,omitempty"`
}
func (opc OAuthProviderCommon) toNgrokEndpointOauth() *ngrok.EndpointOAuth {
return &ngrok.EndpointOAuth{
OptionsPassthrough: opc.OptionsPassthrough,
CookiePrefix: opc.CookiePrefix,
InactivityTimeout: uint32(opc.InactivityTimeout.Duration.Seconds()),
MaximumDuration: uint32(opc.MaximumDuration.Duration.Seconds()),
AuthCheckInterval: uint32(opc.AuthCheckInterval.Duration.Seconds()),
Provider: ngrok.EndpointOAuthProvider{},
}
}
func (opc OAuthProviderCommon) ClientSecretKeyRef() *SecretKeyRef {
return opc.ClientSecret
}
type EndpointOAuth struct {
// configuration for using github as the identity provider
Github *EndpointOAuthGitHub `json:"github,omitempty"`
// configuration for using facebook as the identity provider
Facebook *EndpointOAuthFacebook `json:"facebook,omitempty"`
// configuration for using microsoft as the identity provider
Microsoft *EndpointOAuthMicrosoft `json:"microsoft,omitempty"`
// configuration for using google as the identity provider
Google *EndpointOAuthGoogle `json:"google,omitempty"`
// configuration for using linkedin as the identity provider
Linkedin *EndpointOAuthLinkedIn `json:"linkedin,omitempty"`
// configuration for using gitlab as the identity provider
Gitlab *EndpointOAuthGitLab `json:"gitlab,omitempty"`
// configuration for using twitch as the identity provider
Twitch *EndpointOAuthTwitch `json:"twitch,omitempty"`
// configuration for using amazon as the identity provider
Amazon *EndpointOAuthAmazon `json:"amazon,omitempty"`
}
type EndpointOAuthGitHub struct {
OAuthProviderCommon `json:",inline"`
// a list of github teams identifiers. users will be allowed access to the endpoint
// if they are a member of any of these teams. identifiers should be in the 'slug'
// format qualified with the org name, e.g. org-name/team-name
Teams []string `json:"teams,omitempty"`
// a list of github org identifiers. users who are members of any of the listed
// organizations will be allowed access. identifiers should be the organization's
// 'slug'
Organizations []string `json:"organizations,omitempty"`
}
func (github *EndpointOAuthGitHub) Provided() bool {
return github != nil
}
func (github *EndpointOAuthGitHub) ToNgrok(clientSecret *string) *ngrok.EndpointOAuth {
if github == nil {
return nil
}
mod := github.toNgrokEndpointOauth()
mod.Provider.Github = &ngrok.EndpointOAuthGitHub{
ClientID: github.ClientID,
ClientSecret: clientSecret,
Scopes: github.Scopes,
EmailAddresses: github.EmailAddresses,
EmailDomains: github.EmailDomains,
Teams: github.Teams,
Organizations: github.Organizations,
}
return mod
}
type EndpointOAuthFacebook struct {
OAuthProviderCommon `json:",inline"`
}
func (facebook *EndpointOAuthFacebook) Provided() bool {
return facebook != nil
}
func (facebook *EndpointOAuthFacebook) ToNgrok(clientSecret *string) *ngrok.EndpointOAuth {
if facebook == nil {
return nil
}
mod := facebook.toNgrokEndpointOauth()
mod.Provider.Facebook = &ngrok.EndpointOAuthFacebook{
ClientID: facebook.ClientID,
ClientSecret: clientSecret,
Scopes: facebook.Scopes,
EmailAddresses: facebook.EmailAddresses,
EmailDomains: facebook.EmailDomains,
}
return mod
}
type EndpointOAuthMicrosoft struct {
OAuthProviderCommon `json:",inline"`
}
func (microsoft *EndpointOAuthMicrosoft) Provided() bool {
return microsoft != nil
}
func (microsoft *EndpointOAuthMicrosoft) ToNgrok(clientSecret *string) *ngrok.EndpointOAuth {
if microsoft == nil {
return nil
}
mod := microsoft.toNgrokEndpointOauth()
mod.Provider.Microsoft = &ngrok.EndpointOAuthMicrosoft{
ClientID: microsoft.ClientID,
ClientSecret: clientSecret,
Scopes: microsoft.Scopes,
EmailAddresses: microsoft.EmailAddresses,
EmailDomains: microsoft.EmailDomains,
}
return mod
}
type EndpointOAuthGoogle struct {
OAuthProviderCommon `json:",inline"`
}
func (google *EndpointOAuthGoogle) Provided() bool {
return google != nil
}
func (google *EndpointOAuthGoogle) ToNgrok(clientSecret *string) *ngrok.EndpointOAuth {
if google == nil {
return nil
}
mod := google.toNgrokEndpointOauth()
mod.Provider.Google = &ngrok.EndpointOAuthGoogle{
ClientID: google.ClientID,
ClientSecret: clientSecret,
Scopes: google.Scopes,
EmailAddresses: google.EmailAddresses,
EmailDomains: google.EmailDomains,
}
return mod
}
type EndpointOAuthLinkedIn struct {
OAuthProviderCommon `json:",inline"`
}
func (linkedin *EndpointOAuthLinkedIn) Provided() bool {
return linkedin != nil
}
func (linkedin *EndpointOAuthLinkedIn) ToNgrok(clientSecret *string) *ngrok.EndpointOAuth {
if linkedin == nil {
return nil
}
mod := linkedin.toNgrokEndpointOauth()
mod.Provider.Linkedin = &ngrok.EndpointOAuthLinkedIn{
ClientID: linkedin.ClientID,
ClientSecret: clientSecret,
Scopes: linkedin.Scopes,
EmailAddresses: linkedin.EmailAddresses,
EmailDomains: linkedin.EmailDomains,
}
return mod
}
type EndpointOAuthGitLab struct {
OAuthProviderCommon `json:",inline"`
}
func (gitlab *EndpointOAuthGitLab) Provided() bool {
return gitlab != nil
}
func (gitlab *EndpointOAuthGitLab) ToNgrok(clientSecret *string) *ngrok.EndpointOAuth {
if gitlab == nil {
return nil
}
mod := gitlab.toNgrokEndpointOauth()
mod.Provider.Gitlab = &ngrok.EndpointOAuthGitLab{
ClientID: gitlab.ClientID,
ClientSecret: clientSecret,
Scopes: gitlab.Scopes,
EmailAddresses: gitlab.EmailAddresses,
EmailDomains: gitlab.EmailDomains,
}
return mod
}
type EndpointOAuthTwitch struct {
OAuthProviderCommon `json:",inline"`
}
func (twitch *EndpointOAuthTwitch) Provided() bool {
return twitch != nil
}
func (twitch *EndpointOAuthTwitch) ToNgrok(clientSecret *string) *ngrok.EndpointOAuth {
if twitch == nil {
return nil
}
mod := twitch.toNgrokEndpointOauth()
mod.Provider.Twitch = &ngrok.EndpointOAuthTwitch{
ClientID: twitch.ClientID,
ClientSecret: clientSecret,
Scopes: twitch.Scopes,
EmailAddresses: twitch.EmailAddresses,
EmailDomains: twitch.EmailDomains,
}
return mod
}
type EndpointOAuthAmazon struct {
OAuthProviderCommon `json:",inline"`
}
func (amazon *EndpointOAuthAmazon) Provided() bool {
return amazon != nil
}
func (amazon *EndpointOAuthAmazon) ToNgrok(clientSecret *string) *ngrok.EndpointOAuth {
if amazon == nil {
return nil
}
mod := amazon.toNgrokEndpointOauth()
mod.Provider.Amazon = &ngrok.EndpointOAuthAmazon{
ClientID: amazon.ClientID,
ClientSecret: clientSecret,
Scopes: amazon.Scopes,
EmailAddresses: amazon.EmailAddresses,
EmailDomains: amazon.EmailDomains,
}
return mod
}
type EndpointPolicy struct {
// Determines if the rule will be applied to traffic
Enabled *bool `json:"enabled,omitempty"`
-45
View File
@@ -1,45 +0,0 @@
package v1alpha1
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/utils/ptr"
)
type oauthProvider interface {
Provided() bool
}
func TestOAuthCommonProvided(t *testing.T) {
oauth := EndpointOAuth{}
providers := []oauthProvider{
oauth.Amazon,
oauth.Facebook,
oauth.Github,
oauth.Gitlab,
oauth.Google,
oauth.Linkedin,
oauth.Microsoft,
oauth.Twitch,
}
// When no provider config is present, all should return false for Provided()
for _, p := range providers {
assert.False(t, p.Provided())
}
microsoft := &EndpointOAuthMicrosoft{}
microsoft.ClientID = ptr.To("a")
oauth = EndpointOAuth{
Microsoft: microsoft,
}
// When a provider is present, it should return true for Provided() and ones
// that are not provided should return false.
assert.True(t, oauth.Microsoft.Provided())
assert.False(t, oauth.Google.Provided())
assert.False(t, oauth.Twitch.Provided())
assert.False(t, oauth.Github.Provided())
}
@@ -1,141 +0,0 @@
/*
MIT License
Copyright (c) 2022 ngrok, Inc.
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 v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type NgrokModuleSetModules struct {
// CircuitBreaker configuration for this module set
CircuitBreaker *EndpointCircuitBreaker `json:"circuitBreaker,omitempty"`
// Compression configuration for this module set
Compression *EndpointCompression `json:"compression,omitempty"`
// Header configuration for this module set
Headers *EndpointHeaders `json:"headers,omitempty"`
// IPRestriction configuration for this module set
IPRestriction *EndpointIPPolicy `json:"ipRestriction,omitempty"`
// OAuth configuration for this module set
OAuth *EndpointOAuth `json:"oauth,omitempty"`
// Policy module is deprecated, use the `NgrokTrafficPolicy` CRD or the Gateway API instead
Policy *EndpointPolicy `json:"policy,omitempty"`
// OIDC configuration for this module set
OIDC *EndpointOIDC `json:"oidc,omitempty"`
// SAML configuration for this module set
SAML *EndpointSAML `json:"saml,omitempty"`
// TLSTermination configuration for this module set
TLSTermination *EndpointTLSTermination `json:"tlsTermination,omitempty"`
// MutualTLS configuration for this module set
MutualTLS *EndpointMutualTLS `json:"mutualTLS,omitempty"`
// WebhookVerification configuration for this module set
WebhookVerification *EndpointWebhookVerification `json:"webhookVerification,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:deprecatedversion:warning="NgrokModuleSet is deprecated and will be removed in a future release. Please migrate to traffic policies instead. See https://github.com/ngrok/ngrok-operator/discussions/654 for more information."
// NgrokModuleSet is the Schema for the ngrokmodules API
type NgrokModuleSet struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Modules NgrokModuleSetModules `json:"modules,omitempty"`
}
func (ms *NgrokModuleSet) IsEmpty() bool {
if ms == nil {
return true
}
modules := ms.Modules
return modules.CircuitBreaker == nil &&
modules.Compression == nil &&
modules.Headers == nil &&
modules.IPRestriction == nil &&
modules.OAuth == nil &&
modules.Policy == nil &&
modules.OIDC == nil &&
modules.SAML == nil &&
modules.TLSTermination == nil &&
modules.MutualTLS == nil &&
modules.WebhookVerification == nil
}
func (ms *NgrokModuleSet) Merge(o *NgrokModuleSet) {
if o.IsEmpty() { // Empty, nothing to merge
return
}
msmod := &ms.Modules
omod := o.Modules
if omod.CircuitBreaker != nil {
msmod.CircuitBreaker = omod.CircuitBreaker
}
if omod.Compression != nil {
msmod.Compression = omod.Compression
}
if omod.Headers != nil {
msmod.Headers = omod.Headers
}
if omod.IPRestriction != nil {
msmod.IPRestriction = omod.IPRestriction
}
if omod.OAuth != nil {
msmod.OAuth = omod.OAuth
}
if omod.Policy != nil {
msmod.Policy = omod.Policy
}
if omod.OIDC != nil {
msmod.OIDC = omod.OIDC
}
if omod.SAML != nil {
msmod.SAML = omod.SAML
}
if omod.TLSTermination != nil {
msmod.TLSTermination = omod.TLSTermination
}
if omod.MutualTLS != nil {
msmod.MutualTLS = omod.MutualTLS
}
if omod.WebhookVerification != nil {
msmod.WebhookVerification = omod.WebhookVerification
}
}
// +kubebuilder:object:root=true
// NgrokModuleSetList contains a list of NgrokModule
type NgrokModuleSetList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []NgrokModuleSet `json:"items"`
}
func init() {
SchemeBuilder.Register(&NgrokModuleSet{}, &NgrokModuleSetList{})
}
@@ -1,116 +0,0 @@
package v1alpha1
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNgrokModuleSetIsEmpty(t *testing.T) {
tests := []struct {
name string
ms *NgrokModuleSet
want bool
}{
{
name: "nil",
ms: nil,
want: true,
},
{
name: "empty",
ms: &NgrokModuleSet{},
want: true,
},
{
name: "non-empty",
ms: &NgrokModuleSet{
Modules: NgrokModuleSetModules{
Headers: &EndpointHeaders{
Request: &EndpointRequestHeaders{
Add: map[string]string{
"key": "value",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.ms.IsEmpty(); got != tt.want {
t.Errorf("NgrokModuleSet.IsEmpty() = %v, want %v", got, tt.want)
}
})
}
}
func TestNgrokModuleSetMerge(t *testing.T) {
addHeaders := &EndpointHeaders{
Request: &EndpointRequestHeaders{
Add: map[string]string{
"key": "value",
},
},
}
compression := &EndpointCompression{
Enabled: true,
}
tests := []struct {
name string
m *NgrokModuleSet
other *NgrokModuleSet
want *NgrokModuleSet
}{
{
name: "both_nil",
m: nil,
other: nil,
want: nil,
},
{
name: "b_nil",
m: &NgrokModuleSet{
Modules: NgrokModuleSetModules{
Headers: addHeaders,
},
},
other: nil,
want: &NgrokModuleSet{
Modules: NgrokModuleSetModules{
Headers: addHeaders,
},
},
},
{
name: "neither_nil",
m: &NgrokModuleSet{
Modules: NgrokModuleSetModules{
Compression: compression,
},
},
other: &NgrokModuleSet{
Modules: NgrokModuleSetModules{
Headers: addHeaders,
},
},
want: &NgrokModuleSet{
Modules: NgrokModuleSetModules{
Headers: addHeaders,
Compression: compression,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.m.Merge(tt.other)
assert.Equal(t, tt.m, tt.want)
})
}
}
-99
View File
@@ -1,99 +0,0 @@
/*
MIT License
Copyright (c) 2022 ngrok, Inc.
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 v1alpha1
import (
"encoding/json"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// TCPEdgeSpec defines the desired state of TCPEdge
type TCPEdgeSpec struct {
ngrokAPICommon `json:",inline"`
// Backend is the definition for the tunnel group backend
// that serves traffic for this edge
// +kubebuilder:validation:Required
Backend TunnelGroupBackend `json:"backend,omitempty"`
// IPRestriction is an IPRestriction to apply to this edge
IPRestriction *EndpointIPPolicy `json:"ipRestriction,omitempty"`
// Policy is the raw json policy string that was applied to the ngrok API
// +kubebuilder:validation:Schemaless
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Type=object
Policy json.RawMessage `json:"policy,omitempty"`
}
// TCPEdgeStatus defines the observed state of TCPEdge
type TCPEdgeStatus struct {
// ID is the unique identifier for this edge
ID string `json:"id,omitempty"`
// URI is the URI of the edge
URI string `json:"uri,omitempty"`
// Hostports served by this edge
Hostports []string `json:"hostports,omitempty"`
// Backend stores the status of the tunnel group backend,
// mainly the ID of the backend
Backend TunnelGroupBackendStatus `json:"backend,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:deprecatedversion:warning="TCPEdge is deprecated and will be removed in a future release. Please migrate to CloudEndpoint or AgentEndpoint instead. See https://github.com/ngrok/ngrok-operator/discussions/654 for more information."
// +kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.status.id`,description="Domain ID"
// +kubebuilder:printcolumn:name="Hostports",type=string,JSONPath=`.status.hostports`,description="Hostports"
// +kubebuilder:printcolumn:name="Backend ID",type=string,JSONPath=`.status.backend.id`,description="Tunnel Group Backend ID"
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age"
// TCPEdge is the Schema for the tcpedges API
type TCPEdge struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TCPEdgeSpec `json:"spec,omitempty"`
Status TCPEdgeStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// TCPEdgeList contains a list of TCPEdge
type TCPEdgeList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TCPEdge `json:"items"`
}
func init() {
SchemeBuilder.Register(&TCPEdge{}, &TCPEdgeList{})
}
-110
View File
@@ -1,110 +0,0 @@
/*
MIT License
Copyright (c) 2022 ngrok, Inc.
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 v1alpha1
import (
"encoding/json"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// TLSEdgeSpec defines the desired state of TLSEdge
type TLSEdgeSpec struct {
ngrokAPICommon `json:",inline"`
// Backend is the definition for the tunnel group backend
// that serves traffic for this edge
// +kubebuilder:validation:Required
Backend TunnelGroupBackend `json:"backend,omitempty"`
// Hostports is a list of hostports served by this edge
// +kubebuilder:validation:Required
Hostports []string `json:"hostports,omitempty"`
// IPRestriction is an IPRestriction to apply to this edge
IPRestriction *EndpointIPPolicy `json:"ipRestriction,omitempty"`
TLSTermination *EndpointTLSTermination `json:"tlsTermination,omitempty"`
MutualTLS *EndpointMutualTLS `json:"mutualTls,omitempty"`
// Policy is the raw json policy string that was applied to the ngrok API
// +kubebuilder:validation:Schemaless
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Type=object
Policy json.RawMessage `json:"policy,omitempty"`
}
// TLSEdgeStatus defines the observed state of TLSEdge
type TLSEdgeStatus struct {
// ID is the unique identifier for this edge
ID string `json:"id,omitempty"`
// URI is the URI of the edge
URI string `json:"uri,omitempty"`
// Hostports served by this edge
Hostports []string `json:"hostports,omitempty"`
// Backend stores the status of the tunnel group backend,
// mainly the ID of the backend
Backend TunnelGroupBackendStatus `json:"backend,omitempty"`
// Map of hostports to the ngrok assigned CNAME targets
CNAMETargets map[string]string `json:"cnameTargets,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:deprecatedversion:warning="TLSEdge is deprecated and will be removed in a future release. Please migrate to CloudEndpoint or AgentEndpoint instead. See https://github.com/ngrok/ngrok-operator/discussions/654 for more information."
// +kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.status.id`,description="Domain ID"
// +kubebuilder:printcolumn:name="Hostports",type=string,JSONPath=`.status.hostports`,description="Hostports"
// +kubebuilder:printcolumn:name="Backend ID",type=string,JSONPath=`.status.backend.id`,description="Tunnel Group Backend ID"
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age"
// TLSEdge is the Schema for the tlsedges API
type TLSEdge struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TLSEdgeSpec `json:"spec,omitempty"`
Status TLSEdgeStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// TLSEdgeList contains a list of TLSEdge
type TLSEdgeList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TLSEdge `json:"items"`
}
func init() {
SchemeBuilder.Register(&TLSEdge{}, &TLSEdgeList{})
}
-108
View File
@@ -1,108 +0,0 @@
/*
MIT License
Copyright (c) 2022 ngrok, Inc.
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 v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
commonv1alpha1 "github.com/ngrok/ngrok-operator/api/common/v1alpha1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// TunnelSpec defines the desired state of Tunnel
type TunnelSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// ForwardsTo is the name and port of the service to forward traffic to
// +kubebuilder:validation:Required
ForwardsTo string `json:"forwardsTo,omitempty"`
// Labels are key/value pairs that are attached to the tunnel
Labels map[string]string `json:"labels,omitempty"`
// The configuration for backend connections to services
BackendConfig *BackendConfig `json:"backend,omitempty"`
// Specifies the protocol to use when connecting to the backend. Currently only http1 and http2 are supported
// with prior knowledge (defaulting to http1).
// +kubebuilder:validation:Optional
// +kubebuilder:validation:Enum=http1;http2
AppProtocol *commonv1alpha1.ApplicationProtocol `json:"appProtocol,omitempty"`
}
// BackendConfig defines the configuration for backend connections to services.
// This can be extended to include ServerName, InsecureSkipVerify, etc. down the road.
type BackendConfig struct {
Protocol string `json:"protocol,omitempty"`
}
// TunnelStatus defines the observed state of Tunnel
type TunnelStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:deprecatedversion:warning="Tunnel is deprecated and will be removed in a future release. Please migrate to AgentEndpoint instead. See https://github.com/ngrok/ngrok-operator/discussions/654 for more information."
// +kubebuilder:printcolumn:name="ForwardsTo",type=string,JSONPath=`.spec.forwardsTo`,description="Service/port to forward to"
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age"
// Tunnel is the Schema for the tunnels API
type Tunnel struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TunnelSpec `json:"spec,omitempty"`
Status TunnelStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// TunnelList contains a list of Tunnel
type TunnelList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Tunnel `json:"items"`
}
type TunnelGroupBackend struct {
ngrokAPICommon `json:",inline"`
// Labels to watch for tunnels on this backend
Labels map[string]string `json:"labels,omitempty"`
}
type TunnelGroupBackendStatus struct {
// ID is the unique identifier for this backend
ID string `json:"id,omitempty"`
}
func init() {
SchemeBuilder.Register(&Tunnel{}, &TunnelList{})
}
File diff suppressed because it is too large Load Diff
-11
View File
@@ -177,17 +177,6 @@ func runAgentController(ctx context.Context, opts agentManagerOpts) error {
// register healthcheck for tunnel driver
healthcheck.RegisterHealthChecker(td)
if err = (&agentcontroller.TunnelReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("tunnel"),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("tunnel-controller"),
TunnelDriver: td,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Tunnel")
os.Exit(1)
}
if err = (&agentcontroller.AgentEndpointReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("agentendpoint"),
+6 -52
View File
@@ -60,7 +60,6 @@ import (
common "github.com/ngrok/ngrok-operator/api/common/v1alpha1"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
ngrokv1alpha1 "github.com/ngrok/ngrok-operator/api/ngrok/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations"
bindingscontroller "github.com/ngrok/ngrok-operator/internal/controller/bindings"
gatewaycontroller "github.com/ngrok/ngrok-operator/internal/controller/gateway"
ingresscontroller "github.com/ngrok/ngrok-operator/internal/controller/ingress"
@@ -526,13 +525,12 @@ func getK8sResourceDriver(ctx context.Context, mgr manager.Manager, options apiM
// enableIngressFeatureSet enables the Ingress feature set for the operator
func enableIngressFeatureSet(_ context.Context, opts apiManagerOpts, mgr ctrl.Manager, driver *managerdriver.Driver, ngrokClientset ngrokapi.Clientset, defaultDomainReclaimPolicy ingressv1alpha1.DomainReclaimPolicy) error {
if err := (&ingresscontroller.IngressReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("ingress"),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("ingress-controller"),
Namespace: opts.namespace,
AnnotationsExtractor: annotations.NewAnnotationsExtractor(),
Driver: driver,
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("ingress"),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("ingress-controller"),
Namespace: opts.namespace,
Driver: driver,
}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("unable to create ingress controller: %w", err)
}
@@ -564,39 +562,6 @@ func enableIngressFeatureSet(_ context.Context, opts apiManagerOpts, mgr ctrl.Ma
os.Exit(1)
}
if err := (&ingresscontroller.TCPEdgeReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("tcp-edge"),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("tcp-edge-controller"),
NgrokClientset: ngrokClientset,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "TCPEdge")
os.Exit(1)
}
if err := (&ingresscontroller.TLSEdgeReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("tls-edge"),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("tls-edge-controller"),
NgrokClientset: ngrokClientset,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "TLSEdge")
os.Exit(1)
}
if err := (&ingresscontroller.HTTPSEdgeReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("https-edge"),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("https-edge-controller"),
NgrokClientset: ngrokClientset,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "HTTPSEdge")
os.Exit(1)
}
if err := (&ingresscontroller.IPPolicyReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("ip-policy"),
@@ -609,17 +574,6 @@ func enableIngressFeatureSet(_ context.Context, opts apiManagerOpts, mgr ctrl.Ma
os.Exit(1)
}
if err := (&ingresscontroller.ModuleSetReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("ngrok-module-set"),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("ngrok-module-set-controller"),
Driver: driver,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "NgrokModuleSet")
os.Exit(1)
}
if err := (&ngrokcontroller.NgrokTrafficPolicyReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("traffic-policy"),
+1 -1
View File
@@ -11,7 +11,7 @@
This is the ngrok ingress controller. It can be deployed and operated to a cluster and operated by a team allowing others to create ingress objects to dynamically self service ingress to their apps and services using a shared ngrok account. This is a great way to get started with ngrok and Kubernetes.
The controller watches for [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) objects and creates the corresponding ngrok tunnels and edges. More details on how these are derived can be found [here](./user-guide/ingress-to-edge-relationship.md). Other ngrok features such as TCP Edges can be configured via [CRDs](./user-guide/crds.md).
The controller watches for [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) objects and creates the corresponding ngrok tunnels and edges. More details on how these are derived can be found [here](./user-guide/ingress-to-edge-relationship.md). Other ngrok features such as Cloud or Agent Endpoints can be configured via [CRDs](./user-guide/crds.md).
If you are looking to install the controller for the first time, see our [deployment-guide](./deployment-guide/README.md).
If it's already installed and you are looking to configure ingress for an app or service, see our [user-guide](./user-guide/README.md).
-3
View File
@@ -39,7 +39,6 @@ The following controllers for the most part manage a single resource and reflect
- [IP Policy Controller](../../internal/controller/ingress/ippolicy_controller.go): It simply watches these CRDs and reflects the changes in the ngrok API.
- [Domain Controller](../../internal/controller/ingress/domain_controller.go): It will watch for domain CRDs and reflect those changes in the ngrok API. It will also update the domain CRD objects' status fields with the current state of the domain in the ngrok API, such as a CNAME target if it's a white label domain.
- [HTTPS Edge Controller](../../internal/controller/ingress/httpsedge_controller.go): This CRD contains all the data necessary to build not just the edge, but also all routes, backends, and route modules by calling various ngrok APIs to combine resources. The HTTPSEdge CRD is the common type other controllers can create based on different source inputs like Ingress objects or Gateway objects.
- [TCP Edge Controller](../../internal/controller/ingress/tcpedge_controller.go): This CRD contains all the data necessary to build the edge and any edge modules configured. It will likely be a first class CRD used by consumers of the controller to create TCP edges because Kubernetes Ingress does not support TCP.
The following controllers are more complex and manage multiple resources and reflect those changes in the ngrok API.
@@ -75,8 +74,6 @@ The API manager is a leader elected manager that runs the following controllers
* Domain Controller
* IP Policy Controller
* HTTPS Edge Controller
* TCP Edge Controller
* TLS Edge Controller
* Service Controller
* NgrokModuleSet Controller
* Gateway Controller
@@ -1,5 +0,0 @@
# 1. Copy this file to ./config.yaml
# 2. Set you unique FQDN in the "value" field below
- op: replace
path: /spec/rules/0/host
value: <UNIQUE DOMAIN>
@@ -1,4 +0,0 @@
config.yaml:
paths:
"/https-echo-plain": "HTTP/2 200"
"/https-echo-tls": "HTTP/2 200"
@@ -1,68 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress-https
spec:
ingressClassName: ngrok
rules:
- host: SET-VIA-config.yaml
http:
paths:
- path: /https-echo-plain
pathType: Prefix
backend:
service:
name: https-echo-svc
port:
number: 80
- path: /https-echo-tls
pathType: Prefix
backend:
service:
name: https-echo-svc
port:
number: 443
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: https-echo-deploy
spec:
selector:
matchLabels:
app: https-echo-app
replicas: 2
template:
metadata:
labels:
app: https-echo-app
spec:
containers:
- name: https-echo-path
image: mendhak/http-https-echo:24
ports:
- containerPort: 8080
name: plain
- containerPort: 8443
name: tls
---
apiVersion: v1
kind: Service
metadata:
name: https-echo-svc
labels:
app: https-echo-app
annotations:
k8s.ngrok.com/app-protocols: '{"https-echo-app-tls-port":"HtTpS","https-echo-app-plain-port":"HtTp"}'
spec:
ports:
- name: https-echo-app-plain-port
port: 80
protocol: TCP
targetPort: 8080
- name: https-echo-app-tls-port
port: 443
protocol: TCP
targetPort: 8443
selector:
app: https-echo-app
@@ -1,10 +0,0 @@
resources:
- hello-https-ingress.yaml
patchesJson6902:
- target:
group: networking.k8s.io
version: v1
kind: Ingress
name: minimal-ingress-https
path: config.yaml
@@ -1,5 +0,0 @@
# 1. Copy this file to ./config.yaml
# 2. Set you unique FQDN in the "value" field below
- op: replace
path: /spec/rules/0/host
value: <UNIQUE DOMAIN>
@@ -1,8 +0,0 @@
# 1. Copy this file to ./config.yaml
# 2. Set you unique FQDN in the "value" field below
- op: replace
path: /spec/rules/0/host
value: <UNIQUE DOMAIN>
- op: replace
path: /spec/rules/1/host
value: <UNIQUE DOMAIN>
@@ -1,3 +0,0 @@
config.yaml:
paths:
"/": "HTTP/2 200"
@@ -1,149 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
annotations:
k8s.ngrok.com/tls-min-version: "1.3"
k8s.ngrok.com/response-headers-add: |
{
"X-SEND-TO-CLIENT": "Value1"
}
spec:
ingressClassName: ngrok
rules:
- host: SET-VIA-config.yaml
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: http-echo-svc
port:
number: 80
- path: /nginx
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
- host: SET-VIA-config.yaml
http:
paths:
- path: /test
pathType: Prefix
backend:
service:
name: http-echo-svc
port:
number: 80
- path: /nginx
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress-pt2
annotations:
k8s.ngrok.com/tls-min-version: "1.3"
k8s.ngrok.com/response-headers-add: |
{
"X-SEND-TO-CLIENT": "Value2"
}
spec:
ingressClassName: ngrok
rules:
- host: SET-VIA-config.yaml
http:
paths:
- path: /second
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
- path: /nginx-second
pathType: Prefix
backend:
service:
name: http-echo-svc
port:
number: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-echo-deploy
spec:
selector:
matchLabels:
app: http-echo-app
replicas: 2
template:
metadata:
labels:
app: http-echo-app
spec:
containers:
- name: http-echo-path2
image: mendhak/http-https-echo:24
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: http-echo-svc
labels:
app: http-echo-app
spec:
ports:
- name: http-echo-app
port: 80
protocol: TCP
targetPort: 8080
selector:
app: http-echo-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
replicas: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.21.3
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- name: nginx
port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx
@@ -1,16 +0,0 @@
resources:
- hello-world-ingress.yaml
patchesJson6902:
- target:
group: networking.k8s.io
version: v1
kind: Ingress
name: minimal-ingress
path: config.yaml
- target:
group: networking.k8s.io
version: v1
kind: Ingress
name: minimal-ingress-pt2
path: config-pt2.yaml
@@ -1,5 +0,0 @@
# 1. Copy this file to ./config.yaml
# 2. Set you unique FQDN in the "value" field below
- op: replace
path: /spec/rules/0/host
value: <UNIQUE DOMAIN>
@@ -1,3 +0,0 @@
config.yaml:
paths:
"/": "HTTP/2 200"
@@ -1,62 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: other
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress-2-namespaced
namespace: other
annotations:
k8s.ngrok.com/tls-min-version: "1.3"
spec:
ingressClassName: ngrok
rules:
- host: SET-VIA-config.yaml
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: http-echo-namespaced-svc
port:
number: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-echo-namespaced-deply
namespace: other
spec:
selector:
matchLabels:
app: http-echo-namespaced-app
replicas: 2
template:
metadata:
labels:
app: http-echo-namespaced-app
spec:
containers:
- name: http-echo-namespaced-app
image: mendhak/http-https-echo:24
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: http-echo-namespaced-svc
namespace: other
labels:
app: http-echo-namespaced-app
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8080
selector:
app: http-echo-namespaced-app
@@ -1,10 +0,0 @@
resources:
- hello-world-namespaced.yaml
patchesJson6902:
- target:
group: networking.k8s.io
version: v1
kind: Ingress
name: minimal-ingress-2-namespaced
path: config.yaml
@@ -1,5 +0,0 @@
# 1. Copy this file to ./config.yaml
# 2. Set you unique FQDN in the "value" field below
- op: replace
path: /spec/rules/0/host
value: <UNIQUE DOMAIN 2>
@@ -1,5 +0,0 @@
# 1. Copy this file to ./config.yaml
# 2. Set you unique FQDN in the "value" field below
- op: replace
path: /spec/rules/0/host
value: <UNIQUE DOMAIN>
@@ -1,6 +0,0 @@
config.yaml:
paths:
"/": "HTTP/2 200"
config-different.yaml:
paths:
"/": "HTTP/2 404"
@@ -1,109 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-class-example
annotations:
k8s.ngrok.com/tls-min-version: "1.3"
spec:
ingressClassName: ngrok
rules:
- host: ingress-class.ngrok.app
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: http-echo-svc-matching-ingress-class
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: different-ingress-class-example
annotations:
k8s.ngrok.com/tls-min-version: "1.3"
spec:
ingressClassName: nginx # Example of using a different class than ngrok
rules:
- host: different-ingress-class.ngrok.app
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: http-echo-svc-non-matching-ingress-class
port:
number: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-echo-deploy-matching-ingress-class
spec:
selector:
matchLabels:
app: http-echo-app-matching-ingress-class
replicas: 2
template:
metadata:
labels:
app: http-echo-app-matching-ingress-class
spec:
containers:
- name: http-echo-path2
image: mendhak/http-https-echo:24
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: http-echo-svc-matching-ingress-class
labels:
app: http-echo-app-matching-ingress-class
spec:
ports:
- name: http-echo-app
port: 80
protocol: TCP
targetPort: 8080
selector:
app: http-echo-app-matching-ingress-class
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-echo-deploy-non-matching-ingress-class
spec:
selector:
matchLabels:
app: http-echo-app-non-matching-ingress-class
replicas: 2
template:
metadata:
labels:
app: http-echo-app-non-matching-ingress-class
spec:
containers:
- name: http-echo-path2
image: mendhak/http-https-echo:24
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: http-echo-svc-non-matching-ingress-class
labels:
app: http-echo-app-non-matching-ingress-class
spec:
ports:
- name: http-echo-app
port: 80
protocol: TCP
targetPort: 8080
selector:
app: http-echo-app-non-matching-ingress-class
@@ -1,16 +0,0 @@
resources:
- ingress-class.yaml
patchesJson6902:
- target:
group: networking.k8s.io
version: v1
kind: Ingress
name: ingress-class-example
path: config.yaml
- target:
group: networking.k8s.io
version: v1
kind: Ingress
name: different-ingress-class-example
path: config-different.yaml
@@ -1,6 +0,0 @@
# 1. Copy this file to ./config.yaml
# 2. Set you unique FQDN in the "value" field below
- op: replace
path: /spec/rules/0/host
value: <UNIQUE DOMAIN>
# e2e-expected: HTTP/2 200
@@ -1,6 +0,0 @@
config.yaml:
paths:
"/": "HTTP/2 200"
"/path2": "HTTP/2 200"
"/path2/": "HTTP/2 200"
"/path2/50x.html": "HTTP/2 200"
@@ -1,10 +0,0 @@
resources:
- multiple-paths.yaml
patchesJson6902:
- target:
group: networking.k8s.io
version: v1
kind: Ingress
name: multiple-paths
path: config.yaml
@@ -1,96 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multiple-paths
annotations:
k8s.ngrok.com/tls-min-version: "1.3"
spec:
ingressClassName: ngrok
rules:
- host: SET-VIA-config.yaml
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: http-echo-svc
port:
number: 80
- path: /path2
pathType: Prefix
backend:
service:
name: http-echo-path2-svc
port:
number: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-echo-deploy
spec:
selector:
matchLabels:
app: http-echo-app
replicas: 2
template:
metadata:
labels:
app: http-echo-app
spec:
containers:
- name: http-echo
image: mendhak/http-https-echo:24
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: http-echo-svc
labels:
app: http-echo-app
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8080
selector:
app: http-echo-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-echo-path2-deploy
spec:
selector:
matchLabels:
app: http-echo-path2-app
replicas: 2
template:
metadata:
labels:
app: http-echo-path2-app
spec:
containers:
- name: http-echo-path2
image: mendhak/http-https-echo:24
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: http-echo-path2-svc
labels:
app: http-echo-path2-app
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8080
selector:
app: http-echo-path2-app
@@ -1,6 +0,0 @@
# 1. Copy this file to ./config.yaml
# 2. Set you unique FQDN in the "value" field below
- op: replace
path: /spec/rules/0/host
value: <UNIQUE DOMAIN>
# e2e-expected: HTTP/2 200
@@ -1,10 +0,0 @@
resources:
- route-modules.yaml
patchesJson6902:
- target:
group: networking.k8s.io
version: v1
kind: Ingress
name: route-modules
path: config.yaml
@@ -1,81 +0,0 @@
---
kind: IPPolicy
apiVersion: ingress.k8s.ngrok.com/v1alpha1
metadata:
name: route-module-policy
spec:
rules:
- action: deny
cidr: "83.25.12.0/24"
---
kind: NgrokModuleSet
apiVersion: ingress.k8s.ngrok.com/v1alpha1
metadata:
name: route-module
modules:
tlsTermination:
minVersion: "1.3"
compression:
enabled: true
ipRestriction:
policies:
- route-module-policy
oauth:
google:
emailDomains:
- "ngrok.com"
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: route-modules
annotations:
k8s.ngrok.com/modules: route-module
spec:
ingressClassName: ngrok
rules:
- host: SET-VIA-config.yaml
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: http-echo-svc
port:
number: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-echo-deploy
spec:
selector:
matchLabels:
app: http-echo-app
replicas: 2
template:
metadata:
labels:
app: http-echo-app
spec:
containers:
- name: http-echo
image: mendhak/http-https-echo:24
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: http-echo-svc
labels:
app: http-echo-app
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8080
selector:
app: http-echo-app
+2 -2
View File
@@ -7,7 +7,6 @@ require (
github.com/go-logr/logr v1.4.2
github.com/gobwas/glob v0.2.3
github.com/google/uuid v1.6.0
github.com/imdario/mergo v0.3.16
github.com/ngrok/ngrok-api-go/v7 v7.3.0
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.36.3
@@ -23,7 +22,6 @@ require (
k8s.io/api v0.32.1
k8s.io/apimachinery v0.32.1
k8s.io/client-go v0.32.1
k8s.io/klog/v2 v2.130.1
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
sigs.k8s.io/controller-runtime v0.20.2
sigs.k8s.io/gateway-api v1.2.1
@@ -57,6 +55,7 @@ require (
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible // indirect
github.com/inconshreveable/log15/v3 v3.0.0-testing.5 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -100,6 +99,7 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.32.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20230103223931-056869c967cd // indirect
sigs.k8s.io/controller-tools v0.16.3 // indirect
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,136 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
name: tcpedges.ingress.k8s.ngrok.com
spec:
group: ingress.k8s.ngrok.com
names:
kind: TCPEdge
listKind: TCPEdgeList
plural: tcpedges
singular: tcpedge
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Domain ID
jsonPath: .status.id
name: ID
type: string
- description: Hostports
jsonPath: .status.hostports
name: Hostports
type: string
- description: Tunnel Group Backend ID
jsonPath: .status.backend.id
name: Backend ID
type: string
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
deprecated: true
deprecationWarning: TCPEdge is deprecated and will be removed in a future release.
Please migrate to CloudEndpoint or AgentEndpoint instead. See https://github.com/ngrok/ngrok-operator/discussions/654
for more information.
name: v1alpha1
schema:
openAPIV3Schema:
description: TCPEdge is the Schema for the tcpedges API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: TCPEdgeSpec defines the desired state of TCPEdge
properties:
backend:
description: |-
Backend is the definition for the tunnel group backend
that serves traffic for this edge
properties:
description:
default: Created by kubernetes-ingress-controller
description: Description is a human-readable description of the
object in the ngrok API/Dashboard
type: string
labels:
additionalProperties:
type: string
description: Labels to watch for tunnels on this backend
type: object
metadata:
default: '{"owned-by":"kubernetes-ingress-controller"}'
description: Metadata is a string of arbitrary data associated
with the object in the ngrok API/Dashboard
type: string
type: object
description:
default: Created by kubernetes-ingress-controller
description: Description is a human-readable description of the object
in the ngrok API/Dashboard
type: string
ipRestriction:
description: IPRestriction is an IPRestriction to apply to this edge
properties:
policies:
items:
type: string
type: array
type: object
metadata:
default: '{"owned-by":"kubernetes-ingress-controller"}'
description: Metadata is a string of arbitrary data associated with
the object in the ngrok API/Dashboard
type: string
policy:
description: Policy is the raw json policy string that was applied
to the ngrok API
type: object
x-kubernetes-preserve-unknown-fields: true
type: object
status:
description: TCPEdgeStatus defines the observed state of TCPEdge
properties:
backend:
description: |-
Backend stores the status of the tunnel group backend,
mainly the ID of the backend
properties:
id:
description: ID is the unique identifier for this backend
type: string
type: object
hostports:
description: Hostports served by this edge
items:
type: string
type: array
id:
description: ID is the unique identifier for this edge
type: string
uri:
description: URI is the URI of the edge
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
@@ -1,175 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
name: tlsedges.ingress.k8s.ngrok.com
spec:
group: ingress.k8s.ngrok.com
names:
kind: TLSEdge
listKind: TLSEdgeList
plural: tlsedges
singular: tlsedge
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Domain ID
jsonPath: .status.id
name: ID
type: string
- description: Hostports
jsonPath: .status.hostports
name: Hostports
type: string
- description: Tunnel Group Backend ID
jsonPath: .status.backend.id
name: Backend ID
type: string
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
deprecated: true
deprecationWarning: TLSEdge is deprecated and will be removed in a future release.
Please migrate to CloudEndpoint or AgentEndpoint instead. See https://github.com/ngrok/ngrok-operator/discussions/654
for more information.
name: v1alpha1
schema:
openAPIV3Schema:
description: TLSEdge is the Schema for the tlsedges API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: TLSEdgeSpec defines the desired state of TLSEdge
properties:
backend:
description: |-
Backend is the definition for the tunnel group backend
that serves traffic for this edge
properties:
description:
default: Created by kubernetes-ingress-controller
description: Description is a human-readable description of the
object in the ngrok API/Dashboard
type: string
labels:
additionalProperties:
type: string
description: Labels to watch for tunnels on this backend
type: object
metadata:
default: '{"owned-by":"kubernetes-ingress-controller"}'
description: Metadata is a string of arbitrary data associated
with the object in the ngrok API/Dashboard
type: string
type: object
description:
default: Created by kubernetes-ingress-controller
description: Description is a human-readable description of the object
in the ngrok API/Dashboard
type: string
hostports:
description: Hostports is a list of hostports served by this edge
items:
type: string
type: array
ipRestriction:
description: IPRestriction is an IPRestriction to apply to this edge
properties:
policies:
items:
type: string
type: array
type: object
metadata:
default: '{"owned-by":"kubernetes-ingress-controller"}'
description: Metadata is a string of arbitrary data associated with
the object in the ngrok API/Dashboard
type: string
mutualTls:
properties:
certificateAuthorities:
description: |-
List of CA IDs that will be used to validate incoming connections to the
edge.
items:
type: string
type: array
type: object
policy:
description: Policy is the raw json policy string that was applied
to the ngrok API
type: object
x-kubernetes-preserve-unknown-fields: true
tlsTermination:
properties:
minVersion:
description: MinVersion is the minimum TLS version to allow for
connections to the edge
type: string
terminateAt:
default: edge
description: |-
TerminateAt determines where the TLS connection should be terminated.
"edge" if the ngrok edge should terminate TLS traffic, "upstream" if TLS
traffic should be passed through to the upstream ngrok agent /
application server for termination.
enum:
- edge
- agent
- upstream
type: string
type: object
type: object
status:
description: TLSEdgeStatus defines the observed state of TLSEdge
properties:
backend:
description: |-
Backend stores the status of the tunnel group backend,
mainly the ID of the backend
properties:
id:
description: ID is the unique identifier for this backend
type: string
type: object
cnameTargets:
additionalProperties:
type: string
description: Map of hostports to the ngrok assigned CNAME targets
type: object
hostports:
description: Hostports served by this edge
items:
type: string
type: array
id:
description: ID is the unique identifier for this edge
type: string
uri:
description: URI is the URI of the edge
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
@@ -1,86 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
name: tunnels.ingress.k8s.ngrok.com
spec:
group: ingress.k8s.ngrok.com
names:
kind: Tunnel
listKind: TunnelList
plural: tunnels
singular: tunnel
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Service/port to forward to
jsonPath: .spec.forwardsTo
name: ForwardsTo
type: string
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
deprecated: true
deprecationWarning: Tunnel is deprecated and will be removed in a future release.
Please migrate to AgentEndpoint instead. See https://github.com/ngrok/ngrok-operator/discussions/654
for more information.
name: v1alpha1
schema:
openAPIV3Schema:
description: Tunnel is the Schema for the tunnels API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: TunnelSpec defines the desired state of Tunnel
properties:
appProtocol:
description: |-
Specifies the protocol to use when connecting to the backend. Currently only http1 and http2 are supported
with prior knowledge (defaulting to http1).
enum:
- http1
- http2
type: string
backend:
description: The configuration for backend connections to services
properties:
protocol:
type: string
type: object
forwardsTo:
description: ForwardsTo is the name and port of the service to forward
traffic to
type: string
labels:
additionalProperties:
type: string
description: Labels are key/value pairs that are attached to the tunnel
type: object
type: object
status:
description: TunnelStatus defines the observed state of Tunnel
type: object
type: object
served: true
storage: true
subresources:
status: {}
@@ -1,27 +0,0 @@
# permissions for end users to edit httpsedges.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
{{- include "ngrok-operator.labels" . | nindent 4 }}
app.kubernetes.io/component: rbac
name: {{ include "ngrok-operator.fullname" . }}-httpsedge-editor-role
rules:
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges/status
verbs:
- get
@@ -1,23 +0,0 @@
# permissions for end users to view httpsedges.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
{{- include "ngrok-operator.labels" . | nindent 4 }}
app.kubernetes.io/component: rbac
name: {{ include "ngrok-operator.fullname" . }}-httpsedge-viewer-role
rules:
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges
verbs:
- get
- list
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges/status
verbs:
- get
@@ -1,27 +0,0 @@
# permissions for end users to edit ngrokmodulesets.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
{{- include "ngrok-operator.labels" . | nindent 4 }}
app.kubernetes.io/component: rbac
name: {{ include "ngrok-operator.fullname" . }}-ngrokmoduleset-editor-role
rules:
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- ngrokmodulesets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- ngrokmodulesets/status
verbs:
- get
@@ -1,23 +0,0 @@
# permissions for end users to view ngrokmodulesets.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
{{- include "ngrok-operator.labels" . | nindent 4 }}
app.kubernetes.io/component: rbac
name: {{ include "ngrok-operator.fullname" . }}-ngrokmoduleset-viewer-role
rules:
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- ngrokmodulesets
verbs:
- get
- list
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- ngrokmodulesets/status
verbs:
- get
-112
View File
@@ -228,32 +228,6 @@ rules:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges/status
verbs:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
@@ -280,92 +254,6 @@ rules:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- ngrokmodulesets
verbs:
- get
- list
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges/status
verbs:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges/status
verbs:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels/status
verbs:
- get
- patch
- update
- apiGroups:
- networking.k8s.io
resources:
@@ -1,27 +0,0 @@
# permissions for end users to edit tcpedges.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
{{- include "ngrok-operator.labels" . | nindent 4 }}
app.kubernetes.io/component: rbac
name: {{ include "ngrok-operator.fullname" . }}-tcpedge-editor-role
rules:
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges/status
verbs:
- get
@@ -1,23 +0,0 @@
# permissions for end users to view tcpedges.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
{{- include "ngrok-operator.labels" . | nindent 4 }}
app.kubernetes.io/component: rbac
name: {{ include "ngrok-operator.fullname" . }}-tcpedge-viewer-role
rules:
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges
verbs:
- get
- list
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges/status
verbs:
- get
@@ -1,27 +0,0 @@
# permissions for end users to edit tlsedges.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
{{- include "ngrok-operator.labels" . | nindent 4 }}
app.kubernetes.io/component: rbac
name: {{ include "ngrok-operator.fullname" . }}-tlsedge-editor-role
rules:
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges/status
verbs:
- get
@@ -1,23 +0,0 @@
# permissions for end users to view tlsedges.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
{{- include "ngrok-operator.labels" . | nindent 4 }}
app.kubernetes.io/component: rbac
name: {{ include "ngrok-operator.fullname" . }}-tlsedge-viewer-role
rules:
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges
verbs:
- get
- list
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges/status
verbs:
- get
@@ -1,27 +0,0 @@
# permissions for end users to edit tunnels.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
{{- include "ngrok-operator.labels" . | nindent 4 }}
app.kubernetes.io/component: rbac
name: {{ include "ngrok-operator.fullname" . }}-tunnel-editor-role
rules:
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels/status
verbs:
- get
@@ -1,23 +0,0 @@
# permissions for end users to view tunnels.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
{{- include "ngrok-operator.labels" . | nindent 4 }}
app.kubernetes.io/component: rbac
name: {{ include "ngrok-operator.fullname" . }}-tunnel-viewer-role
rules:
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels
verbs:
- get
- list
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels/status
verbs:
- get
@@ -4,7 +4,7 @@ Should match all-options snapshot:
kind: Deployment
metadata:
annotations:
checksum/controller-role: 7a9dd902b42a29f69ffbd6c3b6dddda72b68e4f0ee3af37b60dc4dfd103d4ac4
checksum/controller-role: e108215aa508729e0a9d9a12bed3b67d1fef8007aecee1e86b8d3ea8f872b829
checksum/rbac: 5d27f1783f54a2ab8e69f9bfce35eef2348fda3f6455526619973781d9549322
labels:
app.kubernetes.io/component: controller
@@ -26,7 +26,7 @@ Should match all-options snapshot:
template:
metadata:
annotations:
checksum/controller-role: 7a9dd902b42a29f69ffbd6c3b6dddda72b68e4f0ee3af37b60dc4dfd103d4ac4
checksum/controller-role: e108215aa508729e0a9d9a12bed3b67d1fef8007aecee1e86b8d3ea8f872b829
checksum/rbac: 5d27f1783f54a2ab8e69f9bfce35eef2348fda3f6455526619973781d9549322
checksum/secret: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
prometheus.io/path: /metrics
@@ -442,32 +442,6 @@ Should match all-options snapshot:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges/status
verbs:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
@@ -494,92 +468,6 @@ Should match all-options snapshot:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- ngrokmodulesets
verbs:
- get
- list
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges/status
verbs:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges/status
verbs:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels/status
verbs:
- get
- patch
- update
- apiGroups:
- networking.k8s.io
resources:
@@ -716,7 +604,7 @@ Should match default snapshot:
kind: Deployment
metadata:
annotations:
checksum/controller-role: 7a9dd902b42a29f69ffbd6c3b6dddda72b68e4f0ee3af37b60dc4dfd103d4ac4
checksum/controller-role: e108215aa508729e0a9d9a12bed3b67d1fef8007aecee1e86b8d3ea8f872b829
checksum/rbac: 5d27f1783f54a2ab8e69f9bfce35eef2348fda3f6455526619973781d9549322
labels:
app.kubernetes.io/component: controller
@@ -738,7 +626,7 @@ Should match default snapshot:
template:
metadata:
annotations:
checksum/controller-role: 7a9dd902b42a29f69ffbd6c3b6dddda72b68e4f0ee3af37b60dc4dfd103d4ac4
checksum/controller-role: e108215aa508729e0a9d9a12bed3b67d1fef8007aecee1e86b8d3ea8f872b829
checksum/rbac: 5d27f1783f54a2ab8e69f9bfce35eef2348fda3f6455526619973781d9549322
checksum/secret: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
prometheus.io/path: /metrics
@@ -1141,32 +1029,6 @@ Should match default snapshot:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- httpsedges/status
verbs:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
@@ -1193,92 +1055,6 @@ Should match default snapshot:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- ngrokmodulesets
verbs:
- get
- list
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tcpedges/status
verbs:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tlsedges/status
verbs:
- get
- patch
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels/finalizers
verbs:
- update
- apiGroups:
- ingress.k8s.ngrok.com
resources:
- tunnels/status
verbs:
- get
- patch
- update
- apiGroups:
- networking.k8s.io
resources:
-95
View File
@@ -20,17 +20,8 @@ import (
"fmt"
"strings"
"github.com/imdario/mergo"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations/compression"
"github.com/ngrok/ngrok-operator/internal/annotations/headers"
"github.com/ngrok/ngrok-operator/internal/annotations/ip_policies"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
"github.com/ngrok/ngrok-operator/internal/annotations/tls"
"github.com/ngrok/ngrok-operator/internal/annotations/webhook_verification"
"github.com/ngrok/ngrok-operator/internal/errors"
networking "k8s.io/api/networking/v1"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -69,8 +60,6 @@ const (
type MappingStrategy string
const (
MappingStrategy_Edges MappingStrategy = "edges"
// The default strategy when translating resources into AgentEndpoint / CloudEndpoint that prioritizes collapsing into a single public AgentEndpoint when possible
MappingStrategy_EndpointsDefault MappingStrategy = "endpoints"
@@ -78,77 +67,6 @@ const (
MappingStrategy_EndpointsVerbose MappingStrategy = "endpoints-verbose"
)
type RouteModules struct {
Compression *ingressv1alpha1.EndpointCompression
Headers *ingressv1alpha1.EndpointHeaders
IPRestriction *ingressv1alpha1.EndpointIPPolicy
TLSTermination *ingressv1alpha1.EndpointTLSTerminationAtEdge
WebhookVerification *ingressv1alpha1.EndpointWebhookVerification
}
type Extractor struct {
annotations map[string]parser.Annotation
}
func NewAnnotationsExtractor() Extractor {
return Extractor{
annotations: map[string]parser.Annotation{
"Compression": compression.NewParser(),
"Headers": headers.NewParser(),
"IPRestriction": ip_policies.NewParser(),
"TLSTermination": tls.NewParser(),
"WebhookVerification": webhook_verification.NewParser(),
},
}
}
// Extract extracts the annotations from an Ingress
func (e Extractor) Extract(ing *networking.Ingress) *RouteModules {
pia := &RouteModules{}
data := make(map[string]interface{})
for name, annotationParser := range e.annotations {
val, err := annotationParser.Parse(ing)
klog.V(5).InfoS("Parsing Ingress annotation", "name", name, "ingress", klog.KObj(ing), "value", val)
if err != nil {
if errors.IsMissingAnnotations(err) {
continue
}
if !errors.IsLocationDenied(err) {
continue
}
_, alreadyDenied := data[DeniedKeyName]
if !alreadyDenied {
errString := err.Error()
data[DeniedKeyName] = &errString
klog.ErrorS(err, "error reading Ingress annotation", "name", name, "ingress", klog.KObj(ing))
continue
}
klog.V(5).ErrorS(err, "error reading Ingress annotation", "name", name, "ingress", klog.KObj(ing))
}
if val != nil {
data[name] = val
}
}
err := mergo.MapWithOverwrite(pia, data)
if err != nil {
klog.ErrorS(err, "unexpected error merging extracted annotations")
}
return pia
}
// Extracts a list of module set names from the annotation
// k8s.ngrok.com/modules: "module1,module2"
func ExtractNgrokModuleSetsFromAnnotations(obj client.Object) ([]string, error) {
return parser.GetStringSliceAnnotation("modules", obj)
}
// Extracts a single traffic policy str from the annotation
// k8s.ngrok.com/traffic-policy: "module1"
func ExtractNgrokTrafficPolicyFromAnnotations(obj client.Object) (string, error) {
@@ -169,19 +87,6 @@ func ExtractNgrokTrafficPolicyFromAnnotations(obj client.Object) (string, error)
return "", nil
}
// Whether or not we should use edges in building the ngrok model for resources. Extracts the value
// from the annotation "k8s.ngrok.com/mapping-strategy" if it is present. Otherwise, it defaults to false
func ExtractUseEdges(obj client.Object) (bool, error) {
val, err := parser.GetStringAnnotation(MappingStrategyAnnotationKey, obj)
if err != nil {
if errors.IsMissingAnnotations(err) {
return false, nil
}
return false, err
}
return strings.EqualFold(val, string(MappingStrategy_Edges)), nil
}
// Whether or not we should use endpoint pooling
// from the annotation "k8s.ngrok.com/pooling-enabled" if it is present. Otherwise, it defaults to false
func ExtractUseEndpointPooling(obj client.Object) (bool, error) {
-61
View File
@@ -81,67 +81,6 @@ func TestExtractNgrokTrafficPolicyFromAnnotations(t *testing.T) {
}
}
func TestExtractUseEdges(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
expected bool
expectedErr error
}{
{
name: "Valid mapping strategy: edges",
annotations: map[string]string{
"k8s.ngrok.com/mapping-strategy": "edges",
},
expected: true,
expectedErr: nil,
},
{
name: "Valid mapping strategy: endpoints",
annotations: map[string]string{
"k8s.ngrok.com/mapping-strategy": "endpoints",
},
expected: false,
expectedErr: nil,
},
{
name: "No annotations (default)",
annotations: nil,
expected: false,
expectedErr: nil,
},
{
name: "Invalid mapping strategy",
annotations: map[string]string{
"k8s.ngrok.com/mapping-strategy": "invalid",
},
expected: false,
expectedErr: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
obj := &networking.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: "default",
Annotations: tc.annotations,
},
}
useEdges, err := annotations.ExtractUseEdges(obj)
if tc.expectedErr != nil {
require.Error(t, err)
assert.Equal(t, tc.expectedErr, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected, useEdges)
}
})
}
}
func TestExtractUseEndpointPooling(t *testing.T) {
tests := []struct {
name string
@@ -1,27 +0,0 @@
package compression
import (
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type compression struct{}
func NewParser() parser.Annotation {
return compression{}
}
// Parse parses the annotations contained in the ingress and returns a
// compression configuration or an error. If no compression annotations are
// found, the returned error an errors.ErrMissingAnnotations.
func (c compression) Parse(obj client.Object) (interface{}, error) {
v, err := parser.GetBoolAnnotation("https-compression", obj)
if err != nil {
return nil, err
}
return &ingressv1alpha1.EndpointCompression{
Enabled: v,
}, nil
}
@@ -1,53 +0,0 @@
package compression
import (
"testing"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
"github.com/ngrok/ngrok-operator/internal/annotations/testutil"
"github.com/ngrok/ngrok-operator/internal/errors"
"github.com/stretchr/testify/assert"
)
func TestCompressionWhenNotSupplied(t *testing.T) {
ing := testutil.NewIngress()
ing.SetAnnotations(map[string]string{})
parsed, err := NewParser().Parse(ing)
assert.Nil(t, parsed)
assert.Error(t, err)
assert.True(t, errors.IsMissingAnnotations(err))
}
func TestCompressionWhenSuppliedAndTrue(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
annotations[parser.GetAnnotationWithPrefix("https-compression")] = "true"
ing.SetAnnotations(annotations)
parsed, err := NewParser().Parse(ing)
assert.NoError(t, err)
compression, ok := parsed.(*ingressv1alpha1.EndpointCompression)
if !ok {
t.Fatalf("expected *ingressv1alpha1.EndpointCompression, got %T", parsed)
}
assert.Equal(t, true, compression.Enabled)
}
func TestCompressionWhenSuppliedAndFalse(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
annotations[parser.GetAnnotationWithPrefix("https-compression")] = "false"
ing.SetAnnotations(annotations)
parsed, err := NewParser().Parse(ing)
assert.NoError(t, err)
compression, ok := parsed.(*ingressv1alpha1.EndpointCompression)
if !ok {
t.Fatalf("expected *ingressv1alpha1.EndpointCompression, got %T", parsed)
}
assert.Equal(t, false, compression.Enabled)
}
-91
View File
@@ -1,91 +0,0 @@
package headers
import (
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
"github.com/ngrok/ngrok-operator/internal/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type EndpointHeaders = ingressv1alpha1.EndpointHeaders
type EndpointRequestHeaders = ingressv1alpha1.EndpointRequestHeaders
type EndpointResponseHeaders = ingressv1alpha1.EndpointResponseHeaders
type headers struct{}
func NewParser() parser.Annotation {
return headers{}
}
func (h headers) Parse(obj client.Object) (interface{}, error) {
parsed := &EndpointHeaders{}
v, err := parser.GetStringSliceAnnotation("request-headers-remove", obj)
if err != nil {
if !errors.IsMissingAnnotations(err) {
return parsed, err
}
}
if len(v) > 0 {
if parsed.Request == nil {
parsed.Request = &EndpointRequestHeaders{}
}
parsed.Request.Remove = v
}
m, err := parser.GetStringMapAnnotation("request-headers-add", obj)
if err != nil {
if !errors.IsMissingAnnotations(err) {
return parsed, err
}
}
if len(m) > 0 {
if parsed.Request == nil {
parsed.Request = &EndpointRequestHeaders{}
}
parsed.Request.Add = m
}
if err != nil {
if !errors.IsMissingAnnotations(err) {
return parsed, err
}
}
v, err = parser.GetStringSliceAnnotation("response-headers-remove", obj)
if err != nil {
if !errors.IsMissingAnnotations(err) {
return parsed, err
}
}
if len(v) > 0 {
if parsed.Response == nil {
parsed.Response = &EndpointResponseHeaders{}
}
parsed.Response.Remove = v
}
m, err = parser.GetStringMapAnnotation("response-headers-add", obj)
if err != nil {
if !errors.IsMissingAnnotations(err) {
return parsed, err
}
}
if len(m) > 0 {
if parsed.Response == nil {
parsed.Response = &EndpointResponseHeaders{}
}
parsed.Response.Add = m
}
// If none of the annotations are present, return the missing annotations error
if parsed.Request == nil && parsed.Response == nil {
return nil, errors.ErrMissingAnnotations
}
return parsed, nil
}
@@ -1,85 +0,0 @@
package headers
import (
"testing"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
"github.com/ngrok/ngrok-operator/internal/annotations/testutil"
"github.com/ngrok/ngrok-operator/internal/errors"
"github.com/stretchr/testify/assert"
)
func TestHeadersWhenNotSupplied(t *testing.T) {
ing := testutil.NewIngress()
ing.SetAnnotations(map[string]string{})
parsed, err := NewParser().Parse(ing)
assert.Nil(t, parsed)
assert.Error(t, err)
assert.True(t, errors.IsMissingAnnotations(err))
}
func TestHeadersWhenRequestHeadersSupplied(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
annotations[parser.GetAnnotationWithPrefix("request-headers-remove")] = "Server"
annotations[parser.GetAnnotationWithPrefix("request-headers-add")] = `{"X-Request-Header": "value"}`
ing.SetAnnotations(annotations)
parsed, err := NewParser().Parse(ing)
assert.NoError(t, err)
assert.NotNil(t, parsed)
endpointHeaders, ok := parsed.(*EndpointHeaders)
if !ok {
t.Fatalf("expected *EndpointHeaders, got %T", parsed)
}
assert.Nil(t, endpointHeaders.Response)
assert.Equal(t, []string{"Server"}, endpointHeaders.Request.Remove)
assert.Equal(t, map[string]string{"X-Request-Header": "value"}, endpointHeaders.Request.Add)
}
func TestHeadersWhenResponseHeadersSupplied(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
annotations[parser.GetAnnotationWithPrefix("response-headers-remove")] = "Server"
annotations[parser.GetAnnotationWithPrefix("response-headers-add")] = `{"X-Response-Header": "value"}`
ing.SetAnnotations(annotations)
parsed, err := NewParser().Parse(ing)
assert.NoError(t, err)
assert.NotNil(t, parsed)
endpointHeaders, ok := parsed.(*EndpointHeaders)
if !ok {
t.Fatalf("expected *EndpointHeaders, got %T", parsed)
}
assert.Nil(t, endpointHeaders.Request)
assert.Equal(t, []string{"Server"}, endpointHeaders.Response.Remove)
assert.Equal(t, map[string]string{"X-Response-Header": "value"}, endpointHeaders.Response.Add)
}
func TestInvalidRequestHeadersAdd(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
// Not valid JSON
annotations[parser.GetAnnotationWithPrefix("request-headers-add")] = `{X-Request-Header: value}`
ing.SetAnnotations(annotations)
_, err := NewParser().Parse(ing)
assert.Error(t, err)
assert.True(t, errors.IsInvalidContent(err))
}
func TestInvalidResponseHeadersAdd(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
// Not valid JSON
annotations[parser.GetAnnotationWithPrefix("response-headers-add")] = `{X-Response-Header: value}`
ing.SetAnnotations(annotations)
_, err := NewParser().Parse(ing)
assert.Error(t, err)
assert.True(t, errors.IsInvalidContent(err))
}
@@ -1,22 +0,0 @@
package ip_policies
import (
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type ipPolicy struct{}
func NewParser() parser.Annotation {
return ipPolicy{}
}
func (p ipPolicy) Parse(obj client.Object) (interface{}, error) {
v, err := parser.GetStringSliceAnnotation("ip-policies", obj)
if err != nil {
return nil, err
}
return &ingressv1alpha1.EndpointIPPolicy{IPPolicies: v}, nil
}
@@ -1,24 +0,0 @@
package ip_policies
import (
"testing"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
"github.com/ngrok/ngrok-operator/internal/annotations/testutil"
"github.com/stretchr/testify/assert"
)
func TestParsesIPPolicies(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
annotations[parser.GetAnnotationWithPrefix("ip-policies")] = "abcd1234,some-test-policy"
ing.SetAnnotations(annotations)
policies, err := NewParser().Parse(ing)
assert.NoError(t, err)
assert.NotNil(t, policies)
assert.Equal(t, policies, &ingressv1alpha1.EndpointIPPolicy{
IPPolicies: []string{"abcd1234", "some-test-policy"},
})
}
-27
View File
@@ -1,27 +0,0 @@
package tls
import (
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type EndpointTLSTerminationAtEdge = ingressv1alpha1.EndpointTLSTerminationAtEdge
type tls struct{}
func NewParser() parser.Annotation {
return tls{}
}
// Parse parses the annotations contained in the ingress and returns a
// tls configuration or an error. If no tls annotations are
// found, the returned error an errors.ErrMissingAnnotations.
func (t tls) Parse(obj client.Object) (interface{}, error) {
v, err := parser.GetStringAnnotation("tls-min-version", obj)
if err != nil {
return nil, err
}
return &EndpointTLSTerminationAtEdge{MinVersion: v}, nil
}
-36
View File
@@ -1,36 +0,0 @@
package tls
import (
"testing"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
"github.com/ngrok/ngrok-operator/internal/annotations/testutil"
"github.com/ngrok/ngrok-operator/internal/errors"
"github.com/stretchr/testify/assert"
)
func TestTLSTerminationWhenNotSupplied(t *testing.T) {
ing := testutil.NewIngress()
ing.SetAnnotations(map[string]string{})
parsed, err := NewParser().Parse(ing)
assert.Nil(t, parsed)
assert.Error(t, err)
assert.True(t, errors.IsMissingAnnotations(err))
}
func TestTLSTerminationWhenSupplied(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
annotations[parser.GetAnnotationWithPrefix("tls-min-version")] = "1.3"
ing.SetAnnotations(annotations)
parsed, err := NewParser().Parse(ing)
assert.NoError(t, err)
tlsTermination, ok := parsed.(*EndpointTLSTerminationAtEdge)
if !ok {
t.Fatalf("expected *EndpointTLSTerminationAtEdge, got %T", parsed)
}
assert.Equal(t, "1.3", tlsTermination.MinVersion)
}
@@ -1,44 +0,0 @@
package webhook_verification
import (
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type EndpointWebhookVerification = ingressv1alpha1.EndpointWebhookVerification
type SecretKeyRef = ingressv1alpha1.SecretKeyRef
type webhookVerification struct{}
func NewParser() parser.Annotation {
return webhookVerification{}
}
func (wv webhookVerification) Parse(obj client.Object) (interface{}, error) {
provider, err := parser.GetStringAnnotation("webhook-verification-provider", obj)
if err != nil {
return nil, err
}
if provider == "sns" {
return &EndpointWebhookVerification{Provider: provider}, nil
}
secretName, err := parser.GetStringAnnotation("webhook-verification-secret-name", obj)
if err != nil {
return nil, err
}
secretKey, err := parser.GetStringAnnotation("webhook-verification-secret-key", obj)
if err != nil {
return nil, err
}
return &EndpointWebhookVerification{
Provider: provider,
SecretRef: &SecretKeyRef{
Name: secretName,
Key: secretKey,
},
}, nil
}
@@ -1,82 +0,0 @@
package webhook_verification
import (
"testing"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
"github.com/ngrok/ngrok-operator/internal/annotations/testutil"
"github.com/ngrok/ngrok-operator/internal/errors"
"github.com/stretchr/testify/assert"
)
func TestWebhookVerificationWhenNotSupplied(t *testing.T) {
ing := testutil.NewIngress()
ing.SetAnnotations(map[string]string{})
parsed, err := NewParser().Parse(ing)
assert.Nil(t, parsed)
assert.Error(t, err)
assert.True(t, errors.IsMissingAnnotations(err))
}
func TestWebhookVerificationWhenSecretRefDataNotSupplied(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
annotations[parser.GetAnnotationWithPrefix("webhook-verification-provider")] = "github"
ing.SetAnnotations(annotations)
parsed, err := NewParser().Parse(ing)
assert.Nil(t, parsed)
assert.Error(t, err)
assert.True(t, errors.IsMissingAnnotations(err))
}
func TestWebhookVerificationWhenSecretRefNameNotSupplied(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
annotations[parser.GetAnnotationWithPrefix("webhook-verification-provider")] = "github"
annotations[parser.GetAnnotationWithPrefix("webhook-verification-secret-name")] = "my-webhook-secret"
ing.SetAnnotations(annotations)
parsed, err := NewParser().Parse(ing)
assert.Nil(t, parsed)
assert.Error(t, err)
assert.True(t, errors.IsMissingAnnotations(err))
}
func TestWebhookVerificationWhenSecretRefKeyNotSupplied(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
annotations[parser.GetAnnotationWithPrefix("webhook-verification-provider")] = "github"
annotations[parser.GetAnnotationWithPrefix("webhook-verification-secret-key")] = "SECRET_TOKEN"
ing.SetAnnotations(annotations)
parsed, err := NewParser().Parse(ing)
assert.Nil(t, parsed)
assert.Error(t, err)
assert.True(t, errors.IsMissingAnnotations(err))
}
func TestWebhookVerificationWhenAnnotationsAreProvided(t *testing.T) {
ing := testutil.NewIngress()
annotations := map[string]string{}
annotations[parser.GetAnnotationWithPrefix("webhook-verification-provider")] = "github"
annotations[parser.GetAnnotationWithPrefix("webhook-verification-secret-name")] = "my-webhook-secret"
annotations[parser.GetAnnotationWithPrefix("webhook-verification-secret-key")] = "SECRET_TOKEN"
ing.SetAnnotations(annotations)
parsed, err := NewParser().Parse(ing)
assert.NoError(t, err)
assert.NotNil(t, parsed)
webhookVerification, ok := parsed.(*ingressv1alpha1.EndpointWebhookVerification)
if !ok {
t.Fatalf("expected *ingressv1alpha1.WebhookVerification, got %T", parsed)
}
assert.Equal(t, "github", webhookVerification.Provider)
assert.Equal(t, "my-webhook-secret", webhookVerification.SecretRef.Name)
assert.Equal(t, "SECRET_TOKEN", webhookVerification.SecretRef.Key)
}
@@ -1,129 +0,0 @@
/*
MIT License
Copyright (c) 2022 ngrok, Inc.
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 agent
import (
"context"
"errors"
"fmt"
"github.com/go-logr/logr"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/controller"
"github.com/ngrok/ngrok-operator/pkg/tunneldriver"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
controllerruntime "sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
// TunnelReconciler reconciles a Tunnel object
type TunnelReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
TunnelDriver *tunneldriver.TunnelDriver
controller *controller.BaseController[*ingressv1alpha1.Tunnel]
}
// SetupWithManager sets up the controller with the Manager
func (r *TunnelReconciler) SetupWithManager(mgr ctrl.Manager) error {
var err error
if r.TunnelDriver == nil {
return errors.New("TunnelDriver is nil")
}
r.controller = &controller.BaseController[*ingressv1alpha1.Tunnel]{
Kube: r.Client,
Log: r.Log,
Recorder: r.Recorder,
Update: r.update,
Delete: r.delete,
StatusID: r.statusID,
}
cont, err := controllerruntime.NewUnmanaged("tunnel-controller", mgr, controllerruntime.Options{
Reconciler: r,
LogConstructor: func(_ *reconcile.Request) logr.Logger {
return r.Log
},
NeedLeaderElection: ptr.To(false),
})
if err != nil {
return err
}
if err := cont.Watch(source.Kind[client.Object](
mgr.GetCache(),
&ingressv1alpha1.Tunnel{},
&handler.EnqueueRequestForObject{},
predicate.Or(
predicate.AnnotationChangedPredicate{},
predicate.GenerationChangedPredicate{},
),
)); err != nil {
return err
}
return mgr.Add(cont)
}
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tunnels,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tunnels/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tunnels/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile
func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
return r.controller.Reconcile(ctx, req, new(ingressv1alpha1.Tunnel))
}
func (r *TunnelReconciler) update(ctx context.Context, tunnel *ingressv1alpha1.Tunnel) error {
tunnelName := r.statusID(tunnel)
return r.TunnelDriver.CreateTunnel(ctx, tunnelName, tunnel.Spec)
}
func (r *TunnelReconciler) delete(ctx context.Context, tunnel *ingressv1alpha1.Tunnel) error {
tunnelName := r.statusID(tunnel)
return r.TunnelDriver.DeleteTunnel(ctx, tunnelName)
}
func (r *TunnelReconciler) statusID(tunnel *ingressv1alpha1.Tunnel) string {
return fmt.Sprintf("%s/%s", tunnel.Namespace, tunnel.Name)
}
@@ -373,9 +373,6 @@ func (r *GatewayReconciler) SetupWithManager(mgr ctrl.Manager) error {
&gatewayv1.HTTPRoute{},
// &corev1.Service{},
&ingressv1alpha1.Domain{},
&ingressv1alpha1.HTTPSEdge{},
// &ingressv1alpha1.Tunnel{},
// &ingressv1alpha1.NgrokModuleSet{},
}
bldr := ctrl.NewControllerManagedBy(mgr).For(&gatewayv1.Gateway{})
@@ -133,9 +133,6 @@ func (r *HTTPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
&gatewayv1.GatewayClass{},
&corev1.Service{},
&ingressv1alpha1.Domain{},
&ingressv1alpha1.HTTPSEdge{},
&ingressv1alpha1.Tunnel{},
// &ingressv1alpha1.NgrokModuleSet{},
}
builder := ctrl.NewControllerManagedBy(mgr).
File diff suppressed because it is too large Load Diff
@@ -1,31 +0,0 @@
package ingress
import (
"github.com/ngrok/ngrok-api-go/v7"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("HTTPSEdgeController", func() {
DescribeTable("isMigratingAuthProviders", func(current *ngrok.HTTPSEdgeRoute, desired *ingressv1alpha1.HTTPSEdgeRouteSpec, expected bool) {
Expect(isMigratingAuthProviders(current, desired)).To(Equal(expected))
},
Entry("no auth", &ngrok.HTTPSEdgeRoute{}, &ingressv1alpha1.HTTPSEdgeRouteSpec{}, false),
Entry("Same Auth: Oauth", &ngrok.HTTPSEdgeRoute{OAuth: &ngrok.EndpointOAuth{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{OAuth: &ingressv1alpha1.EndpointOAuth{}}, false),
Entry("Same Auth: SAML", &ngrok.HTTPSEdgeRoute{SAML: &ngrok.EndpointSAML{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{SAML: &ingressv1alpha1.EndpointSAML{}}, false),
Entry("Same Auth: OIDC", &ngrok.HTTPSEdgeRoute{OIDC: &ngrok.EndpointOIDC{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{OIDC: &ingressv1alpha1.EndpointOIDC{}}, false),
Entry("Added Oauth", &ngrok.HTTPSEdgeRoute{}, &ingressv1alpha1.HTTPSEdgeRouteSpec{OAuth: &ingressv1alpha1.EndpointOAuth{}}, false),
Entry("Added SAML", &ngrok.HTTPSEdgeRoute{}, &ingressv1alpha1.HTTPSEdgeRouteSpec{SAML: &ingressv1alpha1.EndpointSAML{}}, false),
Entry("Added OIDC", &ngrok.HTTPSEdgeRoute{}, &ingressv1alpha1.HTTPSEdgeRouteSpec{OIDC: &ingressv1alpha1.EndpointOIDC{}}, false),
Entry("Removed Oauth", &ngrok.HTTPSEdgeRoute{OAuth: &ngrok.EndpointOAuth{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{}, false),
Entry("Removed SAML", &ngrok.HTTPSEdgeRoute{SAML: &ngrok.EndpointSAML{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{}, false),
Entry("Removed OIDC", &ngrok.HTTPSEdgeRoute{OIDC: &ngrok.EndpointOIDC{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{}, false),
Entry("Removed Oauth and Added Saml", &ngrok.HTTPSEdgeRoute{OAuth: &ngrok.EndpointOAuth{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{SAML: &ingressv1alpha1.EndpointSAML{}}, true),
Entry("Removed Oauth and Added OIDC", &ngrok.HTTPSEdgeRoute{OAuth: &ngrok.EndpointOAuth{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{OIDC: &ingressv1alpha1.EndpointOIDC{}}, true),
Entry("Removed SAML and Added Oauth", &ngrok.HTTPSEdgeRoute{SAML: &ngrok.EndpointSAML{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{OAuth: &ingressv1alpha1.EndpointOAuth{}}, true),
Entry("Removed SAML and Added OIDC", &ngrok.HTTPSEdgeRoute{SAML: &ngrok.EndpointSAML{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{OIDC: &ingressv1alpha1.EndpointOIDC{}}, true),
Entry("Removed OIDC and Added Oauth", &ngrok.HTTPSEdgeRoute{OIDC: &ngrok.EndpointOIDC{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{OAuth: &ingressv1alpha1.EndpointOAuth{}}, true),
Entry("Removed OIDC and Added SAML", &ngrok.HTTPSEdgeRoute{OIDC: &ngrok.EndpointOIDC{}}, &ingressv1alpha1.HTTPSEdgeRouteSpec{SAML: &ingressv1alpha1.EndpointSAML{}}, true),
)
})
@@ -6,7 +6,6 @@ import (
"github.com/go-logr/logr"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
ngrokv1alpha1 "github.com/ngrok/ngrok-operator/api/ngrok/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations"
"github.com/ngrok/ngrok-operator/internal/controller"
internalerrors "github.com/ngrok/ngrok-operator/internal/errors"
"github.com/ngrok/ngrok-operator/pkg/managerdriver"
@@ -22,12 +21,11 @@ import (
// https://pkg.go.dev/sigs.k8s.io/controller-runtime#section-readme
type IngressReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
Namespace string
AnnotationsExtractor annotations.Extractor
Driver *managerdriver.Driver
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
Namespace string
Driver *managerdriver.Driver
}
func (r *IngressReconciler) SetupWithManager(mgr ctrl.Manager) error {
@@ -35,9 +33,6 @@ func (r *IngressReconciler) SetupWithManager(mgr ctrl.Manager) error {
&netv1.IngressClass{},
&corev1.Service{},
&ingressv1alpha1.Domain{},
&ingressv1alpha1.HTTPSEdge{},
&ingressv1alpha1.Tunnel{},
&ingressv1alpha1.NgrokModuleSet{},
&ngrokv1alpha1.NgrokTrafficPolicy{},
}
@@ -58,7 +53,6 @@ func (r *IngressReconciler) SetupWithManager(mgr ctrl.Manager) error {
// +kubebuilder:rbac:groups="networking.k8s.io",resources=ingresses,verbs=get;list;watch;update
// +kubebuilder:rbac:groups="networking.k8s.io",resources=ingresses/status,verbs=get;list;watch;update
// +kubebuilder:rbac:groups="networking.k8s.io",resources=ingressclasses,verbs=get;list;watch
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=ngrokmodulesets,verbs=get;list;watch
// +kubebuilder:rbac:groups=ngrok.k8s.ngrok.com,resources=ngroktrafficpolicies,verbs=get;list;watch
// This reconcile function is called by the controller-runtime manager.
@@ -1,49 +0,0 @@
package ingress
import (
"context"
"github.com/go-logr/logr"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/pkg/managerdriver"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
type ModuleSetReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
Driver *managerdriver.Driver
}
func (r *ModuleSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&ingressv1alpha1.NgrokModuleSet{}).
WithEventFilter(predicate.Or(
predicate.AnnotationChangedPredicate{},
predicate.GenerationChangedPredicate{},
)).
Complete(r)
}
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=ngrokmodulesets,verbs=get;list;watch
// This reconcile function is called by the controller-runtime manager.
// It is invoked whenever there is an event that occurs for a resource
// being watched (in our case, NgrokModuleSets). If you tail the controller
// logs and delete, update, edit ngrokmoduleset objects, you see the events come in.
func (r *ModuleSetReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) {
if err := r.Driver.SyncEdges(ctx, r.Client); err != nil {
return ctrl.Result{}, err
}
if err := r.Driver.SyncEndpoints(ctx, r.Client); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
+10 -358
View File
@@ -35,7 +35,6 @@ import (
"github.com/go-logr/logr"
"github.com/ngrok/ngrok-api-go/v7"
common "github.com/ngrok/ngrok-operator/api/common/v1alpha1"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
ngrokv1alpha1 "github.com/ngrok/ngrok-operator/api/ngrok/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations"
"github.com/ngrok/ngrok-operator/internal/annotations/parser"
@@ -45,7 +44,6 @@ import (
"github.com/ngrok/ngrok-operator/internal/ngrokapi"
"github.com/ngrok/ngrok-operator/internal/resolvers"
"github.com/ngrok/ngrok-operator/internal/trafficpolicy"
"github.com/ngrok/ngrok-operator/internal/util"
"github.com/ngrok/ngrok-operator/pkg/managerdriver"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
@@ -106,9 +104,6 @@ func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
}
owns := []client.Object{
&ingressv1alpha1.Tunnel{},
&ingressv1alpha1.TCPEdge{},
&ingressv1alpha1.TLSEdge{},
&ngrokv1alpha1.AgentEndpoint{},
&ngrokv1alpha1.CloudEndpoint{},
}
@@ -125,11 +120,6 @@ func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return shouldHandleService(svc)
},
}).
// Watch modulesets for changes
Watches(
&ingressv1alpha1.NgrokModuleSet{},
handler.EnqueueRequestsFromMapFunc(r.findServicesForModuleSet),
).
// Watch traffic policies for changes
Watches(
&ngrokv1alpha1.NgrokTrafficPolicy{},
@@ -156,23 +146,8 @@ func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
}
}
// Index the services by the module set(s) they reference
err := mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Service{}, ModuleSetPath, func(obj client.Object) []string {
moduleSets, err := annotations.ExtractNgrokModuleSetsFromAnnotations(obj)
if err != nil {
return nil
}
// Note: We are returning a slice of strings here for the field indexer. Checking for equality later, means
// that only one of the module sets needs to match for the service to be returned.
return moduleSets
})
if err != nil {
return err
}
// Index the services by the traffic policy they reference
err = mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Service{}, TrafficPolicyPath, func(obj client.Object) []string {
err := mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Service{}, TrafficPolicyPath, func(obj client.Object) []string {
policy, err := annotations.ExtractNgrokTrafficPolicyFromAnnotations(obj)
if err != nil {
return nil
@@ -192,11 +167,7 @@ func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;update
// +kubebuilder:rbac:groups="",resources=services/status,verbs=get;list;watch;patch;update
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=ngrokmodulesets,verbs=get;list;watch
// +kubebuilder:rbac:groups=ngrok.k8s.ngrok.com,resources=ngroktrafficpolicies,verbs=get;list;watch
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tunnels,verbs=get;list;watch;create;update;delete
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tcpedges,verbs=get;list;watch;create;update;delete
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tlsedges,verbs=get;list;watch;create;update;delete
// This reconcile function is called by the controller-runtime manager.
// It is invoked whenever there is an event that occurs for a resource
@@ -213,9 +184,6 @@ func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}
subResourceReconcilers := serviceSubresourceReconcilers{
newServiceTCPEdgeReconciler(),
newServiceTLSEdgeReconciler(),
newServiceTunnelReconciler(),
newServiceCloudEndpointReconciler(),
newServiceAgentEndpointReconciler(),
}
@@ -267,7 +235,7 @@ func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}
}
// Once we clean up the tunnels and TCP edges, we can remove the finalizer if it exists. We don't
// Once we clean up the Cloud/Agent Endpoints, we can remove the finalizer if it exists. We don't
// care about registering a finalizer since we only care about load balancer services
if err := controller.RemoveAndSyncFinalizer(ctx, r.Client, svc); err != nil {
log.Error(err, "Failed to remove finalizer")
@@ -289,29 +257,17 @@ func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
var desired []client.Object
mappingStrategy, err := managerdriver.MappingStrategyAnnotationToIR(svc)
// If the annotation is not valid, we still return a reasonable default mapping strategy. This error
// is not fatal, so just log it and an event and continue
if err != nil {
log.Error(err, fmt.Sprintf("Failed to get %q annotation", annotations.MappingStrategyAnnotation))
// TODO: Add an event to the service
return ctrl.Result{}, err
}
// Best effort to try to use endpoints(if configured via annotation and eventually as a global default).
// If the conversion of modulesets -> trafficpolicy fails or there is some other error such that we can't
// build the desired endpoints correctly, we will fall back to using tunnels and edges
// and just bubble up the error as an event on the service
if mappingStrategy != ir.IRMappingStrategy_Edges {
desired, err = r.buildEndpoints(ctx, svc, mappingStrategy)
if err != nil {
log.Error(err, "Failed to build desired endpoints")
r.Recorder.Event(svc, corev1.EventTypeWarning, "FailedToBuildEndpoints", err.Error())
desired, err = r.buildTunnelAndEdge(ctx, svc)
}
} else {
desired, err = r.buildTunnelAndEdge(ctx, svc)
log.Error(err, "Failed to get mapping strategy annotation")
r.Recorder.Event(svc, corev1.EventTypeWarning, "FailedToGetMappingStrategy", err.Error())
}
desired, err = r.buildEndpoints(ctx, svc, mappingStrategy)
if err != nil {
log.Error(err, "Failed to build desired resources")
log.Error(err, "Failed to build desired endpoints")
r.Recorder.Event(svc, corev1.EventTypeWarning, "FailedToBuildEndpoints", err.Error())
return ctrl.Result{}, err
}
@@ -336,39 +292,6 @@ func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
return ctrl.Result{}, nil
}
func (r *ServiceReconciler) findServicesForModuleSet(ctx context.Context, moduleSet client.Object) []reconcile.Request {
log := r.Log
moduleSetNamespace := moduleSet.GetNamespace()
moduleSetName := moduleSet.GetName()
log.V(3).Info("Finding services for module set", "namespace", moduleSetNamespace, "name", moduleSetName)
services := &corev1.ServiceList{}
listOpts := &client.ListOptions{
Namespace: moduleSetNamespace,
FieldSelector: fields.OneTermEqualSelector(ModuleSetPath, moduleSetName),
}
err := r.Client.List(ctx, services, listOpts)
if err != nil {
log.Error(err, "Failed to list services for module set")
return []reconcile.Request{}
}
requests := make([]reconcile.Request, len(services.Items))
for i, svc := range services.Items {
svcNamespace := svc.GetNamespace()
svcName := svc.GetName()
requests[i] = reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: svcNamespace,
Name: svcName,
},
}
log.V(3).Info("Triggering reconciliation for service", "namespace", svcNamespace, "name", svcName)
}
return requests
}
func (r *ServiceReconciler) findServicesForTrafficPolicy(ctx context.Context, policy client.Object) []reconcile.Request {
log := r.Log
@@ -447,22 +370,6 @@ func (r *ServiceReconciler) buildEndpoints(ctx context.Context, svc *corev1.Serv
// The final traffic policy that will be applied to the listener endpoint
tp := trafficpolicy.NewTrafficPolicy()
// Get the modules from the service annotations
moduleSet, err := getNgrokModuleSetForService(ctx, r.Client, svc)
if err != nil {
log.Error(err, "Failed to get module sets")
return objects, err
}
// If there are modulesets defined on the service, create a traffic policy from them
// and merge it with the existing traffic policy
moduleSetsTrafficPolicy, err := util.NewTrafficPolicyFromModuleset(ctx, moduleSet, r.SecretResolver, r.IPPolicyResolver)
if err != nil {
log.Error(err, "Failed to create traffic policy from module set", "moduleSet", moduleSet)
return objects, err
}
tp.Merge(moduleSetsTrafficPolicy)
// If an explicit traffic policy is defined on the service, merge it with the existing traffic policy
// before adding the forward-internal action.
// TODO: We still need to handle legacy traffic policy conversion
@@ -679,115 +586,6 @@ func (r *ServiceReconciler) getListenerURL(svc *corev1.Service) (string, error)
return "tcp://", nil
}
func (r *ServiceReconciler) buildTunnelAndEdge(ctx context.Context, svc *corev1.Service) ([]client.Object, error) {
log := ctrl.LoggerFrom(ctx)
port := svc.Spec.Ports[0].Port
objects := make([]client.Object, 0)
backendLabels := map[string]string{
"k8s.ngrok.com/namespace": svc.Namespace,
"k8s.ngrok.com/service": svc.Name,
"k8s.ngrok.com/service-uid": string(svc.UID),
"k8s.ngrok.com/port": strconv.Itoa(int(port)),
}
tunnel := &ingressv1alpha1.Tunnel{
ObjectMeta: metav1.ObjectMeta{
GenerateName: svc.Name + "-",
Namespace: svc.Namespace,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(svc, corev1.SchemeGroupVersion.WithKind("Service")),
},
},
Spec: ingressv1alpha1.TunnelSpec{
ForwardsTo: fmt.Sprintf("%s.%s.%s:%d", svc.Name, svc.Namespace, r.ClusterDomain, port),
Labels: backendLabels,
},
}
objects = append(objects, tunnel)
// Get the modules from the service annotations
moduleSets, err := getNgrokModuleSetForService(ctx, r.Client, svc)
if err != nil {
log.Error(err, "Failed to get module sets")
return objects, err
}
policy, err := getNgrokTrafficPolicyForService(ctx, r.Client, svc)
if err != nil {
log.Error(err, "Failed to get traffic policy")
return objects, err
}
listenerURL, err := r.getListenerURL(svc)
if err != nil {
r.Recorder.Event(svc, corev1.EventTypeWarning, "FailedToGetListenerURL", err.Error())
return objects, err
}
parsedURL, err := url.Parse(listenerURL)
if err != nil {
return objects, err
}
if parsedURL.Scheme == "tcp" {
edge := &ingressv1alpha1.TCPEdge{
ObjectMeta: metav1.ObjectMeta{
GenerateName: svc.Name + "-",
Namespace: svc.Namespace,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(svc, corev1.SchemeGroupVersion.WithKind("Service")),
},
Annotations: map[string]string{},
},
Spec: ingressv1alpha1.TCPEdgeSpec{
Backend: ingressv1alpha1.TunnelGroupBackend{
Labels: backendLabels,
},
},
}
if moduleSets != nil {
edge.Spec.IPRestriction = moduleSets.Modules.IPRestriction
}
if policy != nil {
edge.Spec.Policy = policy.Spec.Policy
}
objects = append(objects, edge)
} else {
edge := &ingressv1alpha1.TLSEdge{
ObjectMeta: metav1.ObjectMeta{
GenerateName: svc.Name + "-",
Namespace: svc.Namespace,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(svc, corev1.SchemeGroupVersion.WithKind("Service")),
},
Annotations: map[string]string{},
},
Spec: ingressv1alpha1.TLSEdgeSpec{
Backend: ingressv1alpha1.TunnelGroupBackend{
Labels: backendLabels,
},
Hostports: []string{
fmt.Sprintf("%s:443", parsedURL.Hostname()),
},
},
}
if moduleSets != nil {
edge.Spec.IPRestriction = moduleSets.Modules.IPRestriction
edge.Spec.MutualTLS = moduleSets.Modules.MutualTLS
edge.Spec.TLSTermination = moduleSets.Modules.TLSTermination
}
if policy != nil {
edge.Spec.Policy = policy.Spec.Policy
}
objects = append(objects, edge)
}
return objects, nil
}
func shouldHandleService(svc *corev1.Service) bool {
return svc.Spec.Type == corev1.ServiceTypeLoadBalancer &&
ptr.Deref(svc.Spec.LoadBalancerClass, "") == NgrokLoadBalancerClass
@@ -896,7 +694,7 @@ func (r *baseSubresourceReconciler[T, PT]) Reconcile(ctx context.Context, c clie
}
// We only support one desired resource of a particular type for now
// If there are cases where we need to create multiple edges or tunnels, we will need to change this handling
// If there are cases where we need to create multiple cloud or agent endpoints, we will need to change this handling
if len(desired) > 1 {
return errors.New("multiple desired resources not supported")
}
@@ -947,123 +745,6 @@ func (r *baseSubresourceReconciler[T, PT]) UpdateServiceStatus(ctx context.Conte
return r.updateStatus(ctx, c, svc, v)
}
func newServiceTCPEdgeReconciler() serviceSubresourceReconciler {
return &baseSubresourceReconciler[ingressv1alpha1.TCPEdge, *ingressv1alpha1.TCPEdge]{
listOwned: func(ctx context.Context, c client.Client, opts ...client.ListOption) ([]ingressv1alpha1.TCPEdge, error) {
edges := &ingressv1alpha1.TCPEdgeList{}
if err := c.List(ctx, edges, opts...); err != nil {
return nil, err
}
return edges.Items, nil
},
matches: func(desired, existing ingressv1alpha1.TCPEdge) bool {
return reflect.DeepEqual(existing.Spec, desired.Spec)
},
mergeExisting: func(desired ingressv1alpha1.TCPEdge, existing *ingressv1alpha1.TCPEdge) {
existing.Spec = desired.Spec
},
updateStatus: func(ctx context.Context, c client.Client, svc *corev1.Service, edge *ingressv1alpha1.TCPEdge) error {
clearIngressStatus := func(svc *corev1.Service) error {
svc.Status.LoadBalancer.Ingress = nil
return c.Status().Update(ctx, svc)
}
if len(edge.Status.Hostports) == 0 {
return clearIngressStatus(svc)
}
host, port, err := parseHostAndPort(edge.Status.Hostports[0])
if err != nil {
return err
}
svc.Status.LoadBalancer.Ingress = []corev1.LoadBalancerIngress{
{
Hostname: host,
Ports: []corev1.PortStatus{
{
Port: port,
Protocol: corev1.ProtocolTCP,
},
},
},
}
return c.Status().Update(ctx, svc)
},
}
}
func newServiceTLSEdgeReconciler() serviceSubresourceReconciler {
return &baseSubresourceReconciler[ingressv1alpha1.TLSEdge, *ingressv1alpha1.TLSEdge]{
listOwned: func(ctx context.Context, c client.Client, opts ...client.ListOption) ([]ingressv1alpha1.TLSEdge, error) {
edges := &ingressv1alpha1.TLSEdgeList{}
if err := c.List(ctx, edges, opts...); err != nil {
return nil, err
}
return edges.Items, nil
},
matches: func(desired, existing ingressv1alpha1.TLSEdge) bool {
return reflect.DeepEqual(existing.Spec, desired.Spec)
},
mergeExisting: func(desired ingressv1alpha1.TLSEdge, existing *ingressv1alpha1.TLSEdge) {
existing.Spec = desired.Spec
},
updateStatus: func(ctx context.Context, c client.Client, svc *corev1.Service, edge *ingressv1alpha1.TLSEdge) error {
clearIngressStatus := func(svc *corev1.Service) error {
svc.Status.LoadBalancer.Ingress = nil
return c.Status().Update(ctx, svc)
}
domain, err := parser.GetStringAnnotation("domain", svc)
if err != nil {
if errors.IsMissingAnnotations(err) {
return clearIngressStatus(svc)
}
return err
}
hostname, ok := edge.Status.CNAMETargets[domain]
if !ok {
hostname = domain // ngrok managed domain case
}
svc.Status.LoadBalancer.Ingress = []corev1.LoadBalancerIngress{
{
Hostname: hostname,
Ports: []corev1.PortStatus{
{
Port: 443,
Protocol: corev1.ProtocolTCP,
},
},
},
}
return c.Status().Update(ctx, svc)
},
}
}
func newServiceTunnelReconciler() serviceSubresourceReconciler {
return &baseSubresourceReconciler[ingressv1alpha1.Tunnel, *ingressv1alpha1.Tunnel]{
listOwned: func(ctx context.Context, c client.Client, opts ...client.ListOption) ([]ingressv1alpha1.Tunnel, error) {
tunnels := &ingressv1alpha1.TunnelList{}
if err := c.List(ctx, tunnels, opts...); err != nil {
return nil, err
}
return tunnels.Items, nil
},
matches: func(desired, existing ingressv1alpha1.Tunnel) bool {
return reflect.DeepEqual(existing.Spec, desired.Spec)
},
mergeExisting: func(desired ingressv1alpha1.Tunnel, existing *ingressv1alpha1.Tunnel) {
existing.Spec = desired.Spec
},
updateStatus: func(_ context.Context, _ client.Client, _ *corev1.Service, _ *ingressv1alpha1.Tunnel) error {
// Tunnels don't interact with the service status
return nil
},
}
}
func newServiceCloudEndpointReconciler() serviceSubresourceReconciler {
return &baseSubresourceReconciler[ngrokv1alpha1.CloudEndpoint, *ngrokv1alpha1.CloudEndpoint]{
listOwned: func(ctx context.Context, c client.Client, opts ...client.ListOption) ([]ngrokv1alpha1.CloudEndpoint, error) {
@@ -1160,35 +841,6 @@ func newServiceAgentEndpointReconciler() serviceSubresourceReconciler {
}
}
// Given a service, it will resolve any ngrok modulesets defined on the service to the
// CRDs and then will merge them in to a single moduleset
func getNgrokModuleSetForService(ctx context.Context, c client.Client, svc *corev1.Service) (*ingressv1alpha1.NgrokModuleSet, error) {
computedModSet := &ingressv1alpha1.NgrokModuleSet{
ObjectMeta: metav1.ObjectMeta{
Namespace: svc.Namespace,
},
}
modules, err := annotations.ExtractNgrokModuleSetsFromAnnotations(svc)
if err != nil {
if errors.IsMissingAnnotations(err) {
return computedModSet, nil
}
return computedModSet, err
}
for _, module := range modules {
// TODO: watch these and cache them so we don't have to make tons of requests
resolvedMod := &ingressv1alpha1.NgrokModuleSet{}
if err := c.Get(ctx, client.ObjectKey{Namespace: svc.Namespace, Name: module}, resolvedMod); err != nil {
return computedModSet, err
}
computedModSet.Merge(resolvedMod)
}
return computedModSet, nil
}
func getNgrokTrafficPolicyForService(ctx context.Context, c client.Client, svc *corev1.Service) (*ngrokv1alpha1.NgrokTrafficPolicy, error) {
policyName, err := annotations.ExtractNgrokTrafficPolicyFromAnnotations(svc)
if err != nil {
@@ -1,524 +0,0 @@
/*
MIT License
Copyright (c) 2022 ngrok, Inc.
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 ingress
import (
"context"
"encoding/json"
"fmt"
"maps"
"reflect"
"slices"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/go-logr/logr"
"github.com/ngrok/ngrok-api-go/v7"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/controller"
"github.com/ngrok/ngrok-operator/internal/events"
"github.com/ngrok/ngrok-operator/internal/ngrokapi"
"github.com/ngrok/ngrok-operator/internal/resolvers"
"github.com/ngrok/ngrok-operator/internal/util"
)
// TCPEdgeReconciler reconciles a TCPEdge object
type TCPEdgeReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
IPPolicyResolver resolvers.IPPolicyResolver
NgrokClientset ngrokapi.Clientset
controller *controller.BaseController[*ingressv1alpha1.TCPEdge]
}
// SetupWithManager sets up the controller with the Manager.
func (r *TCPEdgeReconciler) SetupWithManager(mgr ctrl.Manager) error {
if r.IPPolicyResolver == nil {
r.IPPolicyResolver = resolvers.NewDefaultIPPolicyResolver(mgr.GetClient())
}
r.controller = &controller.BaseController[*ingressv1alpha1.TCPEdge]{
Kube: r.Client,
Log: r.Log,
Recorder: r.Recorder,
StatusID: func(cr *ingressv1alpha1.TCPEdge) string { return cr.Status.ID },
Create: r.create,
Update: r.update,
Delete: r.delete,
}
return ctrl.NewControllerManagedBy(mgr).
For(&ingressv1alpha1.TCPEdge{}).
Watches(
&ingressv1alpha1.IPPolicy{},
r.controller.NewEnqueueRequestForMapFunc(r.listTCPEdgesForIPPolicy),
).
Complete(r)
}
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tcpedges,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tcpedges/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tcpedges/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile
func (r *TCPEdgeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
return r.controller.Reconcile(ctx, req, new(ingressv1alpha1.TCPEdge))
}
func (r *TCPEdgeReconciler) create(ctx context.Context, edge *ingressv1alpha1.TCPEdge) error {
log := ctrl.LoggerFrom(ctx)
if err := r.reconcileTunnelGroupBackend(ctx, edge); err != nil {
return err
}
if err := r.reserveAddrIfEmpty(ctx, edge); err != nil {
return err
}
// Try to find the edge by the backend labels
resp, err := r.findEdgeByBackendLabels(ctx, edge.Spec.Backend.Labels)
if err != nil {
return err
}
if resp != nil {
return r.updateEdge(ctx, edge, resp)
}
// No edge has been created for this edge, create one
log.Info("Creating new TCPEdge", "namespace", edge.Namespace, "name", edge.Name)
resp, err = r.NgrokClientset.TCPEdges().Create(ctx, &ngrok.TCPEdgeCreate{
Description: edge.Spec.Description,
Metadata: edge.Spec.Metadata,
Backend: &ngrok.EndpointBackendMutate{
BackendID: edge.Status.Backend.ID,
},
})
if err != nil {
return err
}
log.Info("Created new TCPEdge", "edge.ID", resp.ID, "name", edge.Name, "namespace", edge.Namespace)
return r.updateEdge(ctx, edge, resp)
}
func (r *TCPEdgeReconciler) update(ctx context.Context, edge *ingressv1alpha1.TCPEdge) error {
log := ctrl.LoggerFrom(ctx)
if err := r.reconcileTunnelGroupBackend(ctx, edge); err != nil {
return err
}
if err := r.reserveAddrIfEmpty(ctx, edge); err != nil {
return err
}
resp, err := r.NgrokClientset.TCPEdges().Get(ctx, edge.Status.ID)
if err != nil {
// If we can't find the edge in the ngrok API, it's been deleted, so clear the ID
// and requeue the edge. When it gets reconciled again, it will be recreated.
if ngrok.IsNotFound(err) {
log.Info("TCPEdge not found, clearing ID and requeuing", "edge.ID", edge.Status.ID)
edge.Status.ID = ""
//nolint:errcheck
r.Status().Update(ctx, edge)
}
return err
}
// If the backend or hostports do not match, update the edge with the desired backend and hostports
if resp.Backend.Backend.ID != edge.Status.Backend.ID ||
!slices.Equal(resp.Hostports, edge.Status.Hostports) {
resp, err = r.NgrokClientset.TCPEdges().Update(ctx, &ngrok.TCPEdgeUpdate{
ID: resp.ID,
Description: ptr.To(edge.Spec.Description),
Metadata: ptr.To(edge.Spec.Metadata),
Hostports: edge.Status.Hostports,
Backend: &ngrok.EndpointBackendMutate{
BackendID: edge.Status.Backend.ID,
},
})
if err != nil {
return err
}
}
return r.updateEdge(ctx, edge, resp)
}
func (r *TCPEdgeReconciler) delete(ctx context.Context, edge *ingressv1alpha1.TCPEdge) error {
err := r.NgrokClientset.TCPEdges().Delete(ctx, edge.Status.ID)
if err == nil || ngrok.IsNotFound(err) {
edge.Status.ID = ""
}
return err
}
func (r *TCPEdgeReconciler) reconcileTunnelGroupBackend(ctx context.Context, edge *ingressv1alpha1.TCPEdge) error {
log := ctrl.LoggerFrom(ctx)
specBackend := edge.Spec.Backend
// First make sure the tunnel group backend matches
if edge.Status.Backend.ID != "" {
log.WithValues("TunnelGroupBackend.ID", edge.Status.Backend.ID)
// A backend has already been created for this edge, make sure the labels match
backend, err := r.NgrokClientset.TunnelGroupBackends().Get(ctx, edge.Status.Backend.ID)
if err != nil {
if ngrok.IsNotFound(err) {
log.Info("TunnelGroupBackend not found, clearing ID and requeuing")
edge.Status.Backend.ID = ""
//nolint:errcheck
r.Status().Update(ctx, edge)
}
return err
}
// If the labels don't match, update the backend with the desired labels
if !maps.Equal(backend.Labels, specBackend.Labels) {
_, err = r.NgrokClientset.TunnelGroupBackends().Update(ctx, &ngrok.TunnelGroupBackendUpdate{
ID: backend.ID,
Metadata: ptr.To(specBackend.Metadata),
Description: ptr.To(specBackend.Description),
Labels: specBackend.Labels,
})
if err != nil {
return err
}
}
log.V(3).Info("Existing TunnelGroupBackend has matching labels", "labels", specBackend.Labels)
return nil
}
// No backend has been created for this edge, create one
backend, err := r.NgrokClientset.TunnelGroupBackends().Create(ctx, &ngrok.TunnelGroupBackendCreate{
Metadata: edge.Spec.Backend.Metadata,
Description: edge.Spec.Backend.Description,
Labels: edge.Spec.Backend.Labels,
})
if err != nil {
return err
}
edge.Status.Backend.ID = backend.ID
return r.Status().Update(ctx, edge)
}
func (r *TCPEdgeReconciler) findEdgeByBackendLabels(ctx context.Context, backendLabels map[string]string) (*ngrok.TCPEdge, error) {
log := ctrl.LoggerFrom(ctx)
log.Info("Searching for existing TCPEdge with backend labels", "labels", backendLabels)
iter := r.NgrokClientset.TCPEdges().List(&ngrok.Paging{})
for iter.Next(ctx) {
edge := iter.Item()
if edge.Backend == nil {
continue
}
backend, err := r.NgrokClientset.TunnelGroupBackends().Get(ctx, edge.Backend.Backend.ID)
if err != nil {
// The backend ID on the edge is invalid and no longer exists in the ngrok API,
// so we'll skip this edge check the next one.
if ngrok.IsNotFound(err) {
continue
}
// We've go an error besides not found. Return the error and
// hopefully the next reconcile will fix it.
return nil, err
}
if backend == nil {
continue
}
if maps.Equal(backend.Labels, backendLabels) {
log.Info("Found existing TCPEdge with matching backend labels", "labels", backendLabels, "edge.ID", edge.ID)
return edge, nil
}
}
return nil, iter.Err()
}
// Update the edge status and modules, called from both create and update.
func (r *TCPEdgeReconciler) updateEdge(ctx context.Context, edge *ingressv1alpha1.TCPEdge, remoteEdge *ngrok.TCPEdge) error {
if err := r.updateEdgeStatus(ctx, edge, remoteEdge); err != nil {
return err
}
if err := r.updateIPRestrictionModule(ctx, edge, remoteEdge); err != nil {
return err
}
return r.updatePolicyModule(ctx, edge, remoteEdge)
}
func (r *TCPEdgeReconciler) updateEdgeStatus(ctx context.Context, edge *ingressv1alpha1.TCPEdge, remoteEdge *ngrok.TCPEdge) error {
edge.Status.ID = remoteEdge.ID
edge.Status.URI = remoteEdge.URI
edge.Status.Hostports = remoteEdge.Hostports
edge.Status.Backend.ID = remoteEdge.Backend.Backend.ID
return r.Status().Update(ctx, edge)
}
func (r *TCPEdgeReconciler) reserveAddrIfEmpty(ctx context.Context, edge *ingressv1alpha1.TCPEdge) error {
log := ctrl.LoggerFrom(ctx)
if len(edge.Status.Hostports) == 0 {
metadata := ReservedAddrMetadata{
Namespace: edge.Namespace,
Name: edge.Name,
OwnerRef: metav1.GetControllerOf(edge),
}
log.V(3).Info("No hostports assigned to edge, assigning one or using existing one")
addr, err := r.findAddrWithMatchingMetadata(ctx, metadata)
if err != nil {
log.Error(err, "Failed to find addr with matching metadata")
return err
}
metadataBytes, err := json.Marshal(metadata)
if err != nil {
return err
}
// If we found an addr with matching metadata, use it
if addr != nil {
description := r.descriptionForEdge(edge)
metadata := string(metadataBytes)
// Update the addr description & metadata. We know the metadata matches, but the name for the edge may have changed
_, _ = r.NgrokClientset.TCPAddresses().Update(ctx, &ngrok.ReservedAddrUpdate{
ID: addr.ID,
Description: &description,
Metadata: &metadata,
})
log.V(3).Info("Found existing addr with matching metadata", "reservedAddr.ID", addr.ID, "reservedAddr.Addr", addr.Addr)
edge.Status.Hostports = []string{addr.Addr}
return r.Status().Update(ctx, edge)
}
// No hostports have been assigned to this edge, assign one
log.V(3).Info("Creating new reserved addr for edge")
addr, err = r.NgrokClientset.TCPAddresses().Create(ctx, &ngrok.ReservedAddrCreate{
Description: r.descriptionForEdge(edge),
Metadata: string(metadataBytes),
})
if err != nil {
return err
}
edge.Status.Hostports = []string{addr.Addr}
return r.Status().Update(ctx, edge)
}
log.V(3).Info("Hostports already assigned to edge", "hostports", edge.Status.Hostports)
return nil
}
func (r *TCPEdgeReconciler) findAddrWithMatchingMetadata(ctx context.Context, metadata ReservedAddrMetadata) (*ngrok.ReservedAddr, error) {
log := ctrl.LoggerFrom(ctx)
iter := r.NgrokClientset.TCPAddresses().List(&ngrok.Paging{})
for iter.Next(ctx) {
addr := iter.Item()
if addr.Metadata == "" {
continue
}
addrMetadata := ReservedAddrMetadata{}
// if we can't unmarshal the metadata, we can't match it, but just continue
if err := json.Unmarshal([]byte(addr.Metadata), &addrMetadata); err != nil {
log.V(3).Info("Failed to unmarshal addr metadata", "addr.ID", addr.ID, "err", err)
continue
}
if metadata.Matches(addrMetadata) {
return addr, nil
}
}
return nil, iter.Err()
}
func (r *TCPEdgeReconciler) descriptionForEdge(edge *ingressv1alpha1.TCPEdge) string {
return fmt.Sprintf("Reserved for %s/%s", edge.Namespace, edge.Name)
}
func (r *TCPEdgeReconciler) updateIPRestrictionModule(ctx context.Context, edge *ingressv1alpha1.TCPEdge, _ *ngrok.TCPEdge) error {
log := ctrl.LoggerFrom(ctx)
if edge.Spec.IPRestriction == nil || len(edge.Spec.IPRestriction.IPPolicies) == 0 {
return r.NgrokClientset.EdgeModules().TCP().IPRestriction().Delete(ctx, edge.Status.ID)
}
policyIds, err := r.IPPolicyResolver.ResolveIPPolicyNamesorIds(ctx, edge.Namespace, edge.Spec.IPRestriction.IPPolicies)
if err != nil {
return err
}
log.Info("Resolved IP Policy NamesOrIDs to IDs", "policyIds", policyIds)
_, err = r.NgrokClientset.EdgeModules().TCP().IPRestriction().Replace(ctx, &ngrok.EdgeIPRestrictionReplace{
ID: edge.Status.ID,
Module: ngrok.EndpointIPPolicyMutate{
IPPolicyIDs: policyIds,
},
})
return err
}
func (r *TCPEdgeReconciler) listTCPEdgesForIPPolicy(ctx context.Context, obj client.Object) []reconcile.Request {
log := ctrl.LoggerFrom(ctx)
log.Info("Listing TCPEdges for ip policy to determine if they need to be reconciled")
policy, ok := obj.(*ingressv1alpha1.IPPolicy)
if !ok {
log.Error(nil, "failed to convert object to IPPolicy", "object", obj)
return []reconcile.Request{}
}
edges := &ingressv1alpha1.TCPEdgeList{}
if err := r.Client.List(ctx, edges); err != nil {
log.Error(err, "failed to list TCPEdges for ippolicy", "name", policy.Name, "namespace", policy.Namespace)
return []reconcile.Request{}
}
recs := []reconcile.Request{}
for _, edge := range edges.Items {
if edge.Spec.IPRestriction == nil {
continue
}
for _, edgePolicyID := range edge.Spec.IPRestriction.IPPolicies {
if edgePolicyID == policy.Name || edgePolicyID == policy.Status.ID {
recs = append(recs, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: edge.GetName(),
Namespace: edge.GetNamespace(),
},
})
break
}
}
}
log.Info("IPPolicy change triggered TCPEdge reconciliation", "count", len(recs), "policy", policy.Name, "namespace", policy.Namespace)
return recs
}
func (r *TCPEdgeReconciler) updatePolicyModule(ctx context.Context, edge *ingressv1alpha1.TCPEdge, remoteEdge *ngrok.TCPEdge) error {
log := ctrl.LoggerFrom(ctx)
client := r.NgrokClientset.EdgeModules().TCP().TrafficPolicy()
trafficPolicy := edge.Spec.Policy
// Early return if nothing to be done
if trafficPolicy == nil {
if remoteEdge.TrafficPolicy == nil {
log.Info("Module matches desired state, skipping update", "module", "Traffic Policy", "comparison", routeModuleComparisonBothNil)
return nil
}
log.Info("Deleting Policy module")
return client.Delete(ctx, edge.Status.ID)
}
parsedTrafficPolicy, err := util.NewTrafficPolicyFromJson(trafficPolicy)
if err != nil {
r.Recorder.Eventf(edge, v1.EventTypeWarning, events.TrafficPolicyParseFailed, "Failed to parse Traffic Policy, possibly malformed.")
return err
}
if parsedTrafficPolicy.IsLegacyPolicy() {
r.Recorder.Eventf(edge, v1.EventTypeWarning, events.PolicyDeprecation, "Traffic Policy is using legacy directions: ['inbound', 'outbound']. Update to new phases: ['on_tcp_connect', 'on_http_request', 'on_http_response']")
}
if parsedTrafficPolicy.Enabled() != nil {
r.Recorder.Eventf(edge, v1.EventTypeWarning, events.PolicyDeprecation, "Traffic Policy has 'enabled' set. This is a legacy option that will stop being supported soon.")
}
apiTrafficPolicy, err := parsedTrafficPolicy.ToAPIJson()
if err != nil {
return err
}
r.Recorder.Eventf(edge, v1.EventTypeNormal, "Update", "Updating Traffic Policy on edge.")
_, err = client.Replace(ctx, &ngrok.EdgeTrafficPolicyReplace{
ID: remoteEdge.ID,
Module: ngrok.EndpointTrafficPolicy{
Enabled: parsedTrafficPolicy.Enabled(),
Value: string(apiTrafficPolicy),
},
})
if err == nil {
r.Recorder.Eventf(edge, v1.EventTypeNormal, "Update", "Traffic Policy successfully updated.")
}
return err
}
type ReservedAddrMetadata struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
OwnerRef *metav1.OwnerReference `json:"ownerRef,omitempty"`
}
// Matches returns true if the metadata is a match for the other metadata
func (m ReservedAddrMetadata) Matches(other ReservedAddrMetadata) bool {
// If the namespaces don't match, they're automatically not a match
if m.Namespace != other.Namespace {
return false
}
// If either have the owner reference set, we'll use those to compare
if m.OwnerRef != nil || other.OwnerRef != nil {
return reflect.DeepEqual(m.OwnerRef, other.OwnerRef)
}
return m.Name == other.Name
}
@@ -1,656 +0,0 @@
/*
MIT License
Copyright (c) 2022 ngrok, Inc.
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 ingress
import (
"context"
"errors"
"maps"
"slices"
"strconv"
"strings"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/go-logr/logr"
"github.com/ngrok/ngrok-api-go/v7"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/controller"
ierr "github.com/ngrok/ngrok-operator/internal/errors"
"github.com/ngrok/ngrok-operator/internal/events"
"github.com/ngrok/ngrok-operator/internal/ngrokapi"
"github.com/ngrok/ngrok-operator/internal/resolvers"
"github.com/ngrok/ngrok-operator/internal/util"
)
// TLSEdgeReconciler reconciles a TLSEdge object
type TLSEdgeReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
IPPolicyResolver resolvers.IPPolicyResolver
NgrokClientset ngrokapi.Clientset
controller *controller.BaseController[*ingressv1alpha1.TLSEdge]
}
// SetupWithManager sets up the controller with the Manager.
func (r *TLSEdgeReconciler) SetupWithManager(mgr ctrl.Manager) error {
if r.IPPolicyResolver == nil {
r.IPPolicyResolver = resolvers.NewDefaultIPPolicyResolver(mgr.GetClient())
}
r.controller = &controller.BaseController[*ingressv1alpha1.TLSEdge]{
Kube: r.Client,
Log: r.Log,
Recorder: r.Recorder,
StatusID: func(cr *ingressv1alpha1.TLSEdge) string { return cr.Status.ID },
Create: r.create,
Update: r.update,
Delete: r.delete,
ErrResult: func(_ controller.BaseControllerOp, _ *ingressv1alpha1.TLSEdge, err error) (ctrl.Result, error) {
if errors.As(err, &ierr.ErrInvalidConfiguration{}) {
return ctrl.Result{}, nil
}
if ngrok.IsErrorCode(err,
7117, // https://ngrok.com/docs/errors/err_ngrok_7117, domain not found
7132, // https://ngrok.com/docs/errors/err_ngrok_7132, hostport already in use
) {
return ctrl.Result{}, err
}
return controller.CtrlResultForErr(err)
},
}
controller := ctrl.NewControllerManagedBy(mgr).
For(&ingressv1alpha1.TLSEdge{}).
Watches(
&ingressv1alpha1.IPPolicy{},
r.controller.NewEnqueueRequestForMapFunc(r.listTLSEdgesForIPPolicy),
).
Watches(
&ingressv1alpha1.Domain{},
r.controller.NewEnqueueRequestForMapFunc(r.listTLSEdgesForDomain),
)
return controller.Complete(r)
}
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tlsedges,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tlsedges/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=tlsedges/finalizers,verbs=update
// +kubebuilder:rbac:groups=ingress.k8s.ngrok.com,resources=domains,verbs=get;list;watch;create;update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile
func (r *TLSEdgeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
return r.controller.Reconcile(ctx, req, new(ingressv1alpha1.TLSEdge))
}
func (r *TLSEdgeReconciler) create(ctx context.Context, edge *ingressv1alpha1.TLSEdge) error {
log := ctrl.LoggerFrom(ctx)
if err := r.reconcileDomains(ctx, edge); err != nil {
return err
}
if err := r.reconcileTunnelGroupBackend(ctx, edge); err != nil {
return err
}
// Try to find the edge by the backend labels
resp, err := r.findEdgeByBackendLabels(ctx, edge.Spec.Backend.Labels)
if err != nil {
return err
}
if resp != nil {
return r.updateEdge(ctx, edge, resp)
}
// No edge has been created for this edge, create one
log.Info("Creating new TLSEdge", "namespace", edge.Namespace, "name", edge.Name)
resp, err = r.NgrokClientset.TLSEdges().Create(ctx, &ngrok.TLSEdgeCreate{
Hostports: edge.Spec.Hostports,
Description: edge.Spec.Description,
Metadata: edge.Spec.Metadata,
Backend: &ngrok.EndpointBackendMutate{
BackendID: edge.Status.Backend.ID,
},
})
if err != nil {
return err
}
log.Info("Created new TLSEdge", "edge.ID", resp.ID, "name", edge.Name, "namespace", edge.Namespace)
return r.updateEdge(ctx, edge, resp)
}
func (r *TLSEdgeReconciler) update(ctx context.Context, edge *ingressv1alpha1.TLSEdge) error {
log := ctrl.LoggerFrom(ctx).WithValues("edge.ID", edge.Status.ID)
if err := r.reconcileDomains(ctx, edge); err != nil {
return err
}
if err := r.reconcileTunnelGroupBackend(ctx, edge); err != nil {
return err
}
log.Info("Fetching TLSEdge from ngrok API")
resp, err := r.NgrokClientset.TLSEdges().Get(ctx, edge.Status.ID)
if err != nil {
// If we can't find the edge in the ngrok API, it's been deleted, so clear the ID
// and requeue the edge. When it gets reconciled again, it will be recreated.
if ngrok.IsNotFound(err) {
log.Info("TLSEdge not found, clearing ID and requeuing")
edge.Status.ID = ""
//nolint:errcheck
r.Status().Update(ctx, edge)
}
return err
}
// If the backend or hostports do not match, update the edge with the desired backend and hostports
if resp.Backend.Backend.ID != edge.Status.Backend.ID ||
!slices.Equal(resp.Hostports, edge.Spec.Hostports) {
log.Info("Backend or hostports do not match, updating edge",
"expected.backend.ID", edge.Status.Backend.ID,
"actual.backend.ID", resp.Backend.Backend.ID,
"expected.hostports", edge.Spec.Hostports,
"actual.hostports", resp.Hostports,
)
resp, err = r.NgrokClientset.TLSEdges().Update(ctx, &ngrok.TLSEdgeUpdate{
ID: resp.ID,
Description: ptr.To(edge.Spec.Description),
Metadata: ptr.To(edge.Spec.Metadata),
Hostports: edge.Spec.Hostports,
Backend: &ngrok.EndpointBackendMutate{
BackendID: edge.Status.Backend.ID,
},
})
if err != nil {
return err
}
}
return r.updateEdge(ctx, edge, resp)
}
// Update the edge status and modules, called from both create and update.
func (r *TLSEdgeReconciler) updateEdge(ctx context.Context, edge *ingressv1alpha1.TLSEdge, resp *ngrok.TLSEdge) error {
if err := r.updateEdgeStatus(ctx, edge, resp); err != nil {
return err
}
if err := r.setTLSTermination(ctx, resp, edge.Spec.TLSTermination); err != nil {
return err
}
if err := r.setMutualTLS(ctx, resp, edge.Spec.MutualTLS); err != nil {
return err
}
if err := r.updateIPRestrictionModule(ctx, edge, resp); err != nil {
return err
}
return r.updatePolicyModule(ctx, edge, resp)
}
func (r *TLSEdgeReconciler) delete(ctx context.Context, edge *ingressv1alpha1.TLSEdge) error {
log := ctrl.LoggerFrom(ctx)
edgeID := edge.Status.ID
log.Info("Deleting TLSEdge", "edge.ID", edgeID)
err := r.NgrokClientset.TLSEdges().Delete(ctx, edgeID)
if err == nil || ngrok.IsNotFound(err) {
log.Info("Deleted TLSEdge", "edge.ID", edgeID)
edge.Status.ID = ""
if err := r.Client.Status().Update(ctx, edge); err != nil {
return err
}
}
return err
}
func (r *TLSEdgeReconciler) reconcileTunnelGroupBackend(ctx context.Context, edge *ingressv1alpha1.TLSEdge) error {
log := ctrl.LoggerFrom(ctx)
specBackend := edge.Spec.Backend
// First make sure the tunnel group backend matches
if edge.Status.Backend.ID != "" {
// A backend has already been created for this edge, make sure the labels match
backend, err := r.NgrokClientset.TunnelGroupBackends().Get(ctx, edge.Status.Backend.ID)
if err != nil {
if ngrok.IsNotFound(err) {
log.Info("TunnelGroupBackend not found, clearing ID and requeuing", "TunnelGroupBackend.ID", edge.Status.Backend.ID)
edge.Status.Backend.ID = ""
//nolint:errcheck
r.Status().Update(ctx, edge)
}
return err
}
// If the labels don't match, update the backend with the desired labels
if !maps.Equal(backend.Labels, specBackend.Labels) {
_, err = r.NgrokClientset.TunnelGroupBackends().Update(ctx, &ngrok.TunnelGroupBackendUpdate{
ID: backend.ID,
Metadata: ptr.To(specBackend.Metadata),
Description: ptr.To(specBackend.Description),
Labels: specBackend.Labels,
})
if err != nil {
return err
}
}
return nil
}
// No backend has been created for this edge, create one
backend, err := r.NgrokClientset.TunnelGroupBackends().Create(ctx, &ngrok.TunnelGroupBackendCreate{
Metadata: edge.Spec.Backend.Metadata,
Description: edge.Spec.Backend.Description,
Labels: edge.Spec.Backend.Labels,
})
if err != nil {
return err
}
edge.Status.Backend.ID = backend.ID
return r.Status().Update(ctx, edge)
}
func (r *TLSEdgeReconciler) setMutualTLS(ctx context.Context, edge *ngrok.TLSEdge, mutualTls *ingressv1alpha1.EndpointMutualTLS) error {
log := ctrl.LoggerFrom(ctx)
client := r.NgrokClientset.EdgeModules().TLS().MutualTLS()
if mutualTls == nil {
if edge.MutualTls == nil {
log.V(1).Info("Edge Mutual TLS matches spec")
return nil
}
log.Info("Deleting Edge Mutual TLS")
return client.Delete(ctx, edge.ID)
}
_, err := client.Replace(ctx, &ngrok.EdgeMutualTLSReplace{
ID: edge.ID,
Module: ngrok.EndpointMutualTLSMutate{
CertificateAuthorityIDs: mutualTls.CertificateAuthorities,
},
})
return err
}
func (r *TLSEdgeReconciler) setTLSTermination(ctx context.Context, edge *ngrok.TLSEdge, tlsTermination *ingressv1alpha1.EndpointTLSTermination) error {
log := ctrl.LoggerFrom(ctx)
client := r.NgrokClientset.EdgeModules().TLS().TLSTermination()
if tlsTermination == nil {
if edge.TlsTermination == nil {
log.V(1).Info("Edge TLS termination matches spec")
return nil
}
log.Info("Deleting Edge TLS termination")
return client.Delete(ctx, edge.ID)
}
_, err := client.Replace(ctx, &ngrok.EdgeTLSTerminationReplace{
ID: edge.ID,
Module: ngrok.EndpointTLSTermination{
TerminateAt: tlsTermination.TerminateAt,
MinVersion: tlsTermination.MinVersion,
},
})
return err
}
func (r *TLSEdgeReconciler) findEdgeByBackendLabels(ctx context.Context, backendLabels map[string]string) (*ngrok.TLSEdge, error) {
log := ctrl.LoggerFrom(ctx).WithValues("labels", backendLabels)
log.Info("Searching for existing TLSEdge with backend labels")
iter := r.NgrokClientset.TLSEdges().List(&ngrok.Paging{})
for iter.Next(ctx) {
edge := iter.Item()
if edge.Backend == nil {
continue
}
backend, err := r.NgrokClientset.TunnelGroupBackends().Get(ctx, edge.Backend.Backend.ID)
if err != nil {
// The backend ID on the edge is invalid and no longer exists in the ngrok API,
// so we'll skip this edge check the next one.
if ngrok.IsNotFound(err) {
continue
}
// We've go an error besides not found. Return the error and
// hopefully the next reconcile will fix it.
return nil, err
}
if backend == nil {
continue
}
if maps.Equal(backend.Labels, backendLabels) {
log.Info("Found existing TLSEdge with matching backend labels", "edge.ID", edge.ID)
return edge, nil
}
}
return nil, iter.Err()
}
func (r *TLSEdgeReconciler) updateEdgeStatus(ctx context.Context, edge *ingressv1alpha1.TLSEdge, remoteEdge *ngrok.TLSEdge) error {
log := ctrl.LoggerFrom(ctx)
log.V(1).Info("Updating Status", "edge.ID", remoteEdge.ID, "edge.hostports", remoteEdge.Hostports)
domains := &ingressv1alpha1.DomainList{}
if err := r.Client.List(ctx, domains, client.InNamespace(edge.Namespace)); err != nil {
return err
}
edgeDomainMap := make(map[string]bool)
for _, hp := range remoteEdge.Hostports {
host, _, err := parseHostAndPort(hp)
if err != nil {
return err
}
edgeDomainMap[host] = true
}
edge.Status.CNAMETargets = map[string]string{}
for _, domain := range domains.Items {
// We don't care about domains that aren't part of this edge
if _, ok := edgeDomainMap[domain.Spec.Domain]; !ok {
log.V(3).Info("Skipping domain not part of edge", "domain", domain.Spec.Domain)
continue
}
if domain.Status.CNAMETarget != nil {
log.V(3).Info("Adding CNAME target to status", "domain", domain.Spec.Domain, "cname", *domain.Status.CNAMETarget)
edge.Status.CNAMETargets[domain.Spec.Domain] = *domain.Status.CNAMETarget
}
}
edge.Status.ID = remoteEdge.ID
edge.Status.URI = remoteEdge.URI
edge.Status.Hostports = remoteEdge.Hostports
edge.Status.Backend.ID = remoteEdge.Backend.Backend.ID
return r.Status().Update(ctx, edge)
}
func (r *TLSEdgeReconciler) updateIPRestrictionModule(ctx context.Context, edge *ingressv1alpha1.TLSEdge, _ *ngrok.TLSEdge) error {
log := ctrl.LoggerFrom(ctx)
if edge.Spec.IPRestriction == nil || len(edge.Spec.IPRestriction.IPPolicies) == 0 {
return r.NgrokClientset.EdgeModules().TLS().IPRestriction().Delete(ctx, edge.Status.ID)
}
policyIds, err := r.IPPolicyResolver.ResolveIPPolicyNamesorIds(ctx, edge.Namespace, edge.Spec.IPRestriction.IPPolicies)
if err != nil {
return err
}
log.Info("Resolved IP Policy NamesOrIDs to IDs", "policyIds", policyIds)
_, err = r.NgrokClientset.EdgeModules().TLS().IPRestriction().Replace(ctx, &ngrok.EdgeIPRestrictionReplace{
ID: edge.Status.ID,
Module: ngrok.EndpointIPPolicyMutate{
IPPolicyIDs: policyIds,
},
})
return err
}
func (r *TLSEdgeReconciler) listTLSEdgesForIPPolicy(ctx context.Context, obj client.Object) []reconcile.Request {
log := ctrl.LoggerFrom(ctx)
log.Info("Listing TLSEdges for ip policy to determine if they need to be reconciled")
policy, ok := obj.(*ingressv1alpha1.IPPolicy)
if !ok {
log.Error(nil, "failed to convert object to IPPolicy", "object", obj)
return []reconcile.Request{}
}
edges := &ingressv1alpha1.TLSEdgeList{}
if err := r.Client.List(ctx, edges); err != nil {
log.Error(err, "failed to list TLSEdges for ippolicy", "name", policy.Name, "namespace", policy.Namespace)
return []reconcile.Request{}
}
recs := []reconcile.Request{}
for _, edge := range edges.Items {
if edge.Spec.IPRestriction == nil {
continue
}
for _, edgePolicyID := range edge.Spec.IPRestriction.IPPolicies {
if edgePolicyID == policy.Name || edgePolicyID == policy.Status.ID {
recs = append(recs, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: edge.GetName(),
Namespace: edge.GetNamespace(),
},
})
break
}
}
}
log.Info("IPPolicy change triggered TLSEdge reconciliation", "count", len(recs), "policy", policy.Name, "namespace", policy.Namespace)
return recs
}
func (r *TLSEdgeReconciler) listTLSEdgesForDomain(ctx context.Context, obj client.Object) []reconcile.Request {
log := ctrl.LoggerFrom(ctx)
log.Info("Listing TLSEdges for domain to determine if they need to be reconciled")
domain, ok := obj.(*ingressv1alpha1.Domain)
if !ok {
log.Error(nil, "failed to convert object to Domain", "object", obj)
return []reconcile.Request{}
}
log = log.WithValues("domain", domain.Name, "namespace", domain.Namespace)
edges := &ingressv1alpha1.TLSEdgeList{}
if err := r.Client.List(ctx, edges); err != nil {
log.Error(err, "failed to list TLSEdges for domain")
return []reconcile.Request{}
}
recs := []reconcile.Request{}
for _, edge := range edges.Items {
for _, hostport := range edge.Spec.Hostports {
host, _, err := parseHostAndPort(hostport)
if err != nil {
log.Error(err, "failed to parse host and port", "hostport", hostport)
continue
}
if host == domain.Spec.Domain {
log.V(1).Info("Found edge with matching hostport to reconcile for domain change", "edge", edge.Name)
recs = append(recs, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: edge.GetName(),
Namespace: edge.GetNamespace(),
},
})
break
}
}
}
log.V(1).Info("Domain change triggered TLSEdge reconciliation", "reconcile_requests", recs)
return recs
}
func (r *TLSEdgeReconciler) updatePolicyModule(ctx context.Context, edge *ingressv1alpha1.TLSEdge, remoteEdge *ngrok.TLSEdge) error {
log := ctrl.LoggerFrom(ctx)
client := r.NgrokClientset.EdgeModules().TLS().TrafficPolicy()
trafficPolicy := edge.Spec.Policy
// Early return if nothing to be done
if trafficPolicy == nil {
if remoteEdge.TrafficPolicy == nil {
log.Info("Module matches desired state, skipping update", "module", "Traffic Policy", "comparison", routeModuleComparisonBothNil)
return nil
}
log.Info("Deleting Traffic Policy module")
return client.Delete(ctx, edge.Status.ID)
}
parsedTrafficPolicy, err := util.NewTrafficPolicyFromJson(trafficPolicy)
if err != nil {
r.Recorder.Eventf(edge, v1.EventTypeWarning, events.TrafficPolicyParseFailed, "Failed to parse Traffic Policy, possibly malformed.")
return err
}
if parsedTrafficPolicy.IsLegacyPolicy() {
r.Recorder.Eventf(edge, v1.EventTypeWarning, events.PolicyDeprecation, "Traffic Policy is using legacy directions: ['inbound', 'outbound']. Update to new phases: ['on_tcp_connect', 'on_http_request', 'on_http_response']")
}
if parsedTrafficPolicy.Enabled() != nil {
r.Recorder.Eventf(edge, v1.EventTypeWarning, events.PolicyDeprecation, "Traffic Policy has 'enabled' set. This is a legacy option that will stop being supported soon.")
}
apiTrafficPolicy, err := parsedTrafficPolicy.ToAPIJson()
if err != nil {
return err
}
r.Recorder.Eventf(edge, v1.EventTypeNormal, "Update", "Updating Traffic Policy on edge.")
_, err = client.Replace(ctx, &ngrok.EdgeTrafficPolicyReplace{
ID: remoteEdge.ID,
Module: ngrok.EndpointTrafficPolicy{
Enabled: parsedTrafficPolicy.Enabled(),
Value: string(apiTrafficPolicy),
},
})
if err == nil {
r.Recorder.Eventf(edge, v1.EventTypeNormal, "Update", "Traffic Policy successfully updated.")
}
return err
}
func (r *TLSEdgeReconciler) reconcileDomains(ctx context.Context, edge *ingressv1alpha1.TLSEdge) error {
log := ctrl.LoggerFrom(ctx)
existing := make(map[string]bool)
domainList := &ingressv1alpha1.DomainList{}
if err := r.Client.List(ctx, domainList, client.InNamespace(edge.Namespace)); err != nil {
return err
}
for _, domain := range domainList.Items {
existing[domain.Spec.Domain] = true
}
// Get the desired domains
desiredDomains, err := r.getDesiredDomains(ctx, edge)
if err != nil {
return err
}
for _, domain := range desiredDomains {
// Already exists, skip
if _, ok := existing[domain.Spec.Domain]; ok {
continue
}
// Doesn't exist, create
log.Info("Creating domain", "name", domain.Name, "namespace", domain.Namespace)
if err := r.Client.Create(ctx, &domain); err != nil {
return err
}
}
return nil
}
func (r *TLSEdgeReconciler) getDesiredDomains(ctx context.Context, edge *ingressv1alpha1.TLSEdge) ([]ingressv1alpha1.Domain, error) {
log := ctrl.LoggerFrom(ctx)
log.V(3).Info("Calculating desired domains")
desired := []ingressv1alpha1.Domain{}
for _, hostport := range edge.Spec.Hostports {
host, _, err := parseHostAndPort(hostport)
if err != nil {
return nil, err
}
domain := ingressv1alpha1.Domain{
ObjectMeta: metav1.ObjectMeta{
Name: strings.ReplaceAll(host, ".", "-"),
Namespace: edge.Namespace,
Annotations: map[string]string{},
},
Spec: ingressv1alpha1.DomainSpec{
Domain: host,
},
}
desired = append(desired, domain)
}
return desired, nil
}
// parses the ngrok hostport string into a hostname and port
func parseHostAndPort(hostport string) (string, int32, error) {
pieces := strings.SplitN(hostport, ":", 2)
if len(pieces) != 2 {
return "", 0, errors.New("invalid hostport")
}
port, err := strconv.ParseInt(pieces[1], 10, 32)
if err != nil {
return "", 0, err
}
return pieces[0], int32(port), nil
}
@@ -85,13 +85,7 @@ func (r *NgrokTrafficPolicyReconciler) Reconcile(ctx context.Context, req ctrl.R
r.Recorder.Eventf(policy, v1.EventTypeWarning, events.PolicyDeprecation, "Traffic Policy has 'enabled' set. This is a legacy option that will stop being supported soon.")
}
if err := r.Driver.SyncEdges(ctx, r.Client); err != nil {
return ctrl.Result{}, err
}
if err := r.Driver.SyncEndpoints(ctx, r.Client); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
return ctrl.Result{}, r.Driver.SyncEndpoints(ctx, r.Client)
}
// SetupWithManager sets up the controller with the Manager.
-85
View File
@@ -4,11 +4,6 @@ import (
"context"
"github.com/ngrok/ngrok-api-go/v7"
tunnel_group_backends "github.com/ngrok/ngrok-api-go/v7/backends/tunnel_group"
https_edges "github.com/ngrok/ngrok-api-go/v7/edges/https"
https_edge_routes "github.com/ngrok/ngrok-api-go/v7/edges/https_routes"
tcp_edges "github.com/ngrok/ngrok-api-go/v7/edges/tcp"
tls_edges "github.com/ngrok/ngrok-api-go/v7/edges/tls"
"github.com/ngrok/ngrok-api-go/v7/endpoints"
"github.com/ngrok/ngrok-api-go/v7/ip_policies"
"github.com/ngrok/ngrok-api-go/v7/ip_policy_rules"
@@ -19,49 +14,31 @@ import (
type Clientset interface {
Domains() DomainClient
EdgeModules() EdgeModulesClientset
Endpoints() EndpointsClient
HTTPSEdges() HTTPSEdgeClient
HTTPSEdgeRoutes() HTTPSEdgeRoutesClient
IPPolicies() IPPoliciesClient
IPPolicyRules() IPPolicyRulesClient
KubernetesOperators() KubernetesOperatorsClient
TCPAddresses() TCPAddressesClient
TCPEdges() TCPEdgesClient
TLSEdges() TLSEdgesClient
TunnelGroupBackends() TunnelGroupBackendsClient
}
type DefaultClientset struct {
domainsClient *reserved_domains.Client
edgeModulesClientset *defaultEdgeModulesClientset
endpointsClient *endpoints.Client
httpsEdgesClient *https_edges.Client
httpsEdgeRoutesClient *https_edge_routes.Client
ipPoliciesClient *ip_policies.Client
ipPolicyRulesClient *ip_policy_rules.Client
kubernetesOperatorsClient *kubernetes_operators.Client
tcpAddrsClient *reserved_addrs.Client
tcpEdgesClient *tcp_edges.Client
tlsEdgesClient *tls_edges.Client
tunnelGroupBackendsClient *tunnel_group_backends.Client
}
// NewClientSet creates a new ClientSet from an ngrok client config.
func NewClientSet(config *ngrok.ClientConfig) *DefaultClientset {
return &DefaultClientset{
domainsClient: reserved_domains.NewClient(config),
edgeModulesClientset: newEdgeModulesClientset(config),
endpointsClient: endpoints.NewClient(config),
httpsEdgesClient: https_edges.NewClient(config),
httpsEdgeRoutesClient: https_edge_routes.NewClient(config),
ipPoliciesClient: ip_policies.NewClient(config),
ipPolicyRulesClient: ip_policy_rules.NewClient(config),
kubernetesOperatorsClient: kubernetes_operators.NewClient(config),
tcpAddrsClient: reserved_addrs.NewClient(config),
tcpEdgesClient: tcp_edges.NewClient(config),
tlsEdgesClient: tls_edges.NewClient(config),
tunnelGroupBackendsClient: tunnel_group_backends.NewClient(config),
}
}
@@ -97,10 +74,6 @@ func (c *DefaultClientset) Domains() DomainClient {
return c.domainsClient
}
func (c *DefaultClientset) EdgeModules() EdgeModulesClientset {
return c.edgeModulesClientset
}
type EndpointsClient interface {
Creator[*ngrok.EndpointCreate, *ngrok.Endpoint]
Reader[*ngrok.Endpoint]
@@ -113,29 +86,6 @@ func (c *DefaultClientset) Endpoints() EndpointsClient {
return c.endpointsClient
}
type HTTPSEdgeClient interface {
Creator[*ngrok.HTTPSEdgeCreate, *ngrok.HTTPSEdge]
Reader[*ngrok.HTTPSEdge]
Updater[*ngrok.HTTPSEdgeUpdate, *ngrok.HTTPSEdge]
Deletor
Lister[*ngrok.HTTPSEdge]
}
func (c *DefaultClientset) HTTPSEdges() HTTPSEdgeClient {
return c.httpsEdgesClient
}
type HTTPSEdgeRoutesClient interface {
Creator[*ngrok.HTTPSEdgeRouteCreate, *ngrok.HTTPSEdgeRoute]
Get(context.Context, *ngrok.EdgeRouteItem) (*ngrok.HTTPSEdgeRoute, error)
Updater[*ngrok.HTTPSEdgeRouteUpdate, *ngrok.HTTPSEdgeRoute]
Delete(context.Context, *ngrok.EdgeRouteItem) error
}
func (c *DefaultClientset) HTTPSEdgeRoutes() HTTPSEdgeRoutesClient {
return c.httpsEdgeRoutesClient
}
type IPPoliciesClient interface {
Creator[*ngrok.IPPolicyCreate, *ngrok.IPPolicy]
Reader[*ngrok.IPPolicy]
@@ -180,38 +130,3 @@ type TCPAddressesClient interface {
func (c *DefaultClientset) TCPAddresses() TCPAddressesClient {
return c.tcpAddrsClient
}
type TLSEdgesClient interface {
Creator[*ngrok.TLSEdgeCreate, *ngrok.TLSEdge]
Reader[*ngrok.TLSEdge]
Updater[*ngrok.TLSEdgeUpdate, *ngrok.TLSEdge]
Deletor
Lister[*ngrok.TLSEdge]
}
func (c *DefaultClientset) TLSEdges() TLSEdgesClient {
return c.tlsEdgesClient
}
type TCPEdgesClient interface {
Creator[*ngrok.TCPEdgeCreate, *ngrok.TCPEdge]
Reader[*ngrok.TCPEdge]
Updater[*ngrok.TCPEdgeUpdate, *ngrok.TCPEdge]
Deletor
Lister[*ngrok.TCPEdge]
}
func (c *DefaultClientset) TCPEdges() TCPEdgesClient {
return c.tcpEdgesClient
}
type TunnelGroupBackendsClient interface {
Creator[*ngrok.TunnelGroupBackendCreate, *ngrok.TunnelGroupBackend]
Reader[*ngrok.TunnelGroupBackend]
Updater[*ngrok.TunnelGroupBackendUpdate, *ngrok.TunnelGroupBackend]
Lister[*ngrok.TunnelGroupBackend]
}
func (c *DefaultClientset) TunnelGroupBackends() TunnelGroupBackendsClient {
return c.tunnelGroupBackendsClient
}
-4
View File
@@ -19,8 +19,4 @@ func ExampleClientset() {
cs := NewClientSet(config)
// Access a client for the domains API.
cs.Domains()
// Access a client for TCP Edge modules
cs.EdgeModules().TCP()
// Access a client for HTTPS Edge Route Modules
cs.EdgeModules().HTTPS().Routes().Compression()
}
@@ -1,52 +0,0 @@
package ngrokapi
import (
"context"
"github.com/ngrok/ngrok-api-go/v7"
)
type EdgeModulesClientset interface {
TCP() TCPEdgeModulesClientset
HTTPS() HTTPSEdgeModulesClientset
TLS() TLSEdgeModulesClientset
}
type edgeModulesClient[R, T any] interface {
Deletor
Replace(context.Context, R) (T, error)
}
type EdgeRouteModulesDeletor interface {
Delete(context.Context, *ngrok.EdgeRouteItem) error
}
type EdgeRouteModulesReplacer[R, T any] interface {
Replace(context.Context, R) (T, error)
}
type defaultEdgeModulesClientset struct {
tcp *defaultTCPEdgeModulesClientset
https *defaultHTTPSEdgeModulesClientset
tls *defaultTLSEdgeModulesClientset
}
func newEdgeModulesClientset(config *ngrok.ClientConfig) *defaultEdgeModulesClientset {
return &defaultEdgeModulesClientset{
tcp: newTCPEdgeModulesClientset(config),
https: newHTTPSEdgeModulesClientset(config),
tls: newTLSEdgeModulesClientset(config),
}
}
func (c *defaultEdgeModulesClientset) TCP() TCPEdgeModulesClientset {
return c.tcp
}
func (c *defaultEdgeModulesClientset) HTTPS() HTTPSEdgeModulesClientset {
return c.https
}
func (c *defaultEdgeModulesClientset) TLS() TLSEdgeModulesClientset {
return c.tls
}
@@ -1,171 +0,0 @@
package ngrokapi
import (
"github.com/ngrok/ngrok-api-go/v7"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_mutual_tls"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_backend"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_circuit_breaker"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_compression"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_ip_restriction"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_oauth"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_oidc"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_request_headers"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_response_headers"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_saml"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_traffic_policy"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_webhook_verification"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_route_websocket_tcp_converter"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/https_edge_tls_termination"
)
type HTTPSEdgeModulesClientset interface {
MutualTLS() HTTPSEdgeModulesMutualTLSClient
Routes() HTTPSEdgeRouteModulesClientset
TLSTermination() HTTPSEdgeModulesTLSTerminationClient
}
type (
HTTPSEdgeModulesMutualTLSClient = edgeModulesClient[*ngrok.EdgeMutualTLSReplace, *ngrok.EndpointMutualTLS]
HTTPSEdgeModulesTLSTerminationClient = edgeModulesClient[*ngrok.EdgeTLSTerminationAtEdgeReplace, *ngrok.EndpointTLSTermination]
)
type defaultHTTPSEdgeModulesClientset struct {
mutualTLS *https_edge_mutual_tls.Client
routes *defaultHTTPSEdgeRouteModulesClientset
tlsTermination *https_edge_tls_termination.Client
}
func newHTTPSEdgeModulesClientset(config *ngrok.ClientConfig) *defaultHTTPSEdgeModulesClientset {
return &defaultHTTPSEdgeModulesClientset{
mutualTLS: https_edge_mutual_tls.NewClient(config),
routes: newHTTPSEdgeRouteModulesClient(config),
tlsTermination: https_edge_tls_termination.NewClient(config),
}
}
func (c *defaultHTTPSEdgeModulesClientset) MutualTLS() HTTPSEdgeModulesMutualTLSClient {
return c.mutualTLS
}
func (c *defaultHTTPSEdgeModulesClientset) Routes() HTTPSEdgeRouteModulesClientset {
return c.routes
}
func (c *defaultHTTPSEdgeModulesClientset) TLSTermination() HTTPSEdgeModulesTLSTerminationClient {
return c.tlsTermination
}
type HTTPSEdgeRouteModulesClientset interface {
Backend() HTTPSEdgeRouteBackendClient
CircuitBreaker() HTTPSEdgeRouteCircuitBreakerClient
Compression() HTTPSEdgeRouteCompressionClient
IPRestriction() HTTPSEdgeRouteIPRestrictionClient
OAuth() HTTPSEdgeRouteOAuthClient
OIDC() HTTPSEdgeRouteOIDCClient
RequestHeaders() HTTPSEdgeRouteRequestHeadersClient
ResponseHeaders() HTTPSEdgeRouteResponseHeadersClient
SAML() HTTPSEdgeRouteSAMLClient
TrafficPolicy() HTTPSEdgeRouteTrafficPolicyClient
WebhookVerification() HTTPSEdgeRouteWebhookVerificationClient
WebsocketTCPConverter() HTTPSEdgeRouteWebsocketTCPConverterClient
}
type defaultHTTPSEdgeRouteModulesClientset struct {
backend *https_edge_route_backend.Client
circuitBreaker *https_edge_route_circuit_breaker.Client
compression *https_edge_route_compression.Client
ipRestriction *https_edge_route_ip_restriction.Client
oauth *https_edge_route_oauth.Client
trafficPolicy *https_edge_route_traffic_policy.Client
oidc *https_edge_route_oidc.Client
requestHeaders *https_edge_route_request_headers.Client
responseHeaders *https_edge_route_response_headers.Client
saml *https_edge_route_saml.Client
webhookVerification *https_edge_route_webhook_verification.Client
websocketTCPConverter *https_edge_route_websocket_tcp_converter.Client
}
func newHTTPSEdgeRouteModulesClient(config *ngrok.ClientConfig) *defaultHTTPSEdgeRouteModulesClientset {
return &defaultHTTPSEdgeRouteModulesClientset{
backend: https_edge_route_backend.NewClient(config),
circuitBreaker: https_edge_route_circuit_breaker.NewClient(config),
compression: https_edge_route_compression.NewClient(config),
ipRestriction: https_edge_route_ip_restriction.NewClient(config),
oauth: https_edge_route_oauth.NewClient(config),
trafficPolicy: https_edge_route_traffic_policy.NewClient(config),
oidc: https_edge_route_oidc.NewClient(config),
requestHeaders: https_edge_route_request_headers.NewClient(config),
responseHeaders: https_edge_route_response_headers.NewClient(config),
saml: https_edge_route_saml.NewClient(config),
webhookVerification: https_edge_route_webhook_verification.NewClient(config),
websocketTCPConverter: https_edge_route_websocket_tcp_converter.NewClient(config),
}
}
type httpsEdgeRouteModulesClient[R, T any] interface {
EdgeRouteModulesDeletor
EdgeRouteModulesReplacer[R, T]
}
type (
HTTPSEdgeRouteBackendClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteBackendReplace, *ngrok.EndpointBackend]
HTTPSEdgeRouteCircuitBreakerClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteCircuitBreakerReplace, *ngrok.EndpointCircuitBreaker]
HTTPSEdgeRouteCompressionClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteCompressionReplace, *ngrok.EndpointCompression]
HTTPSEdgeRouteIPRestrictionClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteIPRestrictionReplace, *ngrok.EndpointIPPolicy]
HTTPSEdgeRouteOAuthClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteOAuthReplace, *ngrok.EndpointOAuth]
HTTPSEdgeRouteOIDCClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteOIDCReplace, *ngrok.EndpointOIDC]
HTTPSEdgeRouteRequestHeadersClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteRequestHeadersReplace, *ngrok.EndpointRequestHeaders]
HTTPSEdgeRouteResponseHeadersClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteResponseHeadersReplace, *ngrok.EndpointResponseHeaders]
HTTPSEdgeRouteSAMLClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteSAMLReplace, *ngrok.EndpointSAML]
HTTPSEdgeRouteTrafficPolicyClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteTrafficPolicyReplace, *ngrok.EndpointTrafficPolicy]
HTTPSEdgeRouteWebhookVerificationClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteWebhookVerificationReplace, *ngrok.EndpointWebhookValidation]
HTTPSEdgeRouteWebsocketTCPConverterClient = httpsEdgeRouteModulesClient[*ngrok.EdgeRouteWebsocketTCPConverterReplace, *ngrok.EndpointWebsocketTCPConverter]
)
func (c *defaultHTTPSEdgeRouteModulesClientset) Backend() HTTPSEdgeRouteBackendClient {
return c.backend
}
func (c *defaultHTTPSEdgeRouteModulesClientset) CircuitBreaker() HTTPSEdgeRouteCircuitBreakerClient {
return c.circuitBreaker
}
func (c *defaultHTTPSEdgeRouteModulesClientset) Compression() HTTPSEdgeRouteCompressionClient {
return c.compression
}
func (c *defaultHTTPSEdgeRouteModulesClientset) IPRestriction() HTTPSEdgeRouteIPRestrictionClient {
return c.ipRestriction
}
func (c *defaultHTTPSEdgeRouteModulesClientset) OAuth() HTTPSEdgeRouteOAuthClient {
return c.oauth
}
func (c *defaultHTTPSEdgeRouteModulesClientset) TrafficPolicy() HTTPSEdgeRouteTrafficPolicyClient {
return c.trafficPolicy
}
func (c *defaultHTTPSEdgeRouteModulesClientset) OIDC() HTTPSEdgeRouteOIDCClient {
return c.oidc
}
func (c *defaultHTTPSEdgeRouteModulesClientset) RequestHeaders() HTTPSEdgeRouteRequestHeadersClient {
return c.requestHeaders
}
func (c *defaultHTTPSEdgeRouteModulesClientset) ResponseHeaders() HTTPSEdgeRouteResponseHeadersClient {
return c.responseHeaders
}
func (c *defaultHTTPSEdgeRouteModulesClientset) SAML() HTTPSEdgeRouteSAMLClient {
return c.saml
}
func (c *defaultHTTPSEdgeRouteModulesClientset) WebhookVerification() HTTPSEdgeRouteWebhookVerificationClient {
return c.webhookVerification
}
func (c *defaultHTTPSEdgeRouteModulesClientset) WebsocketTCPConverter() HTTPSEdgeRouteWebsocketTCPConverterClient {
return c.websocketTCPConverter
}
@@ -1,46 +0,0 @@
package ngrokapi
import (
"github.com/ngrok/ngrok-api-go/v7"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/tcp_edge_backend"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/tcp_edge_ip_restriction"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/tcp_edge_traffic_policy"
)
type TCPEdgeModulesClientset interface {
Backend() TCPEdgeModulesBackendClient
IPRestriction() TCPEdgeModulesIPRestrictionClient
TrafficPolicy() TCPEdgeModulesTrafficPolicyClient
}
type (
TCPEdgeModulesBackendClient = edgeModulesClient[*ngrok.EdgeBackendReplace, *ngrok.EndpointBackend]
TCPEdgeModulesIPRestrictionClient = edgeModulesClient[*ngrok.EdgeIPRestrictionReplace, *ngrok.EndpointIPPolicy]
TCPEdgeModulesTrafficPolicyClient = edgeModulesClient[*ngrok.EdgeTrafficPolicyReplace, *ngrok.EndpointTrafficPolicy]
)
type defaultTCPEdgeModulesClientset struct {
backend *tcp_edge_backend.Client
ipRestriction *tcp_edge_ip_restriction.Client
trafficPolicy *tcp_edge_traffic_policy.Client
}
func newTCPEdgeModulesClientset(config *ngrok.ClientConfig) *defaultTCPEdgeModulesClientset {
return &defaultTCPEdgeModulesClientset{
backend: tcp_edge_backend.NewClient(config),
ipRestriction: tcp_edge_ip_restriction.NewClient(config),
trafficPolicy: tcp_edge_traffic_policy.NewClient(config),
}
}
func (c *defaultTCPEdgeModulesClientset) Backend() TCPEdgeModulesBackendClient {
return c.backend
}
func (c *defaultTCPEdgeModulesClientset) IPRestriction() TCPEdgeModulesIPRestrictionClient {
return c.ipRestriction
}
func (c *defaultTCPEdgeModulesClientset) TrafficPolicy() TCPEdgeModulesTrafficPolicyClient {
return c.trafficPolicy
}
@@ -1,64 +0,0 @@
package ngrokapi
import (
"github.com/ngrok/ngrok-api-go/v7"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/tls_edge_backend"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/tls_edge_ip_restriction"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/tls_edge_mutual_tls"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/tls_edge_tls_termination"
"github.com/ngrok/ngrok-api-go/v7/edge_modules/tls_edge_traffic_policy"
)
type TLSEdgeModulesClientset interface {
Backend() TLSEdgeModulesBackendClient
IPRestriction() TLSEdgeModulesIPRestrictionClient
MutualTLS() TLSEdgeModulesMutualTLSClient
TLSTermination() TLSEdgeModulesTLSTerminationClient
TrafficPolicy() TLSEdgeModulesTrafficPolicyClient
}
type (
TLSEdgeModulesBackendClient = edgeModulesClient[*ngrok.EdgeBackendReplace, *ngrok.EndpointBackend]
TLSEdgeModulesIPRestrictionClient = edgeModulesClient[*ngrok.EdgeIPRestrictionReplace, *ngrok.EndpointIPPolicy]
TLSEdgeModulesMutualTLSClient = edgeModulesClient[*ngrok.EdgeMutualTLSReplace, *ngrok.EndpointMutualTLS]
TLSEdgeModulesTLSTerminationClient = edgeModulesClient[*ngrok.EdgeTLSTerminationReplace, *ngrok.EndpointTLSTermination]
TLSEdgeModulesTrafficPolicyClient = edgeModulesClient[*ngrok.EdgeTrafficPolicyReplace, *ngrok.EndpointTrafficPolicy]
)
type defaultTLSEdgeModulesClientset struct {
backend *tls_edge_backend.Client
ipRestriction *tls_edge_ip_restriction.Client
mutualTLS *tls_edge_mutual_tls.Client
tlsTermination *tls_edge_tls_termination.Client
trafficPolicy *tls_edge_traffic_policy.Client
}
func newTLSEdgeModulesClientset(config *ngrok.ClientConfig) *defaultTLSEdgeModulesClientset {
return &defaultTLSEdgeModulesClientset{
backend: tls_edge_backend.NewClient(config),
ipRestriction: tls_edge_ip_restriction.NewClient(config),
mutualTLS: tls_edge_mutual_tls.NewClient(config),
tlsTermination: tls_edge_tls_termination.NewClient(config),
trafficPolicy: tls_edge_traffic_policy.NewClient(config),
}
}
func (c *defaultTLSEdgeModulesClientset) Backend() TLSEdgeModulesBackendClient {
return c.backend
}
func (c *defaultTLSEdgeModulesClientset) IPRestriction() TLSEdgeModulesIPRestrictionClient {
return c.ipRestriction
}
func (c *defaultTLSEdgeModulesClientset) MutualTLS() TLSEdgeModulesMutualTLSClient {
return c.mutualTLS
}
func (c *defaultTLSEdgeModulesClientset) TLSTermination() TLSEdgeModulesTLSTerminationClient {
return c.tlsTermination
}
func (c *defaultTLSEdgeModulesClientset) TrafficPolicy() TLSEdgeModulesTrafficPolicyClient {
return c.trafficPolicy
}
-24
View File
@@ -51,9 +51,6 @@ type CacheStores struct {
// Ngrok Stores
DomainV1 cache.Store
TunnelV1 cache.Store
HTTPSEdgeV1 cache.Store
NgrokModuleV1 cache.Store
NgrokTrafficPolicyV1 cache.Store
AgentEndpointV1 cache.Store
CloudEndpointV1 cache.Store
@@ -81,9 +78,6 @@ func NewCacheStores(logger logr.Logger) CacheStores {
ReferenceGrant: cache.NewStore(keyFunc),
// Ngrok Stores
DomainV1: cache.NewStore(keyFunc),
TunnelV1: cache.NewStore(keyFunc),
HTTPSEdgeV1: cache.NewStore(keyFunc),
NgrokModuleV1: cache.NewStore(keyFunc),
NgrokTrafficPolicyV1: cache.NewStore(keyFunc),
AgentEndpointV1: cache.NewStore(keyFunc),
CloudEndpointV1: cache.NewStore(keyFunc),
@@ -152,12 +146,6 @@ func (c CacheStores) Get(obj runtime.Object) (item interface{}, exists bool, err
// ----------------------------------------------------------------------------
case *ingressv1alpha1.Domain:
return c.DomainV1.Get(obj)
case *ingressv1alpha1.Tunnel:
return c.TunnelV1.Get(obj)
case *ingressv1alpha1.HTTPSEdge:
return c.HTTPSEdgeV1.Get(obj)
case *ingressv1alpha1.NgrokModuleSet:
return c.NgrokModuleV1.Get(obj)
case *ngrokv1alpha1.NgrokTrafficPolicy:
return c.NgrokTrafficPolicyV1.Get(obj)
case *ngrokv1alpha1.AgentEndpoint:
@@ -213,12 +201,6 @@ func (c CacheStores) Add(obj runtime.Object) error {
// ----------------------------------------------------------------------------
case *ingressv1alpha1.Domain:
return c.DomainV1.Add(obj)
case *ingressv1alpha1.Tunnel:
return c.TunnelV1.Add(obj)
case *ingressv1alpha1.HTTPSEdge:
return c.HTTPSEdgeV1.Add(obj)
case *ingressv1alpha1.NgrokModuleSet:
return c.NgrokModuleV1.Add(obj)
case *ngrokv1alpha1.NgrokTrafficPolicy:
return c.NgrokTrafficPolicyV1.Add(obj)
case *ngrokv1alpha1.AgentEndpoint:
@@ -275,12 +257,6 @@ func (c CacheStores) Delete(obj runtime.Object) error {
// ----------------------------------------------------------------------------
case *ingressv1alpha1.Domain:
return c.DomainV1.Delete(obj)
case *ingressv1alpha1.Tunnel:
return c.TunnelV1.Delete(obj)
case *ingressv1alpha1.HTTPSEdge:
return c.HTTPSEdgeV1.Delete(obj)
case *ingressv1alpha1.NgrokModuleSet:
return c.NgrokModuleV1.Delete(obj)
case *ngrokv1alpha1.NgrokTrafficPolicy:
return c.NgrokTrafficPolicyV1.Delete(obj)
case *ngrokv1alpha1.AgentEndpoint:
+1 -58
View File
@@ -24,7 +24,6 @@ import (
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
"github.com/ngrok/ngrok-operator/internal/annotations"
"github.com/ngrok/ngrok-operator/internal/errors"
corev1 "k8s.io/api/core/v1"
@@ -53,7 +52,6 @@ type Storer interface {
GetNamespaceV1(name string) (*corev1.Namespace, error)
GetConfigMapV1(name, namespace string) (*corev1.ConfigMap, error)
GetNgrokIngressV1(name, namespace string) (*netv1.Ingress, error)
GetNgrokModuleSetV1(name, namespace string) (*ingressv1alpha1.NgrokModuleSet, error)
GetNgrokTrafficPolicyV1(name, namespace string) (*ngrokv1alpha1.NgrokTrafficPolicy, error)
GetGateway(name string, namespace string) (*gatewayv1.Gateway, error)
GetGatewayClass(name string) (*gatewayv1.GatewayClass, error)
@@ -75,9 +73,6 @@ type Storer interface {
ListReferenceGrants() []*gatewayv1beta1.ReferenceGrant
ListDomainsV1() []*ingressv1alpha1.Domain
ListTunnelsV1() []*ingressv1alpha1.Tunnel
ListHTTPSEdgesV1() []*ingressv1alpha1.HTTPSEdge
ListNgrokModuleSetsV1() []*ingressv1alpha1.NgrokModuleSet
}
// Store implements Storer and can be used to list Ingress, Services
@@ -160,10 +155,6 @@ func (s Store) GetNgrokIngressV1(name, namespace string) (*netv1.Ingress, error)
return ing, nil
}
func (s Store) GetNgrokModuleSetV1(name, namespace string) (*ingressv1alpha1.NgrokModuleSet, error) {
return genericGetByKey[ingressv1alpha1.NgrokModuleSet](s.stores.NgrokModuleV1, getKey(name, namespace))
}
func (s Store) GetNgrokTrafficPolicyV1(name, namespace string) (*ngrokv1alpha1.NgrokTrafficPolicy, error) {
return genericGetByKey[ngrokv1alpha1.NgrokTrafficPolicy](s.stores.NgrokTrafficPolicyV1, getKey(name, namespace))
}
@@ -285,21 +276,6 @@ func (s Store) ListDomainsV1() []*ingressv1alpha1.Domain {
return genericListSorted[ingressv1alpha1.Domain](s.log, s.stores.DomainV1)
}
// ListTunnelsV1 returns the list of Tunnels in the Tunnel v1 store.
func (s Store) ListTunnelsV1() []*ingressv1alpha1.Tunnel {
return genericListSorted[ingressv1alpha1.Tunnel](s.log, s.stores.TunnelV1)
}
// ListHTTPSEdgesV1 returns the list of HTTPSEdges in the HTTPSEdge v1 store.
func (s Store) ListHTTPSEdgesV1() []*ingressv1alpha1.HTTPSEdge {
return genericListSorted[ingressv1alpha1.HTTPSEdge](s.log, s.stores.HTTPSEdgeV1)
}
// ListNgrokModuleSetsV1 returns the list of NgrokModules in the NgrokModuleSet v1 store.
func (s Store) ListNgrokModuleSetsV1() []*ingressv1alpha1.NgrokModuleSet {
return genericListSorted[ingressv1alpha1.NgrokModuleSet](s.log, s.stores.NgrokModuleV1)
}
func genericList[T any, PT interface {
*T
client.Object
@@ -378,13 +354,6 @@ func (s Store) shouldHandleIngressCheckClass(ing *netv1.Ingress) (bool, error) {
// shouldHandleIngressIsValid checks if the ingress spec meets controller requirements.
func (s Store) shouldHandleIngressIsValid(ing *netv1.Ingress) (bool, error) {
errs := errors.NewErrInvalidIngressSpec()
useEdges, err := annotations.ExtractUseEdges(ing)
if err != nil {
errs.AddError(fmt.Sprintf("failed to check %q annotation. defaulting to using endpoints: %s",
annotations.MappingStrategyAnnotation,
err.Error(),
))
}
if len(ing.Spec.Rules) == 0 {
errs.AddError("At least one rule is required to be set")
} else {
@@ -392,38 +361,12 @@ func (s Store) shouldHandleIngressIsValid(ing *netv1.Ingress) (bool, error) {
if rule.Host == "" {
errs.AddError("A host is required to be set for each rule")
}
if rule.HTTP != nil {
for _, path := range rule.HTTP.Paths {
switch {
case path.Backend.Resource != nil:
if useEdges {
errs.AddError(fmt.Sprintf("Resource backends are not supported for ingresses with the %q: %q annotation. Ingresses provided by endpoints instead of edges do support default backends",
annotations.MappingStrategyAnnotation,
annotations.MappingStrategy_Edges,
))
}
case path.Backend.Service == nil:
errs.AddError(fmt.Sprintf("A valid service backend is required for this ingress since a resource backend was not provided (resource backends are only supported for ingresses without the %q: %q annotation.)",
annotations.MappingStrategyAnnotation,
annotations.MappingStrategy_Edges,
))
}
}
} else {
if rule.HTTP == nil {
errs.AddError("HTTP rules are required for ingress")
}
}
}
if ing.Spec.DefaultBackend != nil {
if useEdges {
errs.AddError(fmt.Sprintf("Default backends are not supported for ingresses with the %q: %q annotation. Ingresses provided by endpoints instead of edges do support default backends",
annotations.MappingStrategyAnnotation,
annotations.MappingStrategy_Edges,
))
}
}
if errs.HasErrors() {
return false, errs
}
+2 -48
View File
@@ -465,51 +465,6 @@ var _ = Describe("Store", func() {
)
})
var _ = Describe("ListNgrokModulesV1", func() {
Context("when there are NgrokModuleSets", func() {
BeforeEach(func() {
m1 := testutils.NewTestNgrokModuleSet("ngrok", "test", true)
Expect(store.Add(&m1)).To(BeNil())
m2 := testutils.NewTestNgrokModuleSet("ngrok", "test2", true)
Expect(store.Add(&m2)).To(BeNil())
m3 := testutils.NewTestNgrokModuleSet("test", "test", true)
Expect(store.Add(&m3)).To(BeNil())
})
It("returns the NgrokModuleSet", func() {
modules := store.ListNgrokModuleSetsV1()
Expect(len(modules)).To(Equal(3))
})
})
Context("when there are no NgrokModuleSets", func() {
It("doesn't error", func() {
modules := store.ListNgrokModuleSetsV1()
Expect(len(modules)).To(Equal(0))
})
})
})
var _ = Describe("GetNgrokModuleSetV1", func() {
Context("when the NgrokModuleSet exists", func() {
BeforeEach(func() {
m := testutils.NewTestNgrokModuleSet("ngrok", "test", true)
Expect(store.Add(&m)).To(BeNil())
})
It("returns the NgrokModuleSet", func() {
modset, err := store.GetNgrokModuleSetV1("ngrok", "test")
Expect(err).ToNot(HaveOccurred())
Expect(modset.Modules.Compression.Enabled).To(Equal(true))
})
})
Context("when the NgrokModuleSet does not exist", func() {
It("returns an error", func() {
modset, err := store.GetNgrokModuleSetV1("does-not-exist", "does-not-exist")
Expect(err).To(HaveOccurred())
Expect(errors.IsErrorNotFound(err)).To(Equal(true))
Expect(modset).To(BeNil())
})
})
})
var _ = Describe("GetNgrokTrafficPolicyV1", func() {
Context("when the NgrokTrafficPolicy exists", func() {
BeforeEach(func() {
@@ -662,9 +617,8 @@ var _ = Describe("Store", func() {
},
}
ok, err := store.shouldHandleIngressIsValid(ing)
Expect(ok).To(BeFalse())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Default backends are not supported"))
Expect(ok).To(BeTrue())
Expect(err).To(Not(HaveOccurred()))
})
})
+48 -23
View File
@@ -118,6 +118,11 @@ func RandomName(prefix string) string {
return prefix + "-" + rand.String(5)
}
// RandomURL generates a random URL.
func RandomURL() string {
return "https://" + RandomName("test-url") + ".ngrok.io"
}
// NewGatewayClass creates a new GatewayClass with a random name to be used in tests. If
// isManaged is true, the controller name will be set to the ngrok gateway controller name.
// If isManaged is false, the controller name will be set to a different value.
@@ -249,29 +254,6 @@ func NewReferenceGrant(name string, namespace string) gatewayv1beta1.ReferenceGr
}
}
func NewHTTPSEdge(name string, namespace string) ingressv1alpha1.HTTPSEdge {
return ingressv1alpha1.HTTPSEdge{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
}
}
func NewTestNgrokModuleSet(name string, namespace string, compressionEnabled bool) ingressv1alpha1.NgrokModuleSet {
return ingressv1alpha1.NgrokModuleSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Modules: ingressv1alpha1.NgrokModuleSetModules{
Compression: &ingressv1alpha1.EndpointCompression{
Enabled: compressionEnabled,
},
},
}
}
func NewTestNgrokTrafficPolicy(name string, namespace string, policyStr string) ngrokv1alpha1.NgrokTrafficPolicy {
return ngrokv1alpha1.NgrokTrafficPolicy{
ObjectMeta: metav1.ObjectMeta{
@@ -283,3 +265,46 @@ func NewTestNgrokTrafficPolicy(name string, namespace string, policyStr string)
},
}
}
type CloudEndpointOpt func(*ngrokv1alpha1.CloudEndpoint)
// NewCloudEndpoint creates a new CloudEndpoint with the given name and namespace.
// The URL is set to a random name for testing purposes.
// You can pass additional options to customize the CloudEndpoint further.
// Example usage:
//
// NewCloudEndpoint
func NewCloudEndpoint(opts ...CloudEndpointOpt) *ngrokv1alpha1.CloudEndpoint {
clep := &ngrokv1alpha1.CloudEndpoint{
ObjectMeta: metav1.ObjectMeta{
Name: RandomName("cloud-endpoint"),
Namespace: RandomName("namespace"),
},
Spec: ngrokv1alpha1.CloudEndpointSpec{
URL: RandomName("url"),
},
}
for _, opt := range opts {
opt(clep)
}
return clep
}
type AgentEndpointOpt func(*ngrokv1alpha1.AgentEndpoint)
func NewAgentEndpoint(opts ...AgentEndpointOpt) *ngrokv1alpha1.AgentEndpoint {
aep := &ngrokv1alpha1.AgentEndpoint{
ObjectMeta: metav1.ObjectMeta{
Name: RandomName("agent-endpoint"),
Namespace: RandomName("namespace"),
},
Spec: ngrokv1alpha1.AgentEndpointSpec{
URL: RandomURL(),
},
}
for _, opt := range opts {
opt(aep)
}
return aep
}
-376
View File
@@ -1,17 +1,7 @@
package util
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/errors"
"github.com/ngrok/ngrok-operator/internal/resolvers"
"github.com/ngrok/ngrok-operator/internal/trafficpolicy"
"k8s.io/utils/ptr"
)
type ActionType string
@@ -288,369 +278,3 @@ func mergeSinglePhase(originalTP map[string][]RawRule, addedRules []RawRule, pha
originalTP[phase] = addedRules
}
func NewTrafficPolicyFromModuleset(ctx context.Context, ms *ingressv1alpha1.NgrokModuleSet, secretResolver resolvers.SecretResolver, ipPolicyResolver resolvers.IPPolicyResolver) (*trafficpolicy.TrafficPolicy, error) {
if ms == nil {
return nil, nil
}
tp := trafficpolicy.NewTrafficPolicy()
converters := []modulesetConverterFunc{
// On TCP Connect Rules (IP Restriction & Mutual TLS)
convertModuleSetIPRestriction(ipPolicyResolver),
convertModuleSetTLS,
// On HTTP Request Rules
convertModuleSetOAuth(secretResolver),
convertModuleSetOIDC(secretResolver),
convertModuleSetSAML,
convertModuleSetCompression,
convertModuleSetCircuitBreaker,
convertModuleSetHeaders,
convertModuleSetWebhookVerification(secretResolver),
}
for _, converter := range converters {
if err := converter(ctx, *ms, tp); err != nil {
return nil, err
}
}
if tp.IsEmpty() {
return nil, nil
}
return tp, nil
}
type modulesetConverterFunc func(ctx context.Context, ms ingressv1alpha1.NgrokModuleSet, tp *trafficpolicy.TrafficPolicy) error
func convertModuleSetTLS(_ context.Context, ms ingressv1alpha1.NgrokModuleSet, tp *trafficpolicy.TrafficPolicy) error {
if ms.Modules.TLSTermination == nil && ms.Modules.MutualTLS == nil {
return nil
}
tlsConfig := trafficpolicy.TLSTerminationConfig{}
if ms.Modules.TLSTermination != nil {
tlsConfig.MinVersion = ms.Modules.TLSTermination.MinVersion
}
if ms.Modules.MutualTLS != nil {
tlsConfig.MutualTLSCertificateAuthorities = ms.Modules.MutualTLS.CertificateAuthorities
}
rule := trafficpolicy.Rule{
Actions: []trafficpolicy.Action{
trafficpolicy.NewTerminateTLSAction(tlsConfig),
},
}
tp.AddRuleOnTCPConnect(rule)
return nil
}
func convertModuleSetIPRestriction(ipPolicyResolver resolvers.IPPolicyResolver) modulesetConverterFunc {
return func(ctx context.Context, ms ingressv1alpha1.NgrokModuleSet, tp *trafficpolicy.TrafficPolicy) error {
if ms.Modules.IPRestriction == nil {
return nil
}
ipPolicies, err := ipPolicyResolver.ResolveIPPolicyNamesorIds(ctx, ms.Namespace, ms.Modules.IPRestriction.IPPolicies)
if err != nil {
return err
}
tp.AddRuleOnTCPConnect(
trafficpolicy.Rule{
Actions: []trafficpolicy.Action{
trafficpolicy.NewRestricIPsActionFromIPPolicies(ipPolicies),
},
},
)
return nil
}
}
func convertModuleSetCompression(_ context.Context, ms ingressv1alpha1.NgrokModuleSet, tp *trafficpolicy.TrafficPolicy) error {
if ms.Modules.Compression == nil {
return nil
}
tp.AddRuleOnHTTPResponse(
trafficpolicy.Rule{
Actions: []trafficpolicy.Action{
trafficpolicy.NewCompressResponseAction(nil),
},
},
)
return nil
}
func convertModuleSetCircuitBreaker(_ context.Context, ms ingressv1alpha1.NgrokModuleSet, tp *trafficpolicy.TrafficPolicy) error {
if ms.Modules.CircuitBreaker == nil {
return nil
}
var volumeThreshold *uint32
if ms.Modules.CircuitBreaker.VolumeThreshold != 0 {
volumeThreshold = ptr.To(ms.Modules.CircuitBreaker.VolumeThreshold)
}
var windowDuration *time.Duration
if ms.Modules.CircuitBreaker.RollingWindow.Duration != 0 {
windowDuration = ptr.To(ms.Modules.CircuitBreaker.RollingWindow.Duration)
}
var trippedDuration *time.Duration
if ms.Modules.CircuitBreaker.TrippedDuration.Duration != 0 {
trippedDuration = ptr.To(ms.Modules.CircuitBreaker.TrippedDuration.Duration)
}
tp.AddRuleOnHTTPRequest(
trafficpolicy.Rule{
Actions: []trafficpolicy.Action{
trafficpolicy.NewCircuitBreakerAction(
ms.Modules.CircuitBreaker.ErrorThresholdPercentage.AsApproximateFloat64(),
volumeThreshold,
windowDuration,
trippedDuration,
),
},
},
)
return nil
}
func convertModuleSetHeaders(_ context.Context, ms ingressv1alpha1.NgrokModuleSet, tp *trafficpolicy.TrafficPolicy) error {
if ms.Modules.Headers == nil {
return nil
}
if ms.Modules.Headers.Request != nil {
actions := []trafficpolicy.Action{}
if len(ms.Modules.Headers.Request.Remove) > 0 {
actions = append(actions, trafficpolicy.NewRemoveHeadersAction(ms.Modules.Headers.Request.Remove))
}
if len(ms.Modules.Headers.Request.Add) > 0 {
actions = append(actions, trafficpolicy.NewAddHeadersAction(ms.Modules.Headers.Request.Add))
}
if len(actions) > 0 {
tp.AddRuleOnHTTPRequest(trafficpolicy.Rule{Actions: actions})
}
}
if ms.Modules.Headers.Response != nil {
actions := []trafficpolicy.Action{}
if len(ms.Modules.Headers.Response.Remove) > 0 {
actions = append(actions, trafficpolicy.NewRemoveHeadersAction(ms.Modules.Headers.Response.Remove))
}
if len(ms.Modules.Headers.Response.Add) > 0 {
actions = append(actions, trafficpolicy.NewAddHeadersAction(ms.Modules.Headers.Response.Add))
}
if len(actions) > 0 {
tp.AddRuleOnHTTPResponse(trafficpolicy.Rule{Actions: actions})
}
}
return nil
}
func convertModuleSetWebhookVerification(secretResolver resolvers.SecretResolver) modulesetConverterFunc {
return func(ctx context.Context, ms ingressv1alpha1.NgrokModuleSet, tp *trafficpolicy.TrafficPolicy) error {
if ms.Modules.WebhookVerification == nil {
return nil
}
provider := ms.Modules.WebhookVerification.Provider
secretRef := ms.Modules.WebhookVerification.SecretRef
// resolve the secretRef to a secret
secret, err := secretResolver.GetSecret(ctx, ms.Namespace, secretRef.Name, secretRef.Key)
if err != nil {
return err
}
tp.AddRuleOnHTTPRequest(
trafficpolicy.Rule{
Actions: []trafficpolicy.Action{
trafficpolicy.NewWebhookVerificationAction(provider, secret),
},
},
)
return nil
}
}
func convertModuleSetSAML(_ context.Context, ms ingressv1alpha1.NgrokModuleSet, _ *trafficpolicy.TrafficPolicy) error {
if ms.Modules.SAML == nil {
return nil
}
return errors.NewErrModulesetNotConvertibleToTrafficPolicy("SAML module is not supported at this time")
}
func convertModuleSetOIDC(secretResolver resolvers.SecretResolver) modulesetConverterFunc {
return func(ctx context.Context, ms ingressv1alpha1.NgrokModuleSet, tp *trafficpolicy.TrafficPolicy) error {
if ms.Modules.OIDC == nil {
return nil
}
mod := ms.Modules.OIDC
var config trafficpolicy.OIDCConfig
config.IssuerURL = mod.Issuer
config.Scopes = mod.Scopes
if mod.OptionsPassthrough {
config.AllowCORSPreflight = &mod.OptionsPassthrough
}
if mod.ClientID != "" {
config.ClientID = &mod.ClientID
}
if mod.ClientSecret.Name != "" && mod.ClientSecret.Key != "" {
secret, err := secretResolver.GetSecret(ctx, ms.Namespace, mod.ClientSecret.Name, mod.ClientSecret.Key)
if err != nil {
return err
}
config.ClientSecret = &secret
}
if mod.MaximumDuration.Duration > 0 {
config.MaxSessionDuration = &mod.MaximumDuration.Duration
}
if mod.InactivityTimeout.Duration > 0 {
config.IdleSessionTimeout = &mod.InactivityTimeout.Duration
}
tp.AddRuleOnHTTPRequest(
trafficpolicy.Rule{
Actions: []trafficpolicy.Action{
trafficpolicy.NewOIDCAction(config),
},
},
)
return nil
}
}
func convertModuleSetOAuth(secretRsolver resolvers.SecretResolver) modulesetConverterFunc {
return func(ctx context.Context, ms ingressv1alpha1.NgrokModuleSet, tp *trafficpolicy.TrafficPolicy) error {
if ms.Modules.OAuth == nil {
return nil
}
var config trafficpolicy.OAuthConfig
var common ingressv1alpha1.OAuthProviderCommon
switch {
case ms.Modules.OAuth.Amazon != nil:
config.Provider = "amazon"
common = ms.Modules.OAuth.Amazon.OAuthProviderCommon
case ms.Modules.OAuth.Facebook != nil:
config.Provider = "facebook"
common = ms.Modules.OAuth.Facebook.OAuthProviderCommon
case ms.Modules.OAuth.Github != nil:
config.Provider = "github"
common = ms.Modules.OAuth.Github.OAuthProviderCommon
case ms.Modules.OAuth.Gitlab != nil:
config.Provider = "gitlab"
common = ms.Modules.OAuth.Gitlab.OAuthProviderCommon
case ms.Modules.OAuth.Google != nil:
config.Provider = "google"
common = ms.Modules.OAuth.Google.OAuthProviderCommon
case ms.Modules.OAuth.Microsoft != nil:
config.Provider = "microsoft"
common = ms.Modules.OAuth.Microsoft.OAuthProviderCommon
case ms.Modules.OAuth.Linkedin != nil:
config.Provider = "linkedin"
common = ms.Modules.OAuth.Linkedin.OAuthProviderCommon
case ms.Modules.OAuth.Twitch != nil:
config.Provider = "twitch"
common = ms.Modules.OAuth.Twitch.OAuthProviderCommon
default:
return errors.NewErrModulesetNotConvertibleToTrafficPolicy("Unable to determine OAuth provider")
}
config.ClientID = common.ClientID
if common.ClientSecret != nil {
secret, err := secretRsolver.GetSecret(ctx, ms.Namespace, common.ClientSecret.Name, common.ClientSecret.Key)
if err != nil {
return err
}
config.ClientSecret = &secret
}
if common.OptionsPassthrough {
config.AllowCORSPreflight = &common.OptionsPassthrough
}
config.Scopes = common.Scopes
if common.InactivityTimeout.Duration > 0 {
config.IdleSessionTimeout = &common.InactivityTimeout.Duration
}
if common.MaximumDuration.Duration > 0 {
config.MaxSessionDuration = &common.MaximumDuration.Duration
}
if common.AuthCheckInterval.Duration > 0 {
config.UserinfoRefreshInterval = &common.AuthCheckInterval.Duration
}
tp.AddRuleOnHTTPRequest(
trafficpolicy.Rule{
Actions: []trafficpolicy.Action{
trafficpolicy.NewOAuthAction(config),
},
},
)
emailExpressions := []string{}
if len(common.EmailAddresses) > 0 {
emails := make([]string, len(common.EmailAddresses))
for i, email := range common.EmailAddresses {
emails[i] = fmt.Sprintf("'%s'", email)
}
cond := fmt.Sprintf("!actions.ngrok.oauth.identity.email in [%s]", strings.Join(emails, ","))
emailExpressions = append(emailExpressions, cond)
}
if len(common.EmailDomains) > 0 {
domains := make([]string, len(common.EmailDomains))
for i, domain := range common.EmailDomains {
domains[i] = fmt.Sprintf("'%s'", domain)
}
cond := fmt.Sprintf("![%s].exists(d, actions.ngrok.oauth.identity.email.endsWith(d))", strings.Join(domains, ","))
emailExpressions = append(emailExpressions, cond)
}
// If there are email expressions, filtering to only allow certain emails or domains, return a 403 Forbidden
// response for any requests that don't match
if len(emailExpressions) > 0 {
tp.AddRuleOnHTTPRequest(
trafficpolicy.Rule{
Expressions: []string{
strings.Join(emailExpressions, " || "),
},
Actions: []trafficpolicy.Action{
trafficpolicy.NewCustomResponseAction(403, "Forbidden", nil),
},
},
)
}
return nil
}
}
-399
View File
@@ -3,16 +3,9 @@ package util
import (
"encoding/json"
"testing"
"time"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/errors"
"github.com/ngrok/ngrok-operator/internal/resolvers"
"github.com/ngrok/ngrok-operator/internal/trafficpolicy"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
)
func TestIsLegacyPolicy(t *testing.T) {
@@ -595,395 +588,3 @@ func newBaseTrafficPolicy(t *testing.T, enabled *bool) *trafficPolicyImpl {
enabled: enabled,
}
}
func TestNewTrafficPolicyFromModuleset(t *testing.T) {
namespace := "default"
secretName := "webhook-secret"
secretKey := "my-key"
secretValue := "shhhhhhhhh"
secretResolver := resolvers.NewStaticSecretResolver()
secretResolver.AddSecret(namespace, secretName, secretKey, secretValue)
ipPolicyResolver := resolvers.NewStaticIPPolicyResolver()
ipPolicyResolver.AddIPPolicy(namespace, "my-ip-policy", "ipp_123456789012345678901234567")
msObjectMeta := metav1.ObjectMeta{
Namespace: namespace,
Name: "my-custom-moduleset",
}
tests := []struct {
name string
moduleset *ingressv1alpha1.NgrokModuleSet
tp *trafficpolicy.TrafficPolicy
err error
}{
{
name: "nil moduleset",
moduleset: nil,
tp: nil,
err: nil,
},
{
name: "moduleset with ngrok managed OAuth",
moduleset: &ingressv1alpha1.NgrokModuleSet{
ObjectMeta: msObjectMeta,
Modules: ingressv1alpha1.NgrokModuleSetModules{
OAuth: &ingressv1alpha1.EndpointOAuth{
Google: &ingressv1alpha1.EndpointOAuthGoogle{
OAuthProviderCommon: ingressv1alpha1.OAuthProviderCommon{
EmailAddresses: []string{"testuser@example.com"},
EmailDomains: []string{"ngrok.com"},
},
},
},
},
},
tp: &trafficpolicy.TrafficPolicy{
OnHTTPRequest: []trafficpolicy.Rule{
{
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_OAuth,
Config: map[string]string{
"provider": "google",
},
},
},
},
{
Expressions: []string{
"!actions.ngrok.oauth.identity.email in ['testuser@example.com'] || !['ngrok.com'].exists(d, actions.ngrok.oauth.identity.email.endsWith(d))",
},
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_CustomResponse,
Config: map[string]interface{}{
"status_code": 403,
"content": "Forbidden",
},
},
},
},
},
},
err: nil,
},
{
name: "moduleset with custom OAuth",
moduleset: &ingressv1alpha1.NgrokModuleSet{
ObjectMeta: msObjectMeta,
Modules: ingressv1alpha1.NgrokModuleSetModules{
OAuth: &ingressv1alpha1.EndpointOAuth{
Google: &ingressv1alpha1.EndpointOAuthGoogle{
OAuthProviderCommon: ingressv1alpha1.OAuthProviderCommon{
ClientID: ptr.To("my-client-id"),
ClientSecret: &ingressv1alpha1.SecretKeyRef{Name: secretName, Key: secretKey},
EmailAddresses: []string{"testuser@example.com"},
EmailDomains: []string{"ngrok.com"},
},
},
},
},
},
tp: &trafficpolicy.TrafficPolicy{
OnHTTPRequest: []trafficpolicy.Rule{
{
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_OAuth,
Config: map[string]string{
"provider": "google",
"client_id": "my-client-id",
"client_secret": secretValue,
},
},
},
},
{
Expressions: []string{
"!actions.ngrok.oauth.identity.email in ['testuser@example.com'] || !['ngrok.com'].exists(d, actions.ngrok.oauth.identity.email.endsWith(d))",
},
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_CustomResponse,
Config: map[string]interface{}{
"status_code": 403,
"content": "Forbidden",
},
},
},
},
},
},
err: nil,
},
{
name: "moduleset with saml",
moduleset: &ingressv1alpha1.NgrokModuleSet{
ObjectMeta: msObjectMeta,
Modules: ingressv1alpha1.NgrokModuleSetModules{
SAML: &ingressv1alpha1.EndpointSAML{
CookiePrefix: "something",
},
},
},
tp: nil,
err: errors.NewErrModulesetNotConvertibleToTrafficPolicy("SAML module is not supported at this time"),
},
{
name: "moduleset with oidc",
moduleset: &ingressv1alpha1.NgrokModuleSet{
ObjectMeta: msObjectMeta,
Modules: ingressv1alpha1.NgrokModuleSetModules{
OIDC: &ingressv1alpha1.EndpointOIDC{
Issuer: "https://acme.com/oauth/v2/oauth-anonymous",
ClientID: "12345",
ClientSecret: ingressv1alpha1.SecretKeyRef{
Name: secretName,
Key: secretKey,
},
},
},
},
tp: &trafficpolicy.TrafficPolicy{
OnHTTPRequest: []trafficpolicy.Rule{
{
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_OIDC,
Config: map[string]string{
"issuer_url": "https://acme.com/oauth/v2/oauth-anonymous",
"client_id": "12345",
"client_secret": secretValue,
},
},
},
},
},
},
err: nil,
},
{
name: "moduleset with webhook verification",
moduleset: &ingressv1alpha1.NgrokModuleSet{
ObjectMeta: msObjectMeta,
Modules: ingressv1alpha1.NgrokModuleSetModules{
WebhookVerification: &ingressv1alpha1.EndpointWebhookVerification{
Provider: "github",
SecretRef: &ingressv1alpha1.SecretKeyRef{
Name: secretName,
Key: secretKey,
},
},
},
},
tp: &trafficpolicy.TrafficPolicy{
OnHTTPRequest: []trafficpolicy.Rule{
{
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_VerifyWebhook,
Config: map[string]string{
"provider": "github",
"secret": secretValue,
},
},
},
},
},
},
err: nil,
},
{
name: "moduleset with headers",
moduleset: &ingressv1alpha1.NgrokModuleSet{
ObjectMeta: msObjectMeta,
Modules: ingressv1alpha1.NgrokModuleSetModules{
Headers: &ingressv1alpha1.EndpointHeaders{
Request: &ingressv1alpha1.EndpointRequestHeaders{
Add: map[string]string{
"X-Header-1": "value1",
},
Remove: []string{"X-Header-2"},
},
Response: &ingressv1alpha1.EndpointResponseHeaders{
Add: map[string]string{
"X-Header-3": "value3",
},
Remove: []string{"X-Header-4"},
},
},
},
},
tp: &trafficpolicy.TrafficPolicy{
OnHTTPRequest: []trafficpolicy.Rule{
{
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_RemoveHeaders,
Config: map[string]interface{}{
"headers": []string{"X-Header-2"},
},
},
{
Type: trafficpolicy.ActionType_AddHeaders,
Config: map[string]interface{}{
"headers": map[string]string{
"X-Header-1": "value1",
},
},
},
},
},
},
OnHTTPResponse: []trafficpolicy.Rule{
{
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_RemoveHeaders,
Config: map[string]interface{}{
"headers": []string{"X-Header-4"},
},
},
{
Type: trafficpolicy.ActionType_AddHeaders,
Config: map[string]interface{}{
"headers": map[string]string{
"X-Header-3": "value3",
},
},
},
},
},
},
},
err: nil,
},
{
name: "multiple modules",
moduleset: &ingressv1alpha1.NgrokModuleSet{
ObjectMeta: msObjectMeta,
Modules: ingressv1alpha1.NgrokModuleSetModules{
Compression: &ingressv1alpha1.EndpointCompression{
Enabled: true,
},
CircuitBreaker: &ingressv1alpha1.EndpointCircuitBreaker{
ErrorThresholdPercentage: *resource.NewMilliQuantity(100, resource.DecimalSI),
TrippedDuration: metav1.Duration{Duration: 2 * time.Minute},
RollingWindow: metav1.Duration{Duration: 1 * time.Minute},
},
IPRestriction: &ingressv1alpha1.EndpointIPPolicy{
IPPolicies: []string{"my-ip-policy", "ipp_123456789012345678901234568"},
},
OAuth: &ingressv1alpha1.EndpointOAuth{
Google: &ingressv1alpha1.EndpointOAuthGoogle{
OAuthProviderCommon: ingressv1alpha1.OAuthProviderCommon{
EmailDomains: []string{"ngrok.com"},
},
},
},
TLSTermination: &ingressv1alpha1.EndpointTLSTermination{
TerminateAt: "edge",
MinVersion: ptr.To("1.2"),
},
MutualTLS: &ingressv1alpha1.EndpointMutualTLS{
CertificateAuthorities: []string{"ca1", "ca2"},
},
},
},
tp: &trafficpolicy.TrafficPolicy{
OnTCPConnect: []trafficpolicy.Rule{
{
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_RestrictIPs,
Config: map[string]interface{}{
"ip_policies": []string{"ipp_123456789012345678901234567", "ipp_123456789012345678901234568"},
},
},
},
},
{
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_TerminateTLS,
Config: map[string]interface{}{
"min_version": "1.2",
"mutual_tls_certificate_authorities": []string{"ca1", "ca2"},
},
},
},
},
},
OnHTTPRequest: []trafficpolicy.Rule{
{
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_OAuth,
Config: map[string]interface{}{
"provider": "google",
},
},
},
},
{
Expressions: []string{
"!['ngrok.com'].exists(d, actions.ngrok.oauth.identity.email.endsWith(d))",
},
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_CustomResponse,
Config: map[string]interface{}{
"status_code": 403,
"content": "Forbidden",
},
},
},
},
{
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_CircuitBreaker,
Config: map[string]interface{}{
"error_threshold": 0.1,
"window_duration": 1 * time.Minute,
"tripped_duration": 2 * time.Minute,
},
},
},
},
},
OnHTTPResponse: []trafficpolicy.Rule{
{
Actions: []trafficpolicy.Action{
{
Type: trafficpolicy.ActionType_CompressResponse,
Config: map[string]interface{}{},
},
},
},
},
},
err: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tp, err := NewTrafficPolicyFromModuleset(t.Context(), tt.moduleset, secretResolver, ipPolicyResolver)
assert.Equal(t, tt.err, err)
jsonTP, err := json.Marshal(tp)
assert.NoError(t, err)
jsonExpectedTP, err := json.Marshal(tt.tp)
assert.NoError(t, err)
assert.JSONEq(t, string(jsonExpectedTP), string(jsonTP))
})
}
}
+2 -2920
View File
File diff suppressed because it is too large Load Diff
+9 -47
View File
@@ -2,11 +2,8 @@ package managerdriver
import (
"context"
"fmt"
"github.com/go-logr/logr"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations"
"golang.org/x/sync/errgroup"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -16,8 +13,7 @@ import (
)
// ingressToDomains constructs domains for edges/endpoints from an input ingress
func ingressToDomains(log logr.Logger, in *netv1.Ingress, newDomainMetadata string, existingDomains map[string]ingressv1alpha1.Domain) (edgeDomains map[string]ingressv1alpha1.Domain, endpointDomains map[string]ingressv1alpha1.Domain) {
edgeDomains = make(map[string]ingressv1alpha1.Domain)
func ingressToDomains(in *netv1.Ingress, newDomainMetadata string, existingDomains map[string]ingressv1alpha1.Domain) (endpointDomains map[string]ingressv1alpha1.Domain) {
endpointDomains = make(map[string]ingressv1alpha1.Domain)
for _, rule := range in.Spec.Rules {
@@ -41,24 +37,13 @@ func ingressToDomains(log logr.Logger, in *netv1.Ingress, newDomainMetadata stri
},
}
domain.Spec.Metadata = newDomainMetadata
// Check the annotation to see if an edge or endpoint is desired from this ingress resource
useEdges, err := annotations.ExtractUseEdges(in)
if err != nil {
log.Error(err, fmt.Sprintf("failed to check %q annotation. defaulting to using endpoints", annotations.MappingStrategyAnnotation))
}
if !useEdges {
endpointDomains[domainName] = domain
} else {
edgeDomains[domainName] = domain
}
endpointDomains[domainName] = domain
}
return edgeDomains, endpointDomains
return endpointDomains
}
// gatewayToDomains constructs domains for edges/endpoints from an input Gateway
func gatewayToDomains(log logr.Logger, in *gatewayv1.Gateway, newDomainMetadata string, existingDomains map[string]ingressv1alpha1.Domain) (edgeDomains map[string]ingressv1alpha1.Domain, endpointDomains map[string]ingressv1alpha1.Domain) {
edgeDomains = make(map[string]ingressv1alpha1.Domain)
func gatewayToDomains(in *gatewayv1.Gateway, newDomainMetadata string, existingDomains map[string]ingressv1alpha1.Domain) (endpointDomains map[string]ingressv1alpha1.Domain) {
endpointDomains = make(map[string]ingressv1alpha1.Domain)
for _, listener := range in.Spec.Listeners {
if listener.Hostname == nil {
@@ -86,18 +71,10 @@ func gatewayToDomains(log logr.Logger, in *gatewayv1.Gateway, newDomainMetadata
}
domain.Spec.Metadata = newDomainMetadata
// Check the annotation to see if an edge or endpoint is desired from this ingress resource
useEdges, err := annotations.ExtractUseEdges(in)
if err != nil {
log.Error(err, fmt.Sprintf("failed to check %q annotation. defaulting to using endpoints", annotations.MappingStrategyAnnotation))
}
if !useEdges {
endpointDomains[domainName] = domain
} else {
edgeDomains[domainName] = domain
}
endpointDomains[domainName] = domain
}
return edgeDomains, endpointDomains
return endpointDomains
}
// applyDomains takes a set of the desired domains and current domains, creates any missing desired domains, and updated existing domains if needed
@@ -145,11 +122,6 @@ type domainSet struct {
endpointIngressDomains map[string]ingressv1alpha1.Domain
endpointGatewayDomains map[string]ingressv1alpha1.Domain
// The following two domain maps track domains for ingress/gateway resources that have opted to
// use edges
edgeIngressDomains map[string]ingressv1alpha1.Domain
edgeGatewayDomains map[string]ingressv1alpha1.Domain
// totalDomains tracks all domains regardless of source
totalDomains map[string]ingressv1alpha1.Domain
}
@@ -158,19 +130,13 @@ func (d *Driver) calculateDomainSet() *domainSet {
ret := &domainSet{
endpointIngressDomains: make(map[string]ingressv1alpha1.Domain),
endpointGatewayDomains: make(map[string]ingressv1alpha1.Domain),
edgeIngressDomains: make(map[string]ingressv1alpha1.Domain),
edgeGatewayDomains: make(map[string]ingressv1alpha1.Domain),
totalDomains: make(map[string]ingressv1alpha1.Domain),
}
// Calculate domains from ingress resources
ingresses := d.store.ListNgrokIngressesV1()
for _, ingress := range ingresses {
edgeDomains, endpointDomains := ingressToDomains(d.log, ingress, d.ingressNgrokMetadata, nil)
for key, val := range edgeDomains {
ret.totalDomains[key] = val
ret.edgeIngressDomains[key] = val
}
endpointDomains := ingressToDomains(ingress, d.ingressNgrokMetadata, nil)
for key, val := range endpointDomains {
ret.totalDomains[key] = val
ret.endpointIngressDomains[key] = val
@@ -180,11 +146,7 @@ func (d *Driver) calculateDomainSet() *domainSet {
// Calculate domains from gateway resources
gateways := d.store.ListGateways()
for _, gateway := range gateways {
edgeDomains, endpointDomains := gatewayToDomains(d.log, gateway, d.gatewayNgrokMetadata, ret.totalDomains)
for key, val := range edgeDomains {
ret.totalDomains[key] = val
ret.edgeGatewayDomains[key] = val
}
endpointDomains := gatewayToDomains(gateway, d.gatewayNgrokMetadata, ret.totalDomains)
for key, val := range endpointDomains {
ret.totalDomains[key] = val
ret.endpointGatewayDomains[key] = val
-101
View File
@@ -241,18 +241,6 @@ func listObjectsForType(ctx context.Context, client client.Reader, v interface{}
domains := &ingressv1alpha1.DomainList{}
err := client.List(ctx, domains)
return util.ToClientObjects(domains.Items), err
case *ingressv1alpha1.HTTPSEdge:
edges := &ingressv1alpha1.HTTPSEdgeList{}
err := client.List(ctx, edges)
return util.ToClientObjects(edges.Items), err
case *ingressv1alpha1.Tunnel:
tunnels := &ingressv1alpha1.TunnelList{}
err := client.List(ctx, tunnels)
return util.ToClientObjects(tunnels.Items), err
case *ingressv1alpha1.NgrokModuleSet:
modules := &ingressv1alpha1.NgrokModuleSetList{}
err := client.List(ctx, modules)
return util.ToClientObjects(modules.Items), err
case *ngrokv1alpha1.NgrokTrafficPolicy:
policies := &ngrokv1alpha1.NgrokTrafficPolicyList{}
err := client.List(ctx, policies)
@@ -301,9 +289,6 @@ func (d *Driver) Seed(ctx context.Context, c client.Reader) error {
&corev1.ConfigMap{},
// CRDs
&ingressv1alpha1.Domain{},
&ingressv1alpha1.HTTPSEdge{},
&ingressv1alpha1.Tunnel{},
&ingressv1alpha1.NgrokModuleSet{},
&ngrokv1alpha1.NgrokTrafficPolicy{},
&ngrokv1alpha1.AgentEndpoint{},
&ngrokv1alpha1.CloudEndpoint{},
@@ -578,8 +563,6 @@ func (d *Driver) Sync(ctx context.Context, c client.Client) error {
// TODO (Alice): move domains, edges, tunnels to translator
domains := d.calculateDomainSet()
desiredEdges := d.calculateHTTPSEdges(domains)
desiredTunnels := d.calculateTunnels()
translator := NewTranslator(
d.log,
@@ -592,25 +575,9 @@ func (d *Driver) Sync(ctx context.Context, c client.Client) error {
)
translationResult := translator.Translate()
currEdges := &ingressv1alpha1.HTTPSEdgeList{}
currTunnels := &ingressv1alpha1.TunnelList{}
currAgentEndpoints := &ngrokv1alpha1.AgentEndpointList{}
currCloudEndpoints := &ngrokv1alpha1.CloudEndpointList{}
if err := c.List(ctx, currEdges, client.MatchingLabels{
labelControllerNamespace: d.managerName.Namespace,
labelControllerName: d.managerName.Name,
}); err != nil {
d.log.Error(err, "error listing edges")
return err
}
if err := c.List(ctx, currTunnels, client.MatchingLabels{
labelControllerNamespace: d.managerName.Namespace,
labelControllerName: d.managerName.Name,
}); err != nil {
d.log.Error(err, "error listing tunnels")
return err
}
if err := c.List(ctx, currAgentEndpoints, client.MatchingLabels{
labelControllerNamespace: d.managerName.Namespace,
labelControllerName: d.managerName.Name,
@@ -630,10 +597,6 @@ func (d *Driver) Sync(ctx context.Context, c client.Client) error {
return err
}
if err := d.applyHTTPSEdges(ctx, c, desiredEdges, currEdges.Items); err != nil {
return err
}
if err := d.applyAgentEndpoints(ctx, c, translationResult.AgentEndpoints, currAgentEndpoints.Items); err != nil {
d.log.Error(err, "applying agent endpoints")
return err
@@ -644,10 +607,6 @@ func (d *Driver) Sync(ctx context.Context, c client.Client) error {
return err
}
if err := d.applyTunnels(ctx, c, desiredTunnels, currTunnels.Items); err != nil {
return err
}
// Update Statuses
return d.updateStatuses(ctx, c)
}
@@ -848,37 +807,6 @@ func (d *Driver) updateHTTPRouteStatuses(_ context.Context, _ client.Client) err
return nil
}
// getTrafficPolicyJSON retrieves the traffic policy for an ingress and falls back to the modSet policy if it doesn't exist.
func (d *Driver) getTrafficPolicyJSON(ingress *netv1.Ingress, modSet *ingressv1alpha1.NgrokModuleSet) (json.RawMessage, error) {
var err error
var policyJSON json.RawMessage
trafficPolicy, err := getNgrokTrafficPolicyForIngress(ingress, d.store)
if err != nil {
d.log.Error(err, "error getting ngrok traffic policy for ingress", "ingress", ingress)
return nil, err
}
if modSet.IsEmpty() {
if trafficPolicy != nil {
return trafficPolicy.Spec.Policy, nil
}
return policyJSON, nil
}
if modSet.Modules.Policy != nil && trafficPolicy != nil {
return nil, fmt.Errorf("cannot have both a traffic policy and a moduleset policy on ingress: %s", ingress.Name)
}
if policyJSON, err = json.Marshal(modSet.Modules.Policy); err != nil {
d.log.Error(err, "cannot convert module-set policy json", "ingress", ingress, "Policy", modSet.Modules.Policy)
return nil, err
}
return policyJSON, nil
}
func (d *Driver) createEndpointPolicyForGateway(rule *gatewayv1.HTTPRouteRule, namespace string) (json.RawMessage, error) {
pathPrefixMatches := []string{}
@@ -1309,41 +1237,12 @@ func (d *Driver) handleRequestRedirectFilter(filter *gatewayv1.HTTPRequestRedire
return nil
}
func (d *Driver) findBackendRefServicePort(backendRef gatewayv1.BackendRef, namespace string) (*corev1.Service, *corev1.ServicePort, error) {
service, err := d.store.GetServiceV1(string(backendRef.Name), namespace)
if err != nil {
return nil, nil, err
}
servicePort, err := findBackendRefServicesPort(d.log, service, &backendRef)
if err != nil {
return nil, nil, err
}
return service, servicePort, nil
}
func (d *Driver) findBackendServicePort(backendSvc netv1.IngressServiceBackend, namespace string) (*corev1.Service, *corev1.ServicePort, error) {
service, err := d.store.GetServiceV1(backendSvc.Name, namespace)
if err != nil {
return nil, nil, err
}
servicePort, err := findServicesPort(d.log, service, backendSvc.Port)
if err != nil {
return nil, nil, err
}
return service, servicePort, nil
}
// MigrateKubernetesIngressControllerLabelsToNgrokOperator migrates the labels from the old Kubernetes Ingress Controller to the new ngrok operator labels
// so that the ngrok operator can take over management of items previously managed by the Kubernetes Ingress Controller.
// TODO: Delete this function after users have migrated from the ngrok Kubernetes Ingress Controller to the ngrok Operator.
func (d *Driver) MigrateKubernetesIngressControllerLabelsToNgrokOperator(ctx context.Context, k8sClient client.Client) error {
typesToMigrate := []interface{}{
&ingressv1alpha1.Domain{},
&ingressv1alpha1.Tunnel{},
&ingressv1alpha1.HTTPSEdge{},
}
for _, t := range typesToMigrate {
+79 -175
View File
@@ -69,9 +69,9 @@ var _ = Describe("Driver", func() {
ic2 := testutils.NewTestIngressClass("test-ingress-class-2", true, true)
d1 := testutils.NewDomainV1("test-domain.com", "test-namespace")
d2 := testutils.NewDomainV1("test-domain-2.com", "test-namespace")
e1 := testutils.NewHTTPSEdge("test-edge", "test-namespace")
e2 := testutils.NewHTTPSEdge("test-edge-2", "test-namespace")
obs := []runtime.Object{ic1, ic2, i1, i2, d1, d2, &e1, &e2}
c1 := testutils.NewCloudEndpoint()
c2 := testutils.NewCloudEndpoint()
obs := []runtime.Object{ic1, ic2, i1, i2, d1, d2, c1, c2}
c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(obs...).Build()
err := driver.Seed(GinkgoT().Context(), c)
@@ -119,19 +119,19 @@ var _ = Describe("Driver", func() {
Expect(err).ToNot(HaveOccurred())
Expect(domains.Items).To(HaveLen(0))
edges := &ingressv1alpha1.HTTPSEdgeList{}
err = c.List(GinkgoT().Context(), &ingressv1alpha1.HTTPSEdgeList{})
agentendpoints := &ngrokv1alpha1.AgentEndpointList{}
err = c.List(GinkgoT().Context(), &ngrokv1alpha1.AgentEndpointList{})
Expect(err).ToNot(HaveOccurred())
Expect(edges.Items).To(HaveLen(0))
Expect(agentendpoints.Items).To(HaveLen(0))
tunnels := &ingressv1alpha1.TunnelList{}
err = c.List(GinkgoT().Context(), &ingressv1alpha1.TunnelList{})
cloudendpoints := &ngrokv1alpha1.CloudEndpointList{}
err = c.List(GinkgoT().Context(), &ngrokv1alpha1.CloudEndpointList{})
Expect(err).ToNot(HaveOccurred())
Expect(tunnels.Items).To(HaveLen(0))
Expect(cloudendpoints.Items).To(HaveLen(0))
})
})
Context("When there are just edge ingresses and edges need to be created", func() {
It("Should create the CRDs", func() {
Context("When the old edges mapping-strategy is used, it defaults to endpoint", func() {
It("Should create AgentEndpoints", func() {
i1 := testutils.NewTestIngressV1("test-ingress", "test-namespace")
if i1.Annotations == nil {
i1.Annotations = map[string]string{}
@@ -167,25 +167,12 @@ var _ = Describe("Driver", func() {
Expect(err).ToNot(HaveOccurred())
Expect(foundDomain.Spec.Domain).To(Equal(i1.Spec.Rules[0].Host))
foundEdges := &ingressv1alpha1.HTTPSEdgeList{}
err = c.List(GinkgoT().Context(), foundEdges)
agentEndpoints := &ngrokv1alpha1.AgentEndpointList{}
err = c.List(GinkgoT().Context(), agentEndpoints, client.InNamespace("test-namespace"))
Expect(err).ToNot(HaveOccurred())
Expect(len(foundEdges.Items)).To(Equal(1))
foundEdge := foundEdges.Items[0]
Expect(err).ToNot(HaveOccurred())
Expect(foundEdge.Spec.Hostports[0]).To(ContainSubstring(i1.Spec.Rules[0].Host))
Expect(foundEdge.Namespace).To(Equal("test-namespace"))
Expect(foundEdge.Name).To(HavePrefix("example-com-"))
Expect(foundEdge.Labels["k8s.ngrok.com/controller-name"]).To(Equal(defaultManagerName))
foundTunnels := &ingressv1alpha1.TunnelList{}
err = c.List(GinkgoT().Context(), foundTunnels)
Expect(err).ToNot(HaveOccurred())
Expect(len(foundTunnels.Items)).To(Equal(1))
foundTunnel := foundTunnels.Items[0]
Expect(foundTunnel.Namespace).To(Equal("test-namespace"))
Expect(foundTunnel.Name).To(HavePrefix("example-80-"))
Expect(foundTunnel.Labels["k8s.ngrok.com/controller-name"]).To(Equal(defaultManagerName))
Expect(len(agentEndpoints.Items)).To(Equal(1))
agentEndpoint := agentEndpoints.Items[0]
Expect(agentEndpoint.Spec.URL).To(Equal("https://" + i1.Spec.Rules[0].Host))
})
})
@@ -196,7 +183,8 @@ var _ = Describe("Driver", func() {
ingress *netv1.Ingress
c client.WithWatch
namespace = "app-proto-namespace"
foundTunnels *ingressv1alpha1.TunnelList
agentEndpoints *ngrokv1alpha1.AgentEndpointList
cloudEndpoints *ngrokv1alpha1.CloudEndpointList
ic = testutils.NewTestIngressClass("app-proto-ingress-class", true, true)
setIngressTargetService = func(i *netv1.Ingress, s *v1.Service) {
// Modify the ingress to include the service
@@ -286,9 +274,14 @@ var _ = Describe("Driver", func() {
Expect(driver.Seed(GinkgoT().Context(), c)).To(BeNil())
Expect(driver.Sync(GinkgoT().Context(), c)).To(BeNil())
// Find the tunnels in this namespace
foundTunnels = &ingressv1alpha1.TunnelList{}
err := c.List(GinkgoT().Context(), foundTunnels, client.InNamespace(namespace))
// Find the agent endpoints in this namespace
agentEndpoints = &ngrokv1alpha1.AgentEndpointList{}
err := c.List(GinkgoT().Context(), agentEndpoints, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred())
// Find the cloud endpoints in this namespace
cloudEndpoints = &ngrokv1alpha1.CloudEndpointList{}
err = c.List(GinkgoT().Context(), cloudEndpoints, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred())
})
@@ -301,14 +294,14 @@ var _ = Describe("Driver", func() {
})
It("Should ignore the unknown appProtocol", func() {
// We expect one tunnel to be created
Expect(len(foundTunnels.Items)).To(Equal(1))
// We expect one agent endpoint to be created
Expect(len(agentEndpoints.Items)).To(Equal(1))
By("Creating a tunnel with no appProtocol and the correct upstream")
foundTunnel := foundTunnels.Items[0]
Expect(foundTunnel.Spec.AppProtocol).To(BeNil())
Expect(foundTunnel.Spec.ForwardsTo).To(Equal("http-service.app-proto-namespace.svc.cluster.local:80"))
Expect(foundTunnel.Spec.BackendConfig.Protocol).To(Equal("HTTP"))
By("Creating an agent endpoint with no appProtocol and the correct upstream")
foundAgentEndpoint := agentEndpoints.Items[0]
Expect(foundAgentEndpoint.Spec.Upstream.ProxyProtocolVersion).To(BeNil())
Expect(foundAgentEndpoint.Spec.Upstream.URL).To(Equal("http://http-service.app-proto-namespace:80"))
Expect(foundAgentEndpoint.Spec.Upstream.Protocol).To(BeNil())
})
})
@@ -320,15 +313,14 @@ var _ = Describe("Driver", func() {
setIngressTargetService(ingress, httpService)
})
It("Should create a tunnel with appProtocol http1", func() {
// We expect one tunnel to be created
Expect(len(foundTunnels.Items)).To(Equal(1))
It("Should create an AgentEndpoint with appProtocol http1", func() {
// We expect one AgentEndpoint to be created
Expect(len(agentEndpoints.Items)).To(Equal(1))
By("Creating a tunnel with appProtocol http1")
foundTunnel := foundTunnels.Items[0]
Expect(foundTunnel.Spec.AppProtocol).To(Equal(ptr.To(common.ApplicationProtocol_HTTP1)))
Expect(foundTunnel.Spec.ForwardsTo).To(Equal("http-service.app-proto-namespace.svc.cluster.local:80"))
Expect(foundTunnel.Spec.BackendConfig.Protocol).To(Equal("HTTP"))
By("Creating an AgentEndpoint with appProtocol http1")
foundAgentEndpoint := agentEndpoints.Items[0]
Expect(foundAgentEndpoint.Spec.Upstream.Protocol).To(Equal(ptr.To(common.ApplicationProtocol_HTTP1)))
Expect(foundAgentEndpoint.Spec.Upstream.URL).To(Equal("http://http-service.app-proto-namespace:80"))
})
})
@@ -341,15 +333,14 @@ var _ = Describe("Driver", func() {
setIngressTargetService(ingress, httpsService)
})
It("Should create a tunnel with appProtocol http2", func() {
// We expect one tunnel to be created
Expect(len(foundTunnels.Items)).To(Equal(1))
It("Should create an AgentEndpoint with an upstream protocol of http2", func() {
// We expect one AgentEndpoint to be created
Expect(len(agentEndpoints.Items)).To(Equal(1))
By("Creating a tunnel with appProtocol http2")
foundTunnel := foundTunnels.Items[0]
Expect(foundTunnel.Spec.AppProtocol).To(Equal(ptr.To(common.ApplicationProtocol_HTTP2)))
Expect(foundTunnel.Spec.ForwardsTo).To(Equal("https-service.app-proto-namespace.svc.cluster.local:443"))
Expect(foundTunnel.Spec.BackendConfig.Protocol).To(Equal("HTTPS"))
By("Creating an AgentEndpoint with appProtocol http2")
foundAgentEndpoint := agentEndpoints.Items[0]
Expect(foundAgentEndpoint.Spec.Upstream.Protocol).To(Equal(ptr.To(common.ApplicationProtocol_HTTP2)))
Expect(foundAgentEndpoint.Spec.Upstream.URL).To(Equal("https://https-service.app-proto-namespace:443"))
})
})
@@ -362,25 +353,14 @@ var _ = Describe("Driver", func() {
setIngressTargetService(ingress, httpsService)
})
It("Should create a tunnel with appProtocol http2", func() {
// We expect one tunnel to be created
Expect(len(foundTunnels.Items)).To(Equal(1))
It("Should create an AgentEndpoint with appProtocol http2", func() {
// We expect one AgentEndpoint to be created
Expect(len(agentEndpoints.Items)).To(Equal(1))
By("Creating a tunnel with appProtocol http2")
foundTunnel := foundTunnels.Items[0]
Expect(foundTunnel.Spec.AppProtocol).To(Equal(ptr.To(common.ApplicationProtocol_HTTP2)))
Expect(foundTunnel.Spec.ForwardsTo).To(Equal("https-service.app-proto-namespace.svc.cluster.local:443"))
Expect(foundTunnel.Spec.BackendConfig.Protocol).To(Equal("HTTPS"))
})
})
When("The Ingress does not specify mapping-strategy: edges", func() {
BeforeEach(func() {
ingress.Annotations = map[string]string{} // Removing the edges annotation should cause the translation to be endpoints instead.
})
It("Should not have any tunnels", func() {
Expect(len(foundTunnels.Items)).To(Equal(0))
By("Creating an AgentEndpoint with appProtocol http2")
foundAgentEndpoint := agentEndpoints.Items[0]
Expect(foundAgentEndpoint.Spec.Upstream.Protocol).To(Equal(ptr.To(common.ApplicationProtocol_HTTP2)))
Expect(foundAgentEndpoint.Spec.Upstream.URL).To(Equal("https://https-service.app-proto-namespace:443"))
})
})
})
@@ -392,7 +372,6 @@ var _ = Describe("Driver", func() {
httpService *v1.Service
ingress *netv1.Ingress
trafficPolicy *ngrokv1alpha1.NgrokTrafficPolicy
foundHTTPEdges *ingressv1alpha1.HTTPSEdgeList
foundAgentEndpoints *ngrokv1alpha1.AgentEndpointList
foundCloudEndpoints *ngrokv1alpha1.CloudEndpointList
ic = testutils.NewTestIngressClass("edge-tp-ingress-class", true, true)
@@ -481,10 +460,6 @@ var _ = Describe("Driver", func() {
Expect(driver.Seed(GinkgoT().Context(), c)).To(Succeed())
Expect(driver.Sync(GinkgoT().Context(), c)).To(Succeed())
// Find the HTTPSEdges in this namespace
foundHTTPEdges = &ingressv1alpha1.HTTPSEdgeList{}
Expect(c.List(GinkgoT().Context(), foundHTTPEdges, client.InNamespace(namespace))).To(Succeed())
// Find the AgentEndpoints in this namespace
foundAgentEndpoints = &ngrokv1alpha1.AgentEndpointList{}
Expect(c.List(GinkgoT().Context(), foundAgentEndpoints, client.InNamespace(namespace))).To(Succeed())
@@ -494,16 +469,15 @@ var _ = Describe("Driver", func() {
Expect(c.List(GinkgoT().Context(), foundCloudEndpoints, client.InNamespace(namespace))).To(Succeed())
})
When("The the ingress is using the edges mapping strategy", func() {
When("The the ingress is using the old edges mapping strategy", func() {
BeforeEach(func() {
controller.AddAnnotations(ingress, map[string]string{
"k8s.ngrok.com/mapping-strategy": "edges",
})
})
It("Should only create an HTTPSEdge", func() {
Expect(len(foundHTTPEdges.Items)).To(Equal(1))
Expect(len(foundAgentEndpoints.Items)).To(Equal(0))
It("Should create an AgentEndpoint", func() {
Expect(len(foundAgentEndpoints.Items)).To(Equal(1))
Expect(len(foundCloudEndpoints.Items)).To(Equal(0))
})
@@ -515,13 +489,24 @@ var _ = Describe("Driver", func() {
})
It("Should use the traffic policy", func() {
edge := foundHTTPEdges.Items[0]
Expect(edge.Spec.Routes).To(HaveLen(1))
foundAgentEndpoint := foundAgentEndpoints.Items[0]
By("Having the traffic policy on all routes on the edge")
for _, route := range edge.Spec.Routes {
Expect(route.Policy).To(Equal(trafficPolicy.Spec.Policy))
}
By("Having the traffic policy on the AgentEndpoint")
Expect(foundAgentEndpoint.Spec.TrafficPolicy.Inline).ToNot(BeNil())
pol, err := trafficpolicy.NewTrafficPolicyFromJSON(foundAgentEndpoint.Spec.TrafficPolicy.Inline)
Expect(err).ToNot(HaveOccurred())
Expect(pol.OnHTTPRequest).To(ContainElement(
trafficpolicy.Rule{
Name: "test-name",
Actions: []trafficpolicy.Action{
{
Type: "compress-response",
Config: map[string]interface{}{},
},
},
},
))
})
})
})
@@ -531,7 +516,6 @@ var _ = Describe("Driver", func() {
It("Should only create an AgentEndpoint", func() {
Expect(len(foundAgentEndpoints.Items)).To(Equal(1))
Expect(len(foundCloudEndpoints.Items)).To(Equal(0))
Expect(len(foundHTTPEdges.Items)).To(Equal(0))
})
When("The traffic policy exists", func() {
@@ -871,85 +855,6 @@ var _ = Describe("Driver", func() {
})
})
Describe("getNgrokModuleSetForIngress", func() {
var ms1, ms2, ms3 *ingressv1alpha1.NgrokModuleSet
BeforeEach(func() {
ms1 = &ingressv1alpha1.NgrokModuleSet{
ObjectMeta: metav1.ObjectMeta{Name: "ms1", Namespace: "test"},
Modules: ingressv1alpha1.NgrokModuleSetModules{
Compression: &ingressv1alpha1.EndpointCompression{
Enabled: true,
},
},
}
ms2 = &ingressv1alpha1.NgrokModuleSet{
ObjectMeta: metav1.ObjectMeta{Name: "ms2", Namespace: "test"},
Modules: ingressv1alpha1.NgrokModuleSetModules{
Compression: &ingressv1alpha1.EndpointCompression{
Enabled: false,
},
IPRestriction: &ingressv1alpha1.EndpointIPPolicy{
IPPolicies: []string{"policy1", "policy2"},
},
},
}
ms3 = &ingressv1alpha1.NgrokModuleSet{
ObjectMeta: metav1.ObjectMeta{Name: "ms3", Namespace: "test"},
Modules: ingressv1alpha1.NgrokModuleSetModules{
Compression: &ingressv1alpha1.EndpointCompression{
Enabled: true,
},
},
}
Expect(driver.store.Add(ms1)).To(BeNil())
Expect(driver.store.Add(ms2)).To(BeNil())
Expect(driver.store.Add(ms3)).To(BeNil())
})
It("Should return an empty module set if the ingress has no modules annotaion", func() {
ing := testutils.NewTestIngressV1("test-ingress", "test")
Expect(driver.store.Add(ing)).To(BeNil())
ms, err := getNgrokModuleSetForObject(ing, driver.store)
Expect(err).To(BeNil())
Expect(ms.Modules.Compression).To(BeNil())
Expect(ms.Modules.Headers).To(BeNil())
Expect(ms.Modules.IPRestriction).To(BeNil())
Expect(ms.Modules.TLSTermination).To(BeNil())
Expect(ms.Modules.WebhookVerification).To(BeNil())
})
It("Should return the matching module set if the ingress has a modules annotaion", func() {
ing := testutils.NewTestIngressV1("test-ingress", "test")
ing.SetAnnotations(map[string]string{"k8s.ngrok.com/modules": "ms1"})
Expect(driver.store.Add(ing)).To(BeNil())
ms, err := getNgrokModuleSetForObject(ing, driver.store)
Expect(err).To(BeNil())
Expect(ms.Modules).To(Equal(ms1.Modules))
})
It("merges modules with the last one winning if multiple module sets are specified", func() {
ing := testutils.NewTestIngressV1("test-ingress", "test")
ing.SetAnnotations(map[string]string{"k8s.ngrok.com/modules": "ms1,ms2,ms3"})
Expect(driver.store.Add(ing)).To(BeNil())
ms, err := getNgrokModuleSetForObject(ing, driver.store)
Expect(err).To(BeNil())
Expect(ms.Modules).To(Equal(
ingressv1alpha1.NgrokModuleSetModules{
Compression: &ingressv1alpha1.EndpointCompression{
Enabled: true, // From ms3
},
IPRestriction: &ingressv1alpha1.EndpointIPPolicy{
IPPolicies: []string{"policy1", "policy2"}, // From ms2
},
},
))
})
})
Describe("createEndpointPolicyForGateway", func() {
var rule *gatewayv1.HTTPRouteRule
var namespace string
@@ -1264,12 +1169,11 @@ var _ = Describe("Driver", func() {
Expect(driver.Seed(GinkgoT().Context(), c)).To(BeNil())
domainSet := driver.calculateDomainSet()
Expect(domainSet.edgeIngressDomains).To(HaveLen(4))
Expect(domainSet.edgeIngressDomains).To(HaveKey("a.customdomain.com"))
Expect(domainSet.edgeIngressDomains).To(HaveKey("b.customdomain.com"))
Expect(domainSet.edgeIngressDomains).To(HaveKey("c.customdomain.com"))
Expect(domainSet.edgeIngressDomains).To(HaveKey("d.customdomain.com"))
Expect(domainSet.endpointIngressDomains).To(HaveLen(0))
Expect(domainSet.endpointIngressDomains).To(HaveLen(4))
Expect(domainSet.endpointIngressDomains).To(HaveKey("a.customdomain.com"))
Expect(domainSet.endpointIngressDomains).To(HaveKey("b.customdomain.com"))
Expect(domainSet.endpointIngressDomains).To(HaveKey("c.customdomain.com"))
Expect(domainSet.endpointIngressDomains).To(HaveKey("d.customdomain.com"))
})
})
})
-464
View File
@@ -1,464 +0,0 @@
package managerdriver
import (
"context"
"fmt"
"reflect"
"strings"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/annotations"
"github.com/ngrok/ngrok-operator/internal/errors"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (d *Driver) SyncEdges(ctx context.Context, c client.Client) error {
if !d.syncAllowConcurrent {
proceed, wait := d.syncStart(true)
if !proceed {
return wait(ctx)
}
defer d.syncDone()
}
d.log.Info("syncing edges state!!")
domains := d.calculateDomainSet()
desiredEdges := d.calculateHTTPSEdges(domains)
currEdges := &ingressv1alpha1.HTTPSEdgeList{}
if err := c.List(ctx, currEdges, client.MatchingLabels{
labelControllerNamespace: d.managerName.Namespace,
labelControllerName: d.managerName.Name,
}); err != nil {
d.log.Error(err, "error listing edges")
return err
}
return d.applyHTTPSEdges(ctx, c, desiredEdges, currEdges.Items)
}
func (d *Driver) applyHTTPSEdges(ctx context.Context, c client.Client, desiredEdges map[string]ingressv1alpha1.HTTPSEdge, currentEdges []ingressv1alpha1.HTTPSEdge) error {
// update or delete edge we don't need anymore
for _, currEdge := range currentEdges {
hostports := currEdge.Spec.Hostports
// If one of the controller-owned edges has more than one hostport, log an error and skip it
// because we can't determine what to do with it.
if len(hostports) != 1 {
d.log.Error(nil, "Existing owned edge has more than 1 hostport", "edge", currEdge, "hostports", hostports)
continue
}
// ngrok only supports https on port 443 and all domains are on port 443
// so we can safely trim the port from the hostport to get the domain
domain := strings.TrimSuffix(hostports[0], ":443")
if desiredEdge, ok := desiredEdges[domain]; ok {
needsUpdate := false
if !reflect.DeepEqual(desiredEdge.Spec, currEdge.Spec) {
currEdge.Spec = desiredEdge.Spec
needsUpdate = true
}
if needsUpdate {
if err := c.Update(ctx, &currEdge); err != nil {
d.log.Error(err, "error updating edge", "desiredEdge", desiredEdge, "currEdge", currEdge)
return err
}
}
// matched and updated the edge, no longer desired
delete(desiredEdges, domain)
} else {
if err := c.Delete(ctx, &currEdge); client.IgnoreNotFound(err) != nil {
d.log.Error(err, "error deleting edge", "edge", currEdge)
return err
}
}
}
// the set of desired edges now only contains new edges, create them
for _, edge := range desiredEdges {
if err := c.Create(ctx, &edge); err != nil {
d.log.Error(err, "error creating edge", "edge", edge)
return err
}
}
return nil
}
func (d *Driver) calculateHTTPSEdges(domains *domainSet) map[string]ingressv1alpha1.HTTPSEdge {
edgeMap := make(map[string]ingressv1alpha1.HTTPSEdge, len(domains.edgeIngressDomains))
for _, domain := range domains.edgeIngressDomains {
edge := ingressv1alpha1.HTTPSEdge{
ObjectMeta: metav1.ObjectMeta{
GenerateName: domain.Name + "-",
Namespace: domain.Namespace,
Labels: d.edgeLabels(),
},
Spec: ingressv1alpha1.HTTPSEdgeSpec{
Hostports: []string{domain.Spec.Domain + ":443"},
},
}
edge.Spec.Metadata = d.ingressNgrokMetadata
edgeMap[domain.Spec.Domain] = edge
}
d.calculateHTTPSEdgesFromIngress(edgeMap)
// No gateway domains, return early
if len(domains.edgeGatewayDomains) == 0 {
return edgeMap
}
if d.gatewayEnabled {
gatewayEdgeMap := make(map[string]ingressv1alpha1.HTTPSEdge)
httproutes := d.store.ListHTTPRoutes()
gateways := d.store.ListGateways()
for _, gtw := range gateways {
gatewayDomains := make(map[string]string)
for _, listener := range gtw.Spec.Listeners {
if listener.Hostname == nil {
continue
}
if listener.Protocol != gatewayv1.HTTPSProtocolType || int(listener.Port) != 443 {
continue
}
if _, hasDomain := domains.edgeGatewayDomains[string(*listener.Hostname)]; !hasDomain {
continue
}
gatewayDomains[string(*listener.Hostname)] = string(*listener.Hostname)
}
if len(gatewayDomains) == 0 {
d.log.Info("no usable domains in gateway, may be missing https listener", "gateway", gtw.Name)
continue
}
for _, httproute := range httproutes {
var routeDomains []string
for _, parent := range httproute.Spec.ParentRefs {
if string(parent.Name) != gtw.Name {
continue
}
var domainOverlap []string
for _, hostname := range httproute.Spec.Hostnames {
domain := string(hostname)
if _, hasDomain := gatewayDomains[domain]; hasDomain {
domainOverlap = append(domainOverlap, domain)
}
}
if len(domainOverlap) == 0 {
// no hostnames overlap with gateway
continue
}
routeDomains = append(routeDomains, domainOverlap...)
}
if len(routeDomains) == 0 {
// no usable domains in route
continue
}
var hostPorts []string
for _, domain := range routeDomains {
hostPorts = append(hostPorts, domain+":443")
}
edge := ingressv1alpha1.HTTPSEdge{
ObjectMeta: metav1.ObjectMeta{
GenerateName: httproute.Name + "-",
Namespace: httproute.Namespace,
Labels: d.edgeLabels(),
},
Spec: ingressv1alpha1.HTTPSEdgeSpec{
Hostports: hostPorts,
},
}
edge.Spec.Metadata = d.gatewayNgrokMetadata
gatewayEdgeMap[routeDomains[0]] = edge
}
}
d.calculateHTTPSEdgesFromGateway(gatewayEdgeMap)
// merge edge maps
for k, v := range gatewayEdgeMap {
edgeMap[k] = v
}
}
return edgeMap
}
func (d *Driver) calculateHTTPSEdgesFromIngress(edgeMap map[string]ingressv1alpha1.HTTPSEdge) {
ingresses := d.store.ListNgrokIngressesV1()
for _, ingress := range ingresses {
useEdges, err := annotations.ExtractUseEdges(ingress)
if err != nil {
d.log.Error(err, fmt.Sprintf("failed to check %q annotation. defaulting to using endpoints", annotations.MappingStrategyAnnotation))
}
if !useEdges {
d.log.Info("the following ingress will be provided by ngrok endpoints instead of edges",
"ingress", fmt.Sprintf("%s.%s", ingress.Name, ingress.Namespace),
)
continue
}
modSet, err := getNgrokModuleSetForObject(ingress, d.store)
if err != nil {
d.log.Error(err, "error getting ngrok moduleset for ingress", "ingress", ingress)
continue
}
policyJSON, err := d.getTrafficPolicyJSON(ingress, modSet)
if err != nil {
d.log.Error(err, "error marshalling JSON Policy for ingress", "ingress", ingress)
continue
}
for _, rule := range ingress.Spec.Rules {
edge, ok := edgeMap[rule.Host]
if !ok {
d.log.Error(err, "could not find edge associated with rule", "host", rule.Host)
continue
}
if modSet.Modules.TLSTermination != nil && modSet.Modules.TLSTermination.MinVersion != nil {
edge.Spec.TLSTermination = &ingressv1alpha1.EndpointTLSTerminationAtEdge{
MinVersion: ptr.Deref(modSet.Modules.TLSTermination.MinVersion, ""),
}
}
if modSet.Modules.MutualTLS != nil {
edge.Spec.MutualTLS = modSet.Modules.MutualTLS
}
// If any rule for an ingress matches, then it applies to this ingress
for _, httpIngressPath := range rule.HTTP.Paths {
matchType := "path_prefix"
if httpIngressPath.PathType != nil {
switch *httpIngressPath.PathType {
case netv1.PathTypePrefix:
matchType = "path_prefix"
case netv1.PathTypeExact:
matchType = "exact_path"
case netv1.PathTypeImplementationSpecific:
matchType = "path_prefix" // Path Prefix seems like a sane default for most cases
default:
d.log.Error(errors.New("unknown path type"), "unknown path type", "pathType", *httpIngressPath.PathType)
continue
}
}
// We only support service backends right now. TODO: support resource backends
if httpIngressPath.Backend.Service == nil {
continue
}
serviceName := httpIngressPath.Backend.Service.Name
serviceUID, servicePort, err := d.getIngressBackend(*httpIngressPath.Backend.Service, ingress.Namespace)
if err != nil {
d.log.Error(err, "could not find port for service", "namespace", ingress.Namespace, "service", serviceName)
continue
}
route := ingressv1alpha1.HTTPSEdgeRouteSpec{
Match: httpIngressPath.Path,
MatchType: matchType,
Backend: ingressv1alpha1.TunnelGroupBackend{
Labels: ngrokLabels(ingress.Namespace, serviceUID, serviceName, servicePort),
},
CircuitBreaker: modSet.Modules.CircuitBreaker,
Compression: modSet.Modules.Compression,
IPRestriction: modSet.Modules.IPRestriction,
Headers: modSet.Modules.Headers,
OAuth: modSet.Modules.OAuth,
Policy: policyJSON,
OIDC: modSet.Modules.OIDC,
SAML: modSet.Modules.SAML,
WebhookVerification: modSet.Modules.WebhookVerification,
}
route.Metadata = d.ingressNgrokMetadata
// Loop through existing routes and check if any match the path and match type
// If they do, warn about it and continue replacing it
for _, existingRoute := range edge.Spec.Routes {
if existingRoute.Match == route.Match && existingRoute.MatchType == route.MatchType {
d.log.Info("replacing existing route", "route", existingRoute.Match, "newRoute", route.Match)
continue
}
}
edge.Spec.Routes = append(edge.Spec.Routes, route)
}
edgeMap[rule.Host] = edge
}
}
}
func (d *Driver) calculateHTTPSEdgesFromGateway(edgeMap map[string]ingressv1alpha1.HTTPSEdge) {
gateways := d.store.ListGateways()
for _, gtw := range gateways {
useEdges, err := annotations.ExtractUseEdges(gtw)
if err != nil {
d.log.Error(err, fmt.Sprintf("failed to check %q annotation. defaulting to using endpoints", annotations.MappingStrategyAnnotation))
}
if !useEdges {
d.log.Info("the following gateway will be provided by ngrok endpoints",
"gateway", fmt.Sprintf("%s.%s", gtw.Name, gtw.Namespace),
)
continue
}
for _, listener := range gtw.Spec.Listeners {
if listener.Hostname == nil {
continue
}
allowedRoutes := listener.AllowedRoutes.Kinds
if len(allowedRoutes) > 0 {
createHttpsedge := false
for _, routeKind := range allowedRoutes {
if routeKind.Kind == "HTTPRoute" {
createHttpsedge = true
}
}
if !createHttpsedge {
continue
}
}
domainName := string(*listener.Hostname)
edge, ok := edgeMap[domainName]
if !ok {
continue
}
// TODO: Calculate routes from httpRoutes
// TODO: skip if no backend services
httproutes := d.store.ListHTTPRoutes()
for _, httproute := range httproutes {
for _, parent := range httproute.Spec.ParentRefs {
if string(parent.Name) != gtw.Name {
// not our gateway so skip
continue
}
if listener.AllowedRoutes != nil && listener.AllowedRoutes.Namespaces.From != nil {
switch *listener.AllowedRoutes.Namespaces.From {
case gatewayv1.NamespacesFromAll:
case gatewayv1.NamespacesFromSame:
if httproute.Namespace != gtw.Namespace {
continue
}
case gatewayv1.NamespacesFromSelector:
if httproute.Namespace != listener.AllowedRoutes.Namespaces.Selector.String() {
continue
}
}
}
// matches our gateway
for _, hostname := range httproute.Spec.Hostnames {
if string(hostname) != string(*listener.Hostname) {
// doesn't match this listener
continue
}
// matches gateway and listener
for _, rule := range httproute.Spec.Rules {
// TODO: resolve rule.Matches
// TODO: resolve rule.Filters
// for v0 we will only resolve the first backendRef
pathMatch := "/"
pathMatchType := "path_prefix"
// first match with a path will be accepted as the route's path
for _, match := range rule.Matches {
if match.Path != nil {
pathMatch = *match.Path.Value
if *match.Path.Type == gatewayv1.PathMatchExact {
pathMatchType = "exact_path"
}
break
}
}
route := ingressv1alpha1.HTTPSEdgeRouteSpec{
Match: pathMatch, // change based on the rule.match
MatchType: pathMatchType, // change based on rule.Matches
}
// TODO: set with values from rules.Filters + rules.Matches
// this HTTPRouteRule comes direct from gateway api yaml, and func returns the policy,
// which goes directly into the edge route in ngrok.
policy, err := d.createEndpointPolicyForGateway(&rule, httproute.Namespace)
if err != nil {
d.log.Error(err, "error creating policy from HTTPRouteRule", "rule", rule)
continue
}
route.Policy = policy
for idx, backendref := range rule.BackendRefs {
// currently the ingress controller doesn't support weighted backends
// so we'll only support one backendref per rule
// TODO: remove when tested with multiple backends
if idx > 0 {
break
}
// handle backendref
refKind := string(*backendref.Kind)
if refKind != "Service" {
// only support services currently
continue
}
refName := string(backendref.Name)
serviceUID, servicePort, err := d.getEdgeBackendRef(backendref.BackendRef, httproute.Namespace)
if err != nil {
d.log.Error(err, "could not find port for service", "namespace", httproute.Namespace, "service", refName)
continue
}
route.Backend = ingressv1alpha1.TunnelGroupBackend{
Labels: ngrokLabels(httproute.Namespace, serviceUID, refName, servicePort),
}
}
route.Metadata = d.gatewayNgrokMetadata
edge.Spec.Routes = append(edge.Spec.Routes, route)
}
}
}
}
edgeMap[domainName] = edge
}
}
}
func (d *Driver) getIngressBackend(backendSvc netv1.IngressServiceBackend, namespace string) (string, int32, error) {
service, servicePort, err := d.findBackendServicePort(backendSvc, namespace)
if err != nil {
return "", 0, err
}
return string(service.UID), servicePort.Port, nil
}
func (d *Driver) getEdgeBackendRef(backendRef gatewayv1.BackendRef, namespace string) (string, int32, error) {
if backendRef.Namespace != nil && string(*backendRef.Namespace) != namespace {
return "", 0, fmt.Errorf("namespace %s not supported", string(*backendRef.Namespace))
}
service, servicePort, err := d.findBackendRefServicePort(backendRef, namespace)
if err != nil {
return "", 0, err
}
return string(service.UID), servicePort.Port, nil
}
func (d *Driver) edgeLabels() map[string]string {
return map[string]string{
labelControllerNamespace: d.managerName.Namespace,
labelControllerName: d.managerName.Name,
}
}

Some files were not shown because too many files have changed in this diff Show More