From d6fd1749935482477a7b2f88ab76dafdac750b6f Mon Sep 17 00:00:00 2001 From: Oliver King Date: Tue, 31 Oct 2023 12:30:21 -0500 Subject: [PATCH] add nginxingresscontroller crd (#121) --- .gitignore | 3 +- Makefile | 55 ++- PROJECT | 20 + api/v1alpha1/groupversion_info.go | 20 + api/v1alpha1/nginxingresscontroller_types.go | 252 ++++++++++ .../nginxingresscontroller_types_test.go | 445 ++++++++++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 133 ++++++ ...tes.azure.com_nginxingresscontrollers.yaml | 207 ++++++++ 8 files changed, 1126 insertions(+), 9 deletions(-) create mode 100644 PROJECT create mode 100644 api/v1alpha1/groupversion_info.go create mode 100644 api/v1alpha1/nginxingresscontroller_types.go create mode 100644 api/v1alpha1/nginxingresscontroller_types_test.go create mode 100644 api/v1alpha1/zz_generated.deepcopy.go create mode 100644 config/crd/bases/approuting.kubernetes.azure.com_nginxingresscontrollers.yaml diff --git a/.gitignore b/.gitignore index 4029db26..87128dc3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ coverage.out .idea/* *.xml -.env \ No newline at end of file +.env +bin/* \ No newline at end of file diff --git a/Makefile b/Makefile index ebb643a1..9f3db4ae 100644 --- a/Makefile +++ b/Makefile @@ -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\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) \ No newline at end of file diff --git a/PROJECT b/PROJECT new file mode 100644 index 00000000..6e2d8acf --- /dev/null +++ b/PROJECT @@ -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" diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go new file mode 100644 index 00000000..68c0beee --- /dev/null +++ b/api/v1alpha1/groupversion_info.go @@ -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 +) diff --git a/api/v1alpha1/nginxingresscontroller_types.go b/api/v1alpha1/nginxingresscontroller_types.go new file mode 100644 index 00000000..1f64b4d7 --- /dev/null +++ b/api/v1alpha1/nginxingresscontroller_types.go @@ -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 +} diff --git a/api/v1alpha1/nginxingresscontroller_types_test.go b/api/v1alpha1/nginxingresscontroller_types_test.go new file mode 100644 index 00000000..861f460f --- /dev/null +++ b/api/v1alpha1/nginxingresscontroller_types_test.go @@ -0,0 +1,445 @@ +package v1alpha1 + +import ( + "fmt" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func validNginxIngressController() NginxIngressController { + return NginxIngressController{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + }, + Spec: NginxIngressControllerSpec{ + IngressClassName: "ingressClassName", + ControllerNamePrefix: "controllerNamePrefix", + }, + } +} + +func TestNginxIngressControllerValid(t *testing.T) { + cases := []struct { + name string + nic NginxIngressController + want string + }{ + { + name: "valid NginxIngressController", + nic: validNginxIngressController(), + want: "", + }, + { + name: "missing controller name prefix", + nic: func() NginxIngressController { + nic := validNginxIngressController() + nic.Spec.ControllerNamePrefix = "" + return nic + }(), + want: "spec.controllerNamePrefix must be specified", + }, + { + name: "controller name prefix starts with non alphanum", + nic: func() NginxIngressController { + nic := validNginxIngressController() + nic.Spec.ControllerNamePrefix = "-controllerNamePrefix" + return nic + }(), + want: "spec.controllerNamePrefix must start with alphanumeric character", + }, + { + name: "controller name prefix contains invalid characters", + nic: func() NginxIngressController { + nic := validNginxIngressController() + nic.Spec.ControllerNamePrefix = "controllerNamePrefix!" + return nic + }(), + want: "spec.controllerNamePrefix must contain only alphanumeric characters, dashes, and periods", + }, + { + name: "controller name prefix too long", + nic: func() NginxIngressController { + nic := validNginxIngressController() + nic.Spec.ControllerNamePrefix = strings.Repeat("a", maxControllerNamePrefix+1) + return nic + }(), + want: fmt.Sprintf("spec.controllerNamePrefix length must be less than or equal to %d characters", maxControllerNamePrefix), + }, + { + name: "missing ingress class name", + nic: func() NginxIngressController { + nic := validNginxIngressController() + nic.Spec.IngressClassName = "" + return nic + }(), + want: "spec.ingressClassName must be specified", + }, + { + name: "ingress class name starts with non alphanum", + nic: func() NginxIngressController { + nic := validNginxIngressController() + nic.Spec.IngressClassName = "-ingressClassName" + return nic + }(), + want: "spec.ingressClassName must start with alphanumeric character", + }, + { + name: "ingress class name contains invalid characters", + nic: func() NginxIngressController { + nic := validNginxIngressController() + nic.Spec.IngressClassName = "ingressClassName!" + return nic + }(), + want: "spec.ingressClassName must contain only alphanumeric characters, dashes, and periods", + }, + { + name: "ingress class name ends with non alphanum", + nic: func() NginxIngressController { + nic := validNginxIngressController() + nic.Spec.IngressClassName = "ingressClassName-" + return nic + }(), + want: "spec.ingressClassName must end with alphanumeric character", + }, + { + name: "long name", + nic: func() NginxIngressController { + nic := validNginxIngressController() + nic.ObjectMeta.Name = strings.Repeat("a", maxNameLength+1) + return nic + }(), + want: fmt.Sprintf("Name length must be less than or equal to %d characters", maxNameLength), + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := c.nic.Valid() + if got != c.want { + t.Errorf("NginxIngressController.Valid() = %v, want %v", got, c.want) + } + }) + } +} + +func TestNginxIngressControllerDefault(t *testing.T) { + t.Run("default ingress class name", func(t *testing.T) { + nic := validNginxIngressController() + nic.Spec.IngressClassName = "" + nic.Default() + expected := nic.Name + if nic.Spec.IngressClassName != expected { + t.Errorf("NginxIngressController.Default() = %v, want %v", nic.Spec.IngressClassName, expected) + } + }) + + t.Run("default controller name prefix", func(t *testing.T) { + nic := validNginxIngressController() + nic.Spec.ControllerNamePrefix = "" + nic.Default() + expected := defaultControllerNamePrefix + if nic.Spec.ControllerNamePrefix != expected { + t.Errorf("NginxIngressController.Default() = %v, want %v", nic.Spec.ControllerNamePrefix, expected) + } + }) + + t.Run("doesn't overwrite ingress class name", func(t *testing.T) { + nic := validNginxIngressController() + existingIngressClassName := "existingIngressClassName" + nic.Spec.IngressClassName = existingIngressClassName + nic.Default() + if nic.Spec.IngressClassName != existingIngressClassName { + t.Errorf("NginxIngressController.Default() = %v, want %v", nic.Spec.IngressClassName, existingIngressClassName) + } + }) + + t.Run("doesn't overwrite controller name prefix", func(t *testing.T) { + nic := validNginxIngressController() + existingControllerNamePrefix := "existingControllerNamePrefix" + nic.Spec.ControllerNamePrefix = existingControllerNamePrefix + nic.Default() + if nic.Spec.ControllerNamePrefix != existingControllerNamePrefix { + t.Errorf("NginxIngressController.Default() = %v, want %v", nic.Spec.ControllerNamePrefix, existingControllerNamePrefix) + } + }) +} + +func TestNginxIngressControllerGetCondition(t *testing.T) { + nic := validNginxIngressController() + cond := metav1.Condition{ + Type: "test", + Status: metav1.ConditionTrue, + } + got := nic.GetCondition(cond.Type) + if got != nil { + t.Errorf("NginxIngressController.GetCondition() = %v, want nil", got) + } + + meta.SetStatusCondition(&nic.Status.Conditions, cond) + got = nic.GetCondition(cond.Type) + if got == nil { + t.Errorf("NginxIngressController.GetCondition() = nil, want %v", cond) + } + if got.Status != cond.Status { + t.Errorf("NginxIngressController.GetCondition() = %v, want %v", got.Status, cond.Status) + } +} + +func TestNginxIngressControllerSetCondition(t *testing.T) { + // new condition + nic := validNginxIngressController() + nic.Generation = 456 + + cond := metav1.Condition{ + Type: "test", + Status: metav1.ConditionTrue, + Reason: "reason", + Message: "message", + } + + nic.SetCondition(cond) + got := nic.GetCondition(cond.Type) + if got == nil { + t.Errorf("NginxIngressController.SetCondition() = nil, want %v", cond) + } + if got.Status != cond.Status { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Status, cond.Status) + } + if got.ObservedGeneration != nic.Generation { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.ObservedGeneration, nic.Generation) + } + if got.Reason != cond.Reason { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Reason, cond.Reason) + } + if got.Message != cond.Message { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Message, cond.Message) + } + + // set same condition + nic.Generation = 789 + nic.SetCondition(cond) + got = nic.GetCondition(cond.Type) + if got == nil { + t.Errorf("NginxIngressController.SetCondition() = nil, want %v", cond) + } + if got.Status != cond.Status { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Status, cond.Status) + } + if got.ObservedGeneration != nic.Generation { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.ObservedGeneration, nic.Generation) + } + if got.Reason != cond.Reason { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Reason, cond.Reason) + } + if got.Message != cond.Message { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Message, cond.Message) + } + + // set different condition + cond2 := metav1.Condition{ + Type: "test2", + Status: metav1.ConditionTrue, + } + nic.SetCondition(cond2) + got = nic.GetCondition(cond2.Type) + if got == nil { + t.Errorf("NginxIngressController.SetCondition() = nil, want %v", cond2) + } + if got.Status != cond2.Status { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Status, cond2.Status) + } + if got.ObservedGeneration != nic.Generation { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.ObservedGeneration, nic.Generation) + } + if got.Reason != cond2.Reason { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Reason, cond2.Reason) + } + if got.Message != cond2.Message { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Message, cond2.Message) + } + + // old condition should not be changed + got = nic.GetCondition(cond.Type) + if got == nil { + t.Errorf("NginxIngressController.SetCondition() = nil, want %v", cond) + } + if got.Status != cond.Status { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Status, cond.Status) + } + if got.ObservedGeneration != nic.Generation { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.ObservedGeneration, nic.Generation) + } + if got.Reason != cond.Reason { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Reason, cond.Reason) + } + if got.Message != cond.Message { + t.Errorf("NginxIngressController.SetCondition() = %v, want %v", got.Message, cond.Message) + } +} + +func TestStartsWithAlphaNum(t *testing.T) { + cases := []struct { + name string + s string + want bool + }{ + { + name: "starts with alpha", + s: "a", + want: true, + }, + { + name: "starts with num", + s: "1", + want: true, + }, + { + name: "empty", + s: "", + want: false, + }, + { + name: "longer starts with alpha", + s: "abc23", + want: true, + }, + { + name: "longer starts with num", + s: "123abc", + want: true, + }, + { + name: "starts with dash", + s: "-abc", + want: false, + }, + { + name: "starts with period", + s: ".123", + want: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := startsWithAlphaNum(c.s) + if got != c.want { + t.Errorf("startsWithAlphaNum(%v) = %v, want %v", c.s, got, c.want) + } + }) + } +} + +func TestEndsWithAlphaNum(t *testing.T) { + cases := []struct { + name string + s string + want bool + }{ + { + name: "ends with alpha", + s: "a", + want: true, + }, + { + name: "ends with num", + s: "1", + want: true, + }, + { + name: "empty", + s: "", + want: false, + }, + { + name: "longer ends with alpha", + s: "abc23b", + want: true, + }, + { + name: "longer ends with num", + s: "123abc22", + want: true, + }, + { + name: "ends with dash", + s: "abc-", + want: false, + }, + { + name: "ends with period", + s: "123.", + want: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := endsWithAlphaNum(c.s) + if got != c.want { + t.Errorf("endsWithAlphaNum(%v) = %v, want %v", c.s, got, c.want) + } + }) + } +} + +func TestOnlyAlphaNumDashPeriod(t *testing.T) { + cases := []struct { + name string + s string + want bool + }{ + { + name: "only alpha", + s: "abc", + want: true, + }, + { + name: "only num", + s: "123", + want: true, + }, + { + name: "only dash", + s: "---", + want: true, + }, + { + name: "only period", + s: "...", + want: true, + }, + { + name: "alpha num dash period", + s: "abc123.-", + want: true, + }, + { + name: "empty", + s: "", + want: true, + }, + { + name: "alpha num dash period with space", + s: "abc 123.-", + want: false, + }, + { + name: "alpha num dash period with underscore", + s: "abc_123.-", + want: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := onlyAlphaNumDashPeriod(c.s) + if got != c.want { + t.Errorf("onlyAlphaNumDashPeriod(%v) = %v, want %v", c.s, got, c.want) + } + }) + } +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..4b7e070e --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,133 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedObjectReference) DeepCopyInto(out *ManagedObjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedObjectReference. +func (in *ManagedObjectReference) DeepCopy() *ManagedObjectReference { + if in == nil { + return nil + } + out := new(ManagedObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NginxIngressController) DeepCopyInto(out *NginxIngressController) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NginxIngressController. +func (in *NginxIngressController) DeepCopy() *NginxIngressController { + if in == nil { + return nil + } + out := new(NginxIngressController) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NginxIngressController) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NginxIngressControllerList) DeepCopyInto(out *NginxIngressControllerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NginxIngressController, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NginxIngressControllerList. +func (in *NginxIngressControllerList) DeepCopy() *NginxIngressControllerList { + if in == nil { + return nil + } + out := new(NginxIngressControllerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NginxIngressControllerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NginxIngressControllerSpec) DeepCopyInto(out *NginxIngressControllerSpec) { + *out = *in + if in.LoadBalancerAnnotations != nil { + in, out := &in.LoadBalancerAnnotations, &out.LoadBalancerAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NginxIngressControllerSpec. +func (in *NginxIngressControllerSpec) DeepCopy() *NginxIngressControllerSpec { + if in == nil { + return nil + } + out := new(NginxIngressControllerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NginxIngressControllerStatus) DeepCopyInto(out *NginxIngressControllerStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ManagedResourceRefs != nil { + in, out := &in.ManagedResourceRefs, &out.ManagedResourceRefs + *out = make([]ManagedObjectReference, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NginxIngressControllerStatus. +func (in *NginxIngressControllerStatus) DeepCopy() *NginxIngressControllerStatus { + if in == nil { + return nil + } + out := new(NginxIngressControllerStatus) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/approuting.kubernetes.azure.com_nginxingresscontrollers.yaml b/config/crd/bases/approuting.kubernetes.azure.com_nginxingresscontrollers.yaml new file mode 100644 index 00000000..e9d38fa3 --- /dev/null +++ b/config/crd/bases/approuting.kubernetes.azure.com_nginxingresscontrollers.yaml @@ -0,0 +1,207 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: nginxingresscontrollers.approuting.kubernetes.azure.com +spec: + group: approuting.kubernetes.azure.com + names: + kind: NginxIngressController + listKind: NginxIngressControllerList + plural: nginxingresscontrollers + shortNames: + - nic + singular: nginxingresscontroller + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.ingressClassName + name: IngressClass + type: string + - jsonPath: .spec.controllerNamePrefix + name: ControllerNamePrefix + type: string + - jsonPath: .status.conditions[?(@.type=="Available")].status + name: Available + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: NginxIngressController is the Schema for the nginxingresscontrollers + 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: NginxIngressControllerSpec defines the desired state of NginxIngressController + properties: + controllerNamePrefix: + default: nginx + description: ControllerNamePrefix is the name to use for the managed + NGINX Ingress Controller resources. + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + ingressClassName: + description: IngressClassName is the name of the IngressClass that + will be used for the NGINX Ingress Controller. Defaults to metadata.name + if not specified. + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + loadBalancerAnnotations: + additionalProperties: + type: string + description: 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 + type: object + type: object + status: + description: NginxIngressControllerStatus defines the observed state of + NginxIngressController + properties: + collisionCount: + description: 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. + format: int32 + type: integer + conditions: + description: Conditions is an array of current observed conditions + for the NGINX Ingress Controller + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerAvailableReplicas: + description: ControllerAvailableReplicas is the number of available + replicas of the NGINX Ingress Controller deployment + format: int32 + type: integer + controllerReadyReplicas: + description: ControllerReadyReplicas is the number of ready replicas + of the NGINX Ingress Controller deployment + format: int32 + type: integer + controllerReplicas: + description: ControllerReplicas is the desired number of replicas + of the NGINX Ingress Controller + format: int32 + type: integer + controllerUnavailableReplicas: + description: ControllerUnavailableReplicas is the number of unavailable + replicas of the NGINX Ingress Controller deployment + format: int32 + type: integer + managedResourceRefs: + description: ManagedResourceRefs is a list of references to the managed + resources + items: + description: ManagedObjectReference is a reference to an object + properties: + apiGroup: + description: APIGroup is the API group of the managed object. + If not specified, the resource is in the core API group + type: string + kind: + description: Kind is the kind of the managed object + type: string + name: + description: Name is the name of the managed object + type: string + namespace: + description: Namespace is the namespace of the managed object. + If not specified, the resource is cluster-scoped + type: string + required: + - kind + - name + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {}