diff --git a/modules/network/example/dev/example_workspace.yaml b/modules/network/example/dev/example_workspace.yaml index 97d1e51..2e98249 100644 --- a/modules/network/example/dev/example_workspace.yaml +++ b/modules/network/example/dev/example_workspace.yaml @@ -1,7 +1,7 @@ modules: network: path: oci://ghcr.io/kusionstack/network - version: 0.2.0 + version: 0.3.0 configs: default: port: diff --git a/modules/network/example/dev/kcl.mod b/modules/network/example/dev/kcl.mod index b13518d..6405cf8 100644 --- a/modules/network/example/dev/kcl.mod +++ b/modules/network/example/dev/kcl.mod @@ -3,8 +3,8 @@ name = "example" [dependencies] kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0" } -service = {oci = "oci://ghcr.io/kusionstack/service", tag = "0.1.0" } -network = { oci = "oci://ghcr.io/kusionstack/network", tag = "0.2.0" } +service = { oci = "oci://ghcr.io/kusionstack/service", tag = "0.2.0" } +network = { oci = "oci://ghcr.io/kusionstack/network", tag = "0.3.0" } [profile] entries = ["main.k"] diff --git a/modules/network/example/dev/main.k b/modules/network/example/dev/main.k index 918a9db..ddee7ce 100644 --- a/modules/network/example/dev/main.k +++ b/modules/network/example/dev/main.k @@ -3,6 +3,7 @@ import kam.v1.app_configuration as ac import service import service.container as c import network as n +import network.ingress as ni nginx: ac.AppConfiguration { workload: service.Service { @@ -16,10 +17,39 @@ nginx: ac.AppConfiguration { "network": n.Network { ports: [ n.Port { - port: 80 - public: True + port: 8080 + targetPort: 80 + public: False } ] + ingress: { + defaultBackend: { + service: { + port: { + number: 8080 + } + } + } + rules: [ + { + http: { + paths: [ + { + path: "/" + pathType: "Prefix" + backend: { + service: { + port: { + number: 8080 + } + } + } + } + ] + } + } + ] + } } } } diff --git a/modules/network/ingress/ingress.k b/modules/network/ingress/ingress.k new file mode 100644 index 0000000..f1cf813 --- /dev/null +++ b/modules/network/ingress/ingress.k @@ -0,0 +1,47 @@ +schema Ingress: + """ Ingress is a collection of rules that allow inbound connections to reach the endpoints defined by a backend. + An Ingress can be configured to give services externally-reachable urls, load balance traffic, terminate SSL, + offer name based virtual hosting etc. + + Attributes + ---------- + defaultBackend: IngressBackend, default is Undefined, optional. + DefaultBackend is the backend that should handle requests that don't match any rule. If Rules are not specified, + DefaultBackend must be specified. If DefaultBackend is not set, the handling of requests that do not match any + of the rules will be up to the Ingress controller. + ingressClassName: str, default is Undefined, optional. + IngressClassName is the name of an IngressClass cluster resource. Ingress controller implementations use this + field to know whether they should be serving this Ingress resource, by a transitive connection + (controller -> IngressClass -> Ingress resource). Although the `kubernetes.io/ingress.class` annotation + (simple constant name) was never formally defined, it was widely supported by Ingress controllers to create a + direct binding between Ingress controller and Ingress resources. Newly created Ingress resources should prefer + using the field. However, even though the annotation is officially deprecated, for backwards compatibility + reasons, ingress controllers should still honor that annotation if present. + rules: [IngressRule], default is Undefined, optional. + Rules is a list of host rules used to configure the Ingress. If unspecified, or no rule matches, all traffic is + sent to the default backend. + tls: [IngressTLS], default is Undefined, optional. + TLS represents the TLS configuration. Currently the Ingress only supports a single TLS port, 443. If multiple + members of this list specify different hosts, they will be multiplexed on the same port according to the hostname + specified through the SNI TLS extension, if the ingress controller fulfilling the ingress supports SNI. + labels: {str:str}, default is Undefined, optional. + Labels are key/value pairs that are attached to the workload. + annotations: {str:str}, default is Undefined, optional. + Annotations are key/value pairs that attach arbitrary non-identifying metadata to the workload. + """ + + # DefaultBackend is the backend that should handle requests that don't match any rule. + defaultBackend?: IngressBackend + + # IngressClassName is the name of an IngressClass cluster resource. + ingressClassName?: str + + # Rules is a list of host rules used to configure the Ingress. + rules?: [IngressRule] + + # TLS represents the TLS configuration. + tls?: [IngressTLS] + + # Labels and annotations can be used to attach arbitrary metadata as key-value pairs to resources. + labels?: {str:str} + annotations?: {str:str} \ No newline at end of file diff --git a/modules/network/ingress/ingress_backend.k b/modules/network/ingress/ingress_backend.k new file mode 100644 index 0000000..a1d9309 --- /dev/null +++ b/modules/network/ingress/ingress_backend.k @@ -0,0 +1,63 @@ +schema IngressBackend: + """ IngressBackend describes all endpoints for a given service and port. + + Attributes + ---------- + resource: TypedLocalObjectReference, default is Undefined, optional. + Resource is an ObjectRef to another Kubernetes resource in the namespace of the Ingress object. If resource is + specified, a service.Name and service.Port must not be specified. This is a mutually exclusive setting with + "Service". + service: IngressServiceBackend, default is Undefined, optional. + Service references a service as a backend. This is a mutually exclusive setting with "Resource". + """ + + # Resource is an ObjectRef to another Kubernetes resource in the namespace of the Ingress object. + resource?: TypedLocalObjectReference + + # Service references a service as a backend. + service?: IngressServiceBackend + + check: + not resource or not service, "resource and number are mutually exclusive" + + +schema IngressServiceBackend: + """ IngressServiceBackend references a Kubernetes Service as a Backend. + + Attributes + ---------- + name: str, default is Undefined, optional. + Name is the referenced service. The service must exist in the same namespace as the Ingress object. + If the name is not set, the generated public service name will be used. + port: ServiceBackendPort, default is Undefined, optional. + Port of the referenced service. A port name or port number is required for a IngressServiceBackend. + """ + + # Name is the referenced service. The service must exist in the same namespace as the Ingress object. + # If the name is not set, the generated public service name will be used. + name?: str + + # Port of the referenced service. A port name or port number is required for a IngressServiceBackend. + port?: ServiceBackendPort + + +schema ServiceBackendPort: + """ ServiceBackendPort is the service port being referenced. A port name or port number is required + for a IngressServiceBackend. + + Attributes + ---------- + name: str, default is Undefined, optional. + Name is the name of the port on the Service. This is a mutually exclusive setting with "Number". + number: int, default is Undefined, optional. + Number is the numerical port number (e.g. 80) on the Service. This is a mutually exclusive setting with "Name". + """ + + # Name is the name of the port on the Service. This is a mutually exclusive setting with "Number". + name?: str + + # Number is the numerical port number (e.g. 80) on the Service. This is a mutually exclusive setting with "Name". + number?: int + + check: + not name or not number, "name and number are mutually exclusive" \ No newline at end of file diff --git a/modules/network/ingress/ingress_class.k b/modules/network/ingress/ingress_class.k new file mode 100644 index 0000000..a5cb7c1 --- /dev/null +++ b/modules/network/ingress/ingress_class.k @@ -0,0 +1,70 @@ +schema IngressClass: + """ IngressClass represents the class of the Ingress, referenced by the Ingress Spec. The + `ingressclass.kubernetes.io/is-default-class` annotation can be used to indicate that an IngressClass should be + considered default. When a single IngressClass resource has this annotation set to true, new Ingress resources + without a class specified will be assigned this default class. + + Attributes + ---------- + controller: str, default is Undefined, optional. + Controller refers to the name of the controller that should handle this class. This allows for different "flavors" + that are controlled by the same controller. For example, you may have different parameters for the same implementing + controller. This should be specified as a domain-prefixed path no more than 250 characters in length, + e.g. "acme.io/ingress-controller". This field is immutable. + parameters: IngressClassParametersReference, default is Undefined, optional. + Parameters is a link to a custom resource containing additional configuration for the controller. This is optional + if the controller does not require extra parameters. + labels: {str:str}, default is Undefined, optional. + Labels are key/value pairs that are attached to the workload. + annotations: {str:str}, default is Undefined, optional. + Annotations are key/value pairs that attach arbitrary non-identifying metadata to the workload. + """ + + # Controller refers to the name of the controller that should handle this class. + controller?: str + + # Parameters is a link to a custom resource containing additional configuration for the controller. + parameters?: IngressClassParametersReference + + # Labels and annotations can be used to attach arbitrary metadata as key-value pairs to resources. + labels?: {str:str} + annotations?: {str:str} + +schema IngressClassParametersReference: + """ IngressClassParametersReference identifies an API object. This can be used to specify a cluster or + namespace-scoped resource. + + Attributes + ---------- + kind: str, default is Undefined, required. + Kind is the type of resource being referenced. + name: str, default is Undefined, required. + Name is the name of resource being referenced. + apiGroup: str, default is Undefined, optional. + ApiGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be + in the core API group. For any other third-party types, APIGroup is required. + namespace: str, default is Undefined, optional. + Namespace is the namespace of the resource being referenced. This field is required when scope is set to "Namespace" + and must be unset when scope is set to "Cluster". + scope: str, default is Undefined, optional. + Scope represents if this refers to a cluster or namespace scoped resource. This may be set to "Cluster" (default) + or "Namespace". + """ + + # Kind is the type of resource being referenced. + kind: str + + # Name is the name of resource being referenced. + name: str + + # ApiGroup is the group for the resource being referenced. + apiGroup?: str + + # Namespace is the namespace of the resource being referenced. + namespace?: str + + # Scope represents if this refers to a cluster or namespace scoped resource. + scope?: str + + check: + scope in ["Namespace", "Cluster"] if scope, "scope value is invalid" \ No newline at end of file diff --git a/modules/network/ingress/ingress_rule.k b/modules/network/ingress/ingress_rule.k new file mode 100644 index 0000000..01869f0 --- /dev/null +++ b/modules/network/ingress/ingress_rule.k @@ -0,0 +1,82 @@ +schema IngressRule: + """ IngressRule represents the rules mapping the paths under a specified host to the related backend services. + Incoming requests are first evaluated for a host match, then routed to the backend associated with the matching IngressRuleValue. + + Attributes + ---------- + host: str, default is Undefined, optional. + Host is the fully qualified domain name of a network host, as defined by RFC 3986. Note the following deviations + from the "host" part of the URI as defined in RFC 3986: 1. IPs are not allowed. Currently an IngressRuleValue can + only apply to the IP in the Spec of the parent Ingress. 2. The : delimiter is not respected because ports are not + allowed. Currently the port of an Ingress is implicitly :80 for http and :443 for https. Both these may change in + the future. Incoming requests are matched against the host before the IngressRuleValue. If the host is unspecified, + the Ingress routes all traffic based on the specified IngressRuleValue. + Host can be "precise" which is a domain name without the terminating dot of a network host (e.g. "foo.bar.com") + or "wildcard", which is a domain name prefixed with a single wildcard label (e.g. ".foo.com"). The wildcard + character '' must appear by itself as the first DNS label and matches only a single label. You cannot have a + wildcard label by itself (e.g. Host == "*"). Requests will be matched against the Host field in the following + way: 1. If host is precise, the request matches this rule if the http host header is equal to Host. 2. If host is + a wildcard, then the request matches this rule if the http host header is to equal to the suffix (removing the + first label) of the wildcard rule. + http: HTTPIngressRuleValue, default is Undefined, optional. + HTTPIngressRuleValue is a list of http selectors pointing to backends. In the example: http:///? -> backend where + parts of the url correspond to RFC 3986, this resource will be used to match against everything after the last '/' + and before the first '?' or '#'. + """ + + # Host is the fully qualified domain name of a network host, as defined by RFC 3986. + host?: str + + # HTTPIngressRuleValue is a list of http selectors pointing to backends. + http?: HTTPIngressRuleValue + + +schema HTTPIngressRuleValue: + """ HTTPIngressRuleValue is a list of http selectors pointing to backends. In the example: + http:///? -> backend where where parts of the url correspond to RFC 3986, this resource will + be used to match against everything after the last '/' and before the first '?' or '#'. + + Attributes + ---------- + paths: [HTTPIngressPath], default is Undefined, required. + Paths is a collection of paths that map requests to backends. + """ + + # Paths is a collection of paths that map requests to backends. + paths: [HTTPIngressPath] + + +schema HTTPIngressPath: + """ HTTPIngressPath associates a path with a backend. Incoming urls matching the path are forwarded to the backend. + + Attributes + ---------- + backend: IngressBackend, default is Undefined, required. + Backend defines the referenced service endpoint to which the traffic will be forwarded to. + pathType: str, default is Undefined, required. + PathType determines the interpretation of the path matching. PathType can be one of the following values: + * Exact: Matches the URL path exactly. * Prefix: Matches based on a URL path prefix split by '/'. Matching is + done on a path element by element basis. A path element refers is the list of labels in the path split by the '/' + separator. A request is a match for path p if every p is an element-wise prefix of p of the request path. Note + that if the last element of the path is a substring of the last element in request path, it is not a match + (e.g. /foo/bar matches /foo/bar/baz, but does not match /foo/barbaz). + ImplementationSpecific: Interpretation of the Path matching is up to the IngressClass. Implementations can treat + this as a separate PathType or treat it identically to Prefix or Exact path types. Implementations are required + to support all path types. + path: str, default is Undefined, optional. + Path is matched against the path of an incoming request. Currently it can contain characters disallowed from the + conventional "path" part of a URL as defined by RFC 3986. Paths must begin with a '/' and must be present when + using PathType with value "Exact" or "Prefix". + """ + + # Backend defines the referenced service endpoint to which the traffic will be forwarded to. + backend: IngressBackend + + # PathType determines the interpretation of the path matching. + pathType: str + + # Path is matched against the path of an incoming request. + path?: str + + check: + pathType in ["Exact", "Prefix", "ImplementationSpecific"] if pathType, "pathType value is invalid" \ No newline at end of file diff --git a/modules/network/ingress/ingress_tls.k b/modules/network/ingress/ingress_tls.k new file mode 100644 index 0000000..69f39ea --- /dev/null +++ b/modules/network/ingress/ingress_tls.k @@ -0,0 +1,20 @@ +schema IngressTLS: + """ IngressTLS describes the transport layer security associated with an ingress. + + Attributes + ---------- + hosts: [str], default is Undefined, optional. + Hosts is a list of hosts included in the TLS certificate. The values in this list must match the name/s used in + the tlsSecret. Defaults to the wildcard host setting for the loadbalancer controller fulfilling this Ingress, if + left unspecified. + secretName: str, default is Undefined, optional. + SecretName is the name of the secret used to terminate TLS traffic on port 443. Field is left optional to allow + TLS routing based on SNI hostname alone. If the SNI host in a listener conflicts with the "Host" header field used + by an IngressRule, the SNI host is used for termination and value of the "Host" header is used for routing. + """ + + # Hosts is a list of hosts included in the TLS certificate. + hosts?: [str] + + # SecretName is the name of the secret used to terminate TLS traffic on port 443. + secretName?: str \ No newline at end of file diff --git a/modules/network/ingress/typed_local_object_reference.k b/modules/network/ingress/typed_local_object_reference.k new file mode 100644 index 0000000..a0799ed --- /dev/null +++ b/modules/network/ingress/typed_local_object_reference.k @@ -0,0 +1,24 @@ +schema TypedLocalObjectReference: + """ TypedLocalObjectReference contains enough information to let you locate the typed referenced object inside the + same namespace. + + Attributes + ---------- + kind: str, default is Undefined, required. + Kind is the type of resource being referenced. + name: str, default is Undefined, required. + Name is the name of resource being referenced. + apiGroup: str, optional. + APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must + be in the core API group. For any other third-party types, APIGroup is required. + """ + + # Kind is the type of resource being referenced. + kind: str + + # Name is the name of resource being referenced. + name: str + + # APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must + # be in the core API group. For any other third-party types, APIGroup is required. + apiGroup?: str \ No newline at end of file diff --git a/modules/network/kcl.mod b/modules/network/kcl.mod index b700717..32a99b8 100644 --- a/modules/network/kcl.mod +++ b/modules/network/kcl.mod @@ -1,3 +1,3 @@ [package] name = "network" -version = "0.2.1-rc.1" +version = "0.3.0" diff --git a/modules/network/network.k b/modules/network/network.k index dd8e8db..5920f89 100644 --- a/modules/network/network.k +++ b/modules/network/network.k @@ -1,4 +1,6 @@ -schema Network: +import ingress as ing + +schema Network: """ Network describes the network accessories of Workload, which typically contains the exposed ports, load balancer and other related resource configs. @@ -6,6 +8,10 @@ schema Network: ---------- ports: [n.Port], default is Undefined, optional. The list of ports which the Workload should get exposed. + ingress: ing.Ingress, default is Undefined, optional. + Ingress is a collection of rules that allow inbound connections to reach the endpoints defined by a backend. + ingressClass: ing.IngressClass, default is Undefined, optional. + IngressClass represents the class of the Ingress, referenced by the Ingress Spec. Examples -------- @@ -29,6 +35,13 @@ schema Network: # The list of ports getting exposed. ports?: [Port] + # Ingress is a collection of rules that allow inbound connections to reach the endpoints defined by a backend. + ingress?: ing.Ingress + + # Ingress is a collection of rules that allow inbound connections to reach the endpoints defined by a backend. + ingressClass?: ing.IngressClass + + schema Port: """ Port defines the exposed port of Workload, which can be used to describe how the Workload get accessed. diff --git a/modules/network/src/Makefile b/modules/network/src/Makefile index 1722df5..b17cc9d 100644 --- a/modules/network/src/Makefile +++ b/modules/network/src/Makefile @@ -2,7 +2,7 @@ TEST?=$$(go list ./... | grep -v 'vendor') ###### chang variables below according to your own modules ### NAMESPACE=kusionstack NAME=network -VERSION=0.2.1-rc.1 +VERSION=0.3.0 BINARY=../bin/kusion-module-${NAME}_${VERSION} LOCAL_ARCH := $(shell uname -m) @@ -17,7 +17,7 @@ export OS_ARCH ?= $(GOARCH_LOCAL) default: install build-darwin: - GOOS=darwin GOARCH=arm64 go build -o ${BINARY} ./${NAME} + GOOS=darwin GOARCH=arm64 go build -o ${BINARY} install: build-darwin # copy module binary to $KUSION_HOME. e.g. ~/.kusion/modules/kusionstack/network/v0.1.0/darwin/arm64/kusion-module-network_0.1.0 diff --git a/modules/network/src/go.mod b/modules/network/src/go.mod index 6718e0e..32ed68c 100644 --- a/modules/network/src/go.mod +++ b/modules/network/src/go.mod @@ -5,10 +5,12 @@ go 1.23.1 toolchain go1.23.2 require ( + github.com/hashicorp/go-hclog v1.6.3 github.com/stretchr/testify v1.10.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.31.3 k8s.io/apimachinery v0.31.3 + k8s.io/kubernetes v1.32.0 kusionstack.io/kusion-api-go v0.13.0 kusionstack.io/kusion-module-framework v0.2.3-beta.6 ) @@ -21,7 +23,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-plugin v1.6.2 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/modules/network/src/go.sum b/modules/network/src/go.sum index 79e3fb8..cbc7ea7 100644 --- a/modules/network/src/go.sum +++ b/modules/network/src/go.sum @@ -150,6 +150,8 @@ k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4= k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kubernetes v1.32.0 h1:4BDBWSolqPrv8GC3YfZw0CJvh5kA1TPnoX0FxDVd+qc= +k8s.io/kubernetes v1.32.0/go.mod h1:tiIKO63GcdPRBHW2WiUFm3C0eoLczl3f7qi56Dm1W8I= k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 h1:jGnCPejIetjiy2gqaJ5V0NLwTpF4wbQ6cZIItJCSHno= k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= kusionstack.io/kusion-api-go v0.13.0 h1:fDrLkgpkBnG7DTSHmCEfO/aL+iv6FZCTZ4ucxaQSuwg= diff --git a/modules/network/src/network.go b/modules/network/src/network.go index dc96669..9530b51 100644 --- a/modules/network/src/network.go +++ b/modules/network/src/network.go @@ -3,13 +3,13 @@ package main import ( "context" "encoding/json" - "errors" "fmt" "runtime/debug" "strings" "gopkg.in/yaml.v3" v1 "k8s.io/api/core/v1" + k8snetworking "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" kusionapiv1 "kusionstack.io/kusion-api-go/api.kusion.io/v1" @@ -20,70 +20,6 @@ import ( "kusionstack.io/kusion-module-framework/pkg/util/workspace" ) -const ( - FieldType = "type" - FieldLabels = "labels" - FieldAnnotations = "annotations" -) - -const ( - CSPAWS = "aws" - CSPAliCloud = "alicloud" -) - -const ( - ProtocolTCP = "TCP" - ProtocolUDP = "UDP" -) - -const ( - k8sKindService = "Service" - suffixPublic = "public" - suffixPrivate = "private" -) - -var ( - ErrEmptyPortConfig = errors.New("empty port config") - ErrEmptyType = errors.New("type must not be empty when public") - ErrUnsupportedType = errors.New("type only support alicloud and aws for now") - ErrInvalidPort = errors.New("port must be between 1 and 65535") - ErrInvalidTargetPort = errors.New("targetPort must be between 1 and 65535 if exist") - ErrInvalidProtocol = errors.New("protocol must be TCP or UDP") - ErrEmptySvcWorkload = errors.New("network port should be binded to a service workload") -) - -// Network describes the network accessories of workload, which typically contains the exposed -// ports, load balancer and other related resource configs. -type Network struct { - Ports []Port `yaml:"ports,omitempty" json:"ports,omitempty"` -} - -// Port defines the exposed port of workload, which can be used to describe how -// the workload get accessed. -type Port struct { - // Type is the specific cloud vendor that provides load balancer, works when Public - // is true, supports CSPAliCloud and CSPAWS for now. - Type string `yaml:"type,omitempty" json:"type,omitempty"` - - // Port is the exposed port of the workload. - Port int `yaml:"port,omitempty" json:"port,omitempty"` - - // TargetPort is the backend container.Container port. - TargetPort int `yaml:"targetPort,omitempty" json:"targetPort,omitempty"` - - // Protocol is protocol used to expose the port, support ProtocolTCP and ProtocolUDP. - Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"` - - // Public defines whether to expose the port through Internet. - Public bool `yaml:"public,omitempty" json:"public,omitempty"` - - // Labels are the attached labels of the port, works only when the Public is true. - Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` - - // Annotations are the attached annotations of the port, works only when the Public is true. - Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` -} - func (network *Network) Generate(ctx context.Context, request *module.GeneratorRequest) (response *module.GeneratorResponse, err error) { // Get the module logger with the generator context. logger := log.GetModuleLogger(ctx) @@ -122,6 +58,24 @@ func (network *Network) Generate(ctx context.Context, request *module.GeneratorR } resources = append(resources, res...) + // Generate network ingress related resources. + ingressRes, err := network.GenerateIngressResource(request) + if err != nil { + return nil, err + } + if ingressRes != nil { + resources = append(resources, *ingressRes) + } + + // Generate network ingressClass related resources. + ingressClassRes, err := network.GenerateIngressClassResource(request) + if err != nil { + return nil, err + } + if ingressClassRes != nil { + resources = append(resources, *ingressClassRes) + } + return &module.GeneratorResponse{ Resources: resources, }, nil @@ -135,6 +89,10 @@ func (network *Network) GetCompleteConfig(devConfig kusionapiv1.Accessory, platf return err } + if err := network.CompleteIngressConfig(devConfig); err != nil { + return err + } + return network.Validate() } @@ -164,6 +122,34 @@ func (network *Network) CompletePortConfig(devConfig kusionapiv1.Accessory, plat network.Ports = append(network.Ports, p) } } + + ingressConf, ok := devConfig["ingress"] + if ok { + ingressYaml, err := yaml.Marshal(ingressConf) + if err != nil { + return err + } + var ingress Ingress + err = yaml.Unmarshal(ingressYaml, &ingress) + if err != nil { + return err + } + network.Ingress = &ingress + } + + ingressClassConf, ok := devConfig["ingressClass"] + if ok { + ingressClassYaml, err := yaml.Marshal(ingressClassConf) + if err != nil { + return err + } + var ingressClass IngressClass + err = yaml.Unmarshal(ingressClassYaml, &ingressClass) + if err != nil { + return err + } + network.IngressClass = &ingressClass + } } var portConfig kusionapiv1.GenericConfig @@ -238,6 +224,40 @@ func (network *Network) Validate() error { return nil } +// CompleteIngressConfig completes the network ingress related config. +func (network *Network) CompleteIngressConfig(devConfig kusionapiv1.Accessory) error { + if devConfig != nil { + ingressConf, ok := devConfig["ingress"] + if ok { + ingressYaml, err := yaml.Marshal(ingressConf) + if err != nil { + return err + } + var ingress Ingress + err = yaml.Unmarshal(ingressYaml, &ingress) + if err != nil { + return err + } + network.Ingress = &ingress + } + + ingressClassConf, ok := devConfig["ingressClass"] + if ok { + ingressClassYaml, err := yaml.Marshal(ingressClassConf) + if err != nil { + return err + } + var ingressClass IngressClass + err = yaml.Unmarshal(ingressClassYaml, &ingressClass) + if err != nil { + return err + } + network.IngressClass = &ingressClass + } + } + return nil +} + // ValidatePortConfig validates whether the port configs are valid or not. func (network *Network) ValidatePortConfig() error { for _, port := range network.Ports { @@ -396,6 +416,174 @@ func toMapStringInterface(i any) (map[string]interface{}, error) { return m, nil } +// GenerateIngressResource generates the resources related to the network ingress. +func (network *Network) GenerateIngressResource(request *module.GeneratorRequest) (*kusionapiv1.Resource, error) { + if network.Ingress == nil { + return nil, nil + } + ingress, err := network.generateIngress(request) + if err != nil { + return nil, err + } + resourceID := module.KubernetesResourceID(ingress.TypeMeta, ingress.ObjectMeta) + resource, err := module.WrapK8sResourceToKusionResource(resourceID, ingress) + if err != nil { + return nil, err + } + return resource, nil +} + +func (network *Network) generateIngress(request *module.GeneratorRequest) (*k8snetworking.Ingress, error) { + appUname := module.UniqueAppName(request.Project, request.Stack, request.App) + resourceName := fmt.Sprintf("%s-%s", appUname, ingressSuffix) + k8sIngress := &k8snetworking.Ingress{ + TypeMeta: metav1.TypeMeta{ + APIVersion: k8snetworking.SchemeGroupVersion.String(), + Kind: K8sKindIngress, + }, + ObjectMeta: metav1.ObjectMeta{ + Labels: network.Ingress.Labels, + Annotations: network.Ingress.Annotations, + Name: resourceName, + Namespace: request.Project, + }, + Spec: k8snetworking.IngressSpec{ + IngressClassName: network.Ingress.IngressClassName, + }, + } + + if network.Ingress.DefaultBackend != nil { + defaultBackend, err := network.toIngressBackend(*network.Ingress.DefaultBackend, appUname) + if err != nil { + return nil, err + } + k8sIngress.Spec.DefaultBackend = defaultBackend + } + + for _, t := range network.Ingress.TLS { + tls := k8snetworking.IngressTLS{ + Hosts: t.Hosts, + SecretName: t.SecretName, + } + k8sIngress.Spec.TLS = append(k8sIngress.Spec.TLS, tls) + } + + var rules []k8snetworking.IngressRule + for _, r := range network.Ingress.Rules { + var rule k8snetworking.IngressRule + if r.HTTP != nil { + var paths []k8snetworking.HTTPIngressPath + for _, p := range r.HTTP.Paths { + httpPath := k8snetworking.HTTPIngressPath{Path: p.Path} + if p.PathType != "" { + httpPath.PathType = &p.PathType + } + + backend, err := network.toIngressBackend(p.Backend, appUname) + if err != nil { + return nil, err + } + if backend != nil { + httpPath.Backend = *backend + } + paths = append(paths, httpPath) + } + rule.HTTP = &k8snetworking.HTTPIngressRuleValue{ + Paths: paths, + } + } + rule.Host = r.Host + rules = append(rules, rule) + } + k8sIngress.Spec.Rules = rules + return k8sIngress, nil +} + +func (network *Network) toIngressBackend(b IngressBackend, appUname string) (*k8snetworking.IngressBackend, error) { + var backend k8snetworking.IngressBackend + if b.Service != nil { + svcName := b.Service.Name + if b.Service.Name == "" { + foundPort := false + for _, port := range network.Ports { + if b.Service.Port.Number == int32(port.Port) { + if port.Public { + svcName = fmt.Sprintf("%s-%s", appUname, suffixPublic) + } else { + svcName = fmt.Sprintf("%s-%s", appUname, suffixPrivate) + } + foundPort = true + break + } + } + if !foundPort { + return nil, fmt.Errorf("not found available service for backend, please check service name or port") + } + } + + backend.Service = &k8snetworking.IngressServiceBackend{ + Name: svcName, + Port: k8snetworking.ServiceBackendPort{ + Name: b.Service.Port.Name, + Number: b.Service.Port.Number, + }, + } + } + + if b.Resource != nil { + backend.Resource = &v1.TypedLocalObjectReference{ + APIGroup: b.Resource.APIGroup, + Kind: b.Resource.Kind, + Name: b.Resource.Name, + } + } + return &backend, nil +} + +// GenerateIngressClassResource generates the resources related to the network ingressClass. +func (network *Network) GenerateIngressClassResource(request *module.GeneratorRequest) (*kusionapiv1.Resource, error) { + if network.IngressClass == nil { + return nil, nil + } + ingressClass := network.generateIngressClass(request) + resourceID := module.KubernetesResourceID(ingressClass.TypeMeta, ingressClass.ObjectMeta) + resource, err := module.WrapK8sResourceToKusionResource(resourceID, ingressClass) + if err != nil { + return nil, err + } + return resource, nil +} + +func (network *Network) generateIngressClass(request *module.GeneratorRequest) *k8snetworking.IngressClass { + appUname := module.UniqueAppName(request.Project, request.Stack, request.App) + resourceName := fmt.Sprintf("%s-%s", appUname, ingressClassSuffix) + k8sIngressClass := &k8snetworking.IngressClass{ + TypeMeta: metav1.TypeMeta{ + APIVersion: k8snetworking.SchemeGroupVersion.String(), + Kind: K8sKindIngressClass, + }, + ObjectMeta: metav1.ObjectMeta{ + Labels: network.IngressClass.Labels, + Annotations: network.IngressClass.Annotations, + Name: resourceName, + }, + Spec: k8snetworking.IngressClassSpec{ + Controller: network.IngressClass.Controller, + }, + } + + if network.IngressClass.Parameters != nil { + k8sIngressClass.Spec.Parameters = &k8snetworking.IngressClassParametersReference{ + APIGroup: network.IngressClass.Parameters.APIGroup, + Kind: network.IngressClass.Parameters.Kind, + Name: network.IngressClass.Parameters.Name, + Scope: network.IngressClass.Parameters.Scope, + Namespace: network.IngressClass.Parameters.Namespace, + } + } + return k8sIngressClass +} + func main() { server.Start(&Network{}) } diff --git a/modules/network/src/network_test.go b/modules/network/src/network_test.go index 8977808..a4cc86f 100644 --- a/modules/network/src/network_test.go +++ b/modules/network/src/network_test.go @@ -203,3 +203,118 @@ func TestNetworkModule_Validate(t *testing.T) { }) } } + +func TestNetworkModule_GenerateIngressResource(t *testing.T) { + r := &module.GeneratorRequest{ + Project: "test-project", + Stack: "test-stack", + App: "test-app", + Workload: kusionapiv1.Accessory{ + "_type": "network.Network", + }, + } + + ingresClassName := "nginx-example" + testcases := []struct { + name string + ingress Ingress + expectedErr bool + }{ + { + name: "Generate ingress resources", + ingress: Ingress{ + Annotations: map[string]string{ + "nginx.ingress.kubernetes.io/rewrite-target": "/", + }, + IngressClassName: &ingresClassName, + DefaultBackend: &IngressBackend{ + Service: &IngressServiceBackend{ + Name: "test", + Port: ServiceBackendPort{ + Number: 80, + }, + }, + }, + TLS: []IngressTLS{ + { + Hosts: []string{"https-example.foo.com"}, + SecretName: "testsecret-tls", + }, + }, + Rules: []IngressRule{ + { + Host: "foo.bar.com", + HTTP: &HTTPIngressRuleValue{ + Paths: []HTTPIngressPath{ + { + Path: "/foo", + PathType: "Prefix", + Backend: IngressBackend{ + Service: &IngressServiceBackend{ + Name: "service1", + Port: ServiceBackendPort{ + Number: 4200, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedErr: false, + }, + } + + for _, tc := range testcases { + network := &Network{ + Ingress: &tc.ingress, + } + t.Run(tc.name, func(t *testing.T) { + _, err := network.GenerateIngressResource(r) + assert.Equal(t, tc.expectedErr, err != nil) + }) + } +} + +func TestNetworkModule_GenerateIngressClassResource(t *testing.T) { + r := &module.GeneratorRequest{ + Project: "test-project", + Stack: "test-stack", + App: "test-app", + Workload: kusionapiv1.Accessory{ + "_type": "network.Network", + }, + } + + apiGroup := "k8s.example.net" + testcases := []struct { + name string + ingressClass IngressClass + expectedErr bool + }{ + { + name: "Generate ingressClass resources", + ingressClass: IngressClass{ + Controller: "example.com/ingress-controller", + Parameters: &IngressClassParametersReference{ + APIGroup: &apiGroup, + Kind: "ClusterIngressParameter", + Name: "external-config-1", + }, + }, + expectedErr: false, + }, + } + + for _, tc := range testcases { + network := &Network{ + IngressClass: &tc.ingressClass, + } + t.Run(tc.name, func(t *testing.T) { + _, err := network.GenerateIngressClassResource(r) + assert.Equal(t, tc.expectedErr, err != nil) + }) + } +} diff --git a/modules/network/src/type.go b/modules/network/src/type.go new file mode 100644 index 0000000..bc9a2fa --- /dev/null +++ b/modules/network/src/type.go @@ -0,0 +1,288 @@ +package main + +import ( + "errors" + + k8snetworking "k8s.io/api/networking/v1" +) + +const ( + FieldType = "type" + FieldLabels = "labels" + FieldAnnotations = "annotations" +) + +const ( + CSPAWS = "aws" + CSPAliCloud = "alicloud" +) + +const ( + ProtocolTCP = "TCP" + ProtocolUDP = "UDP" +) + +const ( + K8sKindIngress = "Ingress" + K8sKindIngressClass = "IngressClass" + k8sKindService = "Service" + suffixPublic = "public" + suffixPrivate = "private" + ingressSuffix = "ingress" + ingressClassSuffix = "ingressclass" +) + +var ( + ErrEmptyPortConfig = errors.New("empty port config") + ErrEmptyType = errors.New("type must not be empty when public") + ErrUnsupportedType = errors.New("type only support alicloud and aws for now") + ErrInvalidPort = errors.New("port must be between 1 and 65535") + ErrInvalidTargetPort = errors.New("targetPort must be between 1 and 65535 if exist") + ErrInvalidProtocol = errors.New("protocol must be TCP or UDP") + ErrEmptySvcWorkload = errors.New("network port should be binded to a service workload") +) + +// Network describes the network accessories of workload, which typically contains the exposed +// ports, load balancer and other related resource configs. +type Network struct { + Ports []Port `yaml:"ports,omitempty" json:"ports,omitempty"` + Ingress *Ingress `yaml:"ingress,omitempty" json:"ingress,omitempty"` + IngressClass *IngressClass `yaml:"ingressClass,omitempty" json:"ingressClass,omitempty"` +} + +// Port defines the exposed port of workload, which can be used to describe how +// the workload get accessed. +type Port struct { + // Type is the specific cloud vendor that provides load balancer, works when Public + // is true, supports CSPAliCloud and CSPAWS for now. + Type string `yaml:"type,omitempty" json:"type,omitempty"` + + // Port is the exposed port of the workload. + Port int `yaml:"port,omitempty" json:"port,omitempty"` + + // TargetPort is the backend container.Container port. + TargetPort int `yaml:"targetPort,omitempty" json:"targetPort,omitempty"` + + // Protocol is protocol used to expose the port, support ProtocolTCP and ProtocolUDP. + Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"` + + // Public defines whether to expose the port through Internet. + Public bool `yaml:"public,omitempty" json:"public,omitempty"` + + // Labels are the attached labels of the port, works only when the Public is true. + Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` + + // Annotations are the attached annotations of the port, works only when the Public is true. + Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` +} + +// Ingress is a collection of rules that allow inbound connections to reach the +// endpoints defined by a backend. An Ingress can be configured to give services +// externally-reachable urls, load balance traffic, terminate SSL, offer name +// based virtual hosting etc. +type Ingress struct { + // Labels are the attached labels of the port, works only when the Public is true. + Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` + + // Annotations are the attached annotations of the port, works only when the Public is true. + Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` + + // ingressClassName is the name of the IngressClass cluster resource. The + // associated IngressClass defines which controller will implement the + // resource. This replaces the deprecated `kubernetes.io/ingress.class` + // annotation. For backwards compatibility, when that annotation is set, it + // must be given precedence over this field. The controller may emit a + // warning if the field and annotation have different values. + // Implementations of this API should ignore Ingresses without a class + // specified. An IngressClass resource may be marked as default, which can + // be used to set a default value for this field. For more information, + // refer to the IngressClass documentation. + IngressClassName *string `yaml:"ingressClassName,omitempty" json:"ingressClassName,omitempty"` + + // defaultBackend is the backend that should handle requests that don't + // match any rule. If Rules are not specified, DefaultBackend must be specified. + // If DefaultBackend is not set, the handling of requests that do not match any + // of the rules will be up to the Ingress controller. + DefaultBackend *IngressBackend `yaml:"defaultBackend,omitempty" json:"defaultBackend,omitempty"` + + // tls represents the TLS configuration. Currently the ingress only supports a + // single TLS port, 443. If multiple members of this list specify different hosts, + // they will be multiplexed on the same port according to the hostname specified + // through the SNI TLS extension, if the ingress controller fulfilling the + // ingress supports SNI. + TLS []IngressTLS `yaml:"tls,omitempty" json:"tls,omitempty"` + + // rules is a list of host rules used to configure the Ingress. If unspecified, or + // no rule matches, all traffic is sent to the default backend. + Rules []IngressRule `yaml:"rules,omitempty" json:"rules,omitempty"` +} + +// IngressBackend describes all endpoints for a given service and port. +type IngressBackend struct { + // service references a service as a backend. + // This is a mutually exclusive setting with "Resource". + Service *IngressServiceBackend `yaml:"service,omitempty" json:"service,omitempty"` + + // resource is an ObjectRef to another Kubernetes resource in the namespace + // of the Ingress object. If resource is specified, a service.Name and + // service.Port must not be specified. + // This is a mutually exclusive setting with "Service". + Resource *TypedLocalObjectReference `yaml:"resource,omitempty" json:"resource,omitempty"` +} + +// IngressServiceBackend references a Kubernetes Service as a Backend. +type IngressServiceBackend struct { + // name is the referenced service. + // The service must exist in the same namespace as the Ingress object. + Name string `yaml:"name,omitempty" json:"name,omitempty"` + + // port of the referenced service. + // A port name or port number is required for a IngressServiceBackend. + Port ServiceBackendPort `yaml:"port,omitempty" json:"port,omitempty"` +} + +// ServiceBackendPort is the service port being referenced. +type ServiceBackendPort struct { + // name is the name of the port on the Service. + // This must be an IANA_SVC_NAME (following RFC6335). + // This is a mutually exclusive setting with "Number". + Name string `yaml:"name,omitempty" json:"name,omitempty"` + + // number is the numerical port number (e.g. 80) on the Service. + // This is a mutually exclusive setting with "Name". + Number int32 `yaml:"number,omitempty" json:"number,omitempty"` +} + +// TypedLocalObjectReference contains enough information to let you locate the typed referenced object inside the same namespace. +type TypedLocalObjectReference struct { + // APIGroup is the group for the resource being referenced. + // If APIGroup is not specified, the specified Kind must be in the core API group. + // For any other third-party types, APIGroup is required. + APIGroup *string `yaml:"apiGroup,omitempty" json:"apiGroup,omitempty"` + + // Kind is the type of resource being referenced + Kind string `yaml:"kind" json:"kind"` + + // Name is the name of resource being referenced + Name string `yaml:"name" json:"name"` +} + +// IngressTLS describes the transport layer security associated with an ingress. +type IngressTLS struct { + // hosts is a list of hosts included in the TLS certificate. The values in + // this list must match the name/s used in the tlsSecret. Defaults to the + // wildcard host setting for the loadbalancer controller fulfilling this + // Ingress, if left unspecified. + Hosts []string `yaml:"hosts,omitempty" json:"hosts,omitempty"` + + // secretName is the name of the secret used to terminate TLS traffic on + // port 443. Field is left optional to allow TLS routing based on SNI + // hostname alone. If the SNI host in a listener conflicts with the "Host" + // header field used by an IngressRule, the SNI host is used for termination + // and value of the "Host" header is used for routing. + SecretName string `yaml:"secretName,omitempty" json:"secretName,omitempty"` +} + +// IngressRule represents the rules mapping the paths under a specified host to +// the related backend services. Incoming requests are first evaluated for a +// host match, then routed to the backend associated with the matching +// IngressRuleValue. +type IngressRule struct { + // host is the fully qualified domain name of a network host, as defined by RFC 3986. + // Note the following deviations from the "host" part of the + // URI as defined in RFC 3986: + // 1. IPs are not allowed. Currently an IngressRuleValue can only apply to + // the IP in the Spec of the parent Ingress. + // 2. The `:` delimiter is not respected because ports are not allowed. + // Currently the port of an Ingress is implicitly :80 for http and + // :443 for https. + // Both these may change in the future. + // Incoming requests are matched against the host before the + // IngressRuleValue. If the host is unspecified, the Ingress routes all + // traffic based on the specified IngressRuleValue. + // + // host can be "precise" which is a domain name without the terminating dot of + // a network host (e.g. "foo.bar.com") or "wildcard", which is a domain name + // prefixed with a single wildcard label (e.g. "*.foo.com"). + // The wildcard character '*' must appear by itself as the first DNS label and + // matches only a single label. You cannot have a wildcard label by itself (e.g. Host == "*"). + // Requests will be matched against the host field in the following way: + // 1. If host is precise, the request matches this rule if the http host header is equal to Host. + // 2. If host is a wildcard, then the request matches this rule if the http host header + // is to equal to the suffix (removing the first label) of the wildcard rule. + Host string `yaml:"host,omitempty" json:"host,omitempty"` + + // HTTP is a list of http selectors pointing to backends. + HTTP *HTTPIngressRuleValue `yaml:"http,omitempty" json:"http,omitempty"` +} + +// HTTPIngressRuleValue is a list of http selectors pointing to backends. +type HTTPIngressRuleValue struct { + // paths is a collection of paths that map requests to backends. + Paths []HTTPIngressPath `yaml:"paths" json:"paths"` +} + +// HTTPIngressPath associates a path with a backend. Incoming urls matching the path are forwarded to the backend. +type HTTPIngressPath struct { + // path is matched against the path of an incoming request. Currently it can + // contain characters disallowed from the conventional "path" part of a URL + // as defined by RFC 3986. Paths must begin with a '/' and must be present + // when using PathType with value "Exact" or "Prefix". + Path string `yaml:"path,omitempty" json:"path,omitempty"` + + // pathType determines the interpretation of the path matching. PathType can + // be one of Exact, Prefix, or ImplementationSpecific. Implementations are + // required to support all path types. + PathType k8snetworking.PathType `yaml:"pathType" json:"pathType"` + + // backend defines the referenced service endpoint to which the traffic + // will be forwarded to. + Backend IngressBackend `yaml:"backend" json:"backend"` +} + +// IngressClass provides information about the class of an Ingress. +type IngressClass struct { + // Labels are the attached labels of the port, works only when the Public is true. + Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` + + // Annotations are the attached annotations of the port, works only when the Public is true. + Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` + + // controller refers to the name of the controller that should handle this + // class. This allows for different "flavors" that are controlled by the + // same controller. For example, you may have different parameters for the + // same implementing controller. This should be specified as a + // domain-prefixed path no more than 250 characters in length, e.g. + // "acme.io/ingress-controller". This field is immutable. + Controller string `yaml:"controller,omitempty" json:"controller,omitempty"` + + // parameters is a link to a custom resource containing additional + // configuration for the controller. This is optional if the controller does + // not require extra parameters. + // +optional + Parameters *IngressClassParametersReference `yaml:"parameters,omitempty" json:"parameters,omitempty"` +} + +// IngressClassParametersReference identifies an API object. This can be used +// to specify a cluster or namespace-scoped resource. +type IngressClassParametersReference struct { + // apiGroup is the group for the resource being referenced. If apiGroup is + // not specified, the specified kind must be in the core API group. For any + // other third-party types, apiGroup is required. + APIGroup *string `yaml:"apiGroup,omitempty" json:"apiGroup,omitempty"` + + // kind is the type of resource being referenced. + Kind string `yaml:"kind" json:"kind"` + + // name is the name of resource being referenced. + Name string `yaml:"name" json:"name"` + + // scope represents if this refers to a cluster or namespace scoped resource. + // This may be set to "Cluster" (default) or "Namespace". + Scope *string `yaml:"scope,omitempty" json:"scope,omitempty"` + + // namespace is the namespace of the resource being referenced. This field is + // required when scope is set to "Namespace" and must be unset when scope is set to + // "Cluster". + Namespace *string `yaml:"namespace,omitempty" json:"namespace,omitempty"` +}