diff --git a/.github/workflows/e2e-with-cluster.yaml b/.github/workflows/e2e-with-cluster.yaml new file mode 100644 index 00000000..3bc18f4e --- /dev/null +++ b/.github/workflows/e2e-with-cluster.yaml @@ -0,0 +1,32 @@ +name: EnvTest with Cluster + +on: + pull_request: + branches: [ main ] + types: [opened,reopened,synchronize,closed] + +permissions: + contents: read + pull-requests: write + id-token: write + +jobs: + minikube: + name: Run E2E tests with Minikube + if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Minikube + run: minikube start --driver=docker + + - name: Install FluxCD + run: | + curl -s https://fluxcd.io/install.sh | sudo bash + flux install --namespace=flux-system --components="source-controller,helm-controller" + + - name: Run e2e tests + run: | + make e2e-test-with-cluster diff --git a/.github/workflows/e2e.yaml b/.github/workflows/helm-install-e2e.yaml similarity index 98% rename from .github/workflows/e2e.yaml rename to .github/workflows/helm-install-e2e.yaml index 084edf95..5e88f2d7 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/helm-install-e2e.yaml @@ -1,4 +1,4 @@ -name: E2E - Helm Chart Deployment and Tests +name: E2E - Helm Chart Deployment Test on: pull_request: @@ -120,8 +120,6 @@ jobs: # kubectl apply -f examples/k3s-manifest-basic.yml # kubectl apply -f examples/k8s-manifest-basic.yml - # kubectl kuttl test - - name: Create or Update Comment with Deployment URL uses: peter-evans/create-or-update-comment@v2 with: diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 1f5b52b3..8547113e 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -18,6 +18,7 @@ jobs: (cd ./chart && helm dep update .) helm lint ./chart --with-subcharts - name: Test - run: go test -coverprofile=coverage.txt -covermode=atomic -v ./... + run: | + go test -coverprofile=coverage.txt -covermode=atomic -v ./src/controllers/... - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 diff --git a/Dockerfile b/Dockerfile index dfa7fd24..fa8d0689 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,15 +14,14 @@ RUN go mod download # Copy the go source COPY main.go main.go -COPY src/api/ src/api/ -COPY src/controllers/ src/controllers/ +COPY src/ src/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-arm64} go build -a -o manager main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details diff --git a/Makefile b/Makefile index 4fe6d83a..5d2b985c 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,3 @@ -#include test/e2e/Makefile - VERSION ?= 1.5.6 # check if we are using MacOS or LINUX and use that to determine the sed command @@ -109,10 +107,16 @@ fmt: ## Run go fmt against code. vet: ## Run go vet against code. go vet ./... -.PHONY: test -test: manifests generate fmt vet envtest ## Run test. +##@ Test + +.PHONY: e2e-test-without-cluster +e2e-test-without-cluster: manifests generate fmt vet envtest ## Run test. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out +.PHONY: e2e-test-with-cluster +e2e-test-with-cluster: manifests generate fmt vet envtest ## Run test. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" ENVTEST_REMOTE=true go test ./... -coverprofile cover.out -v + ##@ Build .PHONY: build @@ -160,28 +164,28 @@ endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | kubectl apply -f - + $(KUSTOMIZE) build src/config/crd | kubectl apply -f - .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + $(KUSTOMIZE) build src/config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build config/default | kubectl apply -f - + cd src/config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build src/config/default | kubectl apply -f - .PHONY: undeploy undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + $(KUSTOMIZE) build src/config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - .PHONY: build-helm-chart build-helm-chart: manifests generate fmt vet kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. # update the crd - $(KUSTOMIZE) build config/crd > chart/templates/uffizziclusters.uffizzi.com_customresourcedefinition.yaml + $(KUSTOMIZE) build src/config/crd > chart/templates/uffizziclusters.uffizzi.com_customresourcedefinition.yaml $(SED) -i'' -e 's/labels:/labels: {{ include "common.labels.standard" . | nindent 4 }}/' chart/templates/uffizziclusters.uffizzi.com_customresourcedefinition.yaml # update roles - cp config/rbac/role.yaml chart/templates/manager-role_clusterrole.yaml + cp src/config/rbac/role.yaml chart/templates/manager-role_clusterrole.yaml $(SED) -i'' -e '/creationTimestamp: null/d' chart/templates/manager-role_clusterrole.yaml $(SED) -i'' -e 's/name: manager-role/name: {{ include "common.names.fullname" . }}-manager-role/' chart/templates/manager-role_clusterrole.yaml $(SED) -i'' -e 's/apiVersion: rbac.authorization.k8s.io\/v1/apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }}/' chart/templates/manager-role_clusterrole.yaml @@ -230,13 +234,13 @@ $(CONTROLLER_GEN): $(LOCALBIN) .PHONY: envtest envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. $(ENVTEST): $(LOCALBIN) - test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + @test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest .PHONY: bundle bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files. operator-sdk generate kustomize manifests -q - cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) - $(KUSTOMIZE) build config/manifests | operator-sdk generate bundle $(BUNDLE_GEN_FLAGS) + cd src/config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) + $(KUSTOMIZE) build src/config/manifests | operator-sdk generate bundle $(BUNDLE_GEN_FLAGS) operator-sdk bundle validate ./bundle .PHONY: bundle-build diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 6242ac72..fe8784de 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -10,7 +10,7 @@ version: 1.5.6 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.5.5" +appVersion: "v1.5.6" dependencies: - name: common repository: https://charts.bitnami.com/bitnami diff --git a/chart/values.yaml b/chart/values.yaml index d3aadca8..c64fc9a9 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -4,7 +4,7 @@ image: repository: docker.io/uffizzi/uffizzi-cluster-operator - tag: v1.5.5 + tag: v1.5.6 # `flux` dependency values flux: helmController: diff --git a/config/crd/bases/uffizzi.com_uffizziclusters.yaml b/config/crd/bases/uffizzi.com_uffizziclusters.yaml new file mode 100644 index 00000000..0df7493c --- /dev/null +++ b/config/crd/bases/uffizzi.com_uffizziclusters.yaml @@ -0,0 +1,338 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: uffizziclusters.uffizzi.com +spec: + group: uffizzi.com + names: + kind: UffizziCluster + listKind: UffizziClusterList + plural: uffizziclusters + shortNames: + - uc + - ucluster + singular: uffizzicluster + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='APIReady')].status + name: APIReady + type: string + - jsonPath: .status.conditions[?(@.type=='DataStoreReady')].status + name: DataStoreReady + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=='Sleep')].status + name: Sleep + type: string + - jsonPath: .status.host + name: Host + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.lastAwakeTime + name: UptimeSinceLastAwake + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: UffizziCluster is the Schema for the UffizziClusters 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: UffizziClusterSpec defines the desired state of UffizziCluster + properties: + apiServer: + description: UffizziClusterAPIServer defines the API server capabilities + of the cluster + properties: + image: + type: string + type: object + distro: + default: k3s + enum: + - k3s + - k8s + type: string + externalDatastore: + default: sqlite + enum: + - etcd + - sqlite + type: string + helm: + items: + properties: + chart: + properties: + name: + type: string + repo: + type: string + version: + type: string + required: + - name + - repo + type: object + release: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + values: + type: string + required: + - chart + - release + type: object + type: array + ingress: + description: UffiClusterIngress defines the ingress capabilities of + the cluster, the basic host can be setup for all + properties: + class: + type: string + host: + type: string + type: object + limitRange: + properties: + default: + properties: + cpu: + default: "0.5" + type: string + ephemeralStorage: + default: 8Gi + type: string + memory: + default: 1Gi + type: string + type: object + defaultRequest: + properties: + cpu: + default: "0.1" + type: string + ephemeralStorage: + default: 1Gi + type: string + memory: + default: 128Mi + type: string + type: object + enabled: + default: true + type: boolean + required: + - enabled + type: object + manifests: + type: string + resourceQuota: + description: UffizziClusterResourceQuota defines the resource quota + which defines the quota of resources a namespace has access to + properties: + count: + properties: + configMaps: + default: 20 + type: integer + endpoints: + default: 10 + type: integer + persistentVolumeClaims: + default: 10 + type: integer + pods: + default: 20 + type: integer + secrets: + default: 20 + type: integer + services: + default: 10 + type: integer + type: object + enabled: + default: true + type: boolean + limits: + properties: + cpu: + default: "0.5" + type: string + ephemeralStorage: + default: 5Gi + type: string + memory: + default: 8Gi + type: string + type: object + requests: + properties: + cpu: + default: "0.5" + type: string + ephemeralStorage: + default: 5Gi + type: string + memory: + default: 1Gi + type: string + storage: + default: 5Gi + type: string + type: object + services: + properties: + loadBalancers: + default: 3 + type: integer + nodePorts: + default: 0 + type: integer + type: object + required: + - enabled + type: object + sleep: + type: boolean + ttl: + type: string + type: object + status: + description: UffizziClusterStatus defines the observed state of UffizziCluster + properties: + conditions: + 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 + helmReleaseRef: + type: string + host: + type: string + kubeConfig: + description: VClusterKubeConfig is the KubeConfig SecretReference + of the related VCluster + properties: + secretRef: + description: SecretKeyReference contains enough information to + locate the referenced Kubernetes Secret object in the same namespace. + Optionally a key can be specified. Use this type instead of + core/v1 SecretKeySelector when the Key is optional and the Optional + field is not applicable. + properties: + key: + description: Key in the Secret, when not specified an implementation-specific + default key is used. + type: string + name: + description: Name of the Secret. + type: string + required: + - name + type: object + type: object + lastAppliedConfiguration: + type: string + lastAppliedHelmReleaseSpec: + type: string + lastAwakeTime: + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 00000000..dde522ed --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,169 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - helm.toolkit.fluxcd.io + resources: + - helmreleases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - helm.toolkit.fluxcd.io + resources: + - helmreleases/finalizers + verbs: + - update +- apiGroups: + - helm.toolkit.fluxcd.io + resources: + - helmreleases/status + verbs: + - get + - patch + - update +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - source.toolkit.fluxcd.io + resources: + - helmrepositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - source.toolkit.fluxcd.io + resources: + - helmrepositories/finalizers + verbs: + - update +- apiGroups: + - source.toolkit.fluxcd.io + resources: + - helmrepositories/status + verbs: + - get + - patch + - update +- apiGroups: + - uffizzi.com + resources: + - uffizziclusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - uffizzi.com + resources: + - uffizziclusters/finalizers + verbs: + - update +- apiGroups: + - uffizzi.com + resources: + - uffizziclusters/status + verbs: + - get + - patch + - update diff --git a/controllers/constants/constants.go b/controllers/constants/constants.go new file mode 100644 index 00000000..1255fc8b --- /dev/null +++ b/controllers/constants/constants.go @@ -0,0 +1 @@ +package constants diff --git a/go.mod b/go.mod index a70d9b17..97c94642 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/fluxcd/helm-controller/api v0.32.2 github.com/fluxcd/pkg/apis/meta v1.0.0 github.com/fluxcd/source-controller/api v0.36.1 + github.com/google/go-cmp v0.5.9 github.com/onsi/ginkgo/v2 v2.9.2 github.com/onsi/gomega v1.27.6 github.com/pkg/errors v0.9.1 @@ -13,6 +14,7 @@ require ( k8s.io/apiextensions-apiserver v0.26.3 k8s.io/apimachinery v0.26.3 k8s.io/client-go v0.26.3 + k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 sigs.k8s.io/controller-runtime v0.14.6 ) @@ -36,7 +38,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.6.9 // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/uuid v1.3.0 // indirect @@ -72,7 +73,6 @@ require ( k8s.io/component-base v0.26.3 // indirect k8s.io/klog/v2 v2.90.1 // indirect k8s.io/kube-openapi v0.0.0-20221110221610-a28e98eb7c70 // indirect - k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/go.sum b/go.sum index d9494069..4b109717 100644 --- a/go.sum +++ b/go.sum @@ -661,4 +661,4 @@ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h6 sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= \ No newline at end of file diff --git a/main.go b/main.go index d1cd083c..d520b73b 100644 --- a/main.go +++ b/main.go @@ -63,6 +63,7 @@ func main() { probeAddr string enableLeaderElection bool concurrentReconciliations int + k8sProvider string ) flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") @@ -71,6 +72,7 @@ func main() { "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.IntVar(&concurrentReconciliations, "concurrent", 5, "The number of concurrent reconciles per controller.") + flag.StringVar(&k8sProvider, "k8s-provider", "", "The k8s provider to use for the UffizziCluster") opts := zap.Options{ Development: true, } @@ -99,8 +101,9 @@ func main() { } // Setup UffizziClusterReconciler if err = (&uffizzicluster.UffizziClusterReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + KubernetesProvider: k8sProvider, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "UffizziCluster") os.Exit(1) diff --git a/src/config/crd/bases/ucluster.uffizzi.com.uffizzi.com_uffizziclusters.yaml b/src/config/crd/bases/ucluster.uffizzi.com.uffizzi.com_uffizziclusters.yaml index 96bbf2e8..ef3a91c8 100644 --- a/src/config/crd/bases/ucluster.uffizzi.com.uffizzi.com_uffizziclusters.yaml +++ b/src/config/crd/bases/ucluster.uffizzi.com.uffizzi.com_uffizziclusters.yaml @@ -5,7 +5,7 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.11.1 creationTimestamp: null - name: UffizziClusters.uffizzi.com + name: uffizziclusters.uffizzi.com spec: group: uffizzi.com names: diff --git a/src/config/crd/patches/cainjection_in_ephemeralclusters.yaml b/src/config/crd/patches/cainjection_in_ephemeralclusters.yaml index 3ad271bd..414b7ebb 100644 --- a/src/config/crd/patches/cainjection_in_ephemeralclusters.yaml +++ b/src/config/crd/patches/cainjection_in_ephemeralclusters.yaml @@ -4,4 +4,4 @@ kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: UffizziClusters.uffizzi.com + name: uffizziclusters.uffizzi.com diff --git a/src/config/manager/manager.yaml b/src/config/manager/manager.yaml index f7cdf2ff..17b04fc8 100644 --- a/src/config/manager/manager.yaml +++ b/src/config/manager/manager.yaml @@ -58,7 +58,7 @@ spec: # - linux securityContext: runAsNonRoot: true - # TODO(user): For common cases that do not require escalating privileges + # TODO(user): For util cases that do not require escalating privileges # it is recommended to ensure that all your Pods/Containers are restrictive. # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted # Please uncomment the following code if your project does NOT have to work on old Kubernetes diff --git a/src/controllers/etcd/etcd_controller.go b/src/controllers/etcd/etcd_controller.go index e0a0fc10..03e9f50b 100644 --- a/src/controllers/etcd/etcd_controller.go +++ b/src/controllers/etcd/etcd_controller.go @@ -19,7 +19,7 @@ package etcd import ( "context" uclusteruffizzicomv1alpha1 "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/constants" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/constants" fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" diff --git a/src/controllers/etcd/helm.go b/src/controllers/etcd/helm.go index c798911e..b02d1f7d 100644 --- a/src/controllers/etcd/helm.go +++ b/src/controllers/etcd/helm.go @@ -3,9 +3,9 @@ package etcd import ( "context" "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/constants" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/build" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/build/etcd" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/constants" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/build" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/build/etcd" fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" fluxsourcev1 "github.com/fluxcd/source-controller/api/v1beta2" "github.com/pkg/errors" diff --git a/src/controllers/uffizzicluster/conditions.go b/src/controllers/uffizzicluster/conditions.go index 4957567a..1736fe42 100644 --- a/src/controllers/uffizzicluster/conditions.go +++ b/src/controllers/uffizzicluster/conditions.go @@ -28,6 +28,29 @@ const ( ReasonAwoken = "Awoken" ) +func GetAllInitializingConditions() []metav1.Condition { + return []metav1.Condition{ + Initializing(), + InitializingAPI(), + InitializingDataStore(), + DefaultSleepState(), + } +} + +func GetAllReadyConditions() []metav1.Condition { + return []metav1.Condition{ + APIReady(), + DataStoreReady(), + } +} + +func GetAllNotReadyConditions() []metav1.Condition { + return []metav1.Condition{ + APINotReady(), + DataStoreNotReady(), + } +} + func Initializing() metav1.Condition { return metav1.Condition{ Type: TypeReady, diff --git a/src/controllers/uffizzicluster/deployment.go b/src/controllers/uffizzicluster/deployment.go index 8b56d97d..17f3c2f1 100644 --- a/src/controllers/uffizzicluster/deployment.go +++ b/src/controllers/uffizzicluster/deployment.go @@ -4,7 +4,7 @@ import ( "context" "errors" "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/build/vcluster" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/build/vcluster" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" diff --git a/src/controllers/uffizzicluster/helm.go b/src/controllers/uffizzicluster/helm.go index 8d6741b7..c2b65ad9 100644 --- a/src/controllers/uffizzicluster/helm.go +++ b/src/controllers/uffizzicluster/helm.go @@ -4,9 +4,9 @@ import ( "context" "encoding/json" uclusteruffizzicomv1alpha1 "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/constants" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/build" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/build/vcluster" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/constants" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/build" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/build/vcluster" fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" fluxsourcev1 "github.com/fluxcd/source-controller/api/v1beta2" "github.com/pkg/errors" diff --git a/src/controllers/uffizzicluster/networkpolicy.go b/src/controllers/uffizzicluster/networkpolicy.go index 6ec26b6c..855a5744 100644 --- a/src/controllers/uffizzicluster/networkpolicy.go +++ b/src/controllers/uffizzicluster/networkpolicy.go @@ -3,7 +3,7 @@ package uffizzicluster import ( "fmt" uclusteruffizzicomv1alpha1 "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/build/vcluster" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/build/vcluster" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/src/controllers/uffizzicluster/statefulset.go b/src/controllers/uffizzicluster/statefulset.go index 60cf6df4..971a77e0 100644 --- a/src/controllers/uffizzicluster/statefulset.go +++ b/src/controllers/uffizzicluster/statefulset.go @@ -5,7 +5,7 @@ import ( "errors" "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" uffizzicluster "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/etcd" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/build/vcluster" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/build/vcluster" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" diff --git a/src/controllers/uffizzicluster/suite_test.go b/src/controllers/uffizzicluster/suite_test.go deleted file mode 100644 index 41fce8dd..00000000 --- a/src/controllers/uffizzicluster/suite_test.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2023. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package uffizzicluster - -import ( - uclusteruffizzicomv1alpha1 "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" - "path/filepath" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - //+kubebuilder:scaffold:imports -) - -// These test use Ginkgo (BDD-style Go testing framework). Refer to -// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. - -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment - -func TestAPIs(t *testing.T) { - RegisterFailHandler(Fail) - - RunSpecs(t, "Controller Suite") -} - -var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - - By("bootstrapping test environment") - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, - ErrorIfCRDPathMissing: true, - } - - var err error - // cfg is defined in this file globally. - cfg, err = testEnv.Start() - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).NotTo(BeNil()) - - err = uclusteruffizzicomv1alpha1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) - - //+kubebuilder:scaffold:scheme - - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient).NotTo(BeNil()) - -}) - -var _ = AfterSuite(func() { - By("tearing down the test environment") - err := testEnv.Stop() - Expect(err).NotTo(HaveOccurred()) -}) diff --git a/src/controllers/uffizzicluster/uffizzicluster_controller.go b/src/controllers/uffizzicluster/uffizzicluster_controller.go index 1115dd40..a8e56e3d 100644 --- a/src/controllers/uffizzicluster/uffizzicluster_controller.go +++ b/src/controllers/uffizzicluster/uffizzicluster_controller.go @@ -20,8 +20,8 @@ import ( "context" "encoding/json" "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/constants" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/build/vcluster" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/constants" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/build/vcluster" "github.com/fluxcd/pkg/apis/meta" "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" @@ -42,17 +42,10 @@ import ( // UffizziClusterReconciler reconciles a UffizziCluster object type UffizziClusterReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + KubernetesProvider string } -type LIFECYCLE_OP_TYPE string - -var ( - LIFECYCLE_OP_TYPE_CREATE LIFECYCLE_OP_TYPE = "create" - LIFECYCLE_OP_TYPE_UPDATE LIFECYCLE_OP_TYPE = "update" - LIFECYCLE_OP_TYPE_DELETE LIFECYCLE_OP_TYPE = "delete" -) - //+kubebuilder:rbac:groups=uffizzi.com,resources=uffizziclusters,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=uffizzi.com,resources=uffizziclusters/status,verbs=get;update;patch //+kubebuilder:rbac:groups=uffizzi.com,resources=uffizziclusters/finalizers,verbs=update @@ -90,12 +83,12 @@ var ( func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var ( - lifecycleOpType LIFECYCLE_OP_TYPE + lifecycleOpType constants.LIFECYCLE_OP_TYPE currentSpec string lastAppliedSpec string ) // default lifecycle operation - lifecycleOpType = LIFECYCLE_OP_TYPE_CREATE + lifecycleOpType = constants.LIFECYCLE_OP_TYPE_CREATE logger := log.FromContext(ctx) // ---------------------- @@ -106,7 +99,7 @@ func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque if err := r.Get(ctx, req.NamespacedName, uCluster); err != nil { // possibly a delete event if k8serrors.IsNotFound(err) { - lifecycleOpType = LIFECYCLE_OP_TYPE_DELETE + lifecycleOpType = constants.LIFECYCLE_OP_TYPE_DELETE logger.Info("UffizziCluster deleted", "NamespacedName", req.NamespacedName) } else { logger.Info("Failed to get UffizziCluster", "NamespacedName", req.NamespacedName, "Error", err) @@ -115,7 +108,7 @@ func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, client.IgnoreNotFound(err) } // if a new ucluster has been created then set the status to have the current ingress spec - if lifecycleOpType == LIFECYCLE_OP_TYPE_CREATE { + if lifecycleOpType == constants.LIFECYCLE_OP_TYPE_CREATE { currentSpecBytes, err := json.Marshal(uCluster.Spec) if err != nil { logger.Error(err, "Failed to marshal current spec") @@ -174,7 +167,7 @@ func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque return result, createEgressError } // helm release does not exist so let's create one - lifecycleOpType = LIFECYCLE_OP_TYPE_CREATE + lifecycleOpType = constants.LIFECYCLE_OP_TYPE_CREATE // create either a k8s based vcluster or a k3s based vcluster newHelmRelease, result, err := r.createVClusterHelmChart(ctx, uCluster, newHelmRelease) if err != nil { @@ -202,7 +195,7 @@ func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque logger.Error(err, "Failed to create HelmRelease, unknown error") return ctrl.Result{}, err } else { - lifecycleOpType = LIFECYCLE_OP_TYPE_UPDATE + lifecycleOpType = constants.LIFECYCLE_OP_TYPE_UPDATE // if helm release already exists then replicate the status conditions onto the uffizzicluster object patch := client.MergeFrom(uCluster.DeepCopy()) mirrorHelmStackConditions(helmRelease, uCluster) @@ -212,7 +205,7 @@ func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque } } // create helm repo for loft if it doesn't already exist - if lifecycleOpType == LIFECYCLE_OP_TYPE_CREATE { + if lifecycleOpType == constants.LIFECYCLE_OP_TYPE_CREATE { err := r.createLoftHelmRepo(ctx, req) if err != nil && k8serrors.IsAlreadyExists(err) { // logger.Info("Loft Helm Repo for UffizziCluster already exists", "NamespacedName", req.NamespacedName) @@ -226,7 +219,7 @@ func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, err } logger.Info("UffizziCluster lastAppliedConfig has been set") - } else if lifecycleOpType == LIFECYCLE_OP_TYPE_DELETE { + } else if lifecycleOpType == constants.LIFECYCLE_OP_TYPE_DELETE { err := r.deleteLoftHelmRepo(ctx, req) if err != nil && k8serrors.IsNotFound(err) { logger.Info("Loft Helm Repo for UffizziCluster already deleted", "NamespacedName", req.NamespacedName) @@ -236,7 +229,7 @@ func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque // UCLUSTER HELM CHART and RELATED RESOURCES _UPDATION_ // ---------------------- var updatedHelmRelease *fluxhelmv2beta1.HelmRelease - if lifecycleOpType == LIFECYCLE_OP_TYPE_UPDATE { + if lifecycleOpType == constants.LIFECYCLE_OP_TYPE_UPDATE { if currentSpec != lastAppliedSpec { if uCluster.Spec.Distro == constants.VCLUSTER_K8S_DISTRO { if updatedHelmRelease, err = r.upsertVClusterK8sHelmRelease(true, ctx, uCluster); err != nil { @@ -257,15 +250,15 @@ func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque // ---------------------- // UCLUSTER SLEEP // ---------------------- - if err := r.reconcileSleepState(ctx, uCluster); err != nil { - if k8serrors.IsNotFound(err) { - // logger.Info("vcluster statefulset not found, requeueing") - return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil - } - // cluster did not sleep - logger.Info("Failed to reconcile sleep state, reconciling again", "Error", err.Error()) - return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil - } + //if err := r.reconcileSleepState(ctx, uCluster); err != nil { + // if k8serrors.IsNotFound(err) { + // // logger.Info("vcluster statefulset not found, requeueing") + // return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil + // } + // // cluster did not sleep + // logger.Info("Failed to reconcile sleep state, reconciling again", "Error", err.Error()) + // return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil + //} return ctrl.Result{}, nil } diff --git a/src/controllers/uffizzicluster/vcluster_test.go b/src/controllers/uffizzicluster/vcluster_test.go index b8fa541a..9eaa7cf9 100644 --- a/src/controllers/uffizzicluster/vcluster_test.go +++ b/src/controllers/uffizzicluster/vcluster_test.go @@ -18,8 +18,8 @@ package uffizzicluster import ( "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/constants" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/build/vcluster" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/constants" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/build/vcluster" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/src/controllers/uffizzicluster/workload.go b/src/controllers/uffizzicluster/workload.go index 9d92aea1..0ea7118d 100644 --- a/src/controllers/uffizzicluster/workload.go +++ b/src/controllers/uffizzicluster/workload.go @@ -3,8 +3,8 @@ package uffizzicluster import ( context "context" "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/constants" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/build/vcluster" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/constants" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/build/vcluster" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/src/controllers/constants/constants.go b/src/pkg/constants/constants.go similarity index 76% rename from src/controllers/constants/constants.go rename to src/pkg/constants/constants.go index 01a7ffe0..104a66ba 100644 --- a/src/controllers/constants/constants.go +++ b/src/pkg/constants/constants.go @@ -29,3 +29,15 @@ const ( WORKLOAD_TYPE_DEPLOYMENT = "deployment" WORKLOAD_TYPE_STATEFULSET = "statefulset" ) + +type LIFECYCLE_OP_TYPE string +type KUBERNETES_PROVIDER string + +const ( + LIFECYCLE_OP_TYPE_CREATE LIFECYCLE_OP_TYPE = "create" + LIFECYCLE_OP_TYPE_UPDATE LIFECYCLE_OP_TYPE = "update" + LIFECYCLE_OP_TYPE_DELETE LIFECYCLE_OP_TYPE = "delete" + NO_KUBE_PROVIDER KUBERNETES_PROVIDER = "" + GKE_KUBE_PROVIDER KUBERNETES_PROVIDER = "gke" + EKS_KUBE_PROVIDER KUBERNETES_PROVIDER = "eks" +) diff --git a/src/controllers/helm/build/etcd/build.go b/src/pkg/helm/build/etcd/build.go similarity index 89% rename from src/controllers/helm/build/etcd/build.go rename to src/pkg/helm/build/etcd/build.go index 0156b59a..2828cd87 100644 --- a/src/controllers/helm/build/etcd/build.go +++ b/src/pkg/helm/build/etcd/build.go @@ -1,9 +1,9 @@ package etcd import ( - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/constants" - helmtypes "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/types" - etcdhelmtypes "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/types/etcd" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/constants" + helmtypes "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/types" + etcdhelmtypes "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/types/etcd" v1 "k8s.io/api/core/v1" ) diff --git a/src/controllers/helm/build/tojson.go b/src/pkg/helm/build/tojson.go similarity index 100% rename from src/controllers/helm/build/tojson.go rename to src/pkg/helm/build/tojson.go diff --git a/src/controllers/helm/build/vcluster/build.go b/src/pkg/helm/build/vcluster/build.go similarity index 97% rename from src/controllers/helm/build/vcluster/build.go rename to src/pkg/helm/build/vcluster/build.go index 0c2daeb7..a16316ef 100644 --- a/src/controllers/helm/build/vcluster/build.go +++ b/src/pkg/helm/build/vcluster/build.go @@ -3,10 +3,10 @@ package vcluster import ( "fmt" "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/constants" "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/etcd" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/types" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/types/vcluster" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/constants" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/types" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/types/vcluster" v1 "k8s.io/api/core/v1" ) diff --git a/src/controllers/helm/build/vcluster/build_test.go b/src/pkg/helm/build/vcluster/build_test.go similarity index 100% rename from src/controllers/helm/build/vcluster/build_test.go rename to src/pkg/helm/build/vcluster/build_test.go diff --git a/src/controllers/helm/types/common.go b/src/pkg/helm/types/common.go similarity index 100% rename from src/controllers/helm/types/common.go rename to src/pkg/helm/types/common.go diff --git a/src/controllers/helm/types/etcd/etcd.go b/src/pkg/helm/types/etcd/etcd.go similarity index 94% rename from src/controllers/helm/types/etcd/etcd.go rename to src/pkg/helm/types/etcd/etcd.go index e7cda28a..d331a1c0 100644 --- a/src/controllers/helm/types/etcd/etcd.go +++ b/src/pkg/helm/types/etcd/etcd.go @@ -1,7 +1,7 @@ package etcd import ( - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/types" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/types" ) type Etcd struct { diff --git a/src/controllers/helm/types/vcluster/vcluster.go b/src/pkg/helm/types/vcluster/vcluster.go similarity index 98% rename from src/controllers/helm/types/vcluster/vcluster.go rename to src/pkg/helm/types/vcluster/vcluster.go index 7c18a468..6d9542ec 100644 --- a/src/controllers/helm/types/vcluster/vcluster.go +++ b/src/pkg/helm/types/vcluster/vcluster.go @@ -2,7 +2,7 @@ package vcluster import ( "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" - "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/helm/types" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/helm/types" ) type Common struct { diff --git a/src/test/e2e/basic-k3s/00-assert.yaml b/src/test/e2e/basic-k3s/00-assert.yaml deleted file mode 100644 index f31cf94a..00000000 --- a/src/test/e2e/basic-k3s/00-assert.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: StatefulSet -apiVersion: apps/v1 -metadata: - name: uc-basic-k3s -spec: - ingress: - host: app.qa-gke.uffizzi.com \ No newline at end of file diff --git a/src/test/e2e/basic-k3s/00-install.yaml b/src/test/e2e/basic-k3s/00-install.yaml deleted file mode 100644 index 9611d025..00000000 --- a/src/test/e2e/basic-k3s/00-install.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: UffizziCluster -apiVersion: uffizzi.com/v1alpha1 -metadata: - name: basic-k3s -spec: - ingress: - host: app.qa-gke.uffizzi.com diff --git a/src/test/e2e/suite_test.go b/src/test/e2e/suite_test.go new file mode 100644 index 00000000..6a8e5b4b --- /dev/null +++ b/src/test/e2e/suite_test.go @@ -0,0 +1,183 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "context" + uffizziv1alpha1 "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/uffizzicluster" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/test/util/diff" + fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" + fluxsourcev1beta2 "github.com/fluxcd/source-controller/api/v1beta2" + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "math/rand" + "os" + "path/filepath" + ctrl "sigs.k8s.io/controller-runtime" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + //+kubebuilder:scaffold:imports +) + +// These test use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc + useExistingCluster = getEnvtestRemote() +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +// write a function to get the value of the ENVTEST_REMOTE environment variable +func getEnvtestRemote() bool { + if os.Getenv("ENVTEST_REMOTE") == "true" { + return true + } + return false +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + UseExistingCluster: &useExistingCluster, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = uffizziv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = fluxhelmv2beta1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = fluxsourcev1beta2.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + ctx, cancel = context.WithCancel(context.TODO()) + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + err = (&uffizzicluster.UffizziClusterReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() +}) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +func testingSeed() { + // Seed the test environment with some data + rand.Seed(12345) +} + +func stringWithCharset(length int, charset string) string { + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} + +func randomString(length int) string { + return stringWithCharset(length, "abcdefghijklmnopqrstuvwxyz") +} + +// CompareSlices compares two slices for equality. It returns true if both slices have the same elements in the same order. +func compareSlices[T comparable](slice1, slice2 []T) bool { + if len(slice1) != len(slice2) { + return false + } + + for i, v := range slice1 { + if v != slice2[i] { + return false + } + } + + return true +} + +// Checks if the required conditions are present and match in the actual conditions slice. +// Both requiredConditions and actualConditions are slices of metav1.Condition. +func containsAllConditions(requiredConditions, actualConditions []metav1.Condition) bool { + for _, requiredCondition := range requiredConditions { + found := false + for _, actualCondition := range actualConditions { + if actualCondition.Type == requiredCondition.Type && + actualCondition.Status == requiredCondition.Status { + // Add more condition checks here if necessary (e.g., Reason, Message) + found = true + d := cmp.Diff(requiredConditions, actualConditions) + GinkgoWriter.Printf(diff.PrintWantGot(d)) + break + } + } + if !found { + return false + } + } + return true +} diff --git a/src/test/e2e/uffizzicluster_controller_test.go b/src/test/e2e/uffizzicluster_controller_test.go new file mode 100644 index 00000000..55267dfb --- /dev/null +++ b/src/test/e2e/uffizzicluster_controller_test.go @@ -0,0 +1,112 @@ +package e2e + +import ( + "context" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/api/v1alpha1" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/controllers/uffizzicluster" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/pkg/constants" + "github.com/UffizziCloud/uffizzi-cluster-operator/src/test/util/diff" + fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" + fluxsourcev1beta2 "github.com/fluxcd/source-controller/api/v1beta2" + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "strings" +) + +var _ = Describe("UffizziCluster Controller", func() { + ctx := context.Background() + testingSeed() + name := "basic" + timeout := "1m" + pollingTimeout := "100ms" + + ns := &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: strings.Join([]string{name, randomString(5)}, "-"), + }, + } + uc := &v1alpha1.UffizziCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns.Name, + }, + } + helmRelease := &fluxhelmv2beta1.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: "uc-" + name, + Namespace: ns.Name, + }, + } + helmRepo := &fluxsourcev1beta2.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.LOFT_HELM_REPO, + Namespace: ns.Name, + }, + } + + Context("When creating UffizziCluster", func() { + It("Should create a UffizziCluster", func() { + // + By("By Creating Namespace for the UffizziCluster") + Expect(k8sClient.Create(ctx, ns)).Should(Succeed()) + + // + By("By creating a new UffizziCluster") + Expect(k8sClient.Create(ctx, uc)).Should(Succeed()) + }) + + It("Should create a HelmRelease and HelmRepository", func() { + // + By("Checking if the HelmRelease was created") + Eventually(func() bool { + if err := k8sClient.Get(ctx, createNamespacesName(helmRelease.Name, ns.Name), helmRelease); err != nil { + return false + } + return true + }) + // + By("Checking if the Loft HelmRepository was created") + Eventually(func() bool { + if err := k8sClient.Get(ctx, createNamespacesName(constants.LOFT_HELM_REPO, ns.Name), helmRepo); err != nil { + return false + } + return true + }) + + }) + + It("Should initialize correctly", func() { + expectedConditions := []metav1.Condition{} + uffizziClusterNSN := createNamespacesName(uc.Name, ns.Name) + By("Check if UffizziCluster initializes correctly") + Eventually(func() bool { + if err := k8sClient.Get(ctx, uffizziClusterNSN, uc); err != nil { + return false + } + expectedConditions = uffizzicluster.GetAllInitializingConditions() + return containsAllConditions(expectedConditions, uc.Status.Conditions) + }, timeout, pollingTimeout).Should(BeTrue()) + d := cmp.Diff(expectedConditions, uc.Status.Conditions) + GinkgoWriter.Printf(diff.PrintWantGot(d)) + }) + }) +}) + +func createNamespacesName(name, namespace string) types.NamespacedName { + return types.NamespacedName{ + Name: name, + Namespace: namespace, + } +} + +func deleteTestNamespace(name string) error { + return k8sClient.Delete(ctx, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }) +} diff --git a/src/test/util/diff/print.go b/src/test/util/diff/print.go new file mode 100644 index 00000000..3c2669d0 --- /dev/null +++ b/src/test/util/diff/print.go @@ -0,0 +1,11 @@ +package diff + +import "fmt" + +// PrintWantGot takes a diff string generated by cmp.Diff and returns it +// in a consistent format for reuse across all of our tests. This +// func assumes that the order of arguments passed to cmp.Diff was +// (want, got) or, in other words, the expectedResult then the actualResult. +func PrintWantGot(diff string) string { + return fmt.Sprintf("(-want, +got): %s", diff) +}