Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add nginxingresscontroller crd #121

Merged
merged 3 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
OliverMKing marked this conversation as resolved.
Show resolved Hide resolved
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
OliverMKing marked this conversation as resolved.
Show resolved Hide resolved
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.
OliverMKing marked this conversation as resolved.
Show resolved Hide resolved

// 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