From 247b7c3111d559d0047ab3a0379f3f5c9bdcba29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20K=C3=A4sser?= Date: Wed, 23 Oct 2024 16:28:31 +0200 Subject: [PATCH] Introduce kcl-version of composition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tobias Kässer --- .gitignore | 5 +- .gitmodules | 2 +- Makefile | 150 ++++++++++------ apis/kcl/generate.k | 44 +++++ apis/kcl/main.k | 313 +++++++++++++++++++++++++++++++++ build | 2 +- crossplane.yaml | 3 + examples/functions.yaml | 11 +- examples/gotpl/network-xr.yaml | 2 +- examples/kcl/network-xr.yaml | 28 +++ project.mk | 3 + 11 files changed, 500 insertions(+), 63 deletions(-) create mode 100644 apis/kcl/generate.k create mode 100644 apis/kcl/main.k create mode 100644 examples/kcl/network-xr.yaml create mode 100644 project.mk diff --git a/.gitignore b/.gitignore index fe34fe6..2a90c50 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ *.xpkg kubeconfig -devbox* \ No newline at end of file +devbox* + +# generated by kcl +apis/kcl/composition.yaml diff --git a/.gitmodules b/.gitmodules index 8f84209..394e1ee 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "build"] path = build - url = https://github.com/crossplane/build + url = https://github.com/crossplane/build.git diff --git a/Makefile b/Makefile index 027db85..d6ff9a7 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,47 @@ +# Usage +# ==================================================================================== +# Generic Makefile to be used across repositories building a crossplane configuration +# package +# +# Available targets: +# +# - `yamllint` +# Runs yamllint for all files in `api`-folder recursively +# +# - `render` +# Runs crossplane render to render the output of the composition. Usefule for quick +# feedback in order to test templating. +# Important note: +# Claims need following annotations in order for render to work (adjust the paths +# if necessary): +# render.crossplane.io/composition-path: apis/pat/composition.yaml +# render.crossplane.io/function-path: examples/functions.yaml +# +# - `e2e` +# Runs full end-to-end test, including creating cluster, setting up the configuration +# and testing if create, import and delete work as expected. +# Available options: +# UPTEST_SKIP_DELETE (default `false`) skips the deletion of any resources created during the test +# UPTEST_SKIP_UPDATE (default `false`) skips testing the update of the claims +# UPTEST_SKIP_IMPORT (default `true`) skips testing the import of resources +# Example: +# `make e2e UPTEST_SKIP_DELETE=true` +# +# Language specific options: +# +# - `KCL` +# KCL_COMPOSITION_PATH can be set to the kcl-file creating composition.yaml. Default: `apis/kcl/generate.k` + # Project Setup -PROJECT_NAME := configuration-aws-network +# ==================================================================================== + +# Include project.mk for project specific settings +include project.mk + +ifndef PROJECT_NAME + $(error PROJECT_NAME is not set. Please create `project.mk` and set it there.) +endif + PROJECT_REPO := github.com/upbound/$(PROJECT_NAME) # NOTE(hasheddan): the platform is insignificant here as Configuration package @@ -11,10 +53,9 @@ PLATFORMS ?= linux_amd64 # ==================================================================================== # Setup Kubernetes tools -UP_VERSION = v0.32.1 +UP_VERSION = v0.34.0 UP_CHANNEL = stable -UPTEST_VERSION = v0.13.1 -CROSSPLANE_CLI_VERSION=v1.16.0 +CROSSPLANE_CLI_VERSION = v1.17.1 -include build/makelib/k8s_tools.mk # ==================================================================================== @@ -28,15 +69,25 @@ XPKG_REG_ORGS_NO_PROMOTE ?= xpkg.upbound.io/upbound XPKGS = $(PROJECT_NAME) -include build/makelib/xpkg.mk -CROSSPLANE_VERSION = 1.16.0-up.1 +CROSSPLANE_VERSION = v1.17.1-up.1 CROSSPLANE_CHART_REPO = https://charts.upbound.io/stable CROSSPLANE_CHART_NAME = universal-crossplane CROSSPLANE_NAMESPACE = upbound-system CROSSPLANE_ARGS = "--enable-usages" -KIND_CLUSTER_NAME = uptest-$(PROJECT_NAME) +KIND_CLUSTER_NAME ?= uptest-$(PROJECT_NAME) + -include build/makelib/local.xpkg.mk -include build/makelib/controlplane.mk +# ==================================================================================== +# Testing + +UPTEST_VERSION = v1.1.2 +UPTEST_LOCAL_DEPLOY_TARGET = local.xpkg.deploy.configuration.$(PROJECT_NAME) +UPTEST_DEFAULT_TIMEOUT = 2400s + +-include build/makelib/uptest.mk + # ==================================================================================== # Targets @@ -59,59 +110,44 @@ submodules: ## Update the submodules, such as the common build scripts. # machinery sets UP to point to tool cache. build.init: $(UP) -# ==================================================================================== -# End to End Testing - -check: -ifndef UPTEST_CLOUD_CREDENTIALS - @$(INFO) Please export UPTEST_CLOUD_CREDENTIALS, e.g. \`export UPTEST_CLOUD_CREDENTIALS=\$\(cat \~/.aws/credentials\)\` - @$(FAIL) +# In case of building a composition with kcl, we must first render the composition-file +KCL_COMPOSITION_PATH ?= apis/kcl/generate.k +LANG_KCL := $(shell find ./apis -type f -name '*.k') +ifdef LANG_KCL +kcl: $(KCL) ## Generate KCL-based Composition + @$(INFO) Generating kcl composition + $(KCL) $(KCL_COMPOSITION_PATH) + @$(OK) Generated kcl composition + +render: kcl +build.init: kcl +.PHONY: kcl endif -# This target requires the following environment variables to be set: -# - UPTEST_CLOUD_CREDENTIALS, cloud credentials for the provider being tested, e.g. export UPTEST_CLOUD_CREDENTIALS=$(cat ~/.aws/credentials) -# - UPTEST_DATASOURCE_PATH (optional), see https://github.com/upbound/uptest#injecting-dynamic-values-and-datasource -SKIP_DELETE ?= -uptest: $(UPTEST) $(KUBECTL) $(KUTTL) - @$(INFO) running automated tests - @KUBECTL=$(KUBECTL) KUTTL=$(KUTTL) CROSSPLANE_NAMESPACE=$(CROSSPLANE_NAMESPACE) CROSSPLANE_CLI=$(CROSSPLANE_CLI) $(UPTEST) e2e "${UPTEST_EXAMPLE_LIST}" --data-source="${UPTEST_DATASOURCE_PATH}" --setup-script=test/setup.sh --default-timeout=2400 $(SKIP_DELETE) || $(FAIL) - @$(OK) running automated tests - -# This target requires the following environment variables to be set: -# - UPTEST_CLOUD_CREDENTIALS, cloud credentials for the provider being tested, e.g. export UPTEST_CLOUD_CREDENTIALS=$(cat ~/.aws/credentials) -e2e: check build controlplane.down controlplane.up local.xpkg.deploy.configuration.$(PROJECT_NAME) uptest ## Run uptest together with all dependencies. Use `make e2e SKIP_DELETE=--skip-delete` to skip deletion of resources. - -render: $(CROSSPLANE_CLI) ${YQ} - @indir="./examples"; \ - for file in $$(find $$indir -type f -name '*.yaml' ); do \ - doc_count=$$(grep -c '^---' "$$file"); \ - if [[ $$doc_count -gt 0 ]]; then \ - continue; \ - fi; \ - COMPOSITION=$$(${YQ} eval '.metadata.annotations."render.crossplane.io/composition-path"' $$file); \ - FUNCTION=$$(${YQ} eval '.metadata.annotations."render.crossplane.io/function-path"' $$file); \ - ENVIRONMENT=$$(${YQ} eval '.metadata.annotations."render.crossplane.io/environment-path"' $$file); \ - OBSERVE=$$(${YQ} eval '.metadata.annotations."render.crossplane.io/observe-path"' $$file); \ - if [[ "$$ENVIRONMENT" == "null" ]]; then \ - ENVIRONMENT=""; \ - fi; \ - if [[ "$$OBSERVE" == "null" ]]; then \ - OBSERVE=""; \ - fi; \ - if [[ "$$COMPOSITION" == "null" || "$$FUNCTION" == "null" ]]; then \ - continue; \ - fi; \ - ENVIRONMENT=$${ENVIRONMENT=="null" ? "" : $$ENVIRONMENT}; \ - OBSERVE=$${OBSERVE=="null" ? "" : $$OBSERVE}; \ - $(CROSSPLANE_CLI) beta render $$file $$COMPOSITION $$FUNCTION $${ENVIRONMENT:+-e $$ENVIRONMENT} $${OBSERVE:+-o $$OBSERVE} -x; \ - done - -yamllint: ## Static yamllint check - @$(INFO) running yamllint - @yamllint ./apis || $(FAIL) - @$(OK) running yamllint +.PHONY: check-examples +check-examples: ## Check examples for sanity + @$(INFO) Checking if package versions in dependencies match examples + @FN_EXAMPLES=$$( \ + find examples -type f -name "*.yaml" | \ + xargs $(YQ) -r -o=json 'select(.kind == "Function" and (.apiVersion | test("^pkg.crossplane.io/"))) | .spec.package' | \ + sort -u); \ + FN_DEPS=$$( \ + $(YQ) '.spec.dependsOn[] | select(.function != null) | (.function + ":" + .version)' crossplane.yaml | \ + sort -u \ + ); \ + if [ "$$FN_EXAMPLES" != "$$FN_DEPS" ]; then \ + echo "Function package versions in examples and in crossplane.yaml don't match!"; \ + echo "" ; \ + echo "Versions in dependencies:"; \ + echo "---" ; \ + echo "$$FN_DEPS"; \ + echo "" ; \ + echo "Versions in examples:"; \ + echo "---" ; \ + echo "$$FN_EXAMPLES"; \ + exit 1; \ + fi; + @$(OK) Package versions are sane help.local: @grep -E '^[a-zA-Z_-]+.*:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' - -.PHONY: uptest e2e render yamllint help.local diff --git a/apis/kcl/generate.k b/apis/kcl/generate.k new file mode 100644 index 0000000..4228134 --- /dev/null +++ b/apis/kcl/generate.k @@ -0,0 +1,44 @@ +import file +import yaml + +_composition = { + apiVersion = "apiextensions.crossplane.io/v1" + kind = "Composition" + metadata = { + name = "kcl.xnetworks.aws.platform.upbound.io" + labels = { + provider = "aws" + function = "kcl" + } + } + spec = { + compositeTypeRef = { + apiVersion = "aws.platform.upbound.io/v1alpha1" + kind = "XNetwork" + } + mode = "Pipeline" + pipeline = [ + { + step = "kcl" + functionRef = { + name = "crossplane-contrib-function-kcl" + } + input = { + apiVersion = "krm.kcl.dev/v1alpha1" + kind = "KCLRun" + spec = { + source = (file.read("apis/kcl/main.k")) + } + } + }, + { + step = "automatically-detect-ready-composed-resources" + functionRef = { + name = "crossplane-contrib-function-auto-ready" + } + } + ] + } +} + +file.write("apis/kcl/composition.yaml", yaml.encode(_composition)) diff --git a/apis/kcl/main.k b/apis/kcl/main.k new file mode 100644 index 0000000..f1a9fc4 --- /dev/null +++ b/apis/kcl/main.k @@ -0,0 +1,313 @@ +import regex + +xrApiVersion = option("params")?.oxr?.apiVersion +xrKind = option("params")?.oxr?.kind +xrName = option("params")?.oxr?.metadata.name +region = option("params")?.oxr?.spec.parameters.region or "" +id = option("params")?.oxr?.spec.parameters.id or "" +vpcCidrBlock = option("params")?.oxr?.spec.parameters.vpcCidrBlock or "" +subnets = option("params")?.oxr?.spec.parameters.subnets or "" +providerConfigRefName = option("params")?.oxr?.spec.parameters.providerConfigName or None + +_metadata = lambda name = str -> any { + { + annotations = {"krm.kcl.dev/composition-resource-name" = name} + } +} + +_defaults = { + deletionPolicy = option("params")?.oxr?.spec.parameters.deletionPolicy or "Delete" + if providerConfigRefName: + providerConfigRef.name = providerConfigRefName +} + +_cidrEscaped = lambda cidr = str -> str { + regex.replace(cidr, "\.|\/", "-") +} + +_formatSubnet = lambda s = dict -> str { + "{}-{}-{}".format(s.availabilityZone, _cidrEscaped(s.cidrBlock), s.type) +} + +_getExternalName = lambda resourceName = str -> str { + id = option("params")?.ocds?[resourceName]?.Resource?.metadata?.annotations?["crossplane.io/external-name"] or None +} + +_items = [{ + apiVersion = "ec2.aws.upbound.io/v1beta1" + kind = "VPC" + metadata = _metadata("vpc") | { + labels = { + "networks.aws.platform.upbound.io/network-id" = id + } + } + spec = _defaults | { + forProvider = { + cidrBlock = vpcCidrBlock + enableDnsHostnames = True + enableDnsSupport = True + tags = { + Name = xrName + } + region = region + } + } +}] + +_items += [{ + apiVersion = "ec2.aws.upbound.io/v1beta1" + kind = "InternetGateway" + metadata = _metadata("igw") | { + labels = { + "networks.aws.platform.upbound.io/network-id" = id + } + } + spec = _defaults | { + forProvider = { + vpcIdSelector = { + matchControllerRef = True + } + region = region + } + } +}] + +_items += [{ + apiVersion = "ec2.aws.upbound.io/v1beta1" + kind = "Subnet" + metadata = _metadata("subnet-" + _formatSubnet(s)) | { + labels = { + zone = s.availabilityZone + if s.type == "private": + access = "private" + else: + access = "public" + "networks.aws.platform.upbound.io/network-id" = id + } + } + spec = _defaults | { + forProvider = { + cidrBlock = s.cidrBlock + if s.type == "public": + mapPublicIpOnLaunch = True + tags = { + if s.type == "private": + "kubernetes.io/role/internal-elb" = "1" + else: + "kubernetes.io/role/elb" = "1" + "networks.aws.platform.upbound.io/network-id" = id + } + region = region + vpcIdSelector = { + matchControllerRef = True + } + availabilityZone = s.availabilityZone + } + } +} for s in subnets] + +_items += [{ + apiVersion = "ec2.aws.upbound.io/v1beta1" + kind = "RouteTable" + metadata = _metadata("rt") | { + labels = { + "networks.aws.platform.upbound.io/network-id" = id + } + } + spec = _defaults | { + forProvider = { + vpcIdSelector = { + matchControllerRef = True + } + region = region + } + } +}] + +_items += [{ + apiVersion = "ec2.aws.upbound.io/v1beta1" + kind = "Route" + metadata = _metadata("route") | { + labels = { + "networks.aws.platform.upbound.io/network-id" = id + } + } + spec = _defaults | { + forProvider = { + destinationCidrBlock = "0.0.0.0/0" + gatewayIdSelector = { + matchControllerRef = True + } + routeTableIdSelector = { + matchControllerRef = True + } + region = region + } + } +}] + +_items += [{ + apiVersion = "ec2.aws.upbound.io/v1beta1" + kind = "MainRouteTableAssociation" + metadata = _metadata("mrt") | { + labels = { + "networks.aws.platform.upbound.io/network-id" = id + } + } + spec = _defaults | { + forProvider = { + routeTableIdSelector = { + matchControllerRef = True + } + vpcIdSelector = { + matchControllerRef = True + } + region = region + } + } +}] + +_items += [{ + apiVersion = "ec2.aws.upbound.io/v1beta1" + kind = "RouteTableAssociation" + metadata = _metadata("rta-" + _formatSubnet(s)) | { + labels = { + "networks.aws.platform.upbound.io/network-id" = id + } + } + spec = _defaults | { + forProvider = { + region = region + routeTableIdSelector = { + matchControllerRef = True + } + subnetIdSelector = { + matchControllerRef = True + matchLabels = { + if s.type == "private": + access = "private" + else: + access = "public" + zone = s.availabilityZone + } + } + } + } +} for s in subnets] + +_items += [{ + apiVersion = "ec2.aws.upbound.io/v1beta1" + kind = "SecurityGroup" + metadata = _metadata("sg") | { + labels = { + "networks.aws.platform.upbound.io/network-id" = id + } + } + spec = _defaults | { + forProvider = { + description = "Allow access to databases" + name = "platform-ref-aws-cluster" + vpcIdSelector = { + matchControllerRef = True + } + region = region + } + } +}] + +_items += [{ + apiVersion = "ec2.aws.upbound.io/v1beta1" + kind = "SecurityGroupRule" + metadata = _metadata("sgr-postgres") | { + labels = { + "networks.aws.platform.upbound.io/network-id" = id + } + } + spec = _defaults | { + forProvider = { + cidrBlocks = [ + "0.0.0.0/0" + ] + description = "Everywhere" + fromPort = 5432 + protocol = "tcp" + securityGroupIdSelector = { + matchControllerRef = True + } + toPort = 5432 + type = "ingress" + region = region + } + } +}] + +_items += [{ + apiVersion = "ec2.aws.upbound.io/v1beta1" + kind = "SecurityGroupRule" + metadata = _metadata("sgr-mysql") | { + labels = { + "networks.aws.platform.upbound.io/network-id" = id + } + } + spec = _defaults | { + forProvider = { + cidrBlocks = [ + "0.0.0.0/0" + ] + description = "Everywhere" + fromPort = 3306 + protocol = "tcp" + securityGroupIdSelector = { + matchControllerRef = True + } + toPort = 3306 + type = "ingress" + region = region + } + } +}] + +vpcId = option("params")?.ocds?.vpc?.Resource?.status?.atProvider?.id or False + +createdSubnets = [ + createdSubnet + for createdSubnet in [ + { + id = _getExternalName(subnetResource.name) + type = subnetResource.type + } + for subnetResource in [ + { + name = "subnet-" + _formatSubnet(s) + type = s.type + } + for s in subnets + ] + ] if createdSubnet.id != None +] + +securityGroupIds = [ + sgId + for sgId in [ + _getExternalName(sgResource) + for sgResource in [ + "sg-" + _formatSubnet(s) + for s in subnets + ] + ] if sgId != None +] + +_items += [{ + apiVersion = xrApiVersion + kind = xrKind + status = { + if vpcId: + vpcId = vpcId + subnetIds = [s.id for s in createdSubnets] + publicSubnetIds = [s.id for s in createdSubnets if s.type == "public"] + privateSubnetIds = [s.id for s in createdSubnets if s.type == "private"] + securityGroupIds = securityGroupIds + } +}] + +items = _items diff --git a/build b/build index 3cf6663..6f2b7c5 160000 --- a/build +++ b/build @@ -1 +1 @@ -Subproject commit 3cf6663fafcf22f5cb3e7b90cf21d981faa52230 +Subproject commit 6f2b7c5f0a88eac426439d6d9b40abd919d89fbe diff --git a/crossplane.yaml b/crossplane.yaml index 390d0b6..6902fb5 100644 --- a/crossplane.yaml +++ b/crossplane.yaml @@ -25,3 +25,6 @@ spec: - function: xpkg.upbound.io/crossplane-contrib/function-go-templating # renovate: datasource=github-releases depName=crossplane-contrib/function-go-templating version: "v0.5.0" + - function: xpkg.upbound.io/crossplane-contrib/function-kcl + # renovate: datasource=github-releases depName=crossplane-contrib/function-kcl + version: "v0.10.3" diff --git a/examples/functions.yaml b/examples/functions.yaml index 779f6df..a414fda 100644 --- a/examples/functions.yaml +++ b/examples/functions.yaml @@ -1,13 +1,20 @@ apiVersion: pkg.crossplane.io/v1beta1 kind: Function +metadata: + name: crossplane-contrib-function-kcl +spec: + package: xpkg.upbound.io/crossplane-contrib/function-kcl:v0.10.3 +--- +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function metadata: name: crossplane-contrib-function-go-templating spec: - package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.4.1 + package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.5.0 --- apiVersion: pkg.crossplane.io/v1beta1 kind: Function metadata: name: crossplane-contrib-function-auto-ready spec: - package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.2.1 \ No newline at end of file + package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.2.1 diff --git a/examples/gotpl/network-xr.yaml b/examples/gotpl/network-xr.yaml index 7019d0b..e0ea37c 100644 --- a/examples/gotpl/network-xr.yaml +++ b/examples/gotpl/network-xr.yaml @@ -1,7 +1,7 @@ apiVersion: aws.platform.upbound.io/v1alpha1 kind: XNetwork metadata: - name: configuration-aws-network + name: configuration-aws-network-gotpl annotations: render.crossplane.io/composition-path: apis/gotpl/composition.yaml render.crossplane.io/function-path: examples/functions.yaml diff --git a/examples/kcl/network-xr.yaml b/examples/kcl/network-xr.yaml new file mode 100644 index 0000000..73f904a --- /dev/null +++ b/examples/kcl/network-xr.yaml @@ -0,0 +1,28 @@ +apiVersion: aws.platform.upbound.io/v1alpha1 +kind: XNetwork +metadata: + name: configuration-aws-network-kcl + annotations: + render.crossplane.io/composition-path: apis/kcl/composition.yaml + render.crossplane.io/function-path: examples/functions.yaml +spec: + compositionSelector: + matchLabels: + function: kcl + parameters: + id: configuration-aws-network + region: us-west-2 + vpcCidrBlock: 192.168.0.0/16 + subnets: + - availabilityZone: us-west-2a + type: public + cidrBlock: 192.168.0.0/18 + - availabilityZone: us-west-2b + type: public + cidrBlock: 192.168.64.0/18 + - availabilityZone: us-west-2a + type: private + cidrBlock: 192.168.128.0/18 + - availabilityZone: us-west-2b + type: private + cidrBlock: 192.168.192.0/18 diff --git a/project.mk b/project.mk new file mode 100644 index 0000000..b3e9949 --- /dev/null +++ b/project.mk @@ -0,0 +1,3 @@ +PROJECT_NAME := configuration-aws-network +UPTEST_INPUT_MANIFESTS := examples/kcl/network-xr.yaml,examples/gotpl/network-xr.yaml +UPTEST_SKIP_UPDATE := true