Skip to content

Commit

Permalink
add nginxingresscontroller crd (#121)
Browse files Browse the repository at this point in the history
  • Loading branch information
OliverMKing authored Oct 31, 2023
1 parent ca0ca91 commit d6fd174
Show file tree
Hide file tree
Showing 8 changed files with 1,126 additions and 9 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ coverage.out
.idea/*
*.xml

.env
.env
bin/*
55 changes: 47 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,32 +1,71 @@
.PHONY: clean dev push push-tester-image e2e run-e2e
# Run `make help` for usage information on commands in this file.

.PHONY: help clean dev push e2e unit crd manifests generate controller-gen

-include .env

# can have values of "public" or "private"
CLUSTER_TYPE="public"
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif

CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
CONTROLLER_TOOLS_VERSION ?= v0.13.0

# Setting SHELL to bash allows bash commands to be executed by recipes.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec

## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
mkdir -p $(LOCALBIN)

help: ## Display this help.
# prints all targets with comments next to them, extracted from this file
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

clean:
clean: ## Cleans the development environment state
rm -rf devenv/state devenv/tf/.terraform.lock.hcl devenv/tf/.terraform devenv/tf/terraform.tfstate devenv/tf/terraform.tfstate.backup

dev:
# can have values of "public" or "private"
CLUSTER_TYPE="public"

dev: clean ## Deploys a development environment useful for testing the operator inside a cluster
terraform --version
cd devenv && mkdir -p state && cd tf && terraform init && terraform apply -auto-approve -var="clustertype=$(CLUSTER_TYPE)"
./devenv/scripts/deploy_operator.sh

push:
push: ## Pushes the current operator code to the current development environment
echo "$(shell cat devenv/state/registry.txt)/app-routing-operator:$(shell date +%s)" > devenv/state/operator-image-tag.txt
az acr login -n `cat devenv/state/registry.txt`
docker build -t `cat devenv/state/operator-image-tag.txt` .
docker push `cat devenv/state/operator-image-tag.txt`
./devenv/scripts/push_image.sh

e2e:
e2e: ## Runs end-to-end tests
# parenthesis preserve current working directory
(cd testing/e2e && \
go run ./main.go infra --subscription=${SUBSCRIPTION_ID} --tenant=${TENANT_ID} --names=${INFRA_NAMES} && \
go run ./main.go deploy)

unit:
unit: ## Runs unit tests
docker build ./devenv/ -t app-routing-dev:latest
docker run --rm -v "$(shell pwd)":/usr/src/project -w /usr/src/project app-routing-dev:latest go test ./...

crd: generate manifests ## Generates all associated files from CRD

manifests: controller-gen ## Generate CRD manifest
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./api/..." output:crd:artifacts:config=config/crd/bases

generate: $(CONTROLLER_GEN) ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
$(CONTROLLER_GEN) object paths="./api/..."


controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten.
$(CONTROLLER_GEN): $(LOCALBIN)
test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \
GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
20 changes: 20 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Code generated by tool. DO NOT EDIT.
# This file is used to track the info used to scaffold your project
# and allow the plugins properly work.
# More info: https://book.kubebuilder.io/reference/project-config.html
domain: kubernetes.azure.com
layout:
- go.kubebuilder.io/v4
projectName: app-routing
repo: github.com/Azure/aks-app-routing-operator
resources:
- api:
crdVersion: v1
namespaced: false
controller: true
domain: kubernetes.azure.com
group: approuting
kind: NginxIngressController
path: github.com/Azure/aks-app-routing-operator/api/v1alpha1
version: v1alpha1
version: "3"
20 changes: 20 additions & 0 deletions api/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Package v1alpha1 contains API Schema definitions for the approuting v1alpha1 API group
// +kubebuilder:object:generate=true
// +groupName=approuting.kubernetes.azure.com
package v1alpha1

import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "approuting.kubernetes.azure.com", Version: "v1alpha1"}

// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
252 changes: 252 additions & 0 deletions api/v1alpha1/nginxingresscontroller_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package v1alpha1

import (
"fmt"
"unicode"

"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func init() {
SchemeBuilder.Register(&NginxIngressController{}, &NginxIngressControllerList{})
}

const (
maxNameLength = 100
maxControllerNamePrefix = 253 - 10 // 253 is the max length of resource names - 10 to account for the length of the suffix https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
)

const (
defaultControllerNamePrefix = "nginx"
)

// Important: Run "make crd" to regenerate code after modifying this file
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// NginxIngressControllerSpec defines the desired state of NginxIngressController
type NginxIngressControllerSpec struct {
// IngressClassName is the name of the IngressClass that will be used for the NGINX Ingress Controller. Defaults to metadata.name if
// not specified.
// +optional
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
IngressClassName string `json:"ingressClassName,omitempty"`

// ControllerNamePrefix is the name to use for the managed NGINX Ingress Controller resources.
// +optional
// +kubebuilder:default=nginx
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
ControllerNamePrefix string `json:"controllerNamePrefix,omitempty"`

// LoadBalancerAnnotations is a map of annotations to apply to the NGINX Ingress Controller's Service. Common annotations
// will be from the Azure LoadBalancer annotations here https://cloud-provider-azure.sigs.k8s.io/topics/loadbalancer/#loadbalancer-annotations
// +optional
LoadBalancerAnnotations map[string]string `json:"loadBalancerAnnotations,omitempty"`
}

// NginxIngressControllerStatus defines the observed state of NginxIngressController
type NginxIngressControllerStatus struct {
// Conditions is an array of current observed conditions for the NGINX Ingress Controller
// +optional
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions"`

// ControllerReplicas is the desired number of replicas of the NGINX Ingress Controller
// +optional
ControllerReplicas int32 `json:"controllerReplicas"`

// ControllerReadyReplicas is the number of ready replicas of the NGINX Ingress Controller deployment
// +optional
ControllerReadyReplicas int32 `json:"controllerReadyReplicas"`

// ControllerAvailableReplicas is the number of available replicas of the NGINX Ingress Controller deployment
// +optional
ControllerAvailableReplicas int32 `json:"controllerAvailableReplicas"`

// ControllerUnavailableReplicas is the number of unavailable replicas of the NGINX Ingress Controller deployment
// +optional
ControllerUnavailableReplicas int32 `json:"controllerUnavailableReplicas"`

// Count of hash collisions for the managed resources. The App Routing Operator uses this field
// as a collision avoidance mechanism when it needs to create the name for the managed resources.
// +optional
CollisionCount int32 `json:"collisionCount"`

// ManagedResourceRefs is a list of references to the managed resources
// +optional
ManagedResourceRefs []ManagedObjectReference `json:"managedResourceRefs,omitempty"`
}

const (
// ConditionTypeAvailable indicates whether the NGINX Ingress Controller is available. Its condition status is one of
// - "True" when the NGINX Ingress Controller is available and can be used
// - "False" when the NGINX Ingress Controller is not available and cannot offer full functionality
// - "Unknown" when the NGINX Ingress Controller's availability cannot be determined
ConditionTypeAvailable = "Available"

// ConditionTypeIngressClassReady indicates whether the IngressClass exists. Its condition status is one of
// - "True" when the IngressClass exists
// - "False" when the IngressClass does not exist
// - "Collision" when the IngressClass exists, but it's not owned by the NginxIngressController.
// - "Unknown" when the IngressClass's existence cannot be determined
ConditionTypeIngressClassReady = "IngressClassReady"

// ConditionTypeControllerAvailable indicates whether the NGINX Ingress Controller deployment is available. Its condition status is one of
// - "True" when the NGINX Ingress Controller deployment is available
// - "False" when the NGINX Ingress Controller deployment is not available
// - "Unknown" when the NGINX Ingress Controller deployment's availability cannot be determined
ConditionTypeControllerAvailable = "ControllerAvailable"

// ConditionTypeProgressing indicates whether the NGINX Ingress Controller availability is progressing. Its condition status is one of
// - "True" when the NGINX Ingress Controller availability is progressing
// - "False" when the NGINX Ingress Controller availability is not progressing
// - "Unknown" when the NGINX Ingress Controller availability's progress cannot be determined
ConditionTypeProgressing = "Progressing"
)

// ManagedObjectReference is a reference to an object
type ManagedObjectReference struct {
// Name is the name of the managed object
Name string `json:"name"`

// Namespace is the namespace of the managed object. If not specified, the resource is cluster-scoped
// +optional
Namespace string `json:"namespace"`

// Kind is the kind of the managed object
Kind string `json:"kind"`

// APIGroup is the API group of the managed object. If not specified, the resource is in the core API group
// +optional
APIGroup string `json:"apiGroup"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:scope=Cluster,shortName=nic
//+kubebuilder:printcolumn:name="IngressClass",type="string",JSONPath=`.spec.ingressClassName`
//+kubebuilder:printcolumn:name="ControllerNamePrefix",type="string",JSONPath=`.spec.controllerNamePrefix`
//+kubebuilder:printcolumn:name="Available",type="string",JSONPath=`.status.conditions[?(@.type=="Available")].status`

// NginxIngressController is the Schema for the nginxingresscontrollers API
type NginxIngressController struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// +required
Spec NginxIngressControllerSpec `json:"spec,omitempty"`

// +optional
Status NginxIngressControllerStatus `json:"status,omitempty"`
}

func (n *NginxIngressController) GetCondition(t string) *metav1.Condition {
return meta.FindStatusCondition(n.Status.Conditions, t)
}

func (n *NginxIngressController) SetCondition(c metav1.Condition) {
current := n.GetCondition(c.Type)

if current != nil && current.Status == c.Status && current.Message == c.Message && current.Reason == c.Reason {
current.ObservedGeneration = n.Generation
return
}

c.ObservedGeneration = n.Generation
c.LastTransitionTime = metav1.Now()
meta.SetStatusCondition(&n.Status.Conditions, c)
}

// Valid checks this NginxIngressController to see if it's valid. Returns a string describing the validation error, if any, or empty string if there is no error.
func (n *NginxIngressController) Valid() string {
// controller name prefix must follow https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
// we don't check for ending because this is a prefix
if n.Spec.ControllerNamePrefix == "" {
return "spec.controllerNamePrefix must be specified"
}

if !startsWithAlphaNum(n.Spec.ControllerNamePrefix) {
return "spec.controllerNamePrefix must start with alphanumeric character"
}

if !onlyAlphaNumDashPeriod(n.Spec.ControllerNamePrefix) {
return "spec.controllerNamePrefix must contain only alphanumeric characters, dashes, and periods"
}

if len(n.Spec.ControllerNamePrefix) > maxControllerNamePrefix {
return fmt.Sprintf("spec.controllerNamePrefix length must be less than or equal to %d characters", maxControllerNamePrefix)

}

// ingress class name must follow https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
if n.Spec.IngressClassName == "" {
return "spec.ingressClassName must be specified"
}

if !startsWithAlphaNum(n.Spec.IngressClassName) {
return "spec.ingressClassName must start with alphanumeric character"
}

if !onlyAlphaNumDashPeriod(n.Spec.IngressClassName) {
return "spec.ingressClassName must contain only alphanumeric characters, dashes, and periods"
}

if !endsWithAlphaNum(n.Spec.IngressClassName) {
return "spec.ingressClassName must end with alphanumeric character"
}

if len(n.Name) > maxNameLength {
return fmt.Sprintf("Name length must be less than or equal to %d characters", maxNameLength)
}

return ""
}

func (n *NginxIngressController) Default() {
if n.Spec.IngressClassName == "" {
n.Spec.IngressClassName = n.Name
}

if n.Spec.ControllerNamePrefix == "" {
n.Spec.ControllerNamePrefix = defaultControllerNamePrefix
}
}

//+kubebuilder:object:root=true
//+kubebuilder:resource:scope=Cluster

// NginxIngressControllerList contains a list of NginxIngressController
type NginxIngressControllerList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []NginxIngressController `json:"items"`
}

func startsWithAlphaNum(s string) bool {
if len(s) == 0 {
return false
}

return unicode.IsLetter(rune(s[0])) || unicode.IsDigit(rune(s[0]))
}

func endsWithAlphaNum(s string) bool {
if len(s) == 0 {
return false
}

return unicode.IsLetter(rune(s[len(s)-1])) || unicode.IsDigit(rune(s[len(s)-1]))
}

func onlyAlphaNumDashPeriod(s string) bool {
for _, c := range s {
if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '-' && c != '.' {
return false
}
}

return true
}
Loading

0 comments on commit d6fd174

Please sign in to comment.